diff --git a/e2e-tests/playwright/support/constant.ts b/e2e-tests/playwright/support/constant.ts index 35aa9bb5f4..8be0ec803d 100644 --- a/e2e-tests/playwright/support/constant.ts +++ b/e2e-tests/playwright/support/constant.ts @@ -3,6 +3,5 @@ export const appsPluginId = 'com.mattermost.apps'; export const boardsPluginId = 'focalboard'; -export const boardsProductId = 'boards'; export const callsPluginId = 'com.mattermost.calls'; export const playbooksPluginId = 'playbooks'; diff --git a/server/boards/admin-scripts/reset-password.sh b/server/boards/admin-scripts/reset-password.sh deleted file mode 100755 index 6c614ed1b2..0000000000 --- a/server/boards/admin-scripts/reset-password.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -if [[ $# < 2 ]] ; then - echo 'reset-password.sh ' - exit 1 -fi - -curl --unix-socket /var/tmp/focalboard_local.socket http://localhost/api/v2/admin/users/$1/password -X POST -H 'Content-Type: application/json' -d '{ "password": "'$2'" }' diff --git a/server/boards/api/admin.go b/server/boards/api/admin.go deleted file mode 100644 index 4c139df5fa..0000000000 --- a/server/boards/api/admin.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "io" - "net/http" - "strings" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type AdminSetPasswordData struct { - Password string `json:"password"` -} - -func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - username := vars["username"] - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var requestData AdminSetPasswordData - err = json.Unmarshal(requestBody, &requestData) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "adminSetPassword", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - auditRec.AddMeta("username", username) - - if !strings.Contains(requestData.Password, "") { - a.errorResponse(w, r, model.NewErrBadRequest("password is required")) - return - } - - err = a.app.UpdateUserPassword(username, requestData.Password) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("AdminSetPassword, username: %s", mlog.String("username", username)) - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} diff --git a/server/boards/api/api.go b/server/boards/api/api.go deleted file mode 100644 index d1851cc931..0000000000 --- a/server/boards/api/api.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "runtime/debug" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/app" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - HeaderRequestedWith = "X-Requested-With" - HeaderRequestedWithXML = "XMLHttpRequest" - UploadFormFileKey = "file" - True = "true" - - ErrorNoTeamCode = 1000 - ErrorNoTeamMessage = "No team" -) - -var ( - ErrHandlerPanic = errors.New("http handler panic") -) - -// ---------------------------------------------------------------------------------------------------- -// REST APIs - -type API struct { - app *app.App - authService string - permissions permissions.PermissionsService - singleUserToken string - MattermostAuth bool - logger mlog.LoggerIFace - audit *audit.Audit - isPlugin bool -} - -func NewAPI( - app *app.App, - singleUserToken string, - authService string, - permissions permissions.PermissionsService, - logger mlog.LoggerIFace, - audit *audit.Audit, - isPlugin bool, -) *API { - return &API{ - app: app, - singleUserToken: singleUserToken, - authService: authService, - permissions: permissions, - logger: logger, - audit: audit, - isPlugin: isPlugin, - } -} - -func (a *API) RegisterRoutes(r *mux.Router) { - apiv2 := r.PathPrefix("/api/v2").Subrouter() - apiv2.Use(a.panicHandler) - apiv2.Use(a.requireCSRFToken) - - /* ToDo: - apiv3 := r.PathPrefix("/api/v3").Subrouter() - apiv3.Use(a.panicHandler) - apiv3.Use(a.requireCSRFToken) - */ - - // V2 routes (ToDo: migrate these to V3 when ready to ship V3) - a.registerUsersRoutes(apiv2) - a.registerAuthRoutes(apiv2) - a.registerMembersRoutes(apiv2) - a.registerCategoriesRoutes(apiv2) - a.registerSharingRoutes(apiv2) - a.registerTeamsRoutes(apiv2) - a.registerAchivesRoutes(apiv2) - a.registerSubscriptionsRoutes(apiv2) - a.registerFilesRoutes(apiv2) - a.registerLimitsRoutes(apiv2) - a.registerInsightsRoutes(apiv2) - a.registerOnboardingRoutes(apiv2) - a.registerSearchRoutes(apiv2) - a.registerConfigRoutes(apiv2) - a.registerBoardsAndBlocksRoutes(apiv2) - a.registerChannelsRoutes(apiv2) - a.registerTemplatesRoutes(apiv2) - a.registerBoardsRoutes(apiv2) - a.registerBlocksRoutes(apiv2) - a.registerContentBlocksRoutes(apiv2) - a.registerStatisticsRoutes(apiv2) - a.registerComplianceRoutes(apiv2) - - // V3 routes - a.registerCardsRoutes(apiv2) - - // System routes are outside the /api/v2 path - a.registerSystemRoutes(r) -} - -func (a *API) RegisterAdminRoutes(r *mux.Router) { - r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST") -} - -func getUserID(r *http.Request) string { - ctx := r.Context() - session, ok := ctx.Value(sessionContextKey).(*model.Session) - if !ok { - return "" - } - return session.UserID -} - -func (a *API) panicHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if p := recover(); p != nil { - a.logger.Error("Http handler panic", - mlog.Any("panic", p), - mlog.String("stack", string(debug.Stack())), - mlog.String("uri", r.URL.Path), - ) - a.errorResponse(w, r, ErrHandlerPanic) - } - }() - - next.ServeHTTP(w, r) - }) -} - -func (a *API) requireCSRFToken(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !a.checkCSRFToken(r) { - a.logger.Error("checkCSRFToken FAILED") - a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED")) - return - } - - next.ServeHTTP(w, r) - }) -} - -func (a *API) checkCSRFToken(r *http.Request) bool { - token := r.Header.Get(HeaderRequestedWith) - return token == HeaderRequestedWithXML -} - -func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool { - query := r.URL.Query() - readToken := query.Get("read_token") - - if len(readToken) < 1 { - return false - } - - isValid, err := a.app.IsValidReadToken(boardID, readToken) - if err != nil { - a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err)) - return false - } - - return isValid -} - -func (a *API) userIsGuest(userID string) (bool, error) { - if a.singleUserToken != "" { - return false, nil - } - return a.app.UserIsGuest(userID) -} - -// Response helpers - -func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) { - a.logger.Error(err.Error()) - errorResponse := model.ErrorResponse{Error: err.Error()} - - switch { - case model.IsErrBadRequest(err): - errorResponse.ErrorCode = http.StatusBadRequest - case model.IsErrUnauthorized(err): - errorResponse.ErrorCode = http.StatusUnauthorized - case model.IsErrForbidden(err): - errorResponse.ErrorCode = http.StatusForbidden - case model.IsErrNotFound(err): - errorResponse.ErrorCode = http.StatusNotFound - case model.IsErrRequestEntityTooLarge(err): - errorResponse.ErrorCode = http.StatusRequestEntityTooLarge - case model.IsErrNotImplemented(err): - errorResponse.ErrorCode = http.StatusNotImplemented - default: - a.logger.Error("API ERROR", - mlog.Int("code", http.StatusInternalServerError), - mlog.Err(err), - mlog.String("api", r.URL.Path), - ) - errorResponse.Error = "internal server error" - errorResponse.ErrorCode = http.StatusInternalServerError - } - - setResponseHeader(w, "Content-Type", "application/json") - data, err := json.Marshal(errorResponse) - if err != nil { - data = []byte("{}") - } - - w.WriteHeader(errorResponse.ErrorCode) - _, _ = w.Write(data) -} - -func stringResponse(w http.ResponseWriter, message string) { - setResponseHeader(w, "Content-Type", "text/plain") - _, _ = fmt.Fprint(w, message) -} - -func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nolint:unparam - setResponseHeader(w, "Content-Type", "application/json") - w.WriteHeader(code) - fmt.Fprint(w, message) -} - -func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam - setResponseHeader(w, "Content-Type", "application/json") - w.WriteHeader(code) - _, _ = w.Write(json) -} - -func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam - header := w.Header() - if header == nil { - return - } - header.Set(key, value) -} diff --git a/server/boards/api/api_test.go b/server/boards/api/api_test.go deleted file mode 100644 index f9f220ab94..0000000000 --- a/server/boards/api/api_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "database/sql" - "fmt" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestErrorResponse(t *testing.T) { - testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)} - - testCases := []struct { - Name string - Error error - ResponseCode int - ResponseBody string - }{ - // bad request - {"ErrBadRequest", model.NewErrBadRequest("bad field"), http.StatusBadRequest, "bad field"}, - {"ErrViewsLimitReached", model.ErrViewsLimitReached, http.StatusBadRequest, "limit reached"}, - {"ErrAuthParam", model.NewErrAuthParam("password is required"), http.StatusBadRequest, "password is required"}, - {"ErrInvalidCategory", model.NewErrInvalidCategory("open"), http.StatusBadRequest, "open"}, - {"ErrBoardMemberIsLastAdmin", model.ErrBoardMemberIsLastAdmin, http.StatusBadRequest, "no admins"}, - {"ErrBoardIDMismatch", model.ErrBoardIDMismatch, http.StatusBadRequest, "Board IDs do not match"}, - - // unauthorized - {"ErrUnauthorized", model.NewErrUnauthorized("not enough permissions"), http.StatusUnauthorized, "not enough permissions"}, - - // forbidden - {"ErrForbidden", model.NewErrForbidden("not enough permissions"), http.StatusForbidden, "not enough permissions"}, - {"ErrPermission", model.NewErrPermission("not enough permissions"), http.StatusForbidden, "not enough permissions"}, - {"ErrPatchUpdatesLimitedCards", model.ErrPatchUpdatesLimitedCards, http.StatusForbidden, "cards that are limited"}, - {"ErrCategoryPermissionDenied", model.ErrCategoryPermissionDenied, http.StatusForbidden, "doesn't belong to user"}, - - // not found - {"ErrNotFound", model.NewErrNotFound("board"), http.StatusNotFound, "board"}, - {"ErrNotAllFound", model.NewErrNotAllFound("block", []string{"1", "2"}), http.StatusNotFound, "not all instances of {block} in {1, 2} found"}, - {"sql.ErrNoRows", sql.ErrNoRows, http.StatusNotFound, "rows"}, - {"ErrNotFound", model.ErrCategoryDeleted, http.StatusNotFound, "category is deleted"}, - - // request entity too large - {"ErrRequestEntityTooLarge", model.ErrRequestEntityTooLarge, http.StatusRequestEntityTooLarge, "entity too large"}, - - // not implemented - {"ErrNotFound", model.ErrInsufficientLicense, http.StatusNotImplemented, "appropriate license required"}, - {"ErrNotImplemented", model.NewErrNotImplemented("not implemented in plugin mode"), http.StatusNotImplemented, "plugin mode"}, - - // internal server error - {"Any other error", ErrHandlerPanic, http.StatusInternalServerError, "internal server error"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s should be a %d code", tc.Name, tc.ResponseCode), func(t *testing.T) { - r := httptest.NewRequest(http.MethodGet, "/test", nil) - w := httptest.NewRecorder() - - testAPI.errorResponse(w, r, tc.Error) - res := w.Result() - - require.Equal(t, tc.ResponseCode, res.StatusCode) - require.Equal(t, "application/json", res.Header.Get("Content-Type")) - b, rErr := io.ReadAll(res.Body) - require.NoError(t, rErr) - res.Body.Close() - require.Contains(t, string(b), tc.ResponseBody) - }) - } -} diff --git a/server/boards/api/archive.go b/server/boards/api/archive.go deleted file mode 100644 index aa4d2c6aa2..0000000000 --- a/server/boards/api/archive.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "fmt" - "net/http" - "time" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - archiveExtension = ".boardarchive" -) - -func (a *API) registerAchivesRoutes(r *mux.Router) { - // Archive APIs - r.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET") - r.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST") - r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") -} - -func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/archive/export archiveExportBoard - // - // Exports an archive of all blocks for one boards. - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Id of board to export - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // content: - // application-octet-stream: - // type: string - // format: binary - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - boardID := vars["boardID"] - userID := getUserID(r) - - // check user has permission to board - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - // if this user has `manage_system` permission and there is a license with the compliance - // feature enabled, then we will allow the export. - license := a.app.GetLicense() - if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) || license == nil || !(*license.Features.Compliance) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - - auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("BoardID", boardID) - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - opts := model.ExportArchiveOptions{ - TeamID: board.TeamID, - BoardIDs: []string{board.ID}, - } - - filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", "attachment; filename="+filename) - w.Header().Set("Content-Transfer-Encoding", "binary") - - if err := a.app.ExportArchive(w, opts); err != nil { - a.errorResponse(w, r, err) - } - - auditRec.Success() -} - -func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/archive/import archiveImport - // - // Import an archive of boards. - // - // --- - // produces: - // - application/json - // consumes: - // - multipart/form-data - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: file - // in: formData - // description: archive file to import - // required: true - // type: file - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session, _ := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - vars := mux.Vars(r) - teamID := vars["teamID"] - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - file, handle, err := r.FormFile(UploadFormFileKey) - if err != nil { - fmt.Fprintf(w, "%v", err) - return - } - defer file.Close() - - auditRec := a.makeAuditRecord(r, "import", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("filename", handle.Filename) - auditRec.AddMeta("size", handle.Size) - - opt := model.ImportArchiveOptions{ - TeamID: teamID, - ModifiedBy: userID, - } - - if err := a.app.ImportArchive(file, opt); err != nil { - a.logger.Debug("Error importing archive", - mlog.String("team_id", teamID), - mlog.Err(err), - ) - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/archive/export archiveExportTeam - // - // Exports an archive of all blocks for all the boards in a team. - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Id of team - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // content: - // application-octet-stream: - // type: string - // format: binary - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - vars := mux.Vars(r) - teamID := vars["teamID"] - - ctx := r.Context() - session, _ := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("TeamID", teamID) - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest) - if err != nil { - a.errorResponse(w, r, err) - return - } - ids := []string{} - for _, board := range boards { - ids = append(ids, board.ID) - } - - opts := model.ExportArchiveOptions{ - TeamID: teamID, - BoardIDs: ids, - } - - filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", "attachment; filename="+filename) - w.Header().Set("Content-Transfer-Encoding", "binary") - - if err := a.app.ExportArchive(w, opts); err != nil { - a.errorResponse(w, r, err) - } - - auditRec.Success() -} diff --git a/server/boards/api/audit.go b/server/boards/api/audit.go deleted file mode 100644 index 432060755b..0000000000 --- a/server/boards/api/audit.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "net/http" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" -) - -// makeAuditRecord creates an audit record pre-populated with data from the request. -func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus string) *audit.Record { //nolint:unparam - ctx := r.Context() - var sessionID string - var userID string - if session, ok := ctx.Value(sessionContextKey).(*model.Session); ok { - sessionID = session.ID - userID = session.UserID - } - - teamID := "unknown" - rec := &audit.Record{ - APIPath: r.URL.Path, - Event: event, - Status: initialStatus, - UserID: userID, - SessionID: sessionID, - Client: r.UserAgent(), - IPAddress: r.RemoteAddr, - Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}}, - } - - return rec -} diff --git a/server/boards/api/auth.go b/server/boards/api/auth.go deleted file mode 100644 index dcac1f68a0..0000000000 --- a/server/boards/api/auth.go +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "encoding/json" - "io" - "net" - "net/http" - "strings" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - "github.com/mattermost/mattermost/server/v8/boards/services/auth" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerAuthRoutes(r *mux.Router) { - // personal-server specific routes. These are not needed in plugin mode. - if !a.isPlugin { - r.HandleFunc("/login", a.handleLogin).Methods("POST") - r.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST") - r.HandleFunc("/register", a.handleRegister).Methods("POST") - r.HandleFunc("/teams/{teamID}/regenerate_signup_token", a.sessionRequired(a.handlePostTeamRegenerateSignupToken)).Methods("POST") - r.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") - } -} - -func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /login login - // - // Login user - // - // --- - // produces: - // - application/json - // parameters: - // - name: body - // in: body - // description: Login request - // required: true - // schema: - // "$ref": "#/definitions/LoginRequest" - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/LoginResponse" - // '401': - // description: invalid login - // schema: - // "$ref": "#/definitions/ErrorResponse" - // '500': - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - if a.singleUserToken != "" { - // Not permitted in single-user mode - a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var loginData model.LoginRequest - err = json.Unmarshal(requestBody, &loginData) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "login", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - auditRec.AddMeta("username", loginData.Username) - auditRec.AddMeta("type", loginData.Type) - - if loginData.Type == "normal" { - token, err := a.app.Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken) - if err != nil { - a.errorResponse(w, r, model.NewErrUnauthorized("incorrect login")) - return - } - json, err := json.Marshal(model.LoginResponse{Token: token}) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, json) - auditRec.Success() - return - } - - a.errorResponse(w, r, model.NewErrBadRequest("invalid login type")) -} - -func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /logout logout - // - // Logout user - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '500': - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - if a.singleUserToken != "" { - // Not permitted in single-user mode - a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode")) - return - } - - ctx := r.Context() - - session := ctx.Value(sessionContextKey).(*model.Session) - - auditRec := a.makeAuditRecord(r, "logout", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - auditRec.AddMeta("userID", session.UserID) - - if err := a.app.Logout(session.ID); err != nil { - a.errorResponse(w, r, model.NewErrUnauthorized("incorrect logout")) - return - } - - auditRec.AddMeta("sessionID", session.ID) - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /register register - // - // Register new user - // - // --- - // produces: - // - application/json - // parameters: - // - name: body - // in: body - // description: Register request - // required: true - // schema: - // "$ref": "#/definitions/RegisterRequest" - // responses: - // '200': - // description: success - // '401': - // description: invalid registration token - // '500': - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - if a.singleUserToken != "" { - // Not permitted in single-user mode - a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var registerData model.RegisterRequest - err = json.Unmarshal(requestBody, ®isterData) - if err != nil { - a.errorResponse(w, r, err) - return - } - registerData.Email = strings.TrimSpace(registerData.Email) - registerData.Username = strings.TrimSpace(registerData.Username) - - // Validate token - if registerData.Token != "" { - team, err2 := a.app.GetRootTeam() - if err2 != nil { - a.errorResponse(w, r, err2) - return - } - - if registerData.Token != team.SignupToken { - a.errorResponse(w, r, model.NewErrUnauthorized("invalid token")) - return - } - } else { - // No signup token, check if no active users - userCount, err2 := a.app.GetRegisteredUserCount() - if err2 != nil { - a.errorResponse(w, r, err2) - return - } - if userCount > 0 { - a.errorResponse(w, r, model.NewErrUnauthorized("no sign-up token and user(s) already exist")) - return - } - } - - if err = registerData.IsValid(); err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "register", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - auditRec.AddMeta("username", registerData.Username) - - err = a.app.RegisterUser(registerData.Username, registerData.Email, registerData.Password) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /users/{userID}/changepassword changePassword - // - // Change a user's password - // - // --- - // produces: - // - application/json - // parameters: - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // - name: body - // in: body - // description: Change password request - // required: true - // schema: - // "$ref": "#/definitions/ChangePasswordRequest" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '400': - // description: invalid request - // schema: - // "$ref": "#/definitions/ErrorResponse" - // '500': - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - if a.singleUserToken != "" { - // Not permitted in single-user mode - a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode")) - return - } - - vars := mux.Vars(r) - userID := vars["userID"] - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var requestData model.ChangePasswordRequest - if err = json.Unmarshal(requestBody, &requestData); err != nil { - a.errorResponse(w, r, err) - return - } - - if err = requestData.IsValid(); err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "changePassword", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - - if err = a.app.ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { - return a.attachSession(handler, true) -} - -func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request), required bool) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - token, _ := auth.ParseAuthTokenFromRequest(r) - - a.logger.Debug(`attachSession`, mlog.Bool("single_user", a.singleUserToken != "")) - if a.singleUserToken != "" { - if required && (token != a.singleUserToken) { - a.errorResponse(w, r, model.NewErrUnauthorized("invalid single user token")) - return - } - - now := utils.GetMillis() - session := &model.Session{ - ID: model.SingleUser, - Token: token, - UserID: model.SingleUser, - AuthService: a.authService, - Props: map[string]interface{}{}, - CreateAt: now, - UpdateAt: now, - } - ctx := context.WithValue(r.Context(), sessionContextKey, session) - handler(w, r.WithContext(ctx)) - return - } - - if a.MattermostAuth && r.Header.Get("Mattermost-User-Id") != "" { - userID := r.Header.Get("Mattermost-User-Id") - now := utils.GetMillis() - session := &model.Session{ - ID: userID, - Token: userID, - UserID: userID, - AuthService: a.authService, - Props: map[string]interface{}{}, - CreateAt: now, - UpdateAt: now, - } - - ctx := context.WithValue(r.Context(), sessionContextKey, session) - handler(w, r.WithContext(ctx)) - return - } - - session, err := a.app.GetSession(token) - if err != nil { - if required { - a.errorResponse(w, r, model.NewErrUnauthorized(err.Error())) - return - } - - handler(w, r) - return - } - - authService := session.AuthService - if authService != a.authService { - msg := `Session authService mismatch` - a.logger.Error(msg, - mlog.String("sessionID", session.ID), - mlog.String("want", a.authService), - mlog.String("got", authService), - ) - a.errorResponse(w, r, model.NewErrUnauthorized(msg)) - return - } - - ctx := context.WithValue(r.Context(), sessionContextKey, session) - handler(w, r.WithContext(ctx)) - } -} - -func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Currently, admin APIs require local unix connections - conn := GetContextConn(r) - if _, isUnix := conn.(*net.UnixConn); !isUnix { - a.errorResponse(w, r, model.NewErrUnauthorized("not a local unix connection")) - return - } - - handler(w, r) - } -} diff --git a/server/boards/api/blocks.go b/server/boards/api/blocks.go deleted file mode 100644 index 17e0668ba8..0000000000 --- a/server/boards/api/blocks.go +++ /dev/null @@ -1,785 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerBlocksRoutes(r *mux.Router) { - // Blocks APIs - r.HandleFunc("/boards/{boardID}/blocks", a.attachSession(a.handleGetBlocks, false)).Methods("GET") - r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") - r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH") - r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") - r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH") - r.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST") - r.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST") -} - -func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/blocks getBlocks - // - // Returns blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: parent_id - // in: query - // description: ID of parent block, omit to specify all blocks - // required: false - // type: string - // - name: type - // in: query - // description: Type of blocks to return, omit to specify all types - // required: false - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Block" - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - query := r.URL.Query() - parentID := query.Get("parent_id") - blockType := query.Get("type") - blockID := query.Get("block_id") - boardID := mux.Vars(r)["boardID"] - - userID := getUserID(r) - - hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) - if userID == "" && !hasValidReadToken { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if !hasValidReadToken { - if board.IsTemplate && board.Type == model.BoardTypeOpen { - if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board template")) - return - } - } else { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - if board.IsTemplate { - var isGuest bool - isGuest, err = a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("guest are not allowed to get board templates")) - return - } - } - } - - auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("parentID", parentID) - auditRec.AddMeta("blockType", blockType) - auditRec.AddMeta("blockID", blockID) - - var blocks []*model.Block - var block *model.Block - switch { - case blockID != "": - block, err = a.app.GetBlockByID(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if block.BoardID != boardID { - message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - - blocks = append(blocks, block) - default: - opts := model.QueryBlocksOptions{ - BoardID: boardID, - ParentID: parentID, - BlockType: model.BlockType(blockType), - } - blocks, err = a.app.GetBlocks(opts) - if err != nil { - a.errorResponse(w, r, err) - return - } - } - - a.logger.Debug("GetBlocks", - mlog.String("boardID", boardID), - mlog.String("parentID", parentID), - mlog.String("blockType", blockType), - mlog.String("blockID", blockID), - mlog.Int("block_count", len(blocks)), - ) - - var bErr error - blocks, bErr = a.app.ApplyCloudLimits(blocks) - if bErr != nil { - a.errorResponse(w, r, err) - return - } - - json, err := json.Marshal(blocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, json) - - auditRec.AddMeta("blockCount", len(blocks)) - auditRec.Success() -} - -func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/blocks updateBlocks - // - // Insert blocks. The specified IDs will only be used to link - // blocks with existing ones, the rest will be replaced by server - // generated IDs - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk inserting) - // required: false - // type: bool - // - name: Body - // in: body - // description: array of blocks to insert or update - // required: true - // schema: - // type: array - // items: - // "$ref": "#/definitions/Block" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // items: - // $ref: '#/definitions/Block' - // type: array - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var blocks []*model.Block - - err = json.Unmarshal(requestBody, &blocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - hasComments := false - hasContents := false - for _, block := range blocks { - // Error checking - if len(block.Type) < 1 { - message := fmt.Sprintf("missing type for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if block.Type == model.TypeComment { - hasComments = true - } else { - hasContents = true - } - - if block.CreateAt < 1 { - message := fmt.Sprintf("invalid createAt for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if block.UpdateAt < 1 { - message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if block.BoardID != boardID { - message := fmt.Sprintf("invalid BoardID for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - } - - if hasContents { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes")) - return - } - } - if hasComments { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to post card comments")) - return - } - } - - blocks = model.GenerateBlockIDs(blocks, a.logger) - - auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("disable_notify", disableNotify) - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - model.StampModificationMetadata(userID, blocks, auditRec) - - // this query param exists when creating template from board, or board from template - sourceBoardID := r.URL.Query().Get("sourceBoardID") - if sourceBoardID != "" { - if updateFileIDsErr := a.app.CopyAndUpdateCardFiles(sourceBoardID, userID, blocks, false); updateFileIDsErr != nil { - a.errorResponse(w, r, updateFileIDsErr) - return - } - } - - newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("POST Blocks", - mlog.Int("block_count", len(blocks)), - mlog.Bool("disable_notify", disableNotify), - ) - - json, err := json.Marshal(newBlocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, json) - - auditRec.AddMeta("blockCount", len(blocks)) - auditRec.Success() -} - -func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /boards/{boardID}/blocks/{blockID} deleteBlock - // - // Deletes a block - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: blockID - // in: path - // description: ID of block to delete - // required: true - // type: string - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk deletion) - // required: false - // type: bool - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '404': - // description: block not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - vars := mux.Vars(r) - boardID := vars["boardID"] - blockID := vars["blockID"] - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes")) - return - } - - block, err := a.app.GetBlockByID(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if block.BoardID != boardID { - message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - - auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("blockID", blockID) - - err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("DELETE Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/blocks/{blockID}/undelete undeleteBlock - // - // Undeletes a block - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: blockID - // in: path - // description: ID of block to undelete - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/BlockPatch" - // '404': - // description: block not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - vars := mux.Vars(r) - blockID := vars["blockID"] - boardID := vars["boardID"] - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - block, err := a.app.GetLastBlockHistoryEntry(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if board.ID != block.BoardID { - message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members")) - return - } - - auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("blockID", blockID) - - undeletedBlock, err := a.app.UndeleteBlock(blockID, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - undeletedBlockData, err := json.Marshal(undeletedBlock) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID)) - jsonBytesResponse(w, http.StatusOK, undeletedBlockData) - - auditRec.Success() -} - -func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /boards/{boardID}/blocks/{blockID} patchBlock - // - // Partially updates a block - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: blockID - // in: path - // description: ID of block to patch - // required: true - // type: string - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk patching) - // required: false - // type: bool - // - name: Body - // in: body - // description: block patch to apply - // required: true - // schema: - // "$ref": "#/definitions/BlockPatch" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '404': - // description: block not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - vars := mux.Vars(r) - boardID := vars["boardID"] - blockID := vars["blockID"] - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes")) - return - } - - block, err := a.app.GetBlockByID(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if block.BoardID != boardID { - message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var patch *model.BlockPatch - err = json.Unmarshal(requestBody, &patch) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("blockID", blockID) - - if _, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify); err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PATCH Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID)) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /boards/{boardID}/blocks/ patchBlocks - // - // Partially updates batch of blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Workspace ID - // required: true - // type: string - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk patching) - // required: false - // type: bool - // - name: Body - // in: body - // description: block Ids and block patches to apply - // required: true - // schema: - // "$ref": "#/definitions/BlockPatchBatch" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - vars := mux.Vars(r) - teamID := vars["teamID"] - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var patches *model.BlockPatchBatch - err = json.Unmarshal(requestBody, &patches) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "patchBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - for i := range patches.BlockIDs { - 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, model.NewErrForbidden("access denied to make board changes")) - return - } - if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to make board changesa")) - return - } - } - - err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PATCH Blocks", mlog.String("patches", strconv.Itoa(len(patches.BlockIDs)))) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/blocks/{blockID}/duplicate duplicateBlock - // - // Returns the new created blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: blockID - // in: path - // description: Block ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Block" - // '404': - // description: board or block not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - blockID := mux.Vars(r)["blockID"] - userID := getUserID(r) - query := r.URL.Query() - asTemplate := query.Get("asTemplate") - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if userID == "" { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - block, err := a.app.GetBlockByID(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if board.ID != block.BoardID { - message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - - if block.Type == model.TypeComment { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to comment on board cards")) - return - } - } else { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards")) - return - } - } - - auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("blockID", blockID) - - a.logger.Debug("DuplicateBlock", - mlog.String("boardID", boardID), - mlog.String("blockID", blockID), - ) - - blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(blocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} diff --git a/server/boards/api/boards.go b/server/boards/api/boards.go deleted file mode 100644 index 2c3176ce78..0000000000 --- a/server/boards/api/boards.go +++ /dev/null @@ -1,679 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerBoardsRoutes(r *mux.Router) { - r.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET") - r.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST") - r.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET") - r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH") - r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE") - r.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST") - r.HandleFunc("/boards/{boardID}/undelete", a.sessionRequired(a.handleUndeleteBoard)).Methods("POST") - r.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET") -} - -func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/boards getBoards - // - // Returns team boards - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Board" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - teamID := mux.Vars(r)["teamID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - auditRec := a.makeAuditRecord(r, "getBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // retrieve boards list - boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetBoards", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(boards)), - ) - - data, err := json.Marshal(boards) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("boardsCount", len(boards)) - auditRec.Success() -} - -func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards createBoard - // - // Creates a new board - // - // --- - // produces: - // - application/json - // parameters: - // - name: Body - // in: body - // description: the board to create - // required: true - // schema: - // "$ref": "#/definitions/Board" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/Board' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var newBoard *model.Board - if err = json.Unmarshal(requestBody, &newBoard); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if newBoard.Type == model.BoardTypeOpen { - if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) { - a.errorResponse(w, r, model.NewErrPermission("access denied to create public boards")) - return - } - } else { - if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) { - a.errorResponse(w, r, model.NewErrPermission("access denied to create private boards")) - return - } - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - if err = newBoard.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - auditRec := a.makeAuditRecord(r, "createBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("teamID", newBoard.TeamID) - auditRec.AddMeta("boardType", newBoard.Type) - - // create board - board, err := a.app.CreateBoard(newBoard, userID, true) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("CreateBoard", - mlog.String("teamID", board.TeamID), - mlog.String("boardID", board.ID), - mlog.String("boardType", string(board.Type)), - mlog.String("userID", userID), - ) - - data, err := json.Marshal(board) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID} getBoard - // - // Returns a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Board" - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) - if userID == "" && !hasValidReadToken { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if !hasValidReadToken { - if board.Type == model.BoardTypePrivate { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } else { - var isGuest bool - isGuest, err = a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - - if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - } - - auditRec := a.makeAuditRecord(r, "getBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - - a.logger.Debug("GetBoard", - mlog.String("boardID", boardID), - ) - - data, err := json.Marshal(board) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /boards/{boardID} patchBoard - // - // Partially updates a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: Body - // in: body - // description: board patch to apply - // required: true - // schema: - // "$ref": "#/definitions/BoardPatch" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/Board' - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - if _, err := a.app.GetBoard(boardID); err != nil { - a.errorResponse(w, r, err) - return - } - - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var patch *model.BoardPatch - if err = json.Unmarshal(requestBody, &patch); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if err = patch.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties")) - return - } - - if patch.Type != nil || patch.MinimumRole != nil { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type")) - return - } - } - if patch.ChannelID != nil { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board access")) - return - } - } - - auditRec := a.makeAuditRecord(r, "patchBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("userID", userID) - - // patch board - updatedBoard, err := a.app.PatchBoard(patch, boardID, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PatchBoard", - mlog.String("boardID", boardID), - mlog.String("userID", userID), - ) - - data, err := json.Marshal(updatedBoard) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /boards/{boardID} deleteBoard - // - // Removes 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 - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - // Check if board exists - if _, err := a.app.GetBoard(boardID); err != nil { - a.errorResponse(w, r, err) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to delete board")) - return - } - - auditRec := a.makeAuditRecord(r, "deleteBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - - if err := a.app.DeleteBoard(boardID, userID); err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("DELETE Board", mlog.String("boardID", boardID)) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/duplicate duplicateBoard - // - // Returns the new created board and all the blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardsAndBlocks' - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - query := r.URL.Query() - asTemplate := query.Get("asTemplate") - toTeam := query.Get("toTeam") - - if userID == "" { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if toTeam == "" { - toTeam = board.TeamID - } - - if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("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, model.NewErrPermission("access denied to board")) - return - } - } else { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - - a.logger.Debug("DuplicateBoard", - mlog.String("boardID", boardID), - ) - - boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(boardsAndBlocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleUndeleteBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/undelete undeleteBoard - // - // Undeletes a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: ID of board to undelete - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - vars := mux.Vars(r) - boardID := vars["boardID"] - - auditRec := a.makeAuditRecord(r, "undeleteBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to undelete board")) - return - } - - err := a.app.UndeleteBoard(boardID, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("UNDELETE Board", mlog.String("boardID", boardID)) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/metadata getBoardMetadata - // - // Returns a board's metadata - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/BoardMetadata" - // '404': - // description: board not found - // '501': - // description: required license not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - board, boardMetadata, err := a.app.GetBoardMetadata(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if board == nil || boardMetadata == nil { - a.errorResponse(w, r, model.NewErrNotFound("board metadata BoardID="+boardID)) - return - } - - if board.Type == model.BoardTypePrivate { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } else { - if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - } - - auditRec := a.makeAuditRecord(r, "getBoardMetadata", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - - data, err := json.Marshal(boardMetadata) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} diff --git a/server/boards/api/boards_and_blocks.go b/server/boards/api/boards_and_blocks.go deleted file mode 100644 index 946840ffa1..0000000000 --- a/server/boards/api/boards_and_blocks.go +++ /dev/null @@ -1,414 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerBoardsAndBlocksRoutes(r *mux.Router) { - // BoardsAndBlocks APIs - r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleCreateBoardsAndBlocks)).Methods("POST") - r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handlePatchBoardsAndBlocks)).Methods("PATCH") - r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleDeleteBoardsAndBlocks)).Methods("DELETE") -} - -func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards-and-blocks insertBoardsAndBlocks - // - // Creates new boards and blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: Body - // in: body - // description: the boards and blocks to create - // required: true - // schema: - // "$ref": "#/definitions/BoardsAndBlocks" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardsAndBlocks' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var newBab *model.BoardsAndBlocks - if err = json.Unmarshal(requestBody, &newBab); err != nil { - a.errorResponse(w, r, err) - return - } - - if len(newBab.Boards) == 0 { - a.errorResponse(w, r, model.NewErrBadRequest("at least one board is required")) - return - } - - teamID := "" - boardIDs := map[string]bool{} - for _, board := range newBab.Boards { - boardIDs[board.ID] = true - - if teamID == "" { - teamID = board.TeamID - continue - } - - if teamID != board.TeamID { - a.errorResponse(w, r, model.NewErrBadRequest("cannot create boards for multiple teams")) - return - } - - if board.ID == "" { - a.errorResponse(w, r, model.NewErrBadRequest("boards need an ID to be referenced from the blocks")) - return - } - } - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board template")) - return - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - 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, model.NewErrBadRequest(message)) - return - } - - if block.CreateAt < 1 { - message := fmt.Sprintf("invalid createAt for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if block.UpdateAt < 1 { - message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if !boardIDs[block.BoardID] { - message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - } - - // IDs of boards and blocks are used to confirm that they're - // linked and then regenerated by the server - newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("teamID", teamID) - auditRec.AddMeta("userID", userID) - auditRec.AddMeta("boardsCount", len(newBab.Boards)) - auditRec.AddMeta("blocksCount", len(newBab.Blocks)) - - // create boards and blocks - bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("CreateBoardsAndBlocks", - mlog.String("teamID", teamID), - mlog.String("userID", userID), - mlog.Int("boardCount", len(bab.Boards)), - mlog.Int("blockCount", len(bab.Blocks)), - ) - - data, err := json.Marshal(bab) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /boards-and-blocks patchBoardsAndBlocks - // - // Patches a set of related boards and blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: Body - // in: body - // description: the patches for the boards and blocks - // required: true - // schema: - // "$ref": "#/definitions/PatchBoardsAndBlocks" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardsAndBlocks' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var pbab *model.PatchBoardsAndBlocks - if err = json.Unmarshal(requestBody, &pbab); err != nil { - a.errorResponse(w, r, err) - return - } - - if err = pbab.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - teamID := "" - boardIDMap := map[string]bool{} - for i, boardID := range pbab.BoardIDs { - boardIDMap[boardID] = true - patch := pbab.BoardPatches[i] - - if err = patch.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties")) - return - } - - if patch.Type != nil || patch.MinimumRole != nil { - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type")) - return - } - } - - board, err2 := a.app.GetBoard(boardID) - if err2 != nil { - a.errorResponse(w, r, err2) - return - } - - if teamID == "" { - teamID = board.TeamID - } - if teamID != board.TeamID { - a.errorResponse(w, r, model.NewErrBadRequest("mismatched team ID")) - return - } - } - - for _, blockID := range pbab.BlockIDs { - block, err2 := a.app.GetBlockByID(blockID) - if err2 != nil { - a.errorResponse(w, r, err2) - return - } - - if _, ok := boardIDMap[block.BoardID]; !ok { - a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID)) - return - } - - if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards")) - return - } - } - - auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardsCount", len(pbab.BoardIDs)) - auditRec.AddMeta("blocksCount", len(pbab.BlockIDs)) - - bab, err := a.app.PatchBoardsAndBlocks(pbab, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PATCH BoardsAndBlocks", - mlog.Int("boardsCount", len(pbab.BoardIDs)), - mlog.Int("blocksCount", len(pbab.BlockIDs)), - ) - - data, err := json.Marshal(bab) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /boards-and-blocks deleteBoardsAndBlocks - // - // Deletes boards and blocks - // - // --- - // produces: - // - application/json - // parameters: - // - name: Body - // in: body - // description: the boards and blocks to delete - // required: true - // schema: - // "$ref": "#/definitions/DeleteBoardsAndBlocks" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var dbab *model.DeleteBoardsAndBlocks - if err = json.Unmarshal(requestBody, &dbab); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - // user must have permission to delete all the boards, and that - // would include the permission to manage their blocks - teamID := "" - boardIDMap := map[string]bool{} - for _, boardID := range dbab.Boards { - boardIDMap[boardID] = true - // all boards in the request should belong to the same team - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if teamID == "" { - teamID = board.TeamID - } - if teamID != board.TeamID { - a.errorResponse(w, r, model.NewErrBadRequest("all boards should belong to the same team")) - return - } - - // permission check - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to delete board")) - return - } - } - - for _, blockID := range dbab.Blocks { - block, err2 := a.app.GetBlockByID(blockID) - if err2 != nil { - a.errorResponse(w, r, err2) - return - } - - if _, ok := boardIDMap[block.BoardID]; !ok { - a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID)) - return - } - - if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards")) - return - } - } - - if err := dbab.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - auditRec := a.makeAuditRecord(r, "deleteBoardsAndBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardsCount", len(dbab.Boards)) - auditRec.AddMeta("blocksCount", len(dbab.Blocks)) - - if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("DELETE BoardsAndBlocks", - mlog.Int("boardsCount", len(dbab.Boards)), - mlog.Int("blocksCount", len(dbab.Blocks)), - ) - - // response - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} diff --git a/server/boards/api/cards.go b/server/boards/api/cards.go deleted file mode 100644 index b847d30370..0000000000 --- a/server/boards/api/cards.go +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - defaultPage = "0" - defaultPerPage = "100" -) - -func (a *API) registerCardsRoutes(r *mux.Router) { - // Cards APIs - r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleCreateCard)).Methods("POST") - r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleGetCards)).Methods("GET") - r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handlePatchCard)).Methods("PATCH") - r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handleGetCard)).Methods("GET") -} - -func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/cards createCard - // - // Creates a new card for the specified board. - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: Body - // in: body - // description: the card to create - // required: true - // schema: - // "$ref": "#/definitions/Card" - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk data inserting) - // required: false - // type: bool - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/Card' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - boardID := mux.Vars(r)["boardID"] - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var newCard *model.Card - if err = json.Unmarshal(requestBody, &newCard); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to create card")) - return - } - - if newCard.BoardID != "" && newCard.BoardID != boardID { - a.errorResponse(w, r, model.ErrBoardIDMismatch) - return - } - - newCard.PopulateWithBoardID(boardID) - if err = newCard.CheckValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - auditRec := a.makeAuditRecord(r, "createCard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - - // create card - card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("CreateCard", - mlog.String("boardID", boardID), - mlog.String("cardID", card.ID), - mlog.String("userID", userID), - ) - - data, err := json.Marshal(card) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/cards getCards - // - // Fetches cards for the specified board. - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: page - // in: query - // description: The page to select (default=0) - // required: false - // type: integer - // - name: per_page - // in: query - // description: Number of cards to return per page(default=100) - // required: false - // type: integer - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Card" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - userID := getUserID(r) - boardID := mux.Vars(r)["boardID"] - - query := r.URL.Query() - strPage := query.Get("page") - strPerPage := query.Get("per_page") - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to fetch cards")) - return - } - - if strPage == "" { - strPage = defaultPage - } - if strPerPage == "" { - strPerPage = defaultPerPage - } - - page, err := strconv.Atoi(strPage) - if err != nil { - message := fmt.Sprintf("invalid `page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - } - - perPage, err := strconv.Atoi(strPerPage) - if err != nil { - message := fmt.Sprintf("invalid `per_page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - } - - auditRec := a.makeAuditRecord(r, "getCards", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("page", page) - auditRec.AddMeta("per_page", perPage) - - cards, err := a.app.GetCardsForBoard(boardID, page, perPage) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetCards", - mlog.String("boardID", boardID), - mlog.String("userID", userID), - mlog.Int("page", page), - mlog.Int("per_page", perPage), - mlog.Int("count", len(cards)), - ) - - data, err := json.Marshal(cards) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /cards/{cardID}/cards patchCard - // - // Patches the specified card. - // - // --- - // produces: - // - application/json - // parameters: - // - name: cardID - // in: path - // description: Card ID - // required: true - // type: string - // - name: Body - // in: body - // description: the card patch - // required: true - // schema: - // "$ref": "#/definitions/CardPatch" - // - name: disable_notify - // in: query - // description: Disables notifications (for bulk data patching) - // required: false - // type: bool - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/Card' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - cardID := mux.Vars(r)["cardID"] - - val := r.URL.Query().Get("disable_notify") - disableNotify := val == True - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - card, err := a.app.GetCardByID(cardID) - if err != nil { - message := fmt.Sprintf("could not fetch card %s: %s", cardID, err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to patch card")) - return - } - - var patch *model.CardPatch - if err = json.Unmarshal(requestBody, &patch); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - auditRec := a.makeAuditRecord(r, "patchCard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", card.BoardID) - auditRec.AddMeta("cardID", card.ID) - - // patch card - cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PatchCard", - mlog.String("boardID", cardPatched.BoardID), - mlog.String("cardID", cardPatched.ID), - mlog.String("userID", userID), - ) - - data, err := json.Marshal(cardPatched) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /cards/{cardID} getCard - // - // Fetches the specified card. - // - // --- - // produces: - // - application/json - // parameters: - // - name: cardID - // in: path - // description: Card ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/Card' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - cardID := mux.Vars(r)["cardID"] - - card, err := a.app.GetCardByID(cardID) - if err != nil { - message := fmt.Sprintf("could not fetch card %s: %s", cardID, err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to fetch card")) - return - } - - auditRec := a.makeAuditRecord(r, "getCard", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", card.BoardID) - auditRec.AddMeta("cardID", card.ID) - - a.logger.Debug("GetCard", - mlog.String("boardID", card.BoardID), - mlog.String("cardID", card.ID), - mlog.String("userID", userID), - ) - - data, err := json.Marshal(card) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} diff --git a/server/boards/api/categories.go b/server/boards/api/categories.go deleted file mode 100644 index c6d86f9bb7..0000000000 --- a/server/boards/api/categories.go +++ /dev/null @@ -1,676 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" -) - -func (a *API) registerCategoriesRoutes(r *mux.Router) { - // Category APIs - r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleCreateCategory)).Methods(http.MethodPost) - r.HandleFunc("/teams/{teamID}/categories/reorder", a.sessionRequired(a.handleReorderCategories)).Methods(http.MethodPut) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleUpdateCategory)).Methods(http.MethodPut) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleDeleteCategory)).Methods(http.MethodDelete) - r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBoards)).Methods(http.MethodGet) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/reorder", a.sessionRequired(a.handleReorderCategoryBoards)).Methods(http.MethodPut) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}", a.sessionRequired(a.handleUpdateCategoryBoard)).Methods(http.MethodPost) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide", a.sessionRequired(a.handleHideBoard)).Methods(http.MethodPut) - r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/unhide", a.sessionRequired(a.handleUnhideBoard)).Methods(http.MethodPut) -} - -func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/categories createCategory - // - // Create a category for boards - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: Body - // in: body - // description: category to create - // required: true - // schema: - // "$ref": "#/definitions/Category" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Category" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var category model.Category - - err = json.Unmarshal(requestBody, &category) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "createCategory", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - // user can only create category for themselves - if category.UserID != session.UserID { - message := fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - vars := mux.Vars(r) - teamID := vars["teamID"] - - if category.TeamID != teamID { - a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch")) - return - } - - if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - createdCategory, err := a.app.CreateCategory(&category) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(createdCategory) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.AddMeta("categoryID", createdCategory.ID) - auditRec.Success() -} - -func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) { - // swagger:operation PUT /teams/{teamID}/categories/{categoryID} updateCategory - // - // Create a category for boards - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID - // required: true - // type: string - // - name: Body - // in: body - // description: category to update - // required: true - // schema: - // "$ref": "#/definitions/Category" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Category" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - categoryID := vars["categoryID"] - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var category model.Category - err = json.Unmarshal(requestBody, &category) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "updateCategory", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - if categoryID != category.ID { - a.errorResponse(w, r, model.NewErrBadRequest("categoryID mismatch in patch and body")) - return - } - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - // user can only update category for themselves - if category.UserID != session.UserID { - a.errorResponse(w, r, model.NewErrBadRequest("user ID mismatch in session and category")) - return - } - - teamID := vars["teamID"] - if category.TeamID != teamID { - a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch")) - return - } - - if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - updatedCategory, err := a.app.UpdateCategory(&category) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(updatedCategory) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /teams/{teamID}/categories/{categoryID} deleteCategory - // - // Delete a category - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - vars := mux.Vars(r) - - userID := session.UserID - teamID := vars["teamID"] - categoryID := vars["categoryID"] - - auditRec := a.makeAuditRecord(r, "deleteCategory", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(deletedCategory) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleGetUserCategoryBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/categories getUserCategoryBoards - // - // Gets the user's board categories - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // items: - // "$ref": "#/definitions/CategoryBoards" - // type: array - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - vars := mux.Vars(r) - teamID := vars["teamID"] - - auditRec := a.makeAuditRecord(r, "getUserCategoryBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(categoryBlocks) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID} updateCategoryBoard - // - // Set the category of a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID - // required: true - // type: string - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - auditRec := a.makeAuditRecord(r, "updateCategoryBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - vars := mux.Vars(r) - categoryID := vars["categoryID"] - boardID := vars["boardID"] - teamID := vars["teamID"] - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - // TODO: Check the category and the team matches - err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, []string{boardID}) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, []byte("ok")) - auditRec.Success() -} - -func (a *API) handleReorderCategories(w http.ResponseWriter, r *http.Request) { - // swagger:operation PUT /teams/{teamID}/categories/reorder handleReorderCategories - // - // Updated sidebar category order - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - teamID := vars["teamID"] - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to category")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var newCategoryOrder []string - - err = json.Unmarshal(requestBody, &newCategoryOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "reorderCategories", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - auditRec.AddMeta("TeamID", teamID) - auditRec.AddMeta("CategoryCount", len(newCategoryOrder)) - - updatedCategoryOrder, err := a.app.ReorderCategories(userID, teamID, newCategoryOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(updatedCategoryOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleReorderCategoryBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation PUT /teams/{teamID}/categories/{categoryID}/boards/reorder handleReorderCategoryBoards - // - // Updates order of boards inside a sidebar category - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - teamID := vars["teamID"] - categoryID := vars["categoryID"] - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - userID := session.UserID - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to category")) - return - } - - category, err := a.app.GetCategory(categoryID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if category.UserID != userID { - a.errorResponse(w, r, model.NewErrPermission("access denied to category")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var newBoardsOrder []string - err = json.Unmarshal(requestBody, &newBoardsOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "reorderCategoryBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - updatedBoardsOrder, err := a.app.ReorderCategoryBoards(userID, teamID, categoryID, newBoardsOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(updatedBoardsOrder) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleHideBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide hideBoard - // - // Hide the specified board for the user - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID to which the board to be hidden belongs to - // required: true - // type: string - // - name: boardID - // in: path - // description: ID of board to be hidden - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Category" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - vars := mux.Vars(r) - teamID := vars["teamID"] - boardID := vars["boardID"] - categoryID := vars["categoryID"] - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to category")) - return - } - - auditRec := a.makeAuditRecord(r, "hideBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("board_id", boardID) - auditRec.AddMeta("team_id", teamID) - auditRec.AddMeta("category_id", categoryID) - - if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, false); err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) handleUnhideBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide unhideBoard - // - // Unhides the specified board for the user - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: categoryID - // in: path - // description: Category ID to which the board to be unhidden belongs to - // required: true - // type: string - // - name: boardID - // in: path - // description: ID of board to be unhidden - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Category" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - vars := mux.Vars(r) - teamID := vars["teamID"] - boardID := vars["boardID"] - categoryID := vars["categoryID"] - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to category")) - return - } - - auditRec := a.makeAuditRecord(r, "unhideBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - - if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, true); err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} diff --git a/server/boards/api/channels.go b/server/boards/api/channels.go deleted file mode 100644 index 5190a2c210..0000000000 --- a/server/boards/api/channels.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerChannelsRoutes(r *mux.Router) { - r.HandleFunc("/teams/{teamID}/channels/{channelID}", a.sessionRequired(a.handleGetChannel)).Methods("GET") -} - -func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/channels/{channelID} getChannel - // - // Returns the requested channel - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: channelID - // in: path - // description: Channel ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Channel" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - teamID := mux.Vars(r)["teamID"] - channelID := mux.Vars(r)["channelID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) { - a.errorResponse(w, r, model.NewErrPermission("access denied to channel")) - return - } - - auditRec := a.makeAuditRecord(r, "getChannel", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - auditRec.AddMeta("channelID", teamID) - - channel, err := a.app.GetChannel(teamID, channelID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetChannel", - mlog.String("teamID", teamID), - mlog.String("channelID", channelID), - ) - - if channel.TeamId != teamID { - if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup { - message := fmt.Sprintf("channel ID=%s on TeamID=%s", channel.Id, teamID) - a.errorResponse(w, r, model.NewErrNotFound(message)) - return - } - } - - data, err := json.Marshal(channel) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} diff --git a/server/boards/api/compliance.go b/server/boards/api/compliance.go deleted file mode 100644 index 913dbe5b01..0000000000 --- a/server/boards/api/compliance.go +++ /dev/null @@ -1,451 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - complianceDefaultPage = "0" - complianceDefaultPerPage = "60" -) - -func (a *API) registerComplianceRoutes(r *mux.Router) { - // Compliance APIs - r.HandleFunc("/admin/boards", a.sessionRequired(a.handleGetBoardsForCompliance)).Methods("GET") - r.HandleFunc("/admin/boards_history", a.sessionRequired(a.handleGetBoardsComplianceHistory)).Methods("GET") - r.HandleFunc("/admin/blocks_history", a.sessionRequired(a.handleGetBlocksComplianceHistory)).Methods("GET") -} - -func (a *API) handleGetBoardsForCompliance(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /admin/boards getBoardsForCompliance - // - // Returns boards for a specific team, or all teams. - // - // Requires a license that includes Compliance feature. Caller must have `manage_system` permissions. - // - // --- - // produces: - // - application/json - // parameters: - // - name: team_id - // in: query - // description: Team ID. If empty then boards across all teams are included. - // required: false - // type: string - // - name: page - // in: query - // description: The page to select (default=0) - // required: false - // type: integer - // - name: per_page - // in: query - // description: Number of boards to return per page(default=60) - // required: false - // type: integer - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: object - // items: - // "$ref": "#/definitions/BoardsComplianceResponse" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - query := r.URL.Query() - teamID := query.Get("team_id") - strPage := query.Get("page") - strPerPage := query.Get("per_page") - - // check for permission `manage_system` - userID := getUserID(r) - if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getAllBoards")) - return - } - - // check for valid license feature: compliance - license := a.app.GetLicense() - if license == nil || !(*license.Features.Compliance) { - a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getAllBoards")) - return - } - - // check for valid team if specified - if teamID != "" { - _, err := a.app.GetTeam(teamID) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID)) - return - } - } - - if strPage == "" { - strPage = complianceDefaultPage - } - if strPerPage == "" { - strPerPage = complianceDefaultPerPage - } - page, err := strconv.Atoi(strPage) - if err != nil { - message := fmt.Sprintf("invalid `page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - perPage, err := strconv.Atoi(strPerPage) - if err != nil { - message := fmt.Sprintf("invalid `per_page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - opts := model.QueryBoardsForComplianceOptions{ - TeamID: teamID, - Page: page, - PerPage: perPage, - } - - boards, more, err := a.app.GetBoardsForCompliance(opts) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetBoardsForCompliance", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(boards)), - mlog.Bool("hasNext", more), - ) - - response := model.BoardsComplianceResponse{ - HasNext: more, - Results: boards, - } - data, err := json.Marshal(response) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} - -func (a *API) handleGetBoardsComplianceHistory(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /admin/boards_history getBoardsComplianceHistory - // - // Returns boards histories for a specific team, or all teams. - // - // Requires a license that includes Compliance feature. Caller must have `manage_system` permissions. - // - // --- - // produces: - // - application/json - // parameters: - // - name: modified_since - // in: query - // description: Filters for boards modified since timestamp; Unix time in milliseconds - // required: true - // type: integer - // - name: include_deleted - // in: query - // description: When true then deleted boards are included. Default=false - // required: false - // type: boolean - // - name: team_id - // in: query - // description: Team ID. If empty then board histories across all teams are included - // required: false - // type: string - // - name: page - // in: query - // description: The page to select (default=0) - // required: false - // type: integer - // - name: per_page - // in: query - // description: Number of board histories to return per page (default=60) - // required: false - // type: integer - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: object - // items: - // "$ref": "#/definitions/BoardsComplianceHistoryResponse" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - query := r.URL.Query() - strModifiedSince := query.Get("modified_since") // required, everything else optional - includeDeleted := query.Get("include_deleted") == "true" - strPage := query.Get("page") - strPerPage := query.Get("per_page") - teamID := query.Get("team_id") - - if strModifiedSince == "" { - a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required")) - return - } - - // check for permission `manage_system` - userID := getUserID(r) - if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBoardsHistory")) - return - } - - // check for valid license feature: compliance - license := a.app.GetLicense() - if license == nil || !(*license.Features.Compliance) { - a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBoardsHistory")) - return - } - - // check for valid team if specified - if teamID != "" { - _, err := a.app.GetTeam(teamID) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID)) - return - } - } - - if strPage == "" { - strPage = complianceDefaultPage - } - if strPerPage == "" { - strPerPage = complianceDefaultPerPage - } - page, err := strconv.Atoi(strPage) - if err != nil { - message := fmt.Sprintf("invalid `page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - perPage, err := strconv.Atoi(strPerPage) - if err != nil { - message := fmt.Sprintf("invalid `per_page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64) - if err != nil { - message := fmt.Sprintf("invalid `modified_since` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - opts := model.QueryBoardsComplianceHistoryOptions{ - ModifiedSince: modifiedSince, - IncludeDeleted: includeDeleted, - TeamID: teamID, - Page: page, - PerPage: perPage, - } - - boards, more, err := a.app.GetBoardsComplianceHistory(opts) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetBoardsComplianceHistory", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(boards)), - mlog.Bool("hasNext", more), - ) - - response := model.BoardsComplianceHistoryResponse{ - HasNext: more, - Results: boards, - } - data, err := json.Marshal(response) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} - -func (a *API) handleGetBlocksComplianceHistory(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /admin/blocks_history getBlocksComplianceHistory - // - // Returns block histories for a specific team, specific board, or all teams and boards. - // - // Requires a license that includes Compliance feature. Caller must have `manage_system` permissions. - // - // --- - // produces: - // - application/json - // parameters: - // - name: modified_since - // in: query - // description: Filters for boards modified since timestamp; Unix time in milliseconds - // required: true - // type: integer - // - name: include_deleted - // in: query - // description: When true then deleted boards are included. Default=false - // required: false - // type: boolean - // - name: team_id - // in: query - // description: Team ID. If empty then block histories across all teams are included - // required: false - // type: string - // - name: board_id - // in: query - // description: Board ID. If empty then block histories for all boards are included - // required: false - // type: string - // - name: page - // in: query - // description: The page to select (default=0) - // required: false - // type: integer - // - name: per_page - // in: query - // description: Number of block histories to return per page (default=60) - // required: false - // type: integer - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: object - // items: - // "$ref": "#/definitions/BlocksComplianceHistoryResponse" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - query := r.URL.Query() - strModifiedSince := query.Get("modified_since") // required, everything else optional - includeDeleted := query.Get("include_deleted") == "true" - strPage := query.Get("page") - strPerPage := query.Get("per_page") - teamID := query.Get("team_id") - boardID := query.Get("board_id") - - if strModifiedSince == "" { - a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required")) - return - } - - // check for permission `manage_system` - userID := getUserID(r) - if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBlocksHistory")) - return - } - - // check for valid license feature: compliance - license := a.app.GetLicense() - if license == nil || !(*license.Features.Compliance) { - a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBlocksHistory")) - return - } - - // check for valid team if specified - if teamID != "" { - _, err := a.app.GetTeam(teamID) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID)) - return - } - } - - // check for valid board if specified - if boardID != "" { - _, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest("invalid board id: "+boardID)) - return - } - } - - if strPage == "" { - strPage = complianceDefaultPage - } - if strPerPage == "" { - strPerPage = complianceDefaultPerPage - } - page, err := strconv.Atoi(strPage) - if err != nil { - message := fmt.Sprintf("invalid `page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - perPage, err := strconv.Atoi(strPerPage) - if err != nil { - message := fmt.Sprintf("invalid `per_page` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64) - if err != nil { - message := fmt.Sprintf("invalid `modified_since` parameter: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - opts := model.QueryBlocksComplianceHistoryOptions{ - ModifiedSince: modifiedSince, - IncludeDeleted: includeDeleted, - TeamID: teamID, - BoardID: boardID, - Page: page, - PerPage: perPage, - } - - blocks, more, err := a.app.GetBlocksComplianceHistory(opts) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetBlocksComplianceHistory", - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - mlog.Int("blocksCount", len(blocks)), - mlog.Bool("hasNext", more), - ) - - response := model.BlocksComplianceHistoryResponse{ - HasNext: more, - Results: blocks, - } - data, err := json.Marshal(response) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} diff --git a/server/boards/api/config.go b/server/boards/api/config.go deleted file mode 100644 index 0a4fe10c9b..0000000000 --- a/server/boards/api/config.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" -) - -func (a *API) registerConfigRoutes(r *mux.Router) { - // Config APIs - r.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET") -} - -func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /clientConfig getClientConfig - // - // Returns the client configuration - // - // --- - // produces: - // - application/json - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/ClientConfig" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - clientConfig := a.app.GetClientConfig() - - configData, err := json.Marshal(clientConfig) - if err != nil { - a.errorResponse(w, r, err) - return - } - jsonBytesResponse(w, http.StatusOK, configData) -} diff --git a/server/boards/api/content_blocks.go b/server/boards/api/content_blocks.go deleted file mode 100644 index 3f72c60b86..0000000000 --- a/server/boards/api/content_blocks.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" -) - -func (a *API) registerContentBlocksRoutes(r *mux.Router) { - // Blocks APIs - r.HandleFunc("/content-blocks/{blockID}/moveto/{where}/{dstBlockID}", a.sessionRequired(a.handleMoveBlockTo)).Methods("POST") -} - -func (a *API) handleMoveBlockTo(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /content-blocks/{blockID}/move/{where}/{dstBlockID} moveBlockTo - // - // Move a block after another block in the parent card - // - // --- - // produces: - // - application/json - // parameters: - // - name: blockID - // in: path - // description: Block ID - // required: true - // type: string - // - name: where - // in: path - // description: Relative location respect destination block (after or before) - // required: true - // type: string - // - name: dstBlockID - // in: path - // description: Destination Block ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Block" - // '404': - // description: board or block not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - blockID := mux.Vars(r)["blockID"] - dstBlockID := mux.Vars(r)["dstBlockID"] - where := mux.Vars(r)["where"] - userID := getUserID(r) - - block, err := a.app.GetBlockByID(blockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - dstBlock, err := a.app.GetBlockByID(dstBlockID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if where != "after" && where != "before" { - a.errorResponse(w, r, model.NewErrBadRequest("invalid where parameter, use before or after")) - return - } - - if userID == "" { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards")) - return - } - - auditRec := a.makeAuditRecord(r, "moveBlockTo", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("blockID", blockID) - auditRec.AddMeta("dstBlockID", dstBlockID) - - err = a.app.MoveContentBlock(block, dstBlock, where, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} diff --git a/server/boards/api/context.go b/server/boards/api/context.go deleted file mode 100644 index cef6cd55b0..0000000000 --- a/server/boards/api/context.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "net" - "net/http" -) - -type contextKey int - -const ( - httpConnContextKey contextKey = iota - sessionContextKey -) - -// SetContextConn stores the connection in the request context. -func SetContextConn(ctx context.Context, c net.Conn) context.Context { - return context.WithValue(ctx, httpConnContextKey, c) -} - -// GetContextConn gets the stored connection from the request context. -func GetContextConn(r *http.Request) net.Conn { - value := r.Context().Value(httpConnContextKey) - if value == nil { - return nil - } - - return value.(net.Conn) -} diff --git a/server/boards/api/files.go b/server/boards/api/files.go deleted file mode 100644 index af23d8c97c..0000000000 --- a/server/boards/api/files.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "strings" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/app" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/platform/shared/web" -) - -// FileUploadResponse is the response to a file upload -// swagger:model -type FileUploadResponse struct { - // The FileID to retrieve the uploaded file - // required: true - FileID string `json:"fileId"` -} - -func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) { - var fileUploadResponse FileUploadResponse - - if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil { - return nil, err - } - return &fileUploadResponse, nil -} - -func FileInfoResponseFromJSON(data io.Reader) (*mm_model.FileInfo, error) { - var fileInfo mm_model.FileInfo - - if err := json.NewDecoder(data).Decode(&fileInfo); err != nil { - return nil, err - } - return &fileInfo, nil -} - -func (a *API) registerFilesRoutes(r *mux.Router) { - // Files API - r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET") - r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}/info", a.attachSession(a.getFileInfo, false)).Methods("GET") - r.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") -} - -func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile - // - // Returns the contents of an uploaded file - // - // --- - // produces: - // - application/json - // - image/jpg - // - image/png - // - image/gif - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: filename - // in: path - // description: name of the file - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '404': - // description: file not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - boardID := vars["boardID"] - filename := vars["filename"] - userID := getUserID(r) - - hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) - if userID == "" && !hasValidReadToken { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "getFile", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("teamID", board.TeamID) - auditRec.AddMeta("filename", filename) - - fileInfo, fileReader, err := a.app.GetFile(board.TeamID, boardID, filename) - if err != nil && !model.IsErrNotFound(err) { - a.errorResponse(w, r, err) - return - } - - if errors.Is(err, app.ErrFileNotFound) && board.ChannelID != "" { - // prior to moving from workspaces to teams, the filepath was constructed from - // workspaceID, which is the channel ID in plugin mode. - // If a file is not found from team ID as we tried above, try looking for it via - // channel ID. - fileReader, err = a.app.GetFileReader(board.ChannelID, boardID, filename) - if err != nil { - a.errorResponse(w, r, err) - return - } - // move file to team location - // nothing to do if there is an error - _ = a.app.MoveFile(board.ChannelID, board.TeamID, boardID, filename) - } - - if err != nil { - // if err is still not nil then it is an error other than `not found` so we must - // return the error to the requestor. fileReader and Fileinfo are nil in this case. - a.errorResponse(w, r, err) - } - - defer fileReader.Close() - - mimeType := "" - var fileSize int64 - if fileInfo != nil { - mimeType = fileInfo.MimeType - fileSize = fileInfo.Size - } - web.WriteFileResponse(filename, mimeType, fileSize, time.Now(), "", fileReader, false, w, r) - auditRec.Success() -} - -func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile - // - // Returns the metadata of an uploaded file - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: filename - // in: path - // description: name of the file - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '404': - // description: file not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - boardID := vars["boardID"] - teamID := vars["teamID"] - filename := vars["filename"] - userID := getUserID(r) - - hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID) - if userID == "" && !hasValidReadToken { - a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board")) - return - } - - if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - - auditRec := a.makeAuditRecord(r, "getFile", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("teamID", teamID) - auditRec.AddMeta("filename", filename) - - fileInfo, err := a.app.GetFileInfo(filename) - if err != nil && !model.IsErrNotFound(err) { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(fileInfo) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} - -func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/boards/{boardID}/files uploadFile - // - // Upload a binary file, attached to a root block - // - // --- - // consumes: - // - multipart/form-data - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: ID of the team - // required: true - // type: string - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: uploaded file - // in: formData - // type: file - // description: The file to upload - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/FileUploadResponse" - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - boardID := vars["boardID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { - a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if a.app.GetConfig().MaxFileSize > 0 { - r.Body = http.MaxBytesReader(w, r.Body, a.app.GetConfig().MaxFileSize) - } - - file, handle, err := r.FormFile(UploadFormFileKey) - if err != nil { - if strings.HasSuffix(err.Error(), "http: request body too large") { - a.errorResponse(w, r, model.ErrRequestEntityTooLarge) - return - } - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - defer file.Close() - - auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("teamID", board.TeamID) - auditRec.AddMeta("filename", handle.Filename) - - fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename, board.IsTemplate) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("uploadFile", - mlog.String("filename", handle.Filename), - mlog.String("fileID", fileID), - ) - data, err := json.Marshal(FileUploadResponse{FileID: fileID}) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("fileID", fileID) - auditRec.Success() -} diff --git a/server/boards/api/insights.go b/server/boards/api/insights.go deleted file mode 100644 index 08721ada3d..0000000000 --- a/server/boards/api/insights.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -func (a *API) registerInsightsRoutes(r *mux.Router) { - // Insights APIs - r.HandleFunc("/teams/{teamID}/boards/insights", a.sessionRequired(a.handleTeamBoardsInsights)).Methods("GET") - r.HandleFunc("/users/me/boards/insights", a.sessionRequired(a.handleUserBoardsInsights)).Methods("GET") -} - -func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/boards/insights handleTeamBoardsInsights - // - // Returns team boards insights - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: time_range - // in: query - // description: duration of data to calculate insights for - // required: true - // type: string - // - name: page - // in: query - // description: page offset for top boards - // required: true - // type: string - // - name: per_page - // in: query - // description: limit for boards in a page. - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/BoardInsight" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - vars := mux.Vars(r) - teamID := vars["teamID"] - userID := getUserID(r) - query := r.URL.Query() - timeRange := query.Get("time_range") - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - auditRec := a.makeAuditRecord(r, "getTeamBoardsInsights", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - page, err := strconv.Atoi(query.Get("page")) - if err != nil { - message := fmt.Sprintf("error converting page parameter to integer: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - if page < 0 { - a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter")) - } - - perPage, err := strconv.Atoi(query.Get("per_page")) - if err != nil { - message := fmt.Sprintf("error converting per_page parameter to integer: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - if perPage < 0 { - a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter")) - } - - userTimezone, aErr := a.app.GetUserTimezone(userID) - if aErr != nil { - message := fmt.Sprintf("Error getting time zone of user: %s", aErr) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - userLocation, _ := time.LoadLocation(userTimezone) - if userLocation == nil { - userLocation = time.Now().UTC().Location() - } - // get unix time for duration - startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation) - if appErr != nil { - a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message)) - return - } - - boardsInsights, err := a.app.GetTeamBoardsInsights(userID, teamID, &mm_model.InsightsOpts{ - StartUnixMilli: mm_model.GetMillisForTime(*startTime), - Page: page, - PerPage: perPage, - }) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(boardsInsights) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("teamBoardsInsightCount", len(boardsInsights.Items)) - auditRec.Success() -} - -func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /users/me/boards/insights getUserBoardsInsights - // - // Returns user boards insights - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: time_range - // in: query - // description: duration of data to calculate insights for - // required: true - // type: string - // - name: page - // in: query - // description: page offset for top boards - // required: true - // type: string - // - name: per_page - // in: query - // description: limit for boards in a page. - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/BoardInsight" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - userID := getUserID(r) - query := r.URL.Query() - teamID := query.Get("team_id") - timeRange := query.Get("time_range") - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - auditRec := a.makeAuditRecord(r, "getUserBoardsInsights", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - page, err := strconv.Atoi(query.Get("page")) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest("error converting page parameter to integer")) - return - } - - if page < 0 { - a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter")) - } - perPage, err := strconv.Atoi(query.Get("per_page")) - if err != nil { - message := fmt.Sprintf("error converting per_page parameter to integer: %s", err) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - if perPage < 0 { - a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter")) - } - userTimezone, aErr := a.app.GetUserTimezone(userID) - if aErr != nil { - message := fmt.Sprintf("Error getting time zone of user: %s", aErr) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - userLocation, _ := time.LoadLocation(userTimezone) - if userLocation == nil { - userLocation = time.Now().UTC().Location() - } - // get unix time for duration - startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation) - if appErr != nil { - a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message)) - return - } - - boardsInsights, err := a.app.GetUserBoardsInsights(userID, teamID, &mm_model.InsightsOpts{ - StartUnixMilli: mm_model.GetMillisForTime(*startTime), - Page: page, - PerPage: perPage, - }) - if err != nil { - a.errorResponse(w, r, err) - return - } - data, err := json.Marshal(boardsInsights) - if err != nil { - a.errorResponse(w, r, err) - return - } - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("userBoardInsightCount", len(boardsInsights.Items)) - auditRec.Success() -} diff --git a/server/boards/api/limits.go b/server/boards/api/limits.go deleted file mode 100644 index b25eb384fb..0000000000 --- a/server/boards/api/limits.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *API) registerLimitsRoutes(r *mux.Router) { - // limits - r.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET") - r.HandleFunc("/teams/{teamID}/notifyadminupgrade", a.sessionRequired(a.handleNotifyAdminUpgrade)).Methods(http.MethodPost) -} - -func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /limits cloudLimits - // - // Fetches the cloud limits of the server. - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/BoardsCloudLimits" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardsCloudLimits, err := a.app.GetBoardsCloudLimits() - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(boardsCloudLimits) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} - -func (a *API) handleNotifyAdminUpgrade(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /api/v2/teams/{teamID}/notifyadminupgrade handleNotifyAdminUpgrade - // - // Notifies admins for upgrade request. - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - vars := mux.Vars(r) - teamID := vars["teamID"] - - if err := a.app.NotifyPortalAdminsUpgradeRequest(teamID); err != nil { - jsonStringResponse(w, http.StatusOK, "{}") - } -} diff --git a/server/boards/api/members.go b/server/boards/api/members.go deleted file mode 100644 index 2c0be1040b..0000000000 --- a/server/boards/api/members.go +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" -) - -func (a *API) registerMembersRoutes(r *mux.Router) { - // Member APIs - r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET") - r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST") - r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT") - r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE") - r.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST") - r.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST") -} - -func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/members getMembersForBoard - // - // Returns the members of the board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/BoardMember" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board members")) - return - } - - auditRec := a.makeAuditRecord(r, "getMembersForBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - - members, err := a.app.GetMembersForBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetMembersForBoard", - mlog.String("boardID", boardID), - mlog.Int("membersCount", len(members)), - ) - - data, err := json.Marshal(members) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/members addMember - // - // Adds a new member to a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: Body - // in: body - // description: membership to replace the current one with - // required: true - // schema: - // "$ref": "#/definitions/BoardMember" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardMember' - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - userID := getUserID(r) - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) && - !(board.Type == model.BoardTypeOpen && a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties)) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var reqBoardMember *model.BoardMember - if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - if reqBoardMember.UserID == "" { - a.errorResponse(w, r, model.NewErrBadRequest("empty userID")) - return - } - - if !a.permissions.HasPermissionToTeam(reqBoardMember.UserID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - newBoardMember := &model.BoardMember{ - UserID: reqBoardMember.UserID, - BoardID: boardID, - SchemeEditor: reqBoardMember.SchemeEditor, - SchemeAdmin: reqBoardMember.SchemeAdmin, - SchemeViewer: reqBoardMember.SchemeViewer, - SchemeCommenter: reqBoardMember.SchemeCommenter, - } - - auditRec := a.makeAuditRecord(r, "addMember", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("addedUserID", reqBoardMember.UserID) - - member, err := a.app.AddMemberToBoard(newBoardMember) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("AddMember", - mlog.String("boardID", board.ID), - mlog.String("addedUserID", reqBoardMember.UserID), - ) - - data, err := json.Marshal(member) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/join joinBoard - // - // Become a member of a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: allow_admin - // in: path - // description: allows admin users to join private boards - // required: false - // type: boolean - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardMember' - // '404': - // description: board not found - // '403': - // description: access denied - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - query := r.URL.Query() - allowAdmin := query.Has("allow_admin") - - userID := getUserID(r) - if userID == "" { - a.errorResponse(w, r, model.NewErrBadRequest("missing user ID")) - return - } - - boardID := mux.Vars(r)["boardID"] - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - isAdmin := false - if board.Type != model.BoardTypeOpen { - if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { - a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board")) - return - } - isAdmin = true - } - - if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("guests not allowed to join boards")) - return - } - - newBoardMember := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin, - SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor, - SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter, - SchemeViewer: board.MinimumRole == model.BoardRoleViewer, - } - - auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("addedUserID", userID) - - member, err := a.app.AddMemberToBoard(newBoardMember) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("JoinBoard", - mlog.String("boardID", board.ID), - mlog.String("addedUserID", userID), - ) - - data, err := json.Marshal(member) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - 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, model.NewErrBadRequest("invalid session")) - return - } - - boardID := mux.Vars(r)["boardID"] - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to board")) - return - } - - board, err := a.app.GetBoard(boardID) - if err != nil { - a.errorResponse(w, r, err) - 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 err != nil { - a.errorResponse(w, r, 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) { - // swagger:operation PUT /boards/{boardID}/members/{userID} updateMember - // - // Updates a board member - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // - name: Body - // in: body - // description: membership to replace the current one with - // required: true - // schema: - // "$ref": "#/definitions/BoardMember" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // $ref: '#/definitions/BoardMember' - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - paramsUserID := mux.Vars(r)["userID"] - userID := getUserID(r) - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var reqBoardMember *model.BoardMember - if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - newBoardMember := &model.BoardMember{ - UserID: paramsUserID, - BoardID: boardID, - SchemeAdmin: reqBoardMember.SchemeAdmin, - SchemeEditor: reqBoardMember.SchemeEditor, - SchemeCommenter: reqBoardMember.SchemeCommenter, - SchemeViewer: reqBoardMember.SchemeViewer, - } - - isGuest, err := a.userIsGuest(paramsUserID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - if isGuest { - newBoardMember.SchemeAdmin = false - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members")) - return - } - - auditRec := a.makeAuditRecord(r, "patchMember", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("patchedUserID", paramsUserID) - - member, err := a.app.UpdateBoardMember(newBoardMember) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("PatchMember", - mlog.String("boardID", boardID), - mlog.String("patchedUserID", paramsUserID), - ) - - data, err := json.Marshal(member) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.Success() -} - -func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /boards/{boardID}/members/{userID} deleteMember - // - // Deletes a member from a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - paramsUserID := mux.Vars(r)["userID"] - userID := getUserID(r) - - if _, err := a.app.GetBoard(boardID); err != nil { - a.errorResponse(w, r, err) - return - } - - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) { - a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members")) - return - } - - auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("boardID", boardID) - auditRec.AddMeta("addedUserID", paramsUserID) - - deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID) - if deleteErr != nil { - a.errorResponse(w, r, deleteErr) - return - } - - a.logger.Debug("DeleteMember", - mlog.String("boardID", boardID), - mlog.String("addedUserID", paramsUserID), - ) - - // response - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} diff --git a/server/boards/api/onboarding.go b/server/boards/api/onboarding.go deleted file mode 100644 index aa2f47917c..0000000000 --- a/server/boards/api/onboarding.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *API) registerOnboardingRoutes(r *mux.Router) { - // Onboarding tour endpoints APIs - r.HandleFunc("/teams/{teamID}/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost) -} - -func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /team/{teamID}/onboard onboard - // - // Onboards a user on Boards. - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: object - // properties: - // teamID: - // type: string - // description: Team ID - // boardID: - // type: string - // description: Board ID - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - teamID := mux.Vars(r)["teamID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to create board")) - return - } - - teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - response := map[string]string{ - "teamID": teamID, - "boardID": boardID, - } - data, err := json.Marshal(response) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} diff --git a/server/boards/api/search.go b/server/boards/api/search.go deleted file mode 100644 index 62d369602a..0000000000 --- a/server/boards/api/search.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerSearchRoutes(r *mux.Router) { - r.HandleFunc("/teams/{teamID}/channels", a.sessionRequired(a.handleSearchMyChannels)).Methods("GET") - r.HandleFunc("/teams/{teamID}/boards/search", a.sessionRequired(a.handleSearchBoards)).Methods("GET") - r.HandleFunc("/teams/{teamID}/boards/search/linkable", a.sessionRequired(a.handleSearchLinkableBoards)).Methods("GET") - r.HandleFunc("/boards/search", a.sessionRequired(a.handleSearchAllBoards)).Methods("GET") -} - -func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/channels searchMyChannels - // - // Returns the user available channels - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: search - // in: query - // description: string to filter channels list - // required: false - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Channel" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - query := r.URL.Query() - searchQuery := query.Get("search") - - teamID := mux.Vars(r)["teamID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - auditRec := a.makeAuditRecord(r, "searchMyChannels", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - - channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GetUserChannels", - mlog.String("teamID", teamID), - mlog.Int("channelsCount", len(channels)), - ) - - data, err := json.Marshal(channels) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("channelsCount", len(channels)) - auditRec.Success() -} - -func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/boards/search searchBoards - // - // Returns the boards that match with a search term in the team - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: q - // in: query - // description: The search term. Must have at least one character - // required: true - // type: string - // - name: field - // in: query - // description: The field to search on for search term. Can be `title`, `property_name`. Defaults to `title` - // required: false - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Board" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - var err error - teamID := mux.Vars(r)["teamID"] - term := r.URL.Query().Get("q") - searchFieldText := r.URL.Query().Get("field") - searchField := model.BoardSearchFieldTitle - if searchFieldText != "" { - searchField, err = model.BoardSearchFieldFromString(searchFieldText) - if err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - } - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - if term == "" { - jsonStringResponse(w, http.StatusOK, "[]") - return - } - - auditRec := a.makeAuditRecord(r, "searchBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // retrieve boards list - boards, err := a.app.SearchBoardsForUser(term, searchField, userID, !isGuest) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("SearchBoards", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(boards)), - ) - - data, err := json.Marshal(boards) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("boardsCount", len(boards)) - auditRec.Success() -} - -func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/boards/search/linkable searchLinkableBoards - // - // Returns the boards that match with a search term in the team and the - // user has permission to manage members - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: q - // in: query - // description: The search term. Must have at least one character - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Board" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - teamID := mux.Vars(r)["teamID"] - term := r.URL.Query().Get("q") - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - if term == "" { - jsonStringResponse(w, http.StatusOK, "[]") - return - } - - auditRec := a.makeAuditRecord(r, "searchLinkableBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - - // retrieve boards list - boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - linkableBoards := []*model.Board{} - for _, board := range boards { - if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionManageBoardRoles) { - linkableBoards = append(linkableBoards, board) - } - } - - a.logger.Debug("SearchLinkableBoards", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(linkableBoards)), - ) - - data, err := json.Marshal(linkableBoards) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("boardsCount", len(linkableBoards)) - auditRec.Success() -} - -func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/search searchAllBoards - // - // Returns the boards that match with a search term - // - // --- - // produces: - // - application/json - // parameters: - // - name: q - // in: query - // description: The search term. Must have at least one character - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Board" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - term := r.URL.Query().Get("q") - userID := getUserID(r) - - if term == "" { - jsonStringResponse(w, http.StatusOK, "[]") - return - } - - auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // retrieve boards list - boards, err := a.app.SearchBoardsForUser(term, model.BoardSearchFieldTitle, userID, !isGuest) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("SearchAllBoards", - mlog.Int("boardsCount", len(boards)), - ) - - data, err := json.Marshal(boards) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("boardsCount", len(boards)) - auditRec.Success() -} diff --git a/server/boards/api/sharing.go b/server/boards/api/sharing.go deleted file mode 100644 index 149c5fc9c1..0000000000 --- a/server/boards/api/sharing.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "errors" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ErrTurningOnSharing = errors.New("turning on sharing for board failed, see log for details") - -func (a *API) registerSharingRoutes(r *mux.Router) { - // Sharing APIs - r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST") - r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handleGetSharing)).Methods("GET") -} - -func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /boards/{boardID}/sharing getSharing - // - // Returns sharing information for a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Sharing" - // '404': - // description: board not found - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - boardID := vars["boardID"] - - userID := getUserID(r) - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board")) - return - } - - auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("boardID", boardID) - - sharing, err := a.app.GetSharing(boardID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - sharingData, err := json.Marshal(sharing) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, sharingData) - - a.logger.Debug("GET sharing", - mlog.String("boardID", boardID), - mlog.String("shareID", sharing.ID), - mlog.Bool("enabled", sharing.Enabled), - ) - auditRec.AddMeta("shareID", sharing.ID) - auditRec.AddMeta("enabled", sharing.Enabled) - auditRec.Success() -} - -func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /boards/{boardID}/sharing postSharing - // - // Sets sharing information for a board - // - // --- - // produces: - // - application/json - // parameters: - // - name: boardID - // in: path - // description: Board ID - // required: true - // type: string - // - name: Body - // in: body - // description: sharing information for a root block - // required: true - // schema: - // "$ref": "#/definitions/Sharing" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - boardID := mux.Vars(r)["boardID"] - - userID := getUserID(r) - if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) { - a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board")) - return - } - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var sharing model.Sharing - err = json.Unmarshal(requestBody, &sharing) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // Stamp boardID from the URL - sharing.ID = boardID - - auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("shareID", sharing.ID) - auditRec.AddMeta("enabled", sharing.Enabled) - - // Stamp ModifiedBy - modifiedBy := userID - if userID == model.SingleUser { - modifiedBy = "" - } - sharing.ModifiedBy = modifiedBy - - if userID == model.SingleUser { - userID = "" - } - - if !a.app.GetClientConfig().EnablePublicSharedBoards { - a.logger.Warn( - "Attempt to turn on sharing for board via API failed, sharing off in configuration.", - mlog.String("boardID", sharing.ID), - mlog.String("userID", userID)) - a.errorResponse(w, r, ErrTurningOnSharing) - return - } - - sharing.ModifiedBy = userID - - err = a.app.UpsertSharing(sharing) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - - a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID)) - auditRec.Success() -} diff --git a/server/boards/api/statistics.go b/server/boards/api/statistics.go deleted file mode 100644 index df1f80e29c..0000000000 --- a/server/boards/api/statistics.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *API) registerStatisticsRoutes(r *mux.Router) { - // statistics - r.HandleFunc("/statistics", a.sessionRequired(a.handleStatistics)).Methods("GET") -} - -func (a *API) handleStatistics(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /statistics handleStatistics - // - // Fetches the statistic of the server. - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/BoardStatistics" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if !a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode")) - return - } - - // user must have right to access analytics - userID := getUserID(r) - if !a.permissions.HasPermissionTo(userID, mm_model.PermissionGetAnalytics) { - a.errorResponse(w, r, model.NewErrPermission("access denied System Statistics")) - return - } - - boardCount, err := a.app.GetBoardCount() - if err != nil { - a.errorResponse(w, r, err) - return - } - cardCount, err := a.app.GetUsedCardsCount() - if err != nil { - a.errorResponse(w, r, err) - return - } - - stats := model.BoardsStatistics{ - Boards: int(boardCount), - Cards: cardCount, - } - data, err := json.Marshal(stats) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) -} diff --git a/server/boards/api/subscriptions.go b/server/boards/api/subscriptions.go deleted file mode 100644 index 1068aab2d2..0000000000 --- a/server/boards/api/subscriptions.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerSubscriptionsRoutes(r *mux.Router) { - // Subscription APIs - r.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST") - r.HandleFunc("/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE") - r.HandleFunc("/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET") -} - -// subscriptions - -func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /subscriptions createSubscription - // - // Creates a subscription to a block for a user. The user will receive change notifications for the block. - // - // --- - // produces: - // - application/json - // parameters: - // - name: Body - // in: body - // description: subscription definition - // required: true - // schema: - // "$ref": "#/definitions/Subscription" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var sub model.Subscription - - if err = json.Unmarshal(requestBody, &sub); err != nil { - a.errorResponse(w, r, err) - return - } - - if err = sub.IsValid(); err != nil { - a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) - return - } - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - auditRec := a.makeAuditRecord(r, "createSubscription", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("subscriber_id", sub.SubscriberID) - auditRec.AddMeta("block_id", sub.BlockID) - - // User can only create subscriptions for themselves (for now) - if session.UserID != sub.SubscriberID { - a.errorResponse(w, r, model.NewErrBadRequest("userID and subscriberID mismatch")) - return - } - - // check for valid block - _, bErr := a.app.GetBlockByID(sub.BlockID) - if bErr != nil { - message := fmt.Sprintf("invalid blockID: %s", bErr) - a.errorResponse(w, r, model.NewErrBadRequest(message)) - return - } - - subNew, err := a.app.CreateSubscription(&sub) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("CREATE subscription", - mlog.String("subscriber_id", subNew.SubscriberID), - mlog.String("block_id", subNew.BlockID), - ) - - json, err := json.Marshal(subNew) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, json) - auditRec.Success() -} - -func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) { - // swagger:operation DELETE /subscriptions/{blockID}/{subscriberID} deleteSubscription - // - // Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block. - // - // --- - // produces: - // - application/json - // parameters: - // - name: blockID - // in: path - // description: Block ID - // required: true - // type: string - // - name: subscriberID - // in: path - // description: Subscriber ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - vars := mux.Vars(r) - blockID := vars["blockID"] - subscriberID := vars["subscriberID"] - - auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - auditRec.AddMeta("block_id", blockID) - auditRec.AddMeta("subscriber_id", subscriberID) - - // User can only delete subscriptions for themselves - if session.UserID != subscriberID { - a.errorResponse(w, r, model.NewErrPermission("access denied")) - return - } - - if _, err := a.app.DeleteSubscription(blockID, subscriberID); err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("DELETE subscription", - mlog.String("blockID", blockID), - mlog.String("subscriberID", subscriberID), - ) - jsonStringResponse(w, http.StatusOK, "{}") - - auditRec.Success() -} - -func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /subscriptions/{subscriberID} getSubscriptions - // - // Gets subscriptions for a user. - // - // --- - // produces: - // - application/json - // parameters: - // - name: subscriberID - // in: path - // description: Subscriber ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - vars := mux.Vars(r) - subscriberID := vars["subscriberID"] - - auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("subscriber_id", subscriberID) - - // User can only get subscriptions for themselves (for now) - if session.UserID != subscriberID { - a.errorResponse(w, r, model.NewErrPermission("access denied")) - return - } - - subs, err := a.app.GetSubscriptions(subscriberID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - a.logger.Debug("GET subscriptions", - mlog.String("subscriberID", subscriberID), - mlog.Int("count", len(subs)), - ) - - json, err := json.Marshal(subs) - if err != nil { - a.errorResponse(w, r, err) - return - } - jsonBytesResponse(w, http.StatusOK, json) - - auditRec.AddMeta("subscription_count", len(subs)) - auditRec.Success() -} diff --git a/server/boards/api/system.go b/server/boards/api/system.go deleted file mode 100644 index 0f99da174d..0000000000 --- a/server/boards/api/system.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" -) - -func (a *API) registerSystemRoutes(r *mux.Router) { - // System APIs - r.HandleFunc("/hello", a.handleHello).Methods("GET") - r.HandleFunc("/ping", a.handlePing).Methods("GET") -} - -func (a *API) handleHello(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /hello hello - // - // Responds with `Hello` if the web service is running. - // - // --- - // produces: - // - text/plain - // responses: - // '200': - // description: success - stringResponse(w, "Hello") -} - -func (a *API) handlePing(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /ping ping - // - // Responds with server metadata if the web service is running. - // - // --- - // produces: - // - application/json - // responses: - // '200': - // description: success - serverMetadata := a.app.GetServerMetadata() - - if a.singleUserToken != "" { - serverMetadata.SKU = "personal_desktop" - } - - if serverMetadata.Edition == "plugin" { - serverMetadata.SKU = "suite" - } - - bytes, err := json.Marshal(serverMetadata) - if err != nil { - a.errorResponse(w, r, err) - } - - jsonStringResponse(w, 200, string(bytes)) -} diff --git a/server/boards/api/system_test.go b/server/boards/api/system_test.go deleted file mode 100644 index 57aa2235cb..0000000000 --- a/server/boards/api/system_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "runtime" - "testing" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/app" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestHello(t *testing.T) { - testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)} - - t.Run("Returns 'Hello' on success", func(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "/hello", nil) - response := httptest.NewRecorder() - - testAPI.handleHello(response, request) - - got := response.Body.String() - want := "Hello" - - if got != want { - t.Errorf("got %q want %q", got, want) - } - - if response.Code != http.StatusOK { - t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK) - } - }) -} - -func TestPing(t *testing.T) { - testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)} - - t.Run("Returns metadata on success", func(t *testing.T) { - request, _ := http.NewRequest(http.MethodGet, "/ping", nil) - response := httptest.NewRecorder() - - testAPI.handlePing(response, request) - - var got app.ServerMetadata - err := json.NewDecoder(response.Body).Decode(&got) - if err != nil { - t.Fatalf("Unable to JSON decode response body %q", response.Body) - } - - want := app.ServerMetadata{ - Version: model.CurrentVersion, - BuildNumber: model.BuildNumber, - BuildDate: model.BuildDate, - Commit: model.BuildHash, - Edition: model.Edition, - DBType: "", - DBVersion: "", - OSType: runtime.GOOS, - OSArch: runtime.GOARCH, - SKU: "personal_server", - } - - if got != want { - t.Errorf("got %q want %q", got, want) - } - - if response.Code != http.StatusOK { - t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK) - } - }) - - t.Run("Sets SKU to 'personal_desktop' when in single-user mode", func(t *testing.T) { - testAPI.singleUserToken = "abc-123-xyz-456" - request, _ := http.NewRequest(http.MethodGet, "/ping", nil) - response := httptest.NewRecorder() - - testAPI.handlePing(response, request) - - var got app.ServerMetadata - err := json.NewDecoder(response.Body).Decode(&got) - if err != nil { - t.Fatalf("Unable to JSON decode response body %q", response.Body) - } - - want := app.ServerMetadata{ - Version: model.CurrentVersion, - BuildNumber: model.BuildNumber, - BuildDate: model.BuildDate, - Commit: model.BuildHash, - Edition: model.Edition, - DBType: "", - DBVersion: "", - OSType: runtime.GOOS, - OSArch: runtime.GOARCH, - SKU: "personal_desktop", - } - - if got != want { - t.Errorf("got %q want %q", got, want) - } - - if response.Code != http.StatusOK { - t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK) - } - }) - - t.Run("Sets SKU to 'suite' when in plugin mode", func(t *testing.T) { - model.Edition = "plugin" - request, _ := http.NewRequest(http.MethodGet, "/ping", nil) - response := httptest.NewRecorder() - - testAPI.handlePing(response, request) - - var got app.ServerMetadata - err := json.NewDecoder(response.Body).Decode(&got) - if err != nil { - t.Fatalf("Unable to JSON decode response body %q", response.Body) - } - - want := app.ServerMetadata{ - Version: model.CurrentVersion, - BuildNumber: model.BuildNumber, - BuildDate: model.BuildDate, - Commit: model.BuildHash, - Edition: "plugin", - DBType: "", - DBVersion: "", - OSType: runtime.GOOS, - OSArch: runtime.GOARCH, - SKU: "suite", - } - - if got != want { - t.Errorf("got %q want %q", got, want) - } - - if response.Code != http.StatusOK { - t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK) - } - }) -} diff --git a/server/boards/api/teams.go b/server/boards/api/teams.go deleted file mode 100644 index da7824a715..0000000000 --- a/server/boards/api/teams.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func (a *API) registerTeamsRoutes(r *mux.Router) { - // Team APIs - r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET") - r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET") - r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET") - r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST") - r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET") -} - -func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams getTeams - // - // Returns information of all the teams - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Team" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - teams, err := a.app.GetTeamsForUser(userID) - if err != nil { - a.errorResponse(w, r, err) - } - - auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamCount", len(teams)) - - data, err := json.Marshal(teams) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID} getTeam - // - // Returns information of the root team - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Team" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - teamID := vars["teamID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - var team *model.Team - var err error - - if a.MattermostAuth { - team, err = a.app.GetTeam(teamID) - if model.IsErrNotFound(err) { - a.errorResponse(w, r, model.NewErrUnauthorized("invalid team")) - } - if err != nil { - a.errorResponse(w, r, err) - } - } else { - team, err = a.app.GetRootTeam() - if err != nil { - a.errorResponse(w, r, err) - return - } - } - - auditRec := a.makeAuditRecord(r, "getTeam", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("resultTeamID", team.ID) - - data, err := json.Marshal(team) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/regenerate_signup_token regenerateSignupToken - // - // Regenerates the signup token for the root team - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - if a.MattermostAuth { - a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode")) - return - } - - team, err := a.app.GetRootTeam() - if err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - team.SignupToken = utils.NewID(utils.IDTypeToken) - - if err = a.app.UpsertTeamSignupToken(*team); err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, "{}") - auditRec.Success() -} - -func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/users getTeamUsers - // - // Returns team users - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: search - // in: query - // description: string to filter users list - // required: false - // type: string - // - name: exclude_bots - // in: query - // description: exclude bot users - // required: false - // type: boolean - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - teamID := vars["teamID"] - userID := getUserID(r) - query := r.URL.Query() - searchQuery := query.Get("search") - excludeBots := r.URL.Query().Get("exclude_bots") == True - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - asGuestUser := "" - if isGuest { - asGuestUser = userID - } - - users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser, excludeBots) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(users) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("userCount", len(users)) - auditRec.Success() -} - -func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /teams/{teamID}/users getTeamUsersByID - // - // Returns a user[] - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // - name: Body - // in: body - // description: []UserIDs to return - // required: true - // type: []string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var userIDs []string - if err = json.Unmarshal(requestBody, &userIDs); err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - vars := mux.Vars(r) - teamID := vars["teamID"] - userID := getUserID(r) - - if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - var users []*model.User - - if len(userIDs) == 0 { - a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty")) - return - } - - if userIDs[0] == model.SingleUser { - ws, _ := a.app.GetRootTeam() - now := utils.GetMillis() - user := &model.User{ - ID: model.SingleUser, - Username: model.SingleUser, - Email: model.SingleUser, - CreateAt: ws.UpdateAt, - UpdateAt: now, - } - users = append(users, user) - } else { - users, err = a.app.GetUsersList(userIDs) - if err != nil { - a.errorResponse(w, r, err) - return - } - - for i, u := range users { - if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { - users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) - } - if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { - users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) - } - } - } - - usersList, err := json.Marshal(users) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, string(usersList)) - auditRec.Success() -} diff --git a/server/boards/api/templates.go b/server/boards/api/templates.go deleted file mode 100644 index 5746761925..0000000000 --- a/server/boards/api/templates.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *API) registerTemplatesRoutes(r *mux.Router) { - r.HandleFunc("/teams/{teamID}/templates", a.sessionRequired(a.handleGetTemplates)).Methods("GET") -} - -func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /teams/{teamID}/templates getTemplates - // - // Returns team templates - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/Board" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - teamID := mux.Vars(r)["teamID"] - userID := getUserID(r) - - if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - a.errorResponse(w, r, model.NewErrPermission("access denied to team")) - return - } - - isGuest, err := a.userIsGuest(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if isGuest { - a.errorResponse(w, r, model.NewErrPermission("access denied to templates")) - return - } - - auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("teamID", teamID) - - // retrieve boards list - boards, err := a.app.GetTemplateBoards(teamID, userID) - if err != nil { - a.errorResponse(w, r, err) - 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", - mlog.String("teamID", teamID), - mlog.Int("boardsCount", len(results)), - ) - - data, err := json.Marshal(results) - if err != nil { - a.errorResponse(w, r, err) - return - } - - // response - jsonBytesResponse(w, http.StatusOK, data) - - auditRec.AddMeta("templatesCount", len(results)) - auditRec.Success() -} diff --git a/server/boards/api/users.go b/server/boards/api/users.go deleted file mode 100644 index 731397d490..0000000000 --- a/server/boards/api/users.go +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func (a *API) registerUsersRoutes(r *mux.Router) { - // Users APIs - r.HandleFunc("/users", a.sessionRequired(a.handleGetUsersList)).Methods("POST") - r.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") - r.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET") - r.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") - r.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut) - r.HandleFunc("/users/me/config", a.sessionRequired(a.handleGetUserPreferences)).Methods(http.MethodGet) -} - -func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) { - // swagger:operation POST /users getUsersList - // - // Returns a user[] - // - // --- - // produces: - // - application/json - // parameters: - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var userIDs []string - if err = json.Unmarshal(requestBody, &userIDs); err != nil { - a.errorResponse(w, r, err) - return - } - - auditRec := a.makeAuditRecord(r, "getUsersList", audit.Fail) - defer a.audit.LogRecord(audit.LevelAuth, auditRec) - - var users []*model.User - - if len(userIDs) == 0 { - a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty")) - return - } - - if userIDs[0] == model.SingleUser { - ws, _ := a.app.GetRootTeam() - now := utils.GetMillis() - user := &model.User{ - ID: model.SingleUser, - Username: model.SingleUser, - Email: model.SingleUser, - CreateAt: ws.UpdateAt, - UpdateAt: now, - } - users = append(users, user) - } else { - users, err = a.app.GetUsersList(userIDs) - if err != nil { - a.errorResponse(w, r, err) - return - } - } - - usersList, err := json.Marshal(users) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonStringResponse(w, http.StatusOK, string(usersList)) - auditRec.Success() -} - -func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /users/me getMe - // - // Returns the currently logged-in user - // - // --- - // produces: - // - application/json - // parameters: - // - name: teamID - // in: path - // description: Team ID - // required: false - // type: string - // - name: channelID - // in: path - // description: Channel ID - // required: false - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - query := r.URL.Query() - teamID := query.Get("teamID") - channelID := query.Get("channelID") - - userID := getUserID(r) - - var user *model.User - var err error - - auditRec := a.makeAuditRecord(r, "getMe", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - if userID == model.SingleUser { - ws, _ := a.app.GetRootTeam() - now := utils.GetMillis() - user = &model.User{ - ID: model.SingleUser, - Username: model.SingleUser, - Email: model.SingleUser, - CreateAt: ws.UpdateAt, - UpdateAt: now, - } - } else { - user, err = a.app.GetUser(userID) - if err != nil { - // ToDo: wrap with an invalid token error - a.errorResponse(w, r, err) - return - } - } - - if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) { - user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id) - } - if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) { - user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id) - } - if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) { - user.Permissions = append(user.Permissions, model.PermissionCreatePost.Id) - } - - userData, err := json.Marshal(user) - if err != nil { - a.errorResponse(w, r, err) - return - } - jsonBytesResponse(w, http.StatusOK, userData) - - auditRec.AddMeta("userID", user.ID) - auditRec.Success() -} - -func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /users/me/memberships getMyMemberships - // - // Returns the currently users board memberships - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // type: array - // items: - // "$ref": "#/definitions/BoardMember" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - auditRec := a.makeAuditRecord(r, "getMyBoardMemberships", audit.Fail) - auditRec.AddMeta("userID", userID) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - members, err := a.app.GetMembersForUser(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - membersData, err := json.Marshal(members) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, membersData) - - auditRec.Success() -} - -func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /users/{userID} getUser - // - // Returns a user - // - // --- - // produces: - // - application/json - // parameters: - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/User" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - vars := mux.Vars(r) - userID := vars["userID"] - - auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - auditRec.AddMeta("userID", userID) - - user, err := a.app.GetUser(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - canSeeUser, err := a.app.CanSeeUser(session.UserID, userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - if !canSeeUser { - a.errorResponse(w, r, model.NewErrNotFound("user ID="+userID)) - return - } - - userData, err := json.Marshal(user) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, userData) - auditRec.Success() -} - -func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) { - // swagger:operation PATCH /users/{userID}/config updateUserConfig - // - // Updates user config - // - // --- - // produces: - // - application/json - // parameters: - // - name: userID - // in: path - // description: User ID - // required: true - // type: string - // - name: Body - // in: body - // description: User config patch to apply - // required: true - // schema: - // "$ref": "#/definitions/UserPreferencesPatch" - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - requestBody, err := io.ReadAll(r.Body) - if err != nil { - a.errorResponse(w, r, err) - return - } - - var patch *model.UserPreferencesPatch - err = json.Unmarshal(requestBody, &patch) - if err != nil { - a.errorResponse(w, r, err) - return - } - - vars := mux.Vars(r) - userID := vars["userID"] - - ctx := r.Context() - session := ctx.Value(sessionContextKey).(*model.Session) - - auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail) - defer a.audit.LogRecord(audit.LevelModify, auditRec) - - // a user can update only own config - if userID != session.UserID { - a.errorResponse(w, r, model.NewErrForbidden("")) - return - } - - updatedConfig, err := a.app.UpdateUserConfig(userID, *patch) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(updatedConfig) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} - -func (a *API) handleGetUserPreferences(w http.ResponseWriter, r *http.Request) { - // swagger:operation GET /users/me/config getUserConfig - // - // Returns an array of user preferences - // - // --- - // produces: - // - application/json - // security: - // - BearerAuth: [] - // responses: - // '200': - // description: success - // schema: - // "$ref": "#/definitions/Preferences" - // default: - // description: internal error - // schema: - // "$ref": "#/definitions/ErrorResponse" - - userID := getUserID(r) - - auditRec := a.makeAuditRecord(r, "getUserConfig", audit.Fail) - defer a.audit.LogRecord(audit.LevelRead, auditRec) - - preferences, err := a.app.GetUserPreferences(userID) - if err != nil { - a.errorResponse(w, r, err) - return - } - - data, err := json.Marshal(preferences) - if err != nil { - a.errorResponse(w, r, err) - return - } - - jsonBytesResponse(w, http.StatusOK, data) - auditRec.Success() -} diff --git a/server/boards/app/app.go b/server/boards/app/app.go deleted file mode 100644 index a0259873ed..0000000000 --- a/server/boards/app/app.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "io" - "sync" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/metrics" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/services/webhook" - "github.com/mattermost/mattermost/server/v8/boards/utils" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -const ( - blockChangeNotifierQueueSize = 1000 - blockChangeNotifierPoolSize = 10 - blockChangeNotifierShutdownTimeout = time.Second * 10 -) - -type servicesAPI interface { - GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) -} - -type ReadCloseSeeker = filestore.ReadCloseSeeker - -type fileBackend interface { - Reader(path string) (ReadCloseSeeker, error) - FileExists(path string) (bool, error) - CopyFile(oldPath, newPath string) error - MoveFile(oldPath, newPath string) error - WriteFile(fr io.Reader, path string) (int64, error) - RemoveFile(path string) error -} - -type Services struct { - Auth *auth.Auth - Store store.Store - FilesBackend fileBackend - Webhook *webhook.Client - Metrics *metrics.Metrics - Notifications *notify.Service - Logger mlog.LoggerIFace - Permissions permissions.PermissionsService - SkipTemplateInit bool - ServicesAPI servicesAPI -} - -type App struct { - config *config.Configuration - store store.Store - auth *auth.Auth - wsAdapter ws.Adapter - filesBackend fileBackend - webhook *webhook.Client - metrics *metrics.Metrics - notifications *notify.Service - logger mlog.LoggerIFace - permissions permissions.PermissionsService - blockChangeNotifier *utils.CallbackQueue - servicesAPI servicesAPI - - cardLimitMux sync.RWMutex - cardLimit int -} - -func (a *App) SetConfig(config *config.Configuration) { - a.config = config -} - -func (a *App) GetConfig() *config.Configuration { - return a.config -} - -func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App { - app := &App{ - config: config, - store: services.Store, - auth: services.Auth, - wsAdapter: wsAdapter, - filesBackend: services.FilesBackend, - webhook: services.Webhook, - metrics: services.Metrics, - notifications: services.Notifications, - logger: services.Logger, - permissions: services.Permissions, - blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger), - servicesAPI: services.ServicesAPI, - } - app.initialize(services.SkipTemplateInit) - return app -} - -func (a *App) CardLimit() int { - a.cardLimitMux.RLock() - defer a.cardLimitMux.RUnlock() - return a.cardLimit -} - -func (a *App) SetCardLimit(cardLimit int) { - a.cardLimitMux.Lock() - defer a.cardLimitMux.Unlock() - a.cardLimit = cardLimit -} - -func (a *App) GetLicense() *mm_model.License { - return a.store.GetLicense() -} diff --git a/server/boards/app/app_test.go b/server/boards/app/app_test.go deleted file mode 100644 index 26462b7ee3..0000000000 --- a/server/boards/app/app_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/services/config" -) - -func TestSetConfig(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("Test Update Config", func(t *testing.T) { - require.False(t, th.App.config.EnablePublicSharedBoards) - newConfiguration := config.Configuration{} - newConfiguration.EnablePublicSharedBoards = true - th.App.SetConfig(&newConfiguration) - - require.True(t, th.App.config.EnablePublicSharedBoards) - }) -} diff --git a/server/boards/app/auth.go b/server/boards/app/auth.go deleted file mode 100644 index 21cc7130f0..0000000000 --- a/server/boards/app/auth.go +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/auth" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/pkg/errors" -) - -const ( - DaysPerMonth = 30 - DaysPerWeek = 7 - HoursPerDay = 24 - MinutesPerHour = 60 - SecondsPerMinute = 60 -) - -// GetSession Get a user active session and refresh the session if is needed. -func (a *App) GetSession(token string) (*model.Session, error) { - return a.auth.GetSession(token) -} - -// IsValidReadToken validates the read token for a block. -func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) { - return a.auth.IsValidReadToken(boardID, readToken) -} - -// GetRegisteredUserCount returns the number of registered users. -func (a *App) GetRegisteredUserCount() (int, error) { - return a.store.GetRegisteredUserCount() -} - -// GetDailyActiveUsers returns the number of daily active users. -func (a *App) GetDailyActiveUsers() (int, error) { - secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay) - return a.store.GetActiveUserCount(secondsAgo) -} - -// GetWeeklyActiveUsers returns the number of weekly active users. -func (a *App) GetWeeklyActiveUsers() (int, error) { - secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay * DaysPerWeek) - return a.store.GetActiveUserCount(secondsAgo) -} - -// GetMonthlyActiveUsers returns the number of monthly active users. -func (a *App) GetMonthlyActiveUsers() (int, error) { - secondsAgo := int64(SecondsPerMinute * MinutesPerHour * HoursPerDay * DaysPerMonth) - return a.store.GetActiveUserCount(secondsAgo) -} - -// GetUser gets an existing active user by id. -func (a *App) GetUser(id string) (*model.User, error) { - if len(id) < 1 { - return nil, errors.New("no user ID") - } - - user, err := a.store.GetUserByID(id) - if err != nil { - return nil, errors.Wrap(err, "unable to find user") - } - return user, nil -} - -func (a *App) GetUsersList(userIDs []string) ([]*model.User, error) { - if len(userIDs) == 0 { - return nil, errors.New("No User IDs") - } - - users, err := a.store.GetUsersList(userIDs, a.config.ShowEmailAddress, a.config.ShowFullName) - if err != nil { - return nil, errors.Wrap(err, "unable to find users") - } - return users, nil -} - -// Login create a new user session if the authentication data is valid. -func (a *App) Login(username, email, password, mfaToken string) (string, error) { - var user *model.User - if username != "" { - var err error - user, err = a.store.GetUserByUsername(username) - if err != nil && !model.IsErrNotFound(err) { - a.metrics.IncrementLoginFailCount(1) - return "", errors.Wrap(err, "invalid username or password") - } - } - - if user == nil && email != "" { - var err error - user, err = a.store.GetUserByEmail(email) - if err != nil && model.IsErrNotFound(err) { - a.metrics.IncrementLoginFailCount(1) - return "", errors.Wrap(err, "invalid username or password") - } - } - - if user == nil { - a.metrics.IncrementLoginFailCount(1) - return "", errors.New("invalid username or password") - } - - if !auth.ComparePassword(user.Password, password) { - a.metrics.IncrementLoginFailCount(1) - a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID)) - return "", errors.New("invalid username or password") - } - - authService := user.AuthService - if authService == "" { - authService = "native" - } - - session := model.Session{ - ID: utils.NewID(utils.IDTypeSession), - Token: utils.NewID(utils.IDTypeToken), - UserID: user.ID, - AuthService: authService, - Props: map[string]interface{}{}, - } - err := a.store.CreateSession(&session) - if err != nil { - return "", errors.Wrap(err, "unable to create session") - } - - a.metrics.IncrementLoginCount(1) - - // TODO: MFA verification - return session.Token, nil -} - -// Logout invalidates the user session. -func (a *App) Logout(sessionID string) error { - err := a.store.DeleteSession(sessionID) - if err != nil { - return errors.Wrap(err, "unable to delete the session") - } - - a.metrics.IncrementLogoutCount(1) - - return nil -} - -// RegisterUser creates a new user if the provided data is valid. -func (a *App) RegisterUser(username, email, password string) error { - var user *model.User - if username != "" { - var err error - user, err = a.store.GetUserByUsername(username) - if err != nil && !model.IsErrNotFound(err) { - return err - } - if user != nil { - return errors.New("The username already exists") - } - } - - if user == nil && email != "" { - var err error - user, err = a.store.GetUserByEmail(email) - if err != nil && !model.IsErrNotFound(err) { - return err - } - if user != nil { - return errors.New("The email already exists") - } - } - - // TODO: Move this into the config - passwordSettings := auth.PasswordSettings{ - MinimumLength: 6, - } - - err := auth.IsPasswordValid(password, passwordSettings) - if err != nil { - return errors.Wrap(err, "Invalid password") - } - - _, err = a.store.CreateUser(&model.User{ - ID: utils.NewID(utils.IDTypeUser), - Username: username, - Email: email, - Password: auth.HashPassword(password), - MfaSecret: "", - AuthService: a.config.AuthMode, - AuthData: "", - }) - if err != nil { - return errors.Wrap(err, "Unable to create the new user") - } - - return nil -} - -func (a *App) UpdateUserPassword(username, password string) error { - err := a.store.UpdateUserPassword(username, auth.HashPassword(password)) - if err != nil { - return err - } - - return nil -} - -func (a *App) ChangePassword(userID, oldPassword, newPassword string) error { - var user *model.User - if userID != "" { - var err error - user, err = a.store.GetUserByID(userID) - if err != nil { - return errors.Wrap(err, "invalid username or password") - } - } - - if user == nil { - return errors.New("invalid username or password") - } - - if !auth.ComparePassword(user.Password, oldPassword) { - a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID)) - return errors.New("invalid username or password") - } - - err := a.store.UpdateUserPasswordByID(userID, auth.HashPassword(newPassword)) - if err != nil { - return errors.Wrap(err, "unable to update password") - } - - return nil -} diff --git a/server/boards/app/auth_test.go b/server/boards/app/auth_test.go deleted file mode 100644 index c360fce844..0000000000 --- a/server/boards/app/auth_test.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/golang/mock/gomock" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/auth" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var mockUser = &model.User{ - ID: utils.NewID(utils.IDTypeUser), - Username: "testUsername", - Email: "testEmail", - Password: auth.HashPassword("testPassword"), -} - -func TestLogin(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testcases := []struct { - title string - userName string - email string - password string - mfa string - isError bool - }{ - {"fail, missing login information", "", "", "", "", true}, - {"fail, invalid username", "badUsername", "", "", "", true}, - {"fail, invalid email", "", "badEmail", "", "", true}, - {"fail, invalid password", "testUsername", "", "badPassword", "", true}, - {"success, using username", "testUsername", "", "testPassword", "", false}, - {"success, using email", "", "testEmail", "testPassword", "", false}, - } - - th.Store.EXPECT().GetUserByUsername("badUsername").Return(nil, errors.New("Bad Username")) - th.Store.EXPECT().GetUserByEmail("badEmail").Return(nil, errors.New("Bad Email")) - th.Store.EXPECT().GetUserByUsername("testUsername").Return(mockUser, nil).Times(2) - th.Store.EXPECT().GetUserByEmail("testEmail").Return(mockUser, nil) - th.Store.EXPECT().CreateSession(gomock.Any()).Return(nil).Times(2) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - token, err := th.App.Login(test.userName, test.email, test.password, test.mfa) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotNil(t, token) - } - }) - } -} - -func TestGetUser(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testcases := []struct { - title string - id string - isError bool - }{ - {"fail, missing id", "", true}, - {"fail, invalid id", "badID", true}, - {"success", "goodID", false}, - } - - th.Store.EXPECT().GetUserByID("badID").Return(nil, errors.New("Bad Id")) - th.Store.EXPECT().GetUserByID("goodID").Return(mockUser, nil) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - token, err := th.App.GetUser(test.id) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotNil(t, token) - } - }) - } -} - -func TestRegisterUser(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testcases := []struct { - title string - userName string - email string - password string - isError bool - }{ - {"fail, missing login information", "", "", "", true}, - {"fail, username exists", "existingUsername", "", "", true}, - {"fail, email exists", "", "existingEmail", "", true}, - {"fail, invalid password", "newUsername", "", "test", true}, - {"success, using email", "", "newEmail", "testPassword", false}, - } - - th.Store.EXPECT().GetUserByUsername("existingUsername").Return(mockUser, nil) - th.Store.EXPECT().GetUserByUsername("newUsername").Return(mockUser, errors.New("user not found")) - th.Store.EXPECT().GetUserByEmail("existingEmail").Return(mockUser, nil) - th.Store.EXPECT().GetUserByEmail("newEmail").Return(nil, model.NewErrNotFound("user")) - th.Store.EXPECT().CreateUser(gomock.Any()).Return(nil, nil) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - err := th.App.RegisterUser(test.userName, test.email, test.password) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestUpdateUserPassword(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testcases := []struct { - title string - userName string - password string - isError bool - }{ - {"fail, missing login information", "", "", true}, - {"fail, invalid username", "badUsername", "", true}, - {"success, username", "testUsername", "testPassword", false}, - } - - th.Store.EXPECT().UpdateUserPassword("", gomock.Any()).Return(errors.New("user not found")) - th.Store.EXPECT().UpdateUserPassword("badUsername", gomock.Any()).Return(errors.New("user not found")) - th.Store.EXPECT().UpdateUserPassword("testUsername", gomock.Any()).Return(nil) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - err := th.App.UpdateUserPassword(test.userName, test.password) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestChangePassword(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testcases := []struct { - title string - userName string - oldPassword string - password string - isError bool - }{ - {"fail, missing login information", "", "", "", true}, - {"fail, invalid userId", "badID", "", "", true}, - {"fail, invalid password", mockUser.ID, "wrongPassword", "newPassword", true}, - {"success, using username", mockUser.ID, "testPassword", "newPassword", false}, - } - - th.Store.EXPECT().GetUserByID("badID").Return(nil, errors.New("userID not found")) - th.Store.EXPECT().GetUserByID(mockUser.ID).Return(mockUser, nil).Times(2) - th.Store.EXPECT().UpdateUserPasswordByID(mockUser.ID, gomock.Any()).Return(nil) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - err := th.App.ChangePassword(test.userName, test.oldPassword, test.password) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/server/boards/app/blocks.go b/server/boards/app/blocks.go deleted file mode 100644 index 10dd0c09d4..0000000000 --- a/server/boards/app/blocks.go +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - "path/filepath" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards") - -func (a *App) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) { - if opts.BoardID == "" { - return []*model.Block{}, nil - } - return a.store.GetBlocks(opts) -} - -func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) { - board, err := a.GetBoard(boardID) - if err != nil { - return nil, err - } - if board == nil { - return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err) - } - - blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate) - if err != nil { - return nil, err - } - - err = a.CopyAndUpdateCardFiles(boardID, userID, blocks, asTemplate) - if err != nil { - return nil, err - } - - a.blockChangeNotifier.Enqueue(func() error { - for _, block := range blocks { - a.wsAdapter.BroadcastBlockChange(board.TeamID, block) - } - return nil - }) - - go func() { - if uErr := a.UpdateCardLimitTimestamp(); uErr != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed duplicating a block", - mlog.Err(uErr), - ) - } - }() - - return blocks, err -} - -func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) (*model.Block, error) { - return a.PatchBlockAndNotify(blockID, blockPatch, modifiedByID, false) -} - -func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch, modifiedByID string, disableNotify bool) (*model.Block, error) { - oldBlock, err := a.store.GetBlock(blockID) - if err != nil { - return nil, err - } - - if a.IsCloudLimited() { - containsLimitedBlocks, lErr := a.ContainsLimitedBlocks([]*model.Block{oldBlock}) - if lErr != nil { - return nil, lErr - } - if containsLimitedBlocks { - return nil, model.ErrPatchUpdatesLimitedCards - } - } - - board, err := a.store.GetBoard(oldBlock.BoardID) - if err != nil { - return nil, err - } - - err = a.store.PatchBlock(blockID, blockPatch, modifiedByID) - if err != nil { - return nil, err - } - - a.metrics.IncrementBlocksPatched(1) - block, err := a.store.GetBlock(blockID) - if err != nil { - return nil, err - } - a.blockChangeNotifier.Enqueue(func() error { - // broadcast on websocket - a.wsAdapter.BroadcastBlockChange(board.TeamID, block) - - // broadcast on webhooks - a.webhook.NotifyUpdate(block) - - // send notifications - if !disableNotify { - a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID) - } - return nil - }) - return block, nil -} - -func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error { - return a.PatchBlocksAndNotify(teamID, blockPatches, modifiedByID, false) -} - -func (a *App) PatchBlocksAndNotify(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string, disableNotify bool) error { - oldBlocks, err := a.store.GetBlocksByIDs(blockPatches.BlockIDs) - if err != nil { - return err - } - - if a.IsCloudLimited() { - containsLimitedBlocks, err := a.ContainsLimitedBlocks(oldBlocks) - if err != nil { - return err - } - if containsLimitedBlocks { - return model.ErrPatchUpdatesLimitedCards - } - } - - if err := a.store.PatchBlocks(blockPatches, modifiedByID); err != nil { - return err - } - - a.blockChangeNotifier.Enqueue(func() error { - a.metrics.IncrementBlocksPatched(len(oldBlocks)) - for i, blockID := range blockPatches.BlockIDs { - newBlock, err := a.store.GetBlock(blockID) - if err != nil { - return err - } - a.wsAdapter.BroadcastBlockChange(teamID, newBlock) - a.webhook.NotifyUpdate(newBlock) - if !disableNotify { - a.notifyBlockChanged(notify.Update, newBlock, oldBlocks[i], modifiedByID) - } - } - return nil - }) - return nil -} - -func (a *App) InsertBlock(block *model.Block, modifiedByID string) error { - return a.InsertBlockAndNotify(block, modifiedByID, false) -} - -func (a *App) InsertBlockAndNotify(block *model.Block, modifiedByID string, disableNotify bool) error { - board, bErr := a.store.GetBoard(block.BoardID) - if bErr != nil { - return bErr - } - - err := a.store.InsertBlock(block, modifiedByID) - if err == nil { - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBlockChange(board.TeamID, block) - a.metrics.IncrementBlocksInserted(1) - a.webhook.NotifyUpdate(block) - if !disableNotify { - a.notifyBlockChanged(notify.Add, block, nil, modifiedByID) - } - return nil - }) - } - - go func() { - if uErr := a.UpdateCardLimitTimestamp(); uErr != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after inserting a block", - mlog.Err(uErr), - ) - } - }() - - return err -} - -func (a *App) isWithinViewsLimit(boardID string, block *model.Block) (bool, error) { - // ToDo: Cloud Limits have been disabled by design. We should - // revisit the decision and update the related code accordingly - - /* - limits, err := a.GetBoardsCloudLimits() - if err != nil { - return false, err - } - - if limits.Views == model.LimitUnlimited { - return true, nil - } - - views, err := a.store.GetBlocksWithParentAndType(boardID, block.ParentID, model.TypeView) - if err != nil { - return false, err - } - - // < rather than <= because we'll be creating new view if this - // check passes. When that view is created, the limit will be reached. - // That's why we need to check for if existing + the being-created - // view doesn't exceed the limit. - return len(views) < limits.Views, nil - */ - - return true, nil -} - -func (a *App) InsertBlocks(blocks []*model.Block, modifiedByID string) ([]*model.Block, error) { - return a.InsertBlocksAndNotify(blocks, modifiedByID, false) -} - -func (a *App) InsertBlocksAndNotify(blocks []*model.Block, modifiedByID string, disableNotify bool) ([]*model.Block, error) { - if len(blocks) == 0 { - return []*model.Block{}, nil - } - - // all blocks must belong to the same board - boardID := blocks[0].BoardID - for _, block := range blocks { - if block.BoardID != boardID { - return nil, ErrBlocksFromMultipleBoards - } - } - - board, err := a.store.GetBoard(boardID) - if err != nil { - return nil, err - } - - needsNotify := make([]*model.Block, 0, len(blocks)) - for i := range blocks { - // this check is needed to whitelist inbuilt template - // initialization. They do contain more than 5 views per board. - if boardID != "0" && blocks[i].Type == model.TypeView { - withinLimit, err := a.isWithinViewsLimit(board.ID, blocks[i]) - if err != nil { - return nil, err - } - - if !withinLimit { - a.logger.Info("views limit reached on board", mlog.String("board_id", blocks[i].ParentID), mlog.String("team_id", board.TeamID)) - return nil, model.ErrViewsLimitReached - } - } - - err := a.store.InsertBlock(blocks[i], modifiedByID) - if err != nil { - return nil, err - } - needsNotify = append(needsNotify, blocks[i]) - - a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i]) - a.metrics.IncrementBlocksInserted(1) - } - - a.blockChangeNotifier.Enqueue(func() error { - for _, b := range needsNotify { - block := b - a.webhook.NotifyUpdate(block) - if !disableNotify { - a.notifyBlockChanged(notify.Add, block, nil, modifiedByID) - } - } - return nil - }) - - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after inserting blocks", - mlog.Err(err), - ) - } - }() - - return blocks, nil -} - -func (a *App) GetBlockByID(blockID string) (*model.Block, error) { - return a.store.GetBlock(blockID) -} - -func (a *App) DeleteBlock(blockID string, modifiedBy string) error { - return a.DeleteBlockAndNotify(blockID, modifiedBy, false) -} - -func (a *App) DeleteBlockAndNotify(blockID string, modifiedBy string, disableNotify bool) error { - block, err := a.store.GetBlock(blockID) - if err != nil { - return err - } - - board, err := a.store.GetBoard(block.BoardID) - if err != nil { - return err - } - - if block == nil { - // deleting non-existing block not considered an error - return nil - } - - err = a.store.DeleteBlock(blockID, modifiedBy) - if err != nil { - return err - } - - if block.Type == model.TypeImage { - fileName, fileIDExists := block.Fields["fileId"] - if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString { - filePath := filepath.Join(block.BoardID, fileName) - err = a.filesBackend.RemoveFile(filePath) - - if err != nil { - a.logger.Error("Error deleting image file", - mlog.String("FilePath", filePath), - mlog.Err(err)) - } - } - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID) - a.metrics.IncrementBlocksDeleted(1) - if !disableNotify { - a.notifyBlockChanged(notify.Delete, block, block, modifiedBy) - } - return nil - }) - - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after deleting a block", - mlog.Err(err), - ) - } - }() - - return nil -} - -func (a *App) GetLastBlockHistoryEntry(blockID 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 { - 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 { - // undeleting non-existing block not considered an error - return nil, nil - } - - err = a.store.UndeleteBlock(blockID, modifiedBy) - if err != nil { - return nil, err - } - - block, err := a.store.GetBlock(blockID) - if model.IsErrNotFound(err) { - a.logger.Error("Error loading the block after a successful undelete, not propagating through websockets or notifications", mlog.String("blockID", blockID)) - return nil, err - } - if err != nil { - return nil, err - } - - board, err := a.store.GetBoard(block.BoardID) - if err != nil { - return nil, err - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBlockChange(board.TeamID, block) - a.metrics.IncrementBlocksInserted(1) - a.webhook.NotifyUpdate(block) - a.notifyBlockChanged(notify.Add, block, nil, modifiedBy) - - return nil - }) - - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after undeleting a block", - mlog.Err(err), - ) - } - }() - - return block, nil -} - -func (a *App) GetBlockCountsByType() (map[string]int64, error) { - return a.store.GetBlockCountsByType() -} - -func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) { - // don't notify if notifications service disabled, or block change is generated via system user. - if a.notifications == nil || modifiedByID == model.SystemUserID { - return - } - - // find card and board for the changed block. - board, card, err := a.getBoardAndCard(block) - if err != nil { - a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err)) - return - } - - boardMember, _ := a.GetMemberForBoard(board.ID, modifiedByID) - if boardMember == nil { - // create temporary guest board member - boardMember = &model.BoardMember{ - BoardID: board.ID, - UserID: modifiedByID, - } - } - - evt := notify.BlockChangeEvent{ - Action: action, - TeamID: board.TeamID, - Board: board, - Card: card, - BlockChanged: block, - BlockOld: oldBlock, - ModifiedBy: boardMember, - } - a.notifications.BlockChanged(evt) -} - -const ( - maxSearchDepth = 50 -) - -// getBoardAndCard returns the first parent of type `card` its board for the specified block. -// `board` and/or `card` may return nil without error if the block does not belong to a board or card. -func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) { - board, err = a.store.GetBoard(block.BoardID) - if err != nil { - return board, card, err - } - - var count int // don't let invalid blocks hierarchy cause infinite loop. - iter := block - for { - count++ - if card == nil && iter.Type == model.TypeCard { - card = iter - } - - if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth { - break - } - - iter, err = a.store.GetBlock(iter.ParentID) - if model.IsErrNotFound(err) { - return board, card, nil - } - if err != nil { - return board, card, err - } - } - return board, card, nil -} diff --git a/server/boards/app/blocks_test.go b/server/boards/app/blocks_test.go deleted file mode 100644 index 392fb1ba79..0000000000 --- a/server/boards/app/blocks_test.go +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type blockError struct { - msg string -} - -func (be blockError) Error() string { - return be.msg -} - -func TestInsertBlock(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("success scenario", func(t *testing.T) { - boardID := testBoardID - block := &model.Block{BoardID: boardID} - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(block, "user-id-1").Return(nil) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - err := th.App.InsertBlock(block, "user-id-1") - require.NoError(t, err) - }) - - t.Run("error scenario", func(t *testing.T) { - boardID := testBoardID - block := &model.Block{BoardID: boardID} - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(block, "user-id-1").Return(blockError{"error"}) - err := th.App.InsertBlock(block, "user-id-1") - require.Error(t, err, "error") - }) -} - -func TestPatchBlocks(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("patchBlocks success scenario", func(t *testing.T) { - blockPatches := model.BlockPatchBatch{ - BlockIDs: []string{"block1"}, - BlockPatches: []model.BlockPatch{ - {Title: mm_model.NewString("new title")}, - }, - } - - block1 := &model.Block{ID: "block1"} - th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]*model.Block{block1}, nil) - th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil) - th.Store.EXPECT().GetBlock("block1").Return(block1, nil) - // this call comes from the WS server notification - th.Store.EXPECT().GetMembersForBoard(gomock.Any()).Times(1) - err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") - require.NoError(t, err) - }) - - t.Run("patchBlocks error scenario", func(t *testing.T) { - blockPatches := model.BlockPatchBatch{BlockIDs: []string{}} - th.Store.EXPECT().GetBlocksByIDs([]string{}).Return(nil, sql.ErrNoRows) - err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") - require.ErrorIs(t, err, sql.ErrNoRows) - }) - - t.Run("cloud limit error scenario", func(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - th.App.SetCardLimit(5) - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - - blockPatches := model.BlockPatchBatch{ - BlockIDs: []string{"block1"}, - BlockPatches: []model.BlockPatch{ - {Title: mm_model.NewString("new title")}, - }, - } - - block1 := &model.Block{ - ID: "block1", - Type: model.TypeCard, - ParentID: "board-id", - BoardID: "board-id", - UpdateAt: 100, - } - - board1 := &model.Board{ - ID: "board-id", - Type: model.BoardTypeOpen, - } - - th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]*model.Block{block1}, nil) - th.Store.EXPECT().GetBoard("board-id").Return(board1, nil) - th.Store.EXPECT().GetLicense().Return(fakeLicense) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil) - err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") - require.ErrorIs(t, err, model.ErrPatchUpdatesLimitedCards) - }) -} - -func TestDeleteBlock(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("success scenario", func(t *testing.T) { - boardID := testBoardID - board := &model.Board{ID: boardID} - block := &model.Block{ - ID: "block-id", - BoardID: board.ID, - } - th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(block, nil) - th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) - th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - err := th.App.DeleteBlock("block-id", "user-id-1") - require.NoError(t, err) - }) - - t.Run("error scenario", func(t *testing.T) { - boardID := testBoardID - board := &model.Board{ID: boardID} - block := &model.Block{ - ID: "block-id", - BoardID: board.ID, - } - th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(block, nil) - th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) - th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) - err := th.App.DeleteBlock("block-id", "user-id-1") - require.Error(t, err, "error") - }) -} - -func TestUndeleteBlock(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("success scenario", func(t *testing.T) { - boardID := testBoardID - board := &model.Board{ID: boardID} - block := &model.Block{ - ID: "block-id", - BoardID: board.ID, - } - th.Store.EXPECT().GetBlockHistory( - gomock.Eq("block-id"), - gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), - ).Return([]*model.Block{block}, nil) - th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) - th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(block, nil) - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - _, err := th.App.UndeleteBlock("block-id", "user-id-1") - require.NoError(t, err) - }) - - t.Run("error scenario", func(t *testing.T) { - block := &model.Block{ - ID: "block-id", - } - th.Store.EXPECT().GetBlockHistory( - gomock.Eq("block-id"), - gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), - ).Return([]*model.Block{block}, nil) - th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) - _, err := th.App.UndeleteBlock("block-id", "user-id-1") - require.Error(t, err, "error") - }) -} - -func TestIsWithinViewsLimit(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - th, tearDown := SetupTestHelper(t) - defer tearDown() - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - - t.Run("within views limit", func(t *testing.T) { - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "board_id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil) - - withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) - assert.NoError(t, err) - assert.True(t, withinLimits) - }) - - t.Run("view limit exactly reached", func(t *testing.T) { - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(1), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "board_id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil) - - withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) - assert.NoError(t, err) - assert.False(t, withinLimits) - }) - - t.Run("view limit already exceeded", func(t *testing.T) { - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "board_id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}, {}, {}}, nil) - - withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) - assert.NoError(t, err) - assert.False(t, withinLimits) - }) - - t.Run("creating first view", func(t *testing.T) { - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "board_id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{}, nil) - - withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) - assert.NoError(t, err) - assert.True(t, withinLimits) - }) - - t.Run("is not a cloud SKU so limits don't apply", func(t *testing.T) { - nonCloudLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(false)}, - } - th.Store.EXPECT().GetLicense().Return(nonCloudLicense) - - withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) - assert.NoError(t, err) - assert.True(t, withinLimits) - }) -} - -func TestInsertBlocks(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("success scenario", func(t *testing.T) { - boardID := testBoardID - block := &model.Block{BoardID: boardID} - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(block, "user-id-1").Return(nil) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") - require.NoError(t, err) - }) - - t.Run("error scenario", func(t *testing.T) { - boardID := testBoardID - block := &model.Block{BoardID: boardID} - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(block, "user-id-1").Return(blockError{"error"}) - _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") - require.Error(t, err, "error") - }) - - t.Run("create view within limits", func(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - boardID := testBoardID - block := &model.Block{ - Type: model.TypeView, - ParentID: "parent_id", - BoardID: boardID, - } - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(block, "user-id-1").Return(nil) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - - // setting up mocks for limits - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "test-board-id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil) - - _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") - require.NoError(t, err) - }) - - t.Run("create view exceeding limits", func(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - boardID := testBoardID - block := &model.Block{ - Type: model.TypeView, - ParentID: "parent_id", - BoardID: boardID, - } - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - - // setting up mocks for limits - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "test-board-id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}, {}}, nil) - - _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") - require.Error(t, err) - }) - - t.Run("creating multiple views, reaching limit in the process", func(t *testing.T) { - t.Skipf("Will be fixed soon") - - boardID := testBoardID - view1 := &model.Block{ - Type: model.TypeView, - ParentID: "parent_id", - BoardID: boardID, - } - - view2 := &model.Block{ - Type: model.TypeView, - ParentID: "parent_id", - BoardID: boardID, - } - - board := &model.Board{ID: boardID} - th.Store.EXPECT().GetBoard(boardID).Return(board, nil) - th.Store.EXPECT().InsertBlock(view1, "user-id-1").Return(nil).Times(2) - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) - - // setting up mocks for limits - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - th.Store.EXPECT().GetLicense().Return(fakeLicense).Times(2) - - cloudLimit := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{ - Views: mm_model.NewInt(2), - }, - } - - opts := model.QueryBlocksOptions{ - BoardID: "test-board-id", - ParentID: "parent_id", - BlockType: model.BlockType("view"), - } - - th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil).Times(2) - th.Store.EXPECT().GetUsedCardsCount().Return(1, nil).Times(2) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil).Times(2) - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil).Times(2) - - _, err := th.App.InsertBlocks([]*model.Block{view1, view2}, "user-id-1") - require.Error(t, err) - }) -} diff --git a/server/boards/app/boards.go b/server/boards/app/boards.go deleted file mode 100644 index 711f9041eb..0000000000 --- a/server/boards/app/boards.go +++ /dev/null @@ -1,726 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ( - ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID") -) - -const linkBoardMessage = "@%s linked the board [%s](%s) with this channel" -const unlinkBoardMessage = "@%s unlinked the board [%s](%s) with this channel" - -var errNoDefaultCategoryFound = errors.New("no default category found for user") - -func (a *App) GetBoard(boardID string) (*model.Board, error) { - board, err := a.store.GetBoard(boardID) - if err != nil { - return nil, err - } - return board, nil -} - -func (a *App) GetBoardCount() (int64, error) { - return a.store.GetBoardCount() -} - -func (a *App) GetBoardMetadata(boardID string) (*model.Board, *model.BoardMetadata, error) { - license := a.store.GetLicense() - if license == nil || !(*license.Features.Compliance) { - return nil, nil, model.ErrInsufficientLicense - } - - board, err := a.GetBoard(boardID) - if model.IsErrNotFound(err) { - // Board may have been deleted, retrieve most recent history instead - board, err = a.getBoardHistory(boardID, true) - if err != nil { - return nil, nil, err - } - } - if err != nil { - return nil, nil, err - } - - earliestTime, _, err := a.getBoardDescendantModifiedInfo(boardID, false) - if err != nil { - return nil, nil, err - } - - latestTime, lastModifiedBy, err := a.getBoardDescendantModifiedInfo(boardID, true) - if err != nil { - return nil, nil, err - } - - boardMetadata := model.BoardMetadata{ - BoardID: boardID, - DescendantFirstUpdateAt: earliestTime, - DescendantLastUpdateAt: latestTime, - CreatedBy: board.CreatedBy, - LastModifiedBy: lastModifiedBy, - } - return board, &boardMetadata, nil -} - -// getBoardForBlock returns the board that owns the specified block. -func (a *App) getBoardForBlock(blockID string) (*model.Board, error) { - block, err := a.GetBlockByID(blockID) - if err != nil { - return nil, fmt.Errorf("cannot get block %s: %w", blockID, err) - } - - board, err := a.GetBoard(block.BoardID) - if err != nil { - return nil, fmt.Errorf("cannot get board %s: %w", block.BoardID, err) - } - - return board, nil -} - -func (a *App) getBoardHistory(boardID string, latest bool) (*model.Board, error) { - opts := model.QueryBoardHistoryOptions{ - Limit: 1, - Descending: latest, - } - boards, err := a.store.GetBoardHistory(boardID, opts) - if err != nil { - return nil, fmt.Errorf("could not get history for board: %w", err) - } - if len(boards) == 0 { - return nil, nil - } - - return boards[0], nil -} - -func (a *App) getBoardDescendantModifiedInfo(boardID string, latest bool) (int64, string, error) { - board, err := a.getBoardHistory(boardID, latest) - if err != nil { - return 0, "", err - } - if board == nil { - return 0, "", fmt.Errorf("history not found for board: %w", err) - } - - var timestamp int64 - modifiedBy := board.ModifiedBy - if latest { - timestamp = board.UpdateAt - } else { - timestamp = board.CreateAt - } - - // use block_history to fetch blocks in case they were deleted and no longer exist in blocks table. - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: latest, - } - blocks, err := a.store.GetBlockHistoryDescendants(boardID, opts) - if err != nil { - return 0, "", fmt.Errorf("could not get blocks history descendants for board: %w", err) - } - if len(blocks) > 0 { - // Compare the board history info with the descendant block info, if it exists - block := blocks[0] - if latest && block.UpdateAt > timestamp { - timestamp = block.UpdateAt - modifiedBy = block.ModifiedBy - } else if !latest && block.CreateAt < timestamp { - timestamp = block.CreateAt - modifiedBy = block.ModifiedBy - } - } - return timestamp, modifiedBy, nil -} - -func (a *App) setBoardCategoryFromSource(sourceBoardID, destinationBoardID, userID, teamID string, asTemplate bool) error { - // find source board's category ID for the user - userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID) - if err != nil { - return err - } - - var destinationCategoryID string - - for _, categoryBoard := range userCategoryBoards { - for _, metadata := range categoryBoard.BoardMetadata { - if metadata.BoardID == sourceBoardID { - // category found! - destinationCategoryID = categoryBoard.ID - break - } - } - } - - if destinationCategoryID == "" { - // if source board is not mapped to a category for this user, - // then move new board to default category - if !asTemplate { - return a.addBoardsToDefaultCategory(userID, teamID, []*model.Board{{ID: destinationBoardID}}) - } - return nil - } - - // now that we have source board's category, - // we send destination board to the same category - return a.AddUpdateUserCategoryBoard(teamID, userID, destinationCategoryID, []string{destinationBoardID}) -} - -func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate) - if err != nil { - return nil, nil, err - } - - // copy any file attachments from the duplicated blocks. - err = a.CopyAndUpdateCardFiles(boardID, userID, bab.Blocks, asTemplate) - if err != nil { - dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab) - if err = a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { - a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(err)) - } - return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err) - } - - if !asTemplate { - for _, board := range bab.Boards { - if categoryErr := a.setBoardCategoryFromSource(boardID, board.ID, userID, toTeam, asTemplate); categoryErr != nil { - return nil, nil, categoryErr - } - } - } - - a.blockChangeNotifier.Enqueue(func() error { - teamID := "" - for _, board := range bab.Boards { - teamID = board.TeamID - a.wsAdapter.BroadcastBoardChange(teamID, board) - } - for _, block := range bab.Blocks { - blk := block - a.wsAdapter.BroadcastBlockChange(teamID, blk) - a.notifyBlockChanged(notify.Add, blk, nil, userID) - } - for _, member := range members { - a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) - } - return nil - }) - - if len(bab.Blocks) != 0 { - go func() { - if uErr := a.UpdateCardLimitTimestamp(); uErr != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after duplicating a board", - mlog.Err(uErr), - ) - } - }() - } - - return bab, members, err -} - -func (a *App) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) { - return a.store.GetBoardsForUserAndTeam(userID, teamID, includePublicBoards) -} - -func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) { - return a.store.GetTemplateBoards(teamID, userID) -} - -func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) { - if board.ID != "" { - return nil, ErrNewBoardCannotHaveID - } - board.ID = utils.NewID(utils.IDTypeBoard) - - var newBoard *model.Board - var member *model.BoardMember - var err error - if addMember { - newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID) - } else { - newBoard, err = a.store.InsertBoard(board, userID) - } - - if err != nil { - return nil, err - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard) - - if newBoard.ChannelID != "" { - members, err := a.GetMembersForBoard(board.ID) - if err != nil { - a.logger.Error("Unable to get the board members", mlog.Err(err)) - } - for _, member := range members { - a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, member.BoardID, member) - } - } else if addMember { - a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member) - } - return nil - }) - - if !board.IsTemplate { - if err := a.addBoardsToDefaultCategory(userID, newBoard.TeamID, []*model.Board{newBoard}); err != nil { - return nil, err - } - } - - return newBoard, nil -} - -func (a *App) addBoardsToDefaultCategory(userID, teamID string, boards []*model.Board) error { - userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID) - if err != nil { - return err - } - - defaultCategoryID := "" - for _, categoryBoard := range userCategoryBoards { - if categoryBoard.Name == defaultCategoryBoards { - defaultCategoryID = categoryBoard.ID - break - } - } - - if defaultCategoryID == "" { - return fmt.Errorf("%w userID: %s", errNoDefaultCategoryFound, userID) - } - - boardIDs := make([]string, len(boards)) - for i := range boards { - boardIDs[i] = boards[i].ID - } - - if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil { - return err - } - - return nil -} - -func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) { - var oldChannelID string - var isTemplate bool - var oldMembers []*model.BoardMember - - if patch.Type != nil || patch.ChannelID != nil { - testChannel := "" - if patch.ChannelID != nil && *patch.ChannelID == "" { - var err error - oldMembers, err = a.GetMembersForBoard(boardID) - if err != nil { - a.logger.Error("Unable to get the board members", mlog.Err(err)) - } - } else if patch.ChannelID != nil && *patch.ChannelID != "" { - testChannel = *patch.ChannelID - } - - board, err := a.store.GetBoard(boardID) - if model.IsErrNotFound(err) { - return nil, model.NewErrNotFound("board ID=" + boardID) - } - if err != nil { - return nil, err - } - oldChannelID = board.ChannelID - isTemplate = board.IsTemplate - if testChannel == "" { - testChannel = oldChannelID - } - - if testChannel != "" { - if !a.permissions.HasPermissionToChannel(userID, testChannel, model.PermissionCreatePost) { - return nil, model.NewErrPermission("access denied to channel") - } - } - } - updatedBoard, err := a.store.PatchBoard(boardID, patch, userID) - if err != nil { - return nil, err - } - - // Post message to channel if linked/unlinked - if patch.ChannelID != nil { - var username string - - user, err := a.store.GetUserByID(userID) - if err != nil { - a.logger.Error("Unable to get the board updater", mlog.Err(err)) - username = "unknown" - } else { - username = user.Username - } - - boardLink := utils.MakeBoardLink(a.config.ServerRoot, updatedBoard.TeamID, updatedBoard.ID) - title := updatedBoard.Title - if title == "" { - title = "Untitled board" // todo: localize this when server has i18n - } - if *patch.ChannelID != "" { - a.postChannelMessage(fmt.Sprintf(linkBoardMessage, username, title, boardLink), updatedBoard.ChannelID) - } else if *patch.ChannelID == "" { - a.postChannelMessage(fmt.Sprintf(unlinkBoardMessage, username, title, boardLink), oldChannelID) - } - } - - // Broadcast Messages to affected users - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard) - - if patch.ChannelID != nil { - if *patch.ChannelID != "" { - members, err := a.GetMembersForBoard(updatedBoard.ID) - if err != nil { - a.logger.Error("Unable to get the board members", mlog.Err(err)) - } - for _, member := range members { - if member.Synthetic { - a.wsAdapter.BroadcastMemberChange(updatedBoard.TeamID, member.BoardID, member) - } - } - } else { - for _, oldMember := range oldMembers { - if oldMember.Synthetic { - a.wsAdapter.BroadcastMemberDelete(updatedBoard.TeamID, boardID, oldMember.UserID) - } - } - } - } - - if patch.Type != nil && isTemplate { - members, err := a.GetMembersForBoard(updatedBoard.ID) - if err != nil { - a.logger.Error("Unable to get the board members", mlog.Err(err)) - } - a.broadcastTeamUsers(updatedBoard.TeamID, updatedBoard.ID, *patch.Type, members) - } - return nil - }) - - return updatedBoard, nil -} - -func (a *App) postChannelMessage(message, channelID string) { - err := a.store.PostMessage(message, "", channelID) - if err != nil { - a.logger.Error("Unable to post the link message to channel", mlog.Err(err)) - } -} - -// broadcastTeamUsers notifies the members of a team when a template changes its type -// from public to private or viceversa. -func (a *App) broadcastTeamUsers(teamID, boardID string, boardType model.BoardType, members []*model.BoardMember) { - users, err := a.GetTeamUsers(teamID, "") - if err != nil { - a.logger.Error("Unable to get the team users", mlog.Err(err)) - } - for _, user := range users { - isMember := false - for _, member := range members { - if member.UserID == user.ID { - isMember = true - break - } - } - if !isMember { - if boardType == model.BoardTypePrivate { - a.wsAdapter.BroadcastMemberDelete(teamID, boardID, user.ID) - } else if boardType == model.BoardTypeOpen { - a.wsAdapter.BroadcastMemberChange(teamID, boardID, &model.BoardMember{UserID: user.ID, BoardID: boardID, SchemeViewer: true, Synthetic: true}) - } - } - } -} - -func (a *App) DeleteBoard(boardID, userID string) error { - board, err := a.store.GetBoard(boardID) - if model.IsErrNotFound(err) { - return nil - } - if err != nil { - return err - } - - if err := a.store.DeleteBoard(boardID, userID); err != nil { - return err - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID) - return nil - }) - - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after deleting a board", - mlog.Err(err), - ) - } - }() - - return nil -} - -func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { - members, err := a.store.GetMembersForBoard(boardID) - if err != nil { - return nil, err - } - - board, err := a.store.GetBoard(boardID) - if err != nil && !model.IsErrNotFound(err) { - return nil, err - } - if board != nil { - for i, m := range members { - if !m.SchemeAdmin { - if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { - members[i].SchemeAdmin = true - } - } - } - } - return members, nil -} - -func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) { - members, err := a.store.GetMembersForUser(userID) - if err != nil { - return nil, err - } - - for i, m := range members { - if !m.SchemeAdmin { - board, err := a.store.GetBoard(m.BoardID) - if err != nil && !model.IsErrNotFound(err) { - return nil, err - } - if board != nil { - if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) { - // if system/team admin - members[i].SchemeAdmin = true - } - } - } - } - return members, nil -} - -func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) { - return a.store.GetMemberForBoard(boardID, userID) -} - -func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) { - board, err := a.store.GetBoard(member.BoardID) - if model.IsErrNotFound(err) { - return nil, nil - } - if err != nil { - return nil, err - } - - existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) - if err != nil && !model.IsErrNotFound(err) { - return nil, err - } - - if existingMembership != nil && !existingMembership.Synthetic { - return existingMembership, nil - } - - newMember, err := a.store.SaveMember(member) - if err != nil { - return nil, err - } - - if !newMember.SchemeAdmin { - if board != nil { - if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) { - newMember.SchemeAdmin = true - } - } - } - - if !board.IsTemplate { - if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil { - return nil, err - } - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) - return nil - }) - - return newMember, nil -} - -func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) { - board, bErr := a.store.GetBoard(member.BoardID) - if model.IsErrNotFound(bErr) { - return nil, nil - } - if bErr != nil { - return nil, bErr - } - - oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) - if model.IsErrNotFound(err) { - return nil, nil - } - if err != nil { - return nil, err - } - - // if we're updating an admin, we need to check that there is at - // least still another admin on the board - if oldMember.SchemeAdmin && !member.SchemeAdmin { - isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID) - if err2 != nil { - return nil, err2 - } - if isLastAdmin { - return nil, model.ErrBoardMemberIsLastAdmin - } - } - - newMember, err := a.store.SaveMember(member) - if err != nil { - return nil, err - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) - return nil - }) - - return newMember, nil -} - -func (a *App) isLastAdmin(userID, boardID string) (bool, error) { - members, err := a.store.GetMembersForBoard(boardID) - if err != nil { - return false, err - } - - for _, m := range members { - if m.SchemeAdmin && m.UserID != userID { - return false, nil - } - } - return true, nil -} - -func (a *App) DeleteBoardMember(boardID, userID string) error { - board, bErr := a.store.GetBoard(boardID) - if model.IsErrNotFound(bErr) { - return nil - } - if bErr != nil { - return bErr - } - - oldMember, err := a.store.GetMemberForBoard(boardID, userID) - if model.IsErrNotFound(err) { - return nil - } - if err != nil { - return err - } - - // if we're removing an admin, we need to check that there is at - // least still another admin on the board - if oldMember.SchemeAdmin { - isLastAdmin, err := a.isLastAdmin(userID, boardID) - if err != nil { - return err - } - if isLastAdmin { - return model.ErrBoardMemberIsLastAdmin - } - } - - if err := a.store.DeleteMember(boardID, userID); err != nil { - return err - } - - a.blockChangeNotifier.Enqueue(func() error { - if syntheticMember, _ := a.GetMemberForBoard(boardID, userID); syntheticMember != nil { - a.wsAdapter.BroadcastMemberChange(board.TeamID, boardID, syntheticMember) - } else { - a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID) - } - return nil - }) - - return nil -} - -func (a *App) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) { - return a.store.SearchBoardsForUser(term, searchField, userID, includePublicBoards) -} - -func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) { - return a.store.SearchBoardsForUserInTeam(teamID, term, userID) -} - -func (a *App) UndeleteBoard(boardID string, modifiedBy string) error { - boards, err := a.store.GetBoardHistory(boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true}) - if err != nil { - return err - } - - if len(boards) == 0 { - // undeleting non-existing board not considered an error - return nil - } - - err = a.store.UndeleteBoard(boardID, modifiedBy) - if err != nil { - return err - } - - board, err := a.store.GetBoard(boardID) - if err != nil { - return err - } - - if board == nil { - a.logger.Error("Error loading the board after undelete, not propagating through websockets or notifications") - return nil - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastBoardChange(board.TeamID, board) - return nil - }) - - go func() { - if err := a.UpdateCardLimitTimestamp(); err != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after undeleting a board", - mlog.Err(err), - ) - } - }() - - return nil -} diff --git a/server/boards/app/boards_and_blocks.go b/server/boards/app/boards_and_blocks.go deleted file mode 100644 index e25ea4ab0b..0000000000 --- a/server/boards/app/boards_and_blocks.go +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) { - var newBab *model.BoardsAndBlocks - var members []*model.BoardMember - var err error - - if addMember { - newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID) - } else { - newBab, err = a.store.CreateBoardsAndBlocks(bab, userID) - } - - if err != nil { - return nil, err - } - - // all new boards should belong to the same team - teamID := newBab.Boards[0].TeamID - - // This can be synchronous because this action is not common - for _, board := range newBab.Boards { - a.wsAdapter.BroadcastBoardChange(teamID, board) - } - - for _, block := range newBab.Blocks { - b := block - a.wsAdapter.BroadcastBlockChange(teamID, b) - a.metrics.IncrementBlocksInserted(1) - a.webhook.NotifyUpdate(b) - a.notifyBlockChanged(notify.Add, b, nil, userID) - } - - if addMember { - for _, member := range members { - a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) - } - } - - if len(newBab.Blocks) != 0 { - go func() { - if uErr := a.UpdateCardLimitTimestamp(); uErr != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after creating boards and blocks", - mlog.Err(uErr), - ) - } - }() - } - - for _, board := range newBab.Boards { - if !board.IsTemplate { - if err := a.addBoardsToDefaultCategory(userID, board.TeamID, []*model.Board{board}); err != nil { - return nil, err - } - } - } - - return newBab, nil -} - -func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { - oldBlocks, err := a.store.GetBlocksByIDs(pbab.BlockIDs) - if err != nil { - return nil, err - } - - if a.IsCloudLimited() { - containsLimitedBlocks, cErr := a.ContainsLimitedBlocks(oldBlocks) - if cErr != nil { - return nil, cErr - } - if containsLimitedBlocks { - return nil, model.ErrPatchUpdatesLimitedCards - } - } - - oldBlocksMap := map[string]*model.Block{} - for _, block := range oldBlocks { - oldBlocksMap[block.ID] = block - } - - bab, err := a.store.PatchBoardsAndBlocks(pbab, userID) - if err != nil { - return nil, err - } - - a.blockChangeNotifier.Enqueue(func() error { - teamID := bab.Boards[0].TeamID - - for _, block := range bab.Blocks { - oldBlock, ok := oldBlocksMap[block.ID] - if !ok { - a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID)) - continue - } - - b := block - a.metrics.IncrementBlocksPatched(1) - a.wsAdapter.BroadcastBlockChange(teamID, b) - a.webhook.NotifyUpdate(b) - a.notifyBlockChanged(notify.Update, b, oldBlock, userID) - } - - for _, board := range bab.Boards { - a.wsAdapter.BroadcastBoardChange(board.TeamID, board) - } - return nil - }) - - return bab, nil -} - -func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error { - firstBoard, err := a.store.GetBoard(dbab.Boards[0]) - if err != nil { - return err - } - - // we need the block entity to notify of the block changes, so we - // fetch and store the blocks first - blocks := []*model.Block{} - for _, blockID := range dbab.Blocks { - block, err := a.store.GetBlock(blockID) - if err != nil { - return err - } - blocks = append(blocks, block) - } - - if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { - return err - } - - a.blockChangeNotifier.Enqueue(func() error { - for _, block := range blocks { - a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID) - a.metrics.IncrementBlocksDeleted(1) - a.notifyBlockChanged(notify.Update, block, block, userID) - } - - for _, boardID := range dbab.Boards { - a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID) - } - return nil - }) - - if len(dbab.Blocks) != 0 { - go func() { - if uErr := a.UpdateCardLimitTimestamp(); uErr != nil { - a.logger.Error( - "UpdateCardLimitTimestamp failed after deleting boards and blocks", - mlog.Err(uErr), - ) - } - }() - } - - return nil -} diff --git a/server/boards/app/boards_test.go b/server/boards/app/boards_test.go deleted file mode 100644 index a0390a5935..0000000000 --- a/server/boards/app/boards_test.go +++ /dev/null @@ -1,780 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/assert" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestAddMemberToBoard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_1" - - boardMember := &model.BoardMember{ - BoardID: boardID, - UserID: userID, - SchemeEditor: true, - } - - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: "board_id_1", - TeamID: "team_id_1", - }, nil) - - th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(nil, nil) - - th.Store.EXPECT().SaveMember(mock.MatchedBy(func(i interface{}) bool { - p := i.(*model.BoardMember) - return p.BoardID == boardID && p.UserID == userID - })).Return(&model.BoardMember{ - BoardID: boardID, - }, nil) - - // for WS change broadcast - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "default_category_id", - Name: "Boards", - Type: "system", - }, - }, - }, nil).Times(2) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) - - addedBoardMember, err := th.App.AddMemberToBoard(boardMember) - require.NoError(t, err) - require.Equal(t, boardID, addedBoardMember.BoardID) - }) - - t.Run("return existing non-synthetic membership if any", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_1" - - boardMember := &model.BoardMember{ - BoardID: boardID, - UserID: userID, - SchemeEditor: true, - } - - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - TeamID: "team_id_1", - }, nil) - - th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(&model.BoardMember{ - UserID: userID, - BoardID: boardID, - Synthetic: false, - }, nil) - - addedBoardMember, err := th.App.AddMemberToBoard(boardMember) - require.NoError(t, err) - require.Equal(t, boardID, addedBoardMember.BoardID) - }) - - t.Run("should convert synthetic membership into natural membership", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_1" - - boardMember := &model.BoardMember{ - BoardID: boardID, - UserID: userID, - SchemeEditor: true, - } - - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: "board_id_1", - TeamID: "team_id_1", - }, nil) - - th.Store.EXPECT().GetMemberForBoard(boardID, userID).Return(&model.BoardMember{ - UserID: userID, - BoardID: boardID, - Synthetic: true, - }, nil) - - th.Store.EXPECT().SaveMember(mock.MatchedBy(func(i interface{}) bool { - p := i.(*model.BoardMember) - return p.BoardID == boardID && p.UserID == userID - })).Return(&model.BoardMember{ - UserID: userID, - BoardID: boardID, - Synthetic: false, - }, nil) - - // for WS change broadcast - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "default_category_id", - Name: "Boards", - Type: "system", - }, - }, - }, nil).Times(2) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil) - th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1) - - addedBoardMember, err := th.App.AddMemberToBoard(boardMember) - require.NoError(t, err) - require.Equal(t, boardID, addedBoardMember.BoardID) - }) -} - -func TestPatchBoard(t *testing.T) { - t.Skip("MM-51699") - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case, title patch", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_1" - const teamID = "team_id_1" - - patchTitle := "Patched Title" - patch := &model.BoardPatch{ - Title: &patchTitle, - } - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - Title: patchTitle, - }, - nil) - - // for WS BroadcastBoardChange - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(1) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, patchTitle, patchedBoard.Title) - }) - - t.Run("patch type open, no users", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypeOpen - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(2) - - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) - th.Store.EXPECT().GetUserByID(userID).Return(&model.User{ID: userID, Username: "UserName"}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 2 times - // - for WS BroadcastBoardChange - // - for AddTeamMembers check - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - - t.Run("patch type private, no users", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypePrivate - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(2) - - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 2 times - // - for WS BroadcastBoardChange - // - for AddTeamMembers check - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - - t.Run("patch type open, single user", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypeOpen - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(2) - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 3 times - // for WS BroadcastBoardChange - // for AddTeamMembers check - // for WS BroadcastMemberChange - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(3) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - - t.Run("patch type private, single user", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypePrivate - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(2) - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 3 times - // for WS BroadcastBoardChange - // for AddTeamMembers check - // for WS BroadcastMemberChange - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(3) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - - t.Run("patch type open, user with member", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypeOpen - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(3) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 2 times - // for WS BroadcastBoardChange - // for AddTeamMembers check - // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(2) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - - t.Run("patch type private, user with member", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - patchType := model.BoardTypePrivate - patch := &model.BoardPatch{ - Type: &patchType, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - ChannelID: "", - }, nil).Times(1) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - - // Type not null will retrieve team members - th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 2 times - // for WS BroadcastBoardChange - // for AddTeamMembers check - // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(2) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) - t.Run("patch type channel, user without post permissions", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - channelID := "myChannel" - patchType := model.BoardTypeOpen - patch := &model.BoardPatch{ - Type: &patchType, - ChannelID: &channelID, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - }, nil).Times(1) - - th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) - _, err := th.App.PatchBoard(patch, boardID, userID) - require.Error(t, err) - }) - - t.Run("patch type channel, user with post permissions", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - channelID := "myChannel" - patch := &model.BoardPatch{ - ChannelID: &channelID, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - }, nil).Times(2) - - th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1) - - th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return( - &model.Board{ - ID: boardID, - TeamID: teamID, - }, - nil) - - // Should call GetMembersForBoard 2 times - // - for WS BroadcastBoardChange - // - for AddTeamMembers check - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2) - - th.Store.EXPECT().PostMessage(utils.Anything, "", "").Times(1) - - patchedBoard, err := th.App.PatchBoard(patch, boardID, userID) - require.NoError(t, err) - require.Equal(t, boardID, patchedBoard.ID) - }) -} - -func TestPatchBoard2(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("patch type remove channel, user without post permissions", func(t *testing.T) { - const boardID = "board_id_1" - const userID = "user_id_2" - const teamID = "team_id_1" - - const channelID = "myChannel" - clearChannel := "" - patchType := model.BoardTypeOpen - patch := &model.BoardPatch{ - Type: &patchType, - ChannelID: &clearChannel, - } - - // Type not nil, will cause board to be reteived - // to check isTemplate - th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{ - ID: boardID, - TeamID: teamID, - IsTemplate: true, - ChannelID: channelID, - }, nil).Times(2) - - th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - // Should call GetMembersForBoard 2 times - // for WS BroadcastBoardChange - // for AddTeamMembers check - // We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).AnyTimes() - - _, err := th.App.PatchBoard(patch, boardID, userID) - require.Error(t, err) - }) -} - -func TestGetBoardCount(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - boardCount := int64(100) - th.Store.EXPECT().GetBoardCount().Return(boardCount, nil) - - count, err := th.App.GetBoardCount() - require.NoError(t, err) - require.Equal(t, boardCount, count) - }) -} - -func TestBoardCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("no boards default category exists", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "category_id_1", Name: "Category 1"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_1"}, - {BoardID: "board_id_2"}, - }, - }, - { - Category: model.Category{ID: "category_id_2", Name: "Category 2"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_3"}, - }, - }, - { - Category: model.Category{ID: "category_id_3", Name: "Category 3"}, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - }, nil).Times(1) - - // when this function is called the second time, the default category is created - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "category_id_1", Name: "Category 1"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_1"}, - {BoardID: "board_id_2"}, - }, - }, - { - Category: model.Category{ID: "category_id_2", Name: "Category 2"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_3"}, - }, - }, - { - Category: model.Category{ID: "category_id_3", Name: "Category 3"}, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - { - Category: model.Category{ID: "default_category_id", Type: model.CategoryTypeSystem, Name: "Boards"}, - }, - }, nil).Times(1) - - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "default_category_id", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "default_category_id", []string{ - "board_id_1", - "board_id_2", - "board_id_3", - }).Return(nil) - - boards := []*model.Board{ - {ID: "board_id_1"}, - {ID: "board_id_2"}, - {ID: "board_id_3"}, - } - - err := th.App.addBoardsToDefaultCategory("user_id", "team_id", boards) - assert.NoError(t, err) - }) -} - -func TestDuplicateBoard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - board := &model.Board{ - ID: "board_id_2", - Title: "Duplicated Board", - } - - block := &model.Block{ - ID: "block_id_1", - Type: "image", - } - - th.Store.EXPECT().DuplicateBoard("board_id_1", "user_id_1", "team_id_1", false).Return( - &model.BoardsAndBlocks{ - Boards: []*model.Board{ - board, - }, - Blocks: []*model.Block{ - block, - }, - }, - []*model.BoardMember{}, - nil, - ) - - th.Store.EXPECT().GetBoard("board_id_1").Return(&model.Board{}, nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - }, - }, nil).Times(3) - - th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "category_id_1", utils.Anything).Return(nil) - - // for WS change broadcast - th.Store.EXPECT().GetMembersForBoard(utils.Anything).Return([]*model.BoardMember{}, nil).Times(2) - - bab, members, err := th.App.DuplicateBoard("board_id_1", "user_id_1", "team_id_1", false) - assert.NoError(t, err) - assert.NotNil(t, bab) - assert.NotNil(t, members) - }) - - t.Run("duplicating board as template should not set it's category", func(t *testing.T) { - board := &model.Board{ - ID: "board_id_2", - Title: "Duplicated Board", - } - - block := &model.Block{ - ID: "block_id_1", - Type: "image", - } - - th.Store.EXPECT().DuplicateBoard("board_id_1", "user_id_1", "team_id_1", true).Return( - &model.BoardsAndBlocks{ - Boards: []*model.Board{ - board, - }, - Blocks: []*model.Block{ - block, - }, - }, - []*model.BoardMember{}, - nil, - ) - - th.Store.EXPECT().GetBoard("board_id_1").Return(&model.Board{}, nil) - - // for WS change broadcast - th.Store.EXPECT().GetMembersForBoard(utils.Anything).Return([]*model.BoardMember{}, nil).Times(2) - - bab, members, err := th.App.DuplicateBoard("board_id_1", "user_id_1", "team_id_1", true) - assert.NoError(t, err) - assert.NotNil(t, bab) - assert.NotNil(t, members) - }) -} - -func TestGetMembersForBoard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - const boardID = "board_id_1" - const userID = "user_id_1" - const teamID = "team_id_1" - - th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{ - { - BoardID: boardID, - UserID: userID, - SchemeEditor: true, - }, - }, nil).Times(3) - th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1) - t.Run("-base case", func(t *testing.T) { - members, err := th.App.GetMembersForBoard(boardID) - assert.NoError(t, err) - assert.NotNil(t, members) - assert.False(t, members[0].SchemeAdmin) - }) - - board := &model.Board{ - ID: boardID, - TeamID: teamID, - } - th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - - t.Run("-team check false ", func(t *testing.T) { - members, err := th.App.GetMembersForBoard(boardID) - assert.NoError(t, err) - assert.NotNil(t, members) - - assert.False(t, members[0].SchemeAdmin) - }) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) - t.Run("-team check true", func(t *testing.T) { - members, err := th.App.GetMembersForBoard(boardID) - assert.NoError(t, err) - assert.NotNil(t, members) - - assert.True(t, members[0].SchemeAdmin) - }) -} - -func TestGetMembersForUser(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - const boardID = "board_id_1" - const userID = "user_id_1" - const teamID = "team_id_1" - - th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{ - { - BoardID: boardID, - UserID: userID, - SchemeEditor: true, - }, - }, nil).Times(3) - th.Store.EXPECT().GetBoard(boardID).Return(nil, nil) - t.Run("-base case", func(t *testing.T) { - members, err := th.App.GetMembersForUser(userID) - assert.NoError(t, err) - assert.NotNil(t, members) - assert.False(t, members[0].SchemeAdmin) - }) - - board := &model.Board{ - ID: boardID, - TeamID: teamID, - } - th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - t.Run("-team check false ", func(t *testing.T) { - members, err := th.App.GetMembersForUser(userID) - assert.NoError(t, err) - assert.NotNil(t, members) - - assert.False(t, members[0].SchemeAdmin) - }) - - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) - t.Run("-team check true", func(t *testing.T) { - members, err := th.App.GetMembersForUser(userID) - assert.NoError(t, err) - assert.NotNil(t, members) - - assert.True(t, members[0].SchemeAdmin) - }) -} diff --git a/server/boards/app/cards.go b/server/boards/app/cards.go deleted file mode 100644 index 2086d6eb26..0000000000 --- a/server/boards/app/cards.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func (a *App) CreateCard(card *model.Card, boardID string, userID string, disableNotify bool) (*model.Card, error) { - // Convert the card struct to a block and insert the block. - now := utils.GetMillis() - - card.ID = utils.NewID(utils.IDTypeCard) - card.BoardID = boardID - card.CreatedBy = userID - card.ModifiedBy = userID - card.CreateAt = now - card.UpdateAt = now - card.DeleteAt = 0 - - block := model.Card2Block(card) - - newBlocks, err := a.InsertBlocksAndNotify([]*model.Block{block}, userID, disableNotify) - if err != nil { - return nil, fmt.Errorf("cannot create card: %w", err) - } - - newCard, err := model.Block2Card(newBlocks[0]) - if err != nil { - return nil, err - } - - return newCard, nil -} - -func (a *App) GetCardsForBoard(boardID string, page int, perPage int) ([]*model.Card, error) { - opts := model.QueryBlocksOptions{ - BoardID: boardID, - BlockType: model.TypeCard, - Page: page, - PerPage: perPage, - } - - blocks, err := a.store.GetBlocks(opts) - if err != nil { - return nil, err - } - - cards := make([]*model.Card, 0, len(blocks)) - var card *model.Card - for _, blk := range blocks { - b := blk - if card, err = model.Block2Card(b); err != nil { - return nil, fmt.Errorf("Block2Card fail: %w", err) - } - cards = append(cards, card) - } - return cards, nil -} - -func (a *App) PatchCard(cardPatch *model.CardPatch, cardID string, userID string, disableNotify bool) (*model.Card, error) { - blockPatch, err := model.CardPatch2BlockPatch(cardPatch) - if err != nil { - return nil, err - } - - newBlock, err := a.PatchBlockAndNotify(cardID, blockPatch, userID, disableNotify) - if err != nil { - return nil, fmt.Errorf("cannot patch card %s: %w", cardID, err) - } - - newCard, err := model.Block2Card(newBlock) - if err != nil { - return nil, err - } - - return newCard, nil -} - -func (a *App) GetCardByID(cardID string) (*model.Card, error) { - cardBlock, err := a.GetBlockByID(cardID) - if err != nil { - return nil, err - } - - card, err := model.Block2Card(cardBlock) - if err != nil { - return nil, err - } - - return card, nil -} diff --git a/server/boards/app/cards_test.go b/server/boards/app/cards_test.go deleted file mode 100644 index 38dbf587d6..0000000000 --- a/server/boards/app/cards_test.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - "reflect" - "testing" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestCreateCard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - } - userID := utils.NewID(utils.IDTypeUser) - - props := makeProps(3) - - card := &model.Card{ - BoardID: board.ID, - CreatedBy: userID, - ModifiedBy: userID, - Title: "test card", - ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)}, - Properties: props, - } - block := model.Card2Block(card) - - t.Run("success scenario", func(t *testing.T) { - th.Store.EXPECT().GetBoard(board.ID).Return(board, nil) - th.Store.EXPECT().InsertBlock(gomock.AssignableToTypeOf(reflect.TypeOf(block)), userID).Return(nil) - th.Store.EXPECT().GetMembersForBoard(board.ID).Return([]*model.BoardMember{}, nil) - - newCard, err := th.App.CreateCard(card, board.ID, userID, false) - - require.NoError(t, err) - require.Equal(t, card.BoardID, newCard.BoardID) - require.Equal(t, card.Title, newCard.Title) - require.Equal(t, card.ContentOrder, newCard.ContentOrder) - require.EqualValues(t, card.Properties, newCard.Properties) - }) - - t.Run("error scenario", func(t *testing.T) { - th.Store.EXPECT().GetBoard(board.ID).Return(board, nil) - th.Store.EXPECT().InsertBlock(gomock.AssignableToTypeOf(reflect.TypeOf(block)), userID).Return(blockError{"error"}) - - newCard, err := th.App.CreateCard(card, board.ID, userID, false) - - require.Error(t, err, "error") - require.Nil(t, newCard) - }) -} - -func TestGetCards(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - } - - const cardCount = 25 - - // make some cards - blocks := make([]*model.Block, 0, cardCount) - for i := 0; i < cardCount; i++ { - card := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - ParentID: board.ID, - Schema: 1, - Type: model.TypeCard, - Title: fmt.Sprintf("card %d", i), - BoardID: board.ID, - } - blocks = append(blocks, card) - } - - t.Run("success scenario", func(t *testing.T) { - opts := model.QueryBlocksOptions{ - BoardID: board.ID, - BlockType: model.TypeCard, - } - - th.Store.EXPECT().GetBlocks(opts).Return(blocks, nil) - - cards, err := th.App.GetCardsForBoard(board.ID, 0, 0) - require.NoError(t, err) - assert.Len(t, cards, cardCount) - }) - - t.Run("error scenario", func(t *testing.T) { - opts := model.QueryBlocksOptions{ - BoardID: board.ID, - BlockType: model.TypeCard, - } - - th.Store.EXPECT().GetBlocks(opts).Return(nil, blockError{"error"}) - - cards, err := th.App.GetCardsForBoard(board.ID, 0, 0) - require.Error(t, err) - require.Nil(t, cards) - }) -} - -func TestPatchCard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - } - userID := utils.NewID(utils.IDTypeUser) - - props := makeProps(3) - - card := &model.Card{ - BoardID: board.ID, - CreatedBy: userID, - ModifiedBy: userID, - Title: "test card for patch", - ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)}, - Properties: copyProps(props), - } - - newTitle := "patched" - newIcon := "😀" - newContentOrder := reverse(card.ContentOrder) - - cardPatch := &model.CardPatch{ - Title: &newTitle, - ContentOrder: &newContentOrder, - Icon: &newIcon, - UpdatedProperties: modifyProps(props), - } - - t.Run("success scenario", func(t *testing.T) { - expectedPatchedCard := cardPatch.Patch(card) - expectedPatchedBlock := model.Card2Block(expectedPatchedCard) - - var blockPatch *model.BlockPatch - th.Store.EXPECT().GetBoard(board.ID).Return(board, nil) - th.Store.EXPECT().PatchBlock(card.ID, gomock.AssignableToTypeOf(reflect.TypeOf(blockPatch)), userID).Return(nil) - th.Store.EXPECT().GetMembersForBoard(board.ID).Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().GetBlock(card.ID).Return(expectedPatchedBlock, nil).AnyTimes() - - patchedCard, err := th.App.PatchCard(cardPatch, card.ID, userID, false) - - require.NoError(t, err) - require.Equal(t, board.ID, patchedCard.BoardID) - require.Equal(t, newTitle, patchedCard.Title) - require.Equal(t, newIcon, patchedCard.Icon) - require.Equal(t, newContentOrder, patchedCard.ContentOrder) - require.EqualValues(t, expectedPatchedCard.Properties, patchedCard.Properties) - }) - - t.Run("error scenario", func(t *testing.T) { - var blockPatch *model.BlockPatch - th.Store.EXPECT().GetBoard(board.ID).Return(board, nil) - th.Store.EXPECT().PatchBlock(card.ID, gomock.AssignableToTypeOf(reflect.TypeOf(blockPatch)), userID).Return(blockError{"error"}) - - patchedCard, err := th.App.PatchCard(cardPatch, card.ID, userID, false) - - require.Error(t, err, "error") - require.Nil(t, patchedCard) - }) -} - -func TestGetCard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - boardID := utils.NewID(utils.IDTypeBoard) - userID := utils.NewID(utils.IDTypeUser) - props := makeProps(5) - contentOrder := []string{utils.NewID(utils.IDTypeUser), utils.NewID(utils.IDTypeUser)} - fields := make(map[string]any) - fields["contentOrder"] = contentOrder - fields["properties"] = props - fields["icon"] = "😀" - fields["isTemplate"] = true - - block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - ParentID: boardID, - Type: model.TypeCard, - Title: "test card", - BoardID: boardID, - Fields: fields, - CreatedBy: userID, - ModifiedBy: userID, - } - - t.Run("success scenario", func(t *testing.T) { - th.Store.EXPECT().GetBlock(block.ID).Return(block, nil) - - card, err := th.App.GetCardByID(block.ID) - - require.NoError(t, err) - require.Equal(t, boardID, card.BoardID) - require.Equal(t, block.Title, card.Title) - require.Equal(t, "😀", card.Icon) - require.Equal(t, true, card.IsTemplate) - require.Equal(t, contentOrder, card.ContentOrder) - require.EqualValues(t, props, card.Properties) - }) - - t.Run("not found", func(t *testing.T) { - bogusID := utils.NewID(utils.IDTypeBlock) - th.Store.EXPECT().GetBlock(bogusID).Return(nil, model.NewErrNotFound(bogusID)) - - card, err := th.App.GetCardByID(bogusID) - - require.Error(t, err, "error") - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, card) - }) - - t.Run("error scenario", func(t *testing.T) { - th.Store.EXPECT().GetBlock(block.ID).Return(nil, blockError{"error"}) - - card, err := th.App.GetCardByID(block.ID) - - require.Error(t, err, "error") - require.Nil(t, card) - }) -} - -// reverse is a helper function to copy and reverse a slice of strings. -func reverse(src []string) []string { - out := make([]string, 0, len(src)) - for i := len(src) - 1; i >= 0; i-- { - out = append(out, src[i]) - } - return out -} - -func makeProps(count int) map[string]any { - props := make(map[string]any) - for i := 0; i < count; i++ { - props[utils.NewID(utils.IDTypeBlock)] = utils.NewID(utils.IDTypeBlock) - } - return props -} - -func copyProps(m map[string]any) map[string]any { - out := make(map[string]any) - for k, v := range m { - out[k] = v - } - return out -} - -func modifyProps(m map[string]any) map[string]any { - out := make(map[string]any) - for k := range m { - out[k] = utils.NewID(utils.IDTypeBlock) - } - return out -} diff --git a/server/boards/app/category.go b/server/boards/app/category.go deleted file mode 100644 index ef769fd934..0000000000 --- a/server/boards/app/category.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var errCategoryNotFound = errors.New("category ID specified in input does not exist for user") -var errCategoriesLengthMismatch = errors.New("cannot update category order, passed list of categories different size than in database") -var ErrCannotDeleteSystemCategory = errors.New("cannot delete a system category") -var ErrCannotUpdateSystemCategory = errors.New("cannot update a system category") - -func (a *App) GetCategory(categoryID string) (*model.Category, error) { - return a.store.GetCategory(categoryID) -} - -func (a *App) CreateCategory(category *model.Category) (*model.Category, error) { - category.Hydrate() - if err := category.IsValid(); err != nil { - return nil, err - } - - if err := a.store.CreateCategory(*category); err != nil { - return nil, err - } - - createdCategory, err := a.store.GetCategory(category.ID) - if err != nil { - return nil, err - } - - go func() { - a.wsAdapter.BroadcastCategoryChange(*createdCategory) - }() - - return createdCategory, nil -} - -func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) { - category.Hydrate() - - if err := category.IsValid(); err != nil { - return nil, err - } - - // verify if category belongs to the user - existingCategory, err := a.store.GetCategory(category.ID) - if err != nil { - return nil, err - } - - if existingCategory.DeleteAt != 0 { - return nil, model.ErrCategoryDeleted - } - - if existingCategory.UserID != category.UserID { - return nil, model.ErrCategoryPermissionDenied - } - - if existingCategory.TeamID != category.TeamID { - return nil, model.ErrCategoryPermissionDenied - } - - // in case type was defaulted above, set to existingCategory.Type - category.Type = existingCategory.Type - if existingCategory.Type == model.CategoryTypeSystem { - // You cannot rename or delete a system category, - // So restoring its name and undeleting it if set so. - category.Name = existingCategory.Name - category.DeleteAt = 0 - } - - category.UpdateAt = utils.GetMillis() - if err = category.IsValid(); err != nil { - return nil, err - } - if err = a.store.UpdateCategory(*category); err != nil { - return nil, err - } - - updatedCategory, err := a.store.GetCategory(category.ID) - if err != nil { - return nil, err - } - - go func() { - a.wsAdapter.BroadcastCategoryChange(*updatedCategory) - }() - - return updatedCategory, nil -} - -func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) { - existingCategory, err := a.store.GetCategory(categoryID) - if err != nil { - return nil, err - } - - // category is already deleted. This avoids - // overriding the original deleted at timestamp - if existingCategory.DeleteAt != 0 { - return existingCategory, nil - } - - // verify if category belongs to the user - if existingCategory.UserID != userID { - return nil, model.ErrCategoryPermissionDenied - } - - // verify if category belongs to the team - if existingCategory.TeamID != teamID { - return nil, model.NewErrInvalidCategory("category doesn't belong to the team") - } - - if existingCategory.Type == model.CategoryTypeSystem { - return nil, ErrCannotDeleteSystemCategory - } - - if err = a.moveBoardsToDefaultCategory(userID, teamID, categoryID); err != nil { - return nil, err - } - - if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil { - return nil, err - } - - deletedCategory, err := a.store.GetCategory(categoryID) - if err != nil { - return nil, err - } - - go func() { - a.wsAdapter.BroadcastCategoryChange(*deletedCategory) - }() - - return deletedCategory, nil -} - -func (a *App) moveBoardsToDefaultCategory(userID, teamID, sourceCategoryID string) error { - // we need a list of boards associated to this category - // so we can move them to user's default Boards category - categoryBoards, err := a.GetUserCategoryBoards(userID, teamID) - if err != nil { - return err - } - - var sourceCategoryBoards *model.CategoryBoards - defaultCategoryID := "" - - // iterate user's categories to find the source category - // and the default category. - // We need source category to get the list of its board - // and the default category to know its ID to - // move source category's boards to. - for i := range categoryBoards { - if categoryBoards[i].ID == sourceCategoryID { - sourceCategoryBoards = &categoryBoards[i] - } - - if categoryBoards[i].Name == defaultCategoryBoards { - defaultCategoryID = categoryBoards[i].ID - } - - // if both categories are found, no need to iterate furthur. - if sourceCategoryBoards != nil && defaultCategoryID != "" { - break - } - } - - if sourceCategoryBoards == nil { - return errCategoryNotFound - } - - if defaultCategoryID == "" { - return fmt.Errorf("moveBoardsToDefaultCategory: %w", errNoDefaultCategoryFound) - } - - boardIDs := make([]string, len(sourceCategoryBoards.BoardMetadata)) - for i := range sourceCategoryBoards.BoardMetadata { - boardIDs[i] = sourceCategoryBoards.BoardMetadata[i].BoardID - } - - if err := a.AddUpdateUserCategoryBoard(teamID, userID, defaultCategoryID, boardIDs); err != nil { - return fmt.Errorf("moveBoardsToDefaultCategory: %w", err) - } - - return nil -} - -func (a *App) ReorderCategories(userID, teamID string, newCategoryOrder []string) ([]string, error) { - if err := a.verifyNewCategoriesMatchExisting(userID, teamID, newCategoryOrder); err != nil { - return nil, err - } - - newOrder, err := a.store.ReorderCategories(userID, teamID, newCategoryOrder) - if err != nil { - return nil, err - } - - go func() { - a.wsAdapter.BroadcastCategoryReorder(teamID, userID, newOrder) - }() - - return newOrder, nil -} - -func (a *App) verifyNewCategoriesMatchExisting(userID, teamID string, newCategoryOrder []string) error { - existingCategories, err := a.store.GetUserCategories(userID, teamID) - if err != nil { - return err - } - - if len(newCategoryOrder) != len(existingCategories) { - return fmt.Errorf( - "%w length new categories: %d, length existing categories: %d, userID: %s, teamID: %s", - errCategoriesLengthMismatch, - len(newCategoryOrder), - len(existingCategories), - userID, - teamID, - ) - } - - existingCategoriesMap := map[string]bool{} - for _, category := range existingCategories { - existingCategoriesMap[category.ID] = true - } - - for _, newCategoryID := range newCategoryOrder { - if _, found := existingCategoriesMap[newCategoryID]; !found { - return fmt.Errorf( - "%w specified category ID: %s, userID: %s, teamID: %s", - errCategoryNotFound, - newCategoryID, - userID, - teamID, - ) - } - } - - return nil -} diff --git a/server/boards/app/category_boards.go b/server/boards/app/category_boards.go deleted file mode 100644 index ebf570f01e..0000000000 --- a/server/boards/app/category_boards.go +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -const defaultCategoryBoards = "Boards" - -var errCategoryBoardsLengthMismatch = errors.New("cannot update category boards order, passed list of categories boards different size than in database") -var errBoardNotFoundInCategory = errors.New("specified board ID not found in specified category ID") -var errBoardMembershipNotFound = errors.New("board membership not found for user's board") - -func (a *App) GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) { - categoryBoards, err := a.store.GetUserCategoryBoards(userID, teamID) - if err != nil { - return nil, err - } - - createdCategoryBoards, err := a.createDefaultCategoriesIfRequired(categoryBoards, userID, teamID) - if err != nil { - return nil, err - } - - categoryBoards = append(categoryBoards, createdCategoryBoards...) - return categoryBoards, nil -} - -func (a *App) createDefaultCategoriesIfRequired(existingCategoryBoards []model.CategoryBoards, userID, teamID string) ([]model.CategoryBoards, error) { - createdCategories := []model.CategoryBoards{} - - boardsCategoryExist := false - for _, categoryBoard := range existingCategoryBoards { - if categoryBoard.Name == defaultCategoryBoards { - boardsCategoryExist = true - } - } - - if !boardsCategoryExist { - createdCategoryBoards, err := a.createBoardsCategory(userID, teamID, existingCategoryBoards) - if err != nil { - return nil, err - } - - createdCategories = append(createdCategories, *createdCategoryBoards) - } - - return createdCategories, nil -} - -func (a *App) createBoardsCategory(userID, teamID string, existingCategoryBoards []model.CategoryBoards) (*model.CategoryBoards, error) { - // create the category - category := model.Category{ - Name: defaultCategoryBoards, - UserID: userID, - TeamID: teamID, - Collapsed: false, - Type: model.CategoryTypeSystem, - SortOrder: len(existingCategoryBoards) * model.CategoryBoardsSortOrderGap, - } - createdCategory, err := a.CreateCategory(&category) - if err != nil { - return nil, fmt.Errorf("createBoardsCategory default category creation failed: %w", err) - } - - // once the category is created, we need to move all boards which do not - // belong to any category, into this category. - - boardMembers, err := a.GetMembersForUser(userID) - if err != nil { - return nil, fmt.Errorf("createBoardsCategory error fetching user's board memberships: %w", err) - } - - boardMemberByBoardID := map[string]*model.BoardMember{} - for _, boardMember := range boardMembers { - boardMemberByBoardID[boardMember.BoardID] = boardMember - } - - createdCategoryBoards := &model.CategoryBoards{ - Category: *createdCategory, - BoardMetadata: []model.CategoryBoardMetadata{}, - } - - // get user's current team's baords - userTeamBoards, err := a.GetBoardsForUserAndTeam(userID, teamID, false) - if err != nil { - return nil, fmt.Errorf("createBoardsCategory error fetching user's team's boards: %w", err) - } - - boardIDsToAdd := []string{} - - for _, board := range userTeamBoards { - boardMembership, ok := boardMemberByBoardID[board.ID] - if !ok { - return nil, fmt.Errorf("createBoardsCategory: %w", errBoardMembershipNotFound) - } - - // boards with implicit access (aka synthetic membership), - // should show up in LHS only when openign them explicitelly. - // So we don't process any synthetic membership boards - // and only add boards with explicit access to, to the the LHS, - // for example, if a user explicitelly added another user to a board. - if boardMembership.Synthetic { - continue - } - - belongsToCategory := false - - for _, categoryBoard := range existingCategoryBoards { - for _, metadata := range categoryBoard.BoardMetadata { - if metadata.BoardID == board.ID { - belongsToCategory = true - break - } - } - - // stop looking into other categories if - // the board was found in a category - if belongsToCategory { - break - } - } - - if !belongsToCategory { - boardIDsToAdd = append(boardIDsToAdd, board.ID) - newBoardMetadata := model.CategoryBoardMetadata{ - BoardID: board.ID, - Hidden: false, - } - createdCategoryBoards.BoardMetadata = append(createdCategoryBoards.BoardMetadata, newBoardMetadata) - } - } - - if len(boardIDsToAdd) > 0 { - if err := a.AddUpdateUserCategoryBoard(teamID, userID, createdCategory.ID, boardIDsToAdd); err != nil { - return nil, fmt.Errorf("createBoardsCategory failed to add category-less board to the default category, defaultCategoryID: %s, error: %w", createdCategory.ID, err) - } - } - - return createdCategoryBoards, nil -} - -func (a *App) AddUpdateUserCategoryBoard(teamID, userID, categoryID string, boardIDs []string) error { - if len(boardIDs) == 0 { - return nil - } - - err := a.store.AddUpdateCategoryBoard(userID, categoryID, boardIDs) - if err != nil { - return err - } - - userCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID) - if err != nil { - return err - } - - var updatedCategory *model.CategoryBoards - for i := range userCategoryBoards { - if userCategoryBoards[i].ID == categoryID { - updatedCategory = &userCategoryBoards[i] - break - } - } - - if updatedCategory == nil { - return errCategoryNotFound - } - - wsPayload := make([]*model.BoardCategoryWebsocketData, len(updatedCategory.BoardMetadata)) - i := 0 - for _, categoryBoardMetadata := range updatedCategory.BoardMetadata { - wsPayload[i] = &model.BoardCategoryWebsocketData{ - BoardID: categoryBoardMetadata.BoardID, - CategoryID: categoryID, - Hidden: categoryBoardMetadata.Hidden, - } - i++ - } - - a.blockChangeNotifier.Enqueue(func() error { - a.wsAdapter.BroadcastCategoryBoardChange( - teamID, - userID, - wsPayload, - ) - return nil - }) - - return nil -} - -func (a *App) ReorderCategoryBoards(userID, teamID, categoryID string, newBoardsOrder []string) ([]string, error) { - if err := a.verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID, newBoardsOrder); err != nil { - return nil, err - } - - newOrder, err := a.store.ReorderCategoryBoards(categoryID, newBoardsOrder) - if err != nil { - return nil, err - } - - go func() { - a.wsAdapter.BroadcastCategoryBoardsReorder(teamID, userID, categoryID, newOrder) - }() - - return newOrder, nil -} - -func (a *App) verifyNewCategoryBoardsMatchExisting(userID, teamID, categoryID string, newBoardsOrder []string) error { - // this function is to ensure that we don't miss specifying - // all boards of the category while reordering. - existingCategoryBoards, err := a.GetUserCategoryBoards(userID, teamID) - if err != nil { - return err - } - - var targetCategoryBoards *model.CategoryBoards - for i := range existingCategoryBoards { - if existingCategoryBoards[i].Category.ID == categoryID { - targetCategoryBoards = &existingCategoryBoards[i] - break - } - } - - if targetCategoryBoards == nil { - return fmt.Errorf("%w categoryID: %s", errCategoryNotFound, categoryID) - } - - if len(targetCategoryBoards.BoardMetadata) != len(newBoardsOrder) { - return fmt.Errorf( - "%w length new category boards: %d, length existing category boards: %d, userID: %s, teamID: %s, categoryID: %s", - errCategoryBoardsLengthMismatch, - len(newBoardsOrder), - len(targetCategoryBoards.BoardMetadata), - userID, - teamID, - categoryID, - ) - } - - existingBoardMap := map[string]bool{} - for _, metadata := range targetCategoryBoards.BoardMetadata { - existingBoardMap[metadata.BoardID] = true - } - - for _, boardID := range newBoardsOrder { - if _, found := existingBoardMap[boardID]; !found { - return fmt.Errorf( - "%w board ID: %s, category ID: %s, userID: %s, teamID: %s", - errBoardNotFoundInCategory, - boardID, - categoryID, - userID, - teamID, - ) - } - } - - return nil -} - -func (a *App) SetBoardVisibility(teamID, userID, categoryID, boardID string, visible bool) error { - if err := a.store.SetBoardVisibility(userID, categoryID, boardID, visible); err != nil { - return fmt.Errorf("SetBoardVisibility: failed to update board visibility: %w", err) - } - - a.wsAdapter.BroadcastCategoryBoardChange(teamID, userID, []*model.BoardCategoryWebsocketData{ - { - BoardID: boardID, - CategoryID: categoryID, - Hidden: !visible, - }, - }) - - return nil -} diff --git a/server/boards/app/category_boards_test.go b/server/boards/app/category_boards_test.go deleted file mode 100644 index d9a4288171..0000000000 --- a/server/boards/app/category_boards_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestGetUserCategoryBoards(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("user had no default category and had boards", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{}, nil).Times(1) - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "boards_category_id", - Type: model.CategoryTypeSystem, - Name: "Boards", - }, - }, - }, nil).Times(1) - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Name: "Boards", - }, nil) - - board1 := &model.Board{ - ID: "board_id_1", - } - - board2 := &model.Board{ - ID: "board_id_2", - } - - board3 := &model.Board{ - ID: "board_id_3", - } - - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{board1, board2, board3}, nil) - - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{ - { - BoardID: "board_id_1", - Synthetic: false, - }, - { - BoardID: "board_id_2", - Synthetic: false, - }, - { - BoardID: "board_id_3", - Synthetic: false, - }, - }, nil) - th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) - - categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "Boards", categoryBoards[0].Name) - assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false}) - }) - - t.Run("user had no default category BUT had no boards", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{}, nil) - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Name: "Boards", - }, nil) - - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) - - categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "Boards", categoryBoards[0].Name) - assert.Equal(t, 0, len(categoryBoards[0].BoardMetadata)) - }) - - t.Run("user already had a default Boards category with boards in it", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{Name: "Boards"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_1", Hidden: false}, - {BoardID: "board_id_2", Hidden: false}, - }, - }, - }, nil) - - categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "Boards", categoryBoards[0].Name) - assert.Equal(t, 2, len(categoryBoards[0].BoardMetadata)) - }) -} - -func TestCreateBoardsCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("user doesn't have any boards - implicit or explicit", func(t *testing.T) { - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Type: "system", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil) - - existingCategoryBoards := []model.CategoryBoards{} - boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) - assert.NoError(t, err) - assert.NotNil(t, boardsCategory) - assert.Equal(t, "Boards", boardsCategory.Name) - assert.Equal(t, 0, len(boardsCategory.BoardMetadata)) - }) - - t.Run("user has implicit access to some board", func(t *testing.T) { - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Type: "system", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{ - { - BoardID: "board_id_1", - Synthetic: true, - }, - { - BoardID: "board_id_2", - Synthetic: true, - }, - { - BoardID: "board_id_3", - Synthetic: true, - }, - }, nil) - th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) - - existingCategoryBoards := []model.CategoryBoards{} - boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) - assert.NoError(t, err) - assert.NotNil(t, boardsCategory) - assert.Equal(t, "Boards", boardsCategory.Name) - - // there should still be no boards in the default category as - // the user had only implicit access to boards - assert.Equal(t, 0, len(boardsCategory.BoardMetadata)) - }) - - t.Run("user has explicit access to some board", func(t *testing.T) { - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Type: "system", - Name: "Boards", - }, nil) - - board1 := &model.Board{ - ID: "board_id_1", - } - board2 := &model.Board{ - ID: "board_id_2", - } - board3 := &model.Board{ - ID: "board_id_3", - } - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{board1, board2, board3}, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{ - { - BoardID: "board_id_1", - Synthetic: false, - }, - { - BoardID: "board_id_2", - Synthetic: false, - }, - { - BoardID: "board_id_3", - Synthetic: false, - }, - }, nil) - th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ - Type: model.CategoryTypeSystem, - ID: "boards_category_id", - Name: "Boards", - }, - }, - }, nil) - - existingCategoryBoards := []model.CategoryBoards{} - boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) - assert.NoError(t, err) - assert.NotNil(t, boardsCategory) - assert.Equal(t, "Boards", boardsCategory.Name) - - // since user has explicit access to three boards, - // they should all end up in the default category - assert.Equal(t, 3, len(boardsCategory.BoardMetadata)) - }) - - t.Run("user has both implicit and explicit access to some board", func(t *testing.T) { - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Type: "system", - Name: "Boards", - }, nil) - - board1 := &model.Board{ - ID: "board_id_1", - } - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{board1}, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{ - { - BoardID: "board_id_1", - Synthetic: false, - }, - { - BoardID: "board_id_2", - Synthetic: true, - }, - { - BoardID: "board_id_3", - Synthetic: true, - }, - }, nil) - th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ - Type: model.CategoryTypeSystem, - ID: "boards_category_id", - Name: "Boards", - }, - }, - }, nil) - - existingCategoryBoards := []model.CategoryBoards{} - boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards) - assert.NoError(t, err) - assert.NotNil(t, boardsCategory) - assert.Equal(t, "Boards", boardsCategory.Name) - - // there was only one explicit board access, - // and so only that one should end up in the - // default category - assert.Equal(t, 1, len(boardsCategory.BoardMetadata)) - }) -} - -func TestReorderCategoryBoards(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "category_id_1", Name: "Category 1"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_1", Hidden: false}, - {BoardID: "board_id_2", Hidden: false}, - }, - }, - { - Category: model.Category{ID: "category_id_2", Name: "Boards", Type: "system"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_3", Hidden: false}, - }, - }, - { - Category: model.Category{ID: "category_id_3", Name: "Category 3"}, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - }, nil) - - th.Store.EXPECT().ReorderCategoryBoards("category_id_1", []string{"board_id_2", "board_id_1"}).Return([]string{"board_id_2", "board_id_1"}, nil) - - newOrder, err := th.App.ReorderCategoryBoards("user_id", "team_id", "category_id_1", []string{"board_id_2", "board_id_1"}) - assert.NoError(t, err) - assert.Equal(t, 2, len(newOrder)) - assert.Equal(t, "board_id_2", newOrder[0]) - assert.Equal(t, "board_id_1", newOrder[1]) - }) - - t.Run("not specifying all boards", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "category_id_1", Name: "Category 1"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_1", Hidden: false}, - {BoardID: "board_id_2", Hidden: false}, - {BoardID: "board_id_3", Hidden: false}, - }, - }, - { - Category: model.Category{ID: "category_id_2", Name: "Boards", Type: "system"}, - BoardMetadata: []model.CategoryBoardMetadata{ - {BoardID: "board_id_3", Hidden: false}, - }, - }, - { - Category: model.Category{ID: "category_id_3", Name: "Category 3"}, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - }, nil) - - newOrder, err := th.App.ReorderCategoryBoards("user_id", "team_id", "category_id_1", []string{"board_id_2", "board_id_1"}) - assert.Error(t, err) - assert.Nil(t, newOrder) - }) -} diff --git a/server/boards/app/category_test.go b/server/boards/app/category_test.go deleted file mode 100644 index f61420b5e9..0000000000 --- a/server/boards/app/category_test.go +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestCreateCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - }, nil) - - category := &model.Category{ - Name: "Category", - UserID: "user_id", - TeamID: "team_id", - Type: "custom", - } - createdCategory, err := th.App.CreateCategory(category) - assert.NotNil(t, createdCategory) - assert.NoError(t, err) - }) - - t.Run("creating invalid category", func(t *testing.T) { - category := &model.Category{ - Name: "", // empty name shouldn't be allowed - UserID: "user_id", - TeamID: "team_id", - Type: "custom", - } - createdCategory, err := th.App.CreateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.Name = "Name" - category.UserID = "" // empty creator user id shouldn't be allowed - createdCategory, err = th.App.CreateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.UserID = "user_id" - category.TeamID = "" // empty TeamID shouldn't be allowed - createdCategory, err = th.App.CreateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.Type = "invalid" // unknown type shouldn't be allowed - createdCategory, err = th.App.CreateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - }) -} - -func TestUpdateCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "custom", - }, nil) - - th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - }, nil) - - category := &model.Category{ - ID: "category_id_1", - Name: "Category", - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "custom", - } - updatedCategory, err := th.App.UpdateCategory(category) - assert.NotNil(t, updatedCategory) - assert.NoError(t, err) - }) - - t.Run("updating invalid category", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "custom", - }, nil) - - category := &model.Category{ - ID: "category_id_1", - Name: "Name", - UserID: "user_id", - TeamID: "team_id", - Type: "custom", - } - - category.ID = "" - createdCategory, err := th.App.UpdateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.ID = "category_id_1" - category.Name = "" - createdCategory, err = th.App.UpdateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.Name = "Name" - category.UserID = "" // empty creator user id shouldn't be allowed - createdCategory, err = th.App.UpdateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.UserID = "user_id" - category.TeamID = "" // empty TeamID shouldn't be allowed - createdCategory, err = th.App.UpdateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - - category.Type = "invalid" // unknown type shouldn't be allowed - createdCategory, err = th.App.UpdateCategory(category) - assert.Nil(t, createdCategory) - assert.Error(t, err) - }) - - t.Run("trying to update someone else's category", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "custom", - }, nil) - - category := &model.Category{ - ID: "category_id_1", - Name: "Category", - UserID: "user_id_2", - TeamID: "team_id_1", - Type: "custom", - } - updatedCategory, err := th.App.UpdateCategory(category) - assert.Nil(t, updatedCategory) - assert.Error(t, err) - }) - - t.Run("trying to update some other team's category", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "custom", - }, nil) - - category := &model.Category{ - ID: "category_id_1", - Name: "Category", - UserID: "user_id_1", - TeamID: "team_id_2", - Type: "custom", - } - updatedCategory, err := th.App.UpdateCategory(category) - assert.Nil(t, updatedCategory) - assert.Error(t, err) - }) - - t.Run("should not be allowed to rename system category", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "system", - }, nil).Times(1) - - th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil) - - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "system", - Collapsed: true, - }, nil).Times(1) - - category := &model.Category{ - ID: "category_id_1", - Name: "Updated Name", - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "system", - } - updatedCategory, err := th.App.UpdateCategory(category) - assert.NotNil(t, updatedCategory) - assert.NoError(t, err) - assert.Equal(t, "Category", updatedCategory.Name) - }) - - t.Run("should be allowed to collapse and expand any category type", func(t *testing.T) { - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "system", - Collapsed: false, - }, nil).Times(1) - - th.Store.EXPECT().UpdateCategory(utils.Anything).Return(nil) - - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "category_id_1", - Name: "Category", - TeamID: "team_id_1", - UserID: "user_id_1", - Type: "system", - Collapsed: true, - }, nil).Times(1) - - category := &model.Category{ - ID: "category_id_1", - Name: "Updated Name", - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "system", - Collapsed: true, - } - updatedCategory, err := th.App.UpdateCategory(category) - assert.NotNil(t, updatedCategory) - assert.NoError(t, err) - assert.Equal(t, "Category", updatedCategory.Name, "The name should have not been updated") - assert.True(t, updatedCategory.Collapsed) - }) -} - -func TestDeleteCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{ - ID: "category_id_1", - DeleteAt: 0, - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "custom", - }, nil) - - th.Store.EXPECT().DeleteCategory("category_id_1", "user_id_1", "team_id_1").Return(nil) - - th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{ - DeleteAt: 10000, - }, nil) - - th.Store.EXPECT().GetUserCategoryBoards("user_id_1", "team_id_1").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "category_id_default", - DeleteAt: 0, - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "default", - Name: "Boards", - }, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - { - Category: model.Category{ - ID: "category_id_1", - DeleteAt: 0, - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "custom", - Name: "Category 1", - }, - BoardMetadata: []model.CategoryBoardMetadata{}, - }, - }, nil) - - deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1") - assert.NotNil(t, deletedCategory) - assert.NoError(t, err) - }) - - t.Run("trying to delete already deleted category", func(t *testing.T) { - th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{ - ID: "category_id_1", - DeleteAt: 1000, - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "custom", - }, nil) - - deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1") - assert.NotNil(t, deletedCategory) - assert.NoError(t, err) - }) - - t.Run("trying to delete system category", func(t *testing.T) { - th.Store.EXPECT().GetCategory("category_id_1").Return(&model.Category{ - ID: "category_id_1", - DeleteAt: 0, - UserID: "user_id_1", - TeamID: "team_id_1", - Type: "system", - }, nil) - - deletedCategory, err := th.App.DeleteCategory("category_id_1", "user_id_1", "team_id_1") - assert.Nil(t, deletedCategory) - assert.Error(t, err) - }) -} - -func TestMoveBoardsToDefaultCategory(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("When default category already exists", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - }, - { - Category: model.Category{ - ID: "category_id_2", - Name: "Custom Category 1", - Type: "custom", - }, - }, - }, nil) - - err := th.App.moveBoardsToDefaultCategory("user_id", "team_id", "category_id_2") - assert.NoError(t, err) - }) - - t.Run("When default category doesn't already exists", func(t *testing.T) { - th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "category_id_2", - Name: "Custom Category 1", - Type: "custom", - }, - }, - }, nil) - - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "default_category_id", - Name: "Boards", - Type: "system", - }, nil) - th.Store.EXPECT().GetMembersForUser("user_id").Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id", "team_id", false).Return([]*model.Board{}, nil) - - err := th.App.moveBoardsToDefaultCategory("user_id", "team_id", "category_id_2") - assert.NoError(t, err) - }) -} - -func TestReorderCategories(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().GetUserCategories("user_id", "team_id").Return([]model.Category{ - { - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - { - ID: "category_id_2", - Name: "Category 2", - Type: "custom", - }, - { - ID: "category_id_3", - Name: "Category 3", - Type: "custom", - }, - }, nil) - - th.Store.EXPECT().ReorderCategories("user_id", "team_id", []string{"category_id_2", "category_id_3", "category_id_1"}). - Return([]string{"category_id_2", "category_id_3", "category_id_1"}, nil) - - newOrder, err := th.App.ReorderCategories("user_id", "team_id", []string{"category_id_2", "category_id_3", "category_id_1"}) - assert.NoError(t, err) - assert.Equal(t, 3, len(newOrder)) - }) - - t.Run("not specifying all categories should fail", func(t *testing.T) { - th.Store.EXPECT().GetUserCategories("user_id", "team_id").Return([]model.Category{ - { - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - { - ID: "category_id_2", - Name: "Category 2", - Type: "custom", - }, - { - ID: "category_id_3", - Name: "Category 3", - Type: "custom", - }, - }, nil) - - newOrder, err := th.App.ReorderCategories("user_id", "team_id", []string{"category_id_2", "category_id_3"}) - assert.Error(t, err) - assert.Nil(t, newOrder) - }) -} - -func TestVerifyNewCategoriesMatchExisting(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - th.Store.EXPECT().GetUserCategories("user_id", "team_id").Return([]model.Category{ - { - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - { - ID: "category_id_2", - Name: "Category 2", - Type: "custom", - }, - { - ID: "category_id_3", - Name: "Category 3", - Type: "custom", - }, - }, nil) - - err := th.App.verifyNewCategoriesMatchExisting("user_id", "team_id", []string{ - "category_id_2", - "category_id_3", - "category_id_1", - }) - assert.NoError(t, err) - }) - - t.Run("different category counts", func(t *testing.T) { - th.Store.EXPECT().GetUserCategories("user_id", "team_id").Return([]model.Category{ - { - ID: "category_id_1", - Name: "Boards", - Type: "system", - }, - { - ID: "category_id_2", - Name: "Category 2", - Type: "custom", - }, - { - ID: "category_id_3", - Name: "Category 3", - Type: "custom", - }, - }, nil) - - err := th.App.verifyNewCategoriesMatchExisting("user_id", "team_id", []string{ - "category_id_2", - "category_id_3", - }) - assert.Error(t, err) - }) -} diff --git a/server/boards/app/clientConfig.go b/server/boards/app/clientConfig.go deleted file mode 100644 index edf367b3eb..0000000000 --- a/server/boards/app/clientConfig.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *App) GetClientConfig() *model.ClientConfig { - return &model.ClientConfig{ - Telemetry: a.config.Telemetry, - TelemetryID: a.config.TelemetryID, - EnablePublicSharedBoards: a.config.EnablePublicSharedBoards, - TeammateNameDisplay: a.config.TeammateNameDisplay, - FeatureFlags: a.config.FeatureFlags, - MaxFileSize: a.config.MaxFileSize, - } -} diff --git a/server/boards/app/clientConfig_test.go b/server/boards/app/clientConfig_test.go deleted file mode 100644 index 03bc132bba..0000000000 --- a/server/boards/app/clientConfig_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/services/config" -) - -func TestGetClientConfig(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("Test Get Client Config", func(t *testing.T) { - newConfiguration := config.Configuration{} - newConfiguration.Telemetry = true - newConfiguration.TelemetryID = "abcde" - newConfiguration.EnablePublicSharedBoards = true - newConfiguration.FeatureFlags = make(map[string]string) - newConfiguration.FeatureFlags["BoardsFeature1"] = "true" - newConfiguration.FeatureFlags["BoardsFeature2"] = "true" - newConfiguration.TeammateNameDisplay = "username" - th.App.SetConfig(&newConfiguration) - - clientConfig := th.App.GetClientConfig() - require.True(t, clientConfig.EnablePublicSharedBoards) - require.True(t, clientConfig.Telemetry) - require.Equal(t, "abcde", clientConfig.TelemetryID) - require.Equal(t, 2, len(clientConfig.FeatureFlags)) - require.Equal(t, "username", clientConfig.TeammateNameDisplay) - }) -} diff --git a/server/boards/app/cloud.go b/server/boards/app/cloud.go deleted file mode 100644 index 1ca3a3ddee..0000000000 --- a/server/boards/app/cloud.go +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var ErrNilPluginAPI = errors.New("server not running in plugin mode") - -// GetBoardsCloudLimits returns the limits of the server, and an empty -// limits struct if there are no limits set. -func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) { - // ToDo: Cloud Limits have been disabled by design. We should - // revisit the decision and update the related code accordingly - /* - if !a.IsCloud() { - return &model.BoardsCloudLimits{}, nil - } - - productLimits, err := a.store.GetCloudLimits() - if err != nil { - return nil, err - } - - usedCards, err := a.store.GetUsedCardsCount() - if err != nil { - return nil, err - } - - cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() - if err != nil { - return nil, err - } - - boardsCloudLimits := &model.BoardsCloudLimits{ - UsedCards: usedCards, - CardLimitTimestamp: cardLimitTimestamp, - } - if productLimits != nil && productLimits.Boards != nil { - if productLimits.Boards.Cards != nil { - boardsCloudLimits.Cards = *productLimits.Boards.Cards - } - if productLimits.Boards.Views != nil { - boardsCloudLimits.Views = *productLimits.Boards.Views - } - } - - return boardsCloudLimits, nil - */ - - return &model.BoardsCloudLimits{}, nil -} - -func (a *App) GetUsedCardsCount() (int, error) { - return a.store.GetUsedCardsCount() -} - -// IsCloud returns true if the server is running as a plugin in a -// cloud licensed server. -func (a *App) IsCloud() bool { - return utils.IsCloudLicense(a.store.GetLicense()) -} - -// IsCloudLimited returns true if the server is running in cloud mode -// and the card limit has been set. -func (a *App) IsCloudLimited() bool { - // ToDo: Cloud Limits have been disabled by design. We should - // revisit the decision and update the related code accordingly - - // return a.CardLimit() != 0 && a.IsCloud() - - return false -} - -// SetCloudLimits sets the limits of the server. -func (a *App) SetCloudLimits(limits *mm_model.ProductLimits) error { - oldCardLimit := a.CardLimit() - - // if the limit object doesn't come complete, we assume limits are - // being disabled - cardLimit := 0 - if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil { - cardLimit = *limits.Boards.Cards - } - - if oldCardLimit != cardLimit { - a.logger.Info( - "setting new cloud limits", - mlog.Int("oldCardLimit", oldCardLimit), - mlog.Int("cardLimit", cardLimit), - ) - a.SetCardLimit(cardLimit) - return a.doUpdateCardLimitTimestamp() - } - - a.logger.Info( - "setting new cloud limits, equivalent to the existing ones", - mlog.Int("cardLimit", cardLimit), - ) - return nil -} - -// doUpdateCardLimitTimestamp performs the update without running any -// checks. -func (a *App) doUpdateCardLimitTimestamp() error { - cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit()) - if err != nil { - return err - } - - a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp) - - return nil -} - -// UpdateCardLimitTimestamp checks if the server is a cloud instance -// with limits applied, and if that's true, recalculates the card -// limit timestamp and propagates the new one to the connected -// clients. -func (a *App) UpdateCardLimitTimestamp() error { - if !a.IsCloudLimited() { - return nil - } - - return a.doUpdateCardLimitTimestamp() -} - -// getTemplateMapForBlocks gets all board ids for the blocks, and -// builds a map with the board IDs as the key and their isTemplate -// field as the value. -func (a *App) getTemplateMapForBlocks(blocks []*model.Block) (map[string]bool, error) { - boardMap := map[string]*model.Board{} - for _, block := range blocks { - if _, ok := boardMap[block.BoardID]; !ok { - board, err := a.store.GetBoard(block.BoardID) - if err != nil { - return nil, err - } - boardMap[block.BoardID] = board - } - } - - templateMap := map[string]bool{} - for boardID, board := range boardMap { - templateMap[boardID] = board.IsTemplate - } - - return templateMap, nil -} - -// ApplyCloudLimits takes a set of blocks and, if the server is cloud -// limited, limits those that are outside of the card limit and don't -// belong to a template. -func (a *App) ApplyCloudLimits(blocks []*model.Block) ([]*model.Block, error) { - // if there is no limit currently being applied, return - if !a.IsCloudLimited() { - return blocks, nil - } - - cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() - if err != nil { - return nil, err - } - - templateMap, err := a.getTemplateMapForBlocks(blocks) - if err != nil { - return nil, err - } - - limitedBlocks := make([]*model.Block, len(blocks)) - for i, block := range blocks { - // if the block belongs to a template, it will never be - // limited - if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate { - limitedBlocks[i] = block - continue - } - - if block.ShouldBeLimited(cardLimitTimestamp) { - limitedBlocks[i] = block.GetLimited() - } else { - limitedBlocks[i] = block - } - } - - return limitedBlocks, nil -} - -// ContainsLimitedBlocks checks if a list of blocks contain any block -// that references a limited card. -func (a *App) ContainsLimitedBlocks(blocks []*model.Block) (bool, error) { - cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() - if err != nil { - return false, err - } - - if cardLimitTimestamp == 0 { - return false, nil - } - - cards := []*model.Block{} - cardIDMap := map[string]bool{} - for _, block := range blocks { - switch block.Type { - case model.TypeCard: - cards = append(cards, block) - default: - cardIDMap[block.ParentID] = true - } - } - - cardIDs := []string{} - // if the card is already present on the set, we don't need to - // fetch it from the database - for cardID := range cardIDMap { - alreadyPresent := false - for _, card := range cards { - if card.ID == cardID { - alreadyPresent = true - break - } - } - - if !alreadyPresent { - cardIDs = append(cardIDs, cardID) - } - } - - if len(cardIDs) > 0 { - fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs) - if fErr != nil { - return false, fErr - } - cards = append(cards, fetchedCards...) - } - - templateMap, err := a.getTemplateMapForBlocks(cards) - if err != nil { - return false, err - } - - for _, card := range cards { - isTemplate, ok := templateMap[card.BoardID] - if !ok { - return false, newErrBoardNotFoundInTemplateMap(card.BoardID) - } - - // if the block belongs to a template, it will never be - // limited - if isTemplate { - continue - } - - if card.ShouldBeLimited(cardLimitTimestamp) { - return true, nil - } - } - - return false, nil -} - -type errBoardNotFoundInTemplateMap struct { - id string -} - -func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap { - return &errBoardNotFoundInTemplateMap{id} -} - -func (eb *errBoardNotFoundInTemplateMap) Error() string { - return fmt.Sprintf("board %q not found in template map", eb.id) -} - -func (a *App) NotifyPortalAdminsUpgradeRequest(teamID string) error { - if a.servicesAPI == nil { - return ErrNilPluginAPI - } - - team, err := a.store.GetTeam(teamID) - if err != nil { - return err - } - - var ofWhat string - if team == nil { - ofWhat = "your organization" - } else { - ofWhat = team.Title - } - - message := fmt.Sprintf("A member of %s has notified you to upgrade this workspace before the trial ends.", ofWhat) - - page := 0 - getUsersOptions := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: page, - } - - for ; true; page++ { - getUsersOptions.Page = page - systemAdmins, appErr := a.servicesAPI.GetUsersFromProfiles(getUsersOptions) - if appErr != nil { - a.logger.Error("failed to fetch system admins", mlog.Int("page_size", getUsersOptions.PerPage), mlog.Int("page", page), mlog.Err(appErr)) - return appErr - } - - if len(systemAdmins) == 0 { - break - } - - receiptUserIDs := []string{} - for _, systemAdmin := range systemAdmins { - receiptUserIDs = append(receiptUserIDs, systemAdmin.Id) - } - - if err := a.store.SendMessage(message, "custom_cloud_upgrade_nudge", receiptUserIDs); err != nil { - return err - } - } - - return nil -} diff --git a/server/boards/app/cloud_test.go b/server/boards/app/cloud_test.go deleted file mode 100644 index 2930306903..0000000000 --- a/server/boards/app/cloud_test.go +++ /dev/null @@ -1,780 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/v8/boards/model" - mockservicesapi "github.com/mattermost/mattermost/server/v8/boards/model/mocks" -) - -func TestIsCloud(t *testing.T) { - t.Run("if it's not running on plugin mode", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().GetLicense().Return(nil) - require.False(t, th.App.IsCloud()) - }) - - t.Run("if it's running on plugin mode but the license is incomplete", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - fakeLicense := &mm_model.License{} - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - require.False(t, th.App.IsCloud()) - - fakeLicense = &mm_model.License{Features: &mm_model.Features{}} - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - require.False(t, th.App.IsCloud()) - }) - - t.Run("if it's running on plugin mode, with a non-cloud license", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(false)}, - } - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - require.False(t, th.App.IsCloud()) - }) - - t.Run("if it's running on plugin mode with a cloud license", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - require.True(t, th.App.IsCloud()) - }) -} - -func TestIsCloudLimited(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - t.Run("if no limit has been set, it should be false", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - require.False(t, th.App.IsCloudLimited()) - }) - - t.Run("if the limit is set, it should be true", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - th.Store.EXPECT().GetLicense().Return(fakeLicense) - - th.App.SetCardLimit(5) - require.True(t, th.App.IsCloudLimited()) - }) -} - -func TestSetCloudLimits(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - t.Run("if the limits are empty, it should do nothing", func(t *testing.T) { - t.Run("limits empty", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - require.NoError(t, th.App.SetCloudLimits(nil)) - require.Zero(t, th.App.CardLimit()) - }) - - t.Run("limits not empty but board limits empty", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - limits := &mm_model.ProductLimits{} - - require.NoError(t, th.App.SetCloudLimits(limits)) - require.Zero(t, th.App.CardLimit()) - }) - - t.Run("limits not empty but board limits values empty", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - limits := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{}, - } - - require.NoError(t, th.App.SetCloudLimits(limits)) - require.Zero(t, th.App.CardLimit()) - }) - }) - - t.Run("if the limits are not empty, it should update them and calculate the new timestamp", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - newCardLimitTimestamp := int64(27) - th.Store.EXPECT().UpdateCardLimitTimestamp(5).Return(newCardLimitTimestamp, nil) - - limits := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{Cards: mm_model.NewInt(5)}, - } - - require.NoError(t, th.App.SetCloudLimits(limits)) - require.Equal(t, 5, th.App.CardLimit()) - }) - - t.Run("if the limits are already set and we unset them, the timestamp will be unset too", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.App.SetCardLimit(20) - - th.Store.EXPECT().UpdateCardLimitTimestamp(0) - - require.NoError(t, th.App.SetCloudLimits(nil)) - - require.Zero(t, th.App.CardLimit()) - }) - - t.Run("if the limits are already set and we try to set the same ones again", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.App.SetCardLimit(20) - - // the call to update card limit timestamp should not happen - // as the limits didn't change - th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0) - - limits := &mm_model.ProductLimits{ - Boards: &mm_model.BoardsLimits{Cards: mm_model.NewInt(20)}, - } - - require.NoError(t, th.App.SetCloudLimits(limits)) - require.Equal(t, 20, th.App.CardLimit()) - }) -} - -func TestUpdateCardLimitTimestamp(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - - t.Run("if the server is a cloud instance but not limited, it should do nothing", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - // the license check will not be done as the limit not being - // set is enough for the method to return - th.Store.EXPECT().GetLicense().Times(0) - // no call to UpdateCardLimitTimestamp should happen as the - // method should shortcircuit if not cloud limited - th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0) - - require.NoError(t, th.App.UpdateCardLimitTimestamp()) - }) - - t.Run("if the server is a cloud instance and the timestamp is set, it should run the update", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.App.SetCardLimit(5) - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - // no call to UpdateCardLimitTimestamp should happen as the - // method should shortcircuit if not cloud limited - th.Store.EXPECT().UpdateCardLimitTimestamp(5) - - require.NoError(t, th.App.UpdateCardLimitTimestamp()) - }) -} - -func TestGetTemplateMapForBlocks(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - t.Run("should fetch the necessary boards from the database", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - IsTemplate: true, - } - - board2 := &model.Board{ - ID: "board2", - Type: model.BoardTypeOpen, - IsTemplate: false, - } - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - }, - { - ID: "card2", - Type: model.TypeCard, - ParentID: "board2", - BoardID: "board2", - }, - { - ID: "text2", - Type: model.TypeText, - ParentID: "card2", - BoardID: "board2", - }, - } - - th.Store.EXPECT(). - GetBoard("board1"). - Return(board1, nil). - Times(1) - th.Store.EXPECT(). - GetBoard("board2"). - Return(board2, nil). - Times(1) - - templateMap, err := th.App.getTemplateMapForBlocks(blocks) - require.NoError(t, err) - require.Len(t, templateMap, 2) - require.Contains(t, templateMap, "board1") - require.True(t, templateMap["board1"]) - require.Contains(t, templateMap, "board2") - require.False(t, templateMap["board2"]) - }) - - t.Run("should fail if the board is not in the database", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - }, - { - ID: "card2", - Type: model.TypeCard, - ParentID: "board2", - BoardID: "board2", - }, - } - - th.Store.EXPECT(). - GetBoard("board1"). - Return(nil, sql.ErrNoRows). - Times(1) - - templateMap, err := th.App.getTemplateMapForBlocks(blocks) - require.ErrorIs(t, err, sql.ErrNoRows) - require.Empty(t, templateMap) - }) -} - -func TestApplyCloudLimits(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - fakeLicense := &mm_model.License{ - Features: &mm_model.Features{Cloud: mm_model.NewBool(true)}, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - IsTemplate: false, - } - - template := &model.Board{ - ID: "template", - Type: model.BoardTypeOpen, - IsTemplate: true, - } - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 100, - }, - { - ID: "text1", - Type: model.TypeText, - ParentID: "card1", - BoardID: "board1", - UpdateAt: 100, - }, - { - ID: "card2", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 200, - }, - { - ID: "card-from-template", - Type: model.TypeCard, - ParentID: "template", - BoardID: "template", - UpdateAt: 1, - }, - } - - t.Run("if the server is not limited, it should return the blocks untouched", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - require.Zero(t, th.App.CardLimit()) - - newBlocks, err := th.App.ApplyCloudLimits(blocks) - require.NoError(t, err) - require.ElementsMatch(t, blocks, newBlocks) - }) - - t.Run("if the server is limited, it should limit the blocks that are beyond the card limit timestamp", func(t *testing.T) { - findBlock := func(blocks []*model.Block, id string) *model.Block { - for _, block := range blocks { - if block.ID == id { - return block - } - } - require.FailNow(t, "block %s not found", id) - return &model.Block{} // this should never be reached - } - - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.App.SetCardLimit(5) - - th.Store.EXPECT().GetLicense().Return(fakeLicense) - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil).Times(1) - th.Store.EXPECT().GetBoard("template").Return(template, nil).Times(1) - - newBlocks, err := th.App.ApplyCloudLimits(blocks) - require.NoError(t, err) - - // should be limited as it's beyond the threshold - require.True(t, findBlock(newBlocks, "card1").Limited) - // only cards are limited - require.False(t, findBlock(newBlocks, "text1").Limited) - // should not be limited as it's not beyond the threshold - require.False(t, findBlock(newBlocks, "card2").Limited) - // cards belonging to templates are never limited - require.False(t, findBlock(newBlocks, "card-from-template").Limited) - }) -} - -func TestContainsLimitedBlocks(t *testing.T) { - t.Skipf("The Cloud Limits feature has been disabled") - - // for all the following tests, the timestamp will be set to 150, - // which means that blocks with an UpdateAt set to 100 will be - // outside the active window and possibly limited, and blocks with - // UpdateAt set to 200 will not - - t.Run("should return false if the card limit timestamp is zero", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 100, - }, - } - - th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(0), nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.False(t, containsLimitedBlocks) - }) - - t.Run("should return true if the block set contains a card that is limited", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 100, - }, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypePrivate, - } - - th.App.SetCardLimit(500) - cardLimitTimestamp := int64(150) - th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.True(t, containsLimitedBlocks) - }) - - t.Run("should return false if that same block belongs to a template", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 100, - }, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - IsTemplate: true, - } - - th.App.SetCardLimit(500) - cardLimitTimestamp := int64(150) - th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.False(t, containsLimitedBlocks) - }) - - t.Run("should return true if the block contains a content block that belongs to a card that should be limited", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "text1", - Type: model.TypeText, - ParentID: "card1", - BoardID: "board1", - UpdateAt: 200, - }, - } - - card1 := &model.Block{ - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 100, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - } - - th.App.SetCardLimit(500) - cardLimitTimestamp := int64(150) - th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil) - th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]*model.Block{card1}, nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.True(t, containsLimitedBlocks) - }) - - t.Run("should return false if that same block belongs to a card that is inside the active window", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - { - ID: "text1", - Type: model.TypeText, - ParentID: "card1", - BoardID: "board1", - UpdateAt: 200, - }, - } - - card1 := &model.Block{ - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 200, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - } - - th.App.SetCardLimit(500) - cardLimitTimestamp := int64(150) - th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil) - th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]*model.Block{card1}, nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.False(t, containsLimitedBlocks) - }) - - t.Run("should reach to the database to fetch the necessary information only in an efficient way", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - blocks := []*model.Block{ - // a content block that references a card that needs - // fetching - { - ID: "text1", - Type: model.TypeText, - ParentID: "card1", - BoardID: "board1", - UpdateAt: 100, - }, - // a board that needs fetching referenced by a card and a content block - { - ID: "card2", - Type: model.TypeCard, - ParentID: "board2", - BoardID: "board2", - // per timestamp should be limited but the board is a - // template - UpdateAt: 100, - }, - { - ID: "text2", - Type: model.TypeText, - ParentID: "card2", - BoardID: "board2", - UpdateAt: 200, - }, - // a content block that references a card and a board, - // both absent - { - ID: "image3", - Type: model.TypeImage, - ParentID: "card3", - BoardID: "board3", - UpdateAt: 100, - }, - } - - card1 := &model.Block{ - ID: "card1", - Type: model.TypeCard, - ParentID: "board1", - BoardID: "board1", - UpdateAt: 200, - } - - card3 := &model.Block{ - ID: "card3", - Type: model.TypeCard, - ParentID: "board3", - BoardID: "board3", - UpdateAt: 200, - } - - board1 := &model.Board{ - ID: "board1", - Type: model.BoardTypeOpen, - } - - board2 := &model.Board{ - ID: "board2", - Type: model.BoardTypeOpen, - IsTemplate: true, - } - - board3 := &model.Board{ - ID: "board3", - Type: model.BoardTypePrivate, - } - - th.App.SetCardLimit(500) - cardLimitTimestamp := int64(150) - th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil) - th.Store.EXPECT().GetBlocksByIDs(gomock.InAnyOrder([]string{"card1", "card3"})).Return([]*model.Block{card1, card3}, nil) - th.Store.EXPECT().GetBoard("board1").Return(board1, nil) - th.Store.EXPECT().GetBoard("board2").Return(board2, nil) - th.Store.EXPECT().GetBoard("board3").Return(board3, nil) - - containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks) - require.NoError(t, err) - require.False(t, containsLimitedBlocks) - }) -} - -func TestNotifyPortalAdminsUpgradeRequest(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("should send message", func(t *testing.T) { - ctrl := gomock.NewController(t) - servicesAPI := mockservicesapi.NewMockServicesAPI(ctrl) - - sysAdmin1 := &mm_model.User{ - Id: "michael-scott", - Username: "Michael Scott", - } - - sysAdmin2 := &mm_model.User{ - Id: "dwight-schrute", - Username: "Dwight Schrute", - } - - getUsersOptionsPage0 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 0, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage0).Return([]*mm_model.User{sysAdmin1, sysAdmin2}, nil) - - getUsersOptionsPage1 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 1, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage1).Return([]*mm_model.User{}, nil) - - th.App.servicesAPI = servicesAPI - - team := &model.Team{ - Title: "Dunder Mifflin", - } - - th.Store.EXPECT().GetTeam("team-id-1").Return(team, nil) - th.Store.EXPECT().SendMessage(gomock.Any(), "custom_cloud_upgrade_nudge", gomock.Any()).Return(nil).Times(1) - - err := th.App.NotifyPortalAdminsUpgradeRequest("team-id-1") - assert.NoError(t, err) - }) - - t.Run("no sys admins found", func(t *testing.T) { - ctrl := gomock.NewController(t) - servicesAPI := mockservicesapi.NewMockServicesAPI(ctrl) - - getUsersOptionsPage0 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 0, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage0).Return([]*mm_model.User{}, nil) - - th.App.servicesAPI = servicesAPI - - team := &model.Team{ - Title: "Dunder Mifflin", - } - - th.Store.EXPECT().GetTeam("team-id-1").Return(team, nil) - - err := th.App.NotifyPortalAdminsUpgradeRequest("team-id-1") - assert.NoError(t, err) - }) - - t.Run("iterate multiple pages", func(t *testing.T) { - ctrl := gomock.NewController(t) - servicesAPI := mockservicesapi.NewMockServicesAPI(ctrl) - - sysAdmin1 := &mm_model.User{ - Id: "michael-scott", - Username: "Michael Scott", - } - - sysAdmin2 := &mm_model.User{ - Id: "dwight-schrute", - Username: "Dwight Schrute", - } - - getUsersOptionsPage0 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 0, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage0).Return([]*mm_model.User{sysAdmin1}, nil) - - getUsersOptionsPage1 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 1, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage1).Return([]*mm_model.User{sysAdmin2}, nil) - - getUsersOptionsPage2 := &mm_model.UserGetOptions{ - Active: true, - Role: mm_model.SystemAdminRoleId, - PerPage: 50, - Page: 2, - } - servicesAPI.EXPECT().GetUsersFromProfiles(getUsersOptionsPage2).Return([]*mm_model.User{}, nil) - - th.App.servicesAPI = servicesAPI - - team := &model.Team{ - Title: "Dunder Mifflin", - } - - th.Store.EXPECT().GetTeam("team-id-1").Return(team, nil) - th.Store.EXPECT().SendMessage(gomock.Any(), "custom_cloud_upgrade_nudge", gomock.Any()).Return(nil).Times(2) - - err := th.App.NotifyPortalAdminsUpgradeRequest("team-id-1") - assert.NoError(t, err) - }) -} diff --git a/server/boards/app/compliance.go b/server/boards/app/compliance.go deleted file mode 100644 index d7713872c4..0000000000 --- a/server/boards/app/compliance.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "github.com/mattermost/mattermost/server/v8/boards/model" - -func (a *App) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) { - return a.store.GetBoardsForCompliance(opts) -} - -func (a *App) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) { - return a.store.GetBoardsComplianceHistory(opts) -} - -func (a *App) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) { - return a.store.GetBlocksComplianceHistory(opts) -} diff --git a/server/boards/app/content_blocks.go b/server/boards/app/content_blocks.go deleted file mode 100644 index fa840d12a6..0000000000 --- a/server/boards/app/content_blocks.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *App) MoveContentBlock(block *model.Block, dstBlock *model.Block, where string, userID string) error { - if block.ParentID != dstBlock.ParentID { - message := fmt.Sprintf("not matching parent %s and %s", block.ParentID, dstBlock.ParentID) - return model.NewErrBadRequest(message) - } - - card, err := a.GetBlockByID(block.ParentID) - if err != nil { - return err - } - - contentOrderData, ok := card.Fields["contentOrder"] - var contentOrder []interface{} - if ok { - contentOrder = contentOrderData.([]interface{}) - } - - newContentOrder := []interface{}{} - foundDst := false - foundSrc := false - for _, id := range contentOrder { - stringID, ok := id.(string) - if !ok { - newContentOrder = append(newContentOrder, id) - continue - } - - if dstBlock.ID == stringID { - foundDst = true - if where == "after" { - newContentOrder = append(newContentOrder, id) - newContentOrder = append(newContentOrder, block.ID) - } else { - newContentOrder = append(newContentOrder, block.ID) - newContentOrder = append(newContentOrder, id) - } - continue - } - - if block.ID == stringID { - foundSrc = true - continue - } - - newContentOrder = append(newContentOrder, id) - } - - if !foundSrc { - message := fmt.Sprintf("source block %s not found", block.ID) - return model.NewErrBadRequest(message) - } - - if !foundDst { - message := fmt.Sprintf("destination block %s not found", dstBlock.ID) - return model.NewErrBadRequest(message) - } - - patch := &model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - "contentOrder": newContentOrder, - }, - } - - _, err = a.PatchBlock(block.ParentID, patch, userID) - if errors.Is(err, model.ErrPatchUpdatesLimitedCards) { - return err - } - if err != nil { - return err - } - return nil -} diff --git a/server/boards/app/content_blocks_test.go b/server/boards/app/content_blocks_test.go deleted file mode 100644 index bf23f9b0cf..0000000000 --- a/server/boards/app/content_blocks_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - "testing" - - "github.com/golang/mock/gomock" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type contentOrderMatcher struct { - contentOrder []string -} - -func NewContentOrderMatcher(contentOrder []string) contentOrderMatcher { - return contentOrderMatcher{contentOrder} -} - -func (com contentOrderMatcher) Matches(x interface{}) bool { - patch, ok := x.(*model.BlockPatch) - if !ok { - return false - } - - contentOrderData, ok := patch.UpdatedFields["contentOrder"] - if !ok { - return false - } - - contentOrder, ok := contentOrderData.([]interface{}) - if !ok { - return false - } - - if len(contentOrder) != len(com.contentOrder) { - return false - } - - for i := range contentOrder { - if contentOrder[i] != com.contentOrder[i] { - return false - } - } - return true -} - -func (com contentOrderMatcher) String() string { - return fmt.Sprint(&model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": com.contentOrder}}) -} - -func TestMoveContentBlock(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - ttCases := []struct { - name string - srcBlock model.Block - dstBlock model.Block - parentBlock *model.Block - where string - userID string - mockPatch bool - mockPatchError error - errorMessage string - expectedContentOrder []string - }{ - { - name: "not matching parents", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "other-test-card"}, - parentBlock: nil, - where: "after", - userID: "user-id", - errorMessage: "not matching parent test-card and other-test-card", - }, - { - name: "parent not found", - srcBlock: model.Block{ID: "test-1", ParentID: "invalid-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "invalid-card"}, - parentBlock: &model.Block{ID: "invalid-card"}, - where: "after", - userID: "user-id", - errorMessage: "{test} not found", - }, - { - name: "valid parent without content order", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card"}, - where: "after", - userID: "user-id", - errorMessage: "source block test-1 not found", - }, - { - name: "valid parent with content order but without test-1 in it", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-2"}}}, - where: "after", - userID: "user-id", - errorMessage: "source block test-1 not found", - }, - { - name: "valid parent with content order but without test-2 in it", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1"}}}, - where: "after", - userID: "user-id", - errorMessage: "destination block test-2 not found", - }, - { - name: "valid request but fail on patchparent with content order", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2"}}}, - where: "after", - userID: "user-id", - mockPatch: true, - mockPatchError: errors.New("test error"), - errorMessage: "test error", - }, - { - name: "valid request with not real change", - srcBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"}, - where: "after", - userID: "user-id", - mockPatch: true, - errorMessage: "", - expectedContentOrder: []string{"test-1", "test-2", "test-3"}, - }, - { - name: "valid request changing order with before", - srcBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"}, - where: "before", - userID: "user-id", - mockPatch: true, - errorMessage: "", - expectedContentOrder: []string{"test-2", "test-1", "test-3"}, - }, - { - name: "valid request changing order with after", - srcBlock: model.Block{ID: "test-1", ParentID: "test-card"}, - dstBlock: model.Block{ID: "test-2", ParentID: "test-card"}, - parentBlock: &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"}, - where: "after", - userID: "user-id", - mockPatch: true, - errorMessage: "", - expectedContentOrder: []string{"test-2", "test-1", "test-3"}, - }, - } - - for _, tc := range ttCases { - t.Run(tc.name, func(t *testing.T) { - if tc.parentBlock != nil { - if tc.parentBlock.ID == "invalid-card" { - th.Store.EXPECT().GetBlock(tc.srcBlock.ParentID).Return(nil, model.NewErrNotFound("test")) - } else { - th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil) - if tc.mockPatch { - if tc.mockPatchError != nil { - th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(nil, tc.mockPatchError) - } else { - th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil) - th.Store.EXPECT().PatchBlock(tc.parentBlock.ID, NewContentOrderMatcher(tc.expectedContentOrder), gomock.Eq("user-id")).Return(nil) - th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil) - th.Store.EXPECT().GetBoard(tc.parentBlock.BoardID).Return(&model.Board{ID: "test-board"}, nil) - // this call comes from the WS server notification - th.Store.EXPECT().GetMembersForBoard(gomock.Any()).Times(1) - } - } - } - } - - err := th.App.MoveContentBlock(&tc.srcBlock, &tc.dstBlock, tc.where, tc.userID) - if tc.errorMessage == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tc.errorMessage) - } - }) - } -} diff --git a/server/boards/app/export.go b/server/boards/app/export.go deleted file mode 100644 index de00185573..0000000000 --- a/server/boards/app/export.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "archive/zip" - "encoding/json" - "fmt" - "io" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ( - newline = []byte{'\n'} -) - -func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) { - boards, err := a.getBoardsForArchive(opt.BoardIDs) - if err != nil { - return err - } - - merr := merror.New() - defer func() { - errs = merr.ErrorOrNil() - }() - - // wrap the writer in a zip. - zw := zip.NewWriter(w) - defer func() { - merr.Append(zw.Close()) - }() - - if err := a.writeArchiveVersion(zw); err != nil { - merr.Append(err) - return - } - - for _, board := range boards { - if err := a.writeArchiveBoard(zw, board, opt); err != nil { - merr.Append(fmt.Errorf("cannot export board %s: %w", board.ID, err)) - return - } - } - return nil -} - -// writeArchiveVersion writes a version file to the zip. -func (a *App) writeArchiveVersion(zw *zip.Writer) error { - archiveHeader := model.ArchiveHeader{ - Version: archiveVersion, - Date: model.GetMillis(), - } - b, _ := json.Marshal(&archiveHeader) - - w, err := zw.Create("version.json") - if err != nil { - return fmt.Errorf("cannot write archive header: %w", err) - } - - if _, err := w.Write(b); err != nil { - return fmt.Errorf("cannot write archive header: %w", err) - } - return nil -} - -// writeArchiveBoard writes a single board to the archive in a zip directory. -func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error { - // create a directory per board - w, err := zw.Create(board.ID + "/board.jsonl") - if err != nil { - return err - } - - // write the board block first - if err = a.writeArchiveBoardLine(w, board); err != nil { - return err - } - - var files []string - // write the board's blocks - // TODO: paginate this - blocks, err := a.GetBlocks(model.QueryBlocksOptions{BoardID: board.ID}) - if err != nil { - return err - } - - for _, block := range blocks { - if err = a.writeArchiveBlockLine(w, block); err != nil { - return err - } - if block.Type == model.TypeImage || block.Type == model.TypeAttachment { - filename, err2 := extractFilename(block) - if err2 != nil { - return err2 - } - files = append(files, filename) - } - } - - boardMembers, err := a.GetMembersForBoard(board.ID) - if err != nil { - return err - } - - for _, boardMember := range boardMembers { - if err = a.writeArchiveBoardMemberLine(w, boardMember); err != nil { - return err - } - } - - // write the files - for _, filename := range files { - if err := a.writeArchiveFile(zw, filename, board.ID, opt); err != nil { - return fmt.Errorf("cannot write file %s to archive: %w", filename, err) - } - } - return nil -} - -// writeArchiveBoardMemberLine writes a single boardMember to the archive. -func (a *App) writeArchiveBoardMemberLine(w io.Writer, boardMember *model.BoardMember) error { - bm, err := json.Marshal(&boardMember) - if err != nil { - return err - } - line := model.ArchiveLine{ - Type: "boardMember", - Data: bm, - } - - bm, err = json.Marshal(&line) - if err != nil { - return err - } - - _, err = w.Write(bm) - if err != nil { - return err - } - - _, err = w.Write(newline) - return err -} - -// writeArchiveBlockLine writes a single block to the archive. -func (a *App) writeArchiveBlockLine(w io.Writer, block *model.Block) error { - b, err := json.Marshal(&block) - if err != nil { - return err - } - line := model.ArchiveLine{ - Type: "block", - Data: b, - } - - b, err = json.Marshal(&line) - if err != nil { - return err - } - - _, err = w.Write(b) - if err != nil { - return err - } - - // jsonl files need a newline - _, err = w.Write(newline) - return err -} - -// writeArchiveBlockLine writes a single block to the archive. -func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error { - b, err := json.Marshal(&board) - if err != nil { - return err - } - line := model.ArchiveLine{ - Type: "board", - Data: b, - } - - b, err = json.Marshal(&line) - if err != nil { - return err - } - - _, err = w.Write(b) - if err != nil { - return err - } - - // jsonl files need a newline - _, err = w.Write(newline) - return err -} - -// writeArchiveFile writes a single file to the archive. -func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error { - dest, err := zw.Create(boardID + "/" + filename) - if err != nil { - return err - } - - _, fileReader, err := a.GetFile(opt.TeamID, boardID, filename) - if err != nil && !model.IsErrNotFound(err) { - return err - } - if err != nil { - // just log this; image file is missing but we'll still export an equivalent board - a.logger.Error("image file missing for export", - mlog.String("filename", filename), - mlog.String("team_id", opt.TeamID), - mlog.String("board_id", boardID), - ) - return nil - } - defer fileReader.Close() - - _, err = io.Copy(dest, fileReader) - return err -} - -// getBoardsForArchive fetches all the specified boards. -func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) { - boards := make([]model.Board, 0, len(boardIDs)) - - for _, id := range boardIDs { - b, err := a.GetBoard(id) - if err != nil { - return nil, fmt.Errorf("could not fetch board %s: %w", id, err) - } - - boards = append(boards, *b) - } - return boards, nil -} - -func extractFilename(block *model.Block) (string, error) { - f, ok := block.Fields["fileId"] - if !ok { - f, ok = block.Fields["attachmentId"] - if !ok { - return "", model.ErrInvalidImageBlock - } - } - - filename, ok := f.(string) - if !ok { - return "", model.ErrInvalidImageBlock - } - return filename, nil -} diff --git a/server/boards/app/files.go b/server/boards/app/files.go deleted file mode 100644 index 5211f2294f..0000000000 --- a/server/boards/app/files.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "fmt" - "io" - "path/filepath" - "strings" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed") -var ErrFileNotFound = errors.New("file not found") - -func (a *App) SaveFile(reader io.Reader, teamID, boardID, filename string, asTemplate bool) (string, error) { - // NOTE: File extension includes the dot - fileExtension := strings.ToLower(filepath.Ext(filename)) - if fileExtension == ".jpeg" { - fileExtension = ".jpg" - } - - createdFilename := utils.NewID(utils.IDTypeNone) - newFileName := fmt.Sprintf(`%s%s`, createdFilename, fileExtension) - if asTemplate { - newFileName = filename - } - filePath := getDestinationFilePath(asTemplate, teamID, boardID, newFileName) - - fileSize, appErr := a.filesBackend.WriteFile(reader, filePath) - if appErr != nil { - return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr) - } - - fileInfo := model.NewFileInfo(filename) - fileInfo.Id = getFileInfoID(createdFilename) - fileInfo.Path = filePath - fileInfo.Size = fileSize - err := a.store.SaveFileInfo(fileInfo) - if err != nil { - return "", err - } - return newFileName, nil -} - -func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) { - if filename == "" { - return nil, errEmptyFilename - } - - // filename is in the format 7. - // we want to extract the part of this as this - // will be the fileinfo id. - fileInfoID := getFileInfoID(strings.Split(filename, ".")[0]) - fileInfo, err := a.store.GetFileInfo(fileInfoID) - if err != nil { - return nil, err - } - - return fileInfo, nil -} - -func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, filestore.ReadCloseSeeker, error) { - fileInfo, filePath, err := a.GetFilePath(teamID, rootID, fileName) - if err != nil { - a.logger.Error("GetFile: Failed to GetFilePath.", mlog.String("Team", teamID), mlog.String("board", rootID), mlog.String("filename", fileName), mlog.Err(err)) - return nil, nil, err - } - - exists, err := a.filesBackend.FileExists(filePath) - if err != nil { - a.logger.Error("GetFile: Failed to check if file exists as path. ", mlog.String("Path", filePath), mlog.Err(err)) - return nil, nil, err - } - if !exists { - return nil, nil, ErrFileNotFound - } - - reader, err := a.filesBackend.Reader(filePath) - if err != nil { - a.logger.Error("GetFile: Failed to get file reader of existing file at path", mlog.String("Path", filePath), mlog.Err(err)) - return nil, nil, err - } - return fileInfo, reader, nil -} - -func (a *App) GetFilePath(teamID, rootID, fileName string) (*mm_model.FileInfo, string, error) { - fileInfo, err := a.GetFileInfo(fileName) - if err != nil && !model.IsErrNotFound(err) { - return nil, "", err - } - - var filePath string - - if fileInfo != nil && fileInfo.Path != "" { - filePath = fileInfo.Path - } else { - filePath = filepath.Join(teamID, rootID, fileName) - } - - return fileInfo, filePath, nil -} - -func getDestinationFilePath(isTemplate bool, teamID, boardID, filename string) string { - // if saving a file for a template, save using the "old method" that is /teamID/boardID/fileName - // this will prevent template files from being deleted by DataRetention, - // which deletes all files inside the "date" subdirectory - if isTemplate { - return filepath.Join(teamID, boardID, filename) - } - return filepath.Join(utils.GetBaseFilePath(), filename) -} - -func getFileInfoID(fileName string) string { - // Boards ids are 27 characters long with a prefix character. - // removing the prefix, returns the 26 character uuid - return fileName[1:] -} - -func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { - filePath := filepath.Join(teamID, rootID, filename) - exists, err := a.filesBackend.FileExists(filePath) - if err != nil { - return nil, err - } - // FIXUP: Check the deprecated old location - if teamID == "0" && !exists { - oldExists, err2 := a.filesBackend.FileExists(filename) - if err2 != nil { - return nil, err2 - } - if oldExists { - err2 := a.filesBackend.MoveFile(filename, filePath) - if err2 != nil { - a.logger.Error("ERROR moving file", - mlog.String("old", filename), - mlog.String("new", filePath), - mlog.Err(err2), - ) - } else { - a.logger.Debug("Moved file", - mlog.String("old", filename), - mlog.String("new", filePath), - ) - } - } - } else if !exists { - return nil, ErrFileNotFound - } - - reader, err := a.filesBackend.Reader(filePath) - if err != nil { - return nil, err - } - - return reader, nil -} - -func (a *App) MoveFile(channelID, teamID, boardID, filename string) error { - oldPath := filepath.Join(channelID, boardID, filename) - newPath := filepath.Join(teamID, boardID, filename) - err := a.filesBackend.MoveFile(oldPath, newPath) - if err != nil { - a.logger.Error("ERROR moving file", - mlog.String("old", oldPath), - mlog.String("new", newPath), - mlog.Err(err), - ) - return err - } - return nil -} - -func (a *App) CopyAndUpdateCardFiles(boardID, userID string, blocks []*model.Block, asTemplate bool) error { - newFileNames, err := a.CopyCardFiles(boardID, blocks, asTemplate) - if err != nil { - a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err)) - } - - // blocks now has updated file ids for any blocks containing files. We need to update the database for them. - blockIDs := make([]string, 0) - blockPatches := make([]model.BlockPatch, 0) - for _, block := range blocks { - if block.Type == model.TypeImage || block.Type == model.TypeAttachment { - if fileID, ok := block.Fields["fileId"].(string); ok { - blockIDs = append(blockIDs, block.ID) - blockPatches = append(blockPatches, model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - "fileId": newFileNames[fileID], - }, - DeletedFields: []string{"attachmentId"}, - }) - } - } - } - a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs))) - - if len(blockIDs) != 0 { - patches := &model.BlockPatchBatch{ - BlockIDs: blockIDs, - BlockPatches: blockPatches, - } - if err := a.store.PatchBlocks(patches, userID); err != nil { - return fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err) - } - } - - return nil -} - -func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block, asTemplate bool) (map[string]string, error) { - // Images attached in cards have a path comprising the card's board ID. - // When we create a template from this board, we need to copy the files - // with the new board ID in path. - // Not doing so causing images in templates (and boards created from this - // template) to fail to load. - - // look up ID of source sourceBoard, which may be different than the blocks. - sourceBoard, err := a.GetBoard(sourceBoardID) - if err != nil || sourceBoard == nil { - return nil, fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err) - } - - var destBoard *model.Board - newFileNames := make(map[string]string) - for _, block := range copiedBlocks { - if block.Type != model.TypeImage && block.Type != model.TypeAttachment { - continue - } - - fileId, isOk := block.Fields["fileId"].(string) - if !isOk { - fileId, isOk = block.Fields["attachmentId"].(string) - if !isOk { - continue - } - } - - // create unique filename - ext := filepath.Ext(fileId) - fileInfoID := utils.NewID(utils.IDTypeNone) - destFilename := fileInfoID + ext - - if destBoard == nil || block.BoardID != destBoard.ID { - destBoard = sourceBoard - if block.BoardID != destBoard.ID { - destBoard, err = a.GetBoard(block.BoardID) - if err != nil { - return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err) - } - } - } - - // GetFilePath will retrieve the correct path - // depending on whether FileInfo table is used for the file. - fileInfo, sourceFilePath, err := a.GetFilePath(sourceBoard.TeamID, sourceBoard.ID, fileId) - if err != nil { - return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err) - } - destinationFilePath := getDestinationFilePath(asTemplate, destBoard.TeamID, destBoard.ID, destFilename) - - if fileInfo == nil { - fileInfo = model.NewFileInfo(destFilename) - } - fileInfo.Id = getFileInfoID(fileInfoID) - fileInfo.Path = destinationFilePath - err = a.store.SaveFileInfo(fileInfo) - if err != nil { - return nil, fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err) - } - - a.logger.Debug( - "Copying card file", - mlog.String("sourceFilePath", sourceFilePath), - mlog.String("destinationFilePath", destinationFilePath), - ) - - if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil { - a.logger.Error( - "CopyCardFiles failed to copy file", - mlog.String("sourceFilePath", sourceFilePath), - mlog.String("destinationFilePath", destinationFilePath), - mlog.Err(err), - ) - } - newFileNames[fileId] = destFilename - } - - return newFileNames, nil -} diff --git a/server/boards/app/files_test.go b/server/boards/app/files_test.go deleted file mode 100644 index a59e4c8bf2..0000000000 --- a/server/boards/app/files_test.go +++ /dev/null @@ -1,569 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "io" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks" -) - -const ( - testFileName = "temp-file-name" - testBoardID = "test-board-id" -) - -var errDummy = errors.New("hello") - -type TestError struct{} - -func (err *TestError) Error() string { return "Mocked File backend error" } - -func TestGetFileReader(t *testing.T) { - testFilePath := filepath.Join("1", "test-board-id", "temp-file-name") - - th, _ := SetupTestHelper(t) - mockedReadCloseSeek := &mocks.ReadCloseSeeker{} - t.Run("should get file reader from filestore successfully", func(t *testing.T) { - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - - fileExistsFunc := func(path string) bool { - return true - } - - fileExistsErrorFunc := func(path string) error { - return nil - } - - mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, _ := th.App.GetFileReader("1", testBoardID, testFileName) - assert.Equal(t, mockedReadCloseSeek, actual) - }) - - t.Run("should get error from filestore when file exists return error", func(t *testing.T) { - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedError := &TestError{} - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - - fileExistsFunc := func(path string) bool { - return false - } - - fileExistsErrorFunc := func(path string) error { - return mockedError - } - - mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, err := th.App.GetFileReader("1", testBoardID, testFileName) - assert.Error(t, err, mockedError) - assert.Nil(t, actual) - }) - - t.Run("should return error, if get reader from file backend returns error", func(t *testing.T) { - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedError := &TestError{} - readerFunc := func(path string) filestore.ReadCloseSeeker { - return nil - } - - readerErrorFunc := func(path string) error { - return mockedError - } - - fileExistsFunc := func(path string) bool { - return false - } - - fileExistsErrorFunc := func(path string) error { - return nil - } - - mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) - actual, err := th.App.GetFileReader("1", testBoardID, testFileName) - assert.Error(t, err, mockedError) - assert.Nil(t, actual) - }) - - t.Run("should move file from old filepath to new filepath, if file doesnot exists in new filepath and workspace id is 0", func(t *testing.T) { - filePath := filepath.Join("0", "test-board-id", "temp-file-name") - workspaceid := "0" - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - - fileExistsFunc := func(path string) bool { - // return true for old path - return path == testFileName - } - - fileExistsErrorFunc := func(path string) error { - return nil - } - - moveFileFunc := func(oldFileName, newFileName string) error { - return nil - } - - mockedFileBackend.On("FileExists", filePath).Return(fileExistsFunc, fileExistsErrorFunc) - mockedFileBackend.On("FileExists", testFileName).Return(fileExistsFunc, fileExistsErrorFunc) - mockedFileBackend.On("MoveFile", testFileName, filePath).Return(moveFileFunc) - mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) - - actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) - assert.Equal(t, mockedReadCloseSeek, actual) - }) - - t.Run("should return file reader, if file doesnot exists in new filepath and old file path", func(t *testing.T) { - filePath := filepath.Join("0", "test-board-id", "temp-file-name") - fileName := testFileName - workspaceid := "0" - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - - fileExistsFunc := func(path string) bool { - // return true for old path - return false - } - - fileExistsErrorFunc := func(path string) error { - return nil - } - - moveFileFunc := func(oldFileName, newFileName string) error { - return nil - } - - mockedFileBackend.On("FileExists", filePath).Return(fileExistsFunc, fileExistsErrorFunc) - mockedFileBackend.On("FileExists", testFileName).Return(fileExistsFunc, fileExistsErrorFunc) - mockedFileBackend.On("MoveFile", fileName, filePath).Return(moveFileFunc) - mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) - - actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) - assert.Equal(t, mockedReadCloseSeek, actual) - }) -} - -func TestSaveFile(t *testing.T) { - th, _ := SetupTestHelper(t) - mockedReadCloseSeek := &mocks.ReadCloseSeeker{} - t.Run("should save file to file store using file backend", func(t *testing.T) { - fileName := "temp-file-name.txt" - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) - - writeFileFunc := func(reader io.Reader, path string) int64 { - paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "boards", paths[0]) - assert.Equal(t, time.Now().Format("20060102"), paths[1]) - fileName = paths[2] - return int64(10) - } - - writeFileErrorFunc := func(reader io.Reader, filePath string) error { - return nil - } - - mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName, false) - assert.Equal(t, fileName, actual) - assert.NoError(t, err) - }) - - t.Run("should save .jpeg file as jpg file to file store using file backend", func(t *testing.T) { - fileName := "temp-file-name.jpeg" - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) - - writeFileFunc := func(reader io.Reader, path string) int64 { - paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "boards", paths[0]) - assert.Equal(t, time.Now().Format("20060102"), paths[1]) - assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) - return int64(10) - } - - writeFileErrorFunc := func(reader io.Reader, filePath string) error { - return nil - } - - mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false) - assert.NoError(t, err) - assert.NotNil(t, actual) - }) - - t.Run("should return error when fileBackend.WriteFile returns error", func(t *testing.T) { - fileName := "temp-file-name.jpeg" - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedError := &TestError{} - - writeFileFunc := func(reader io.Reader, path string) int64 { - paths := strings.Split(path, string(os.PathSeparator)) - assert.Equal(t, "boards", paths[0]) - assert.Equal(t, time.Now().Format("20060102"), paths[1]) - assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) - return int64(10) - } - - writeFileErrorFunc := func(reader io.Reader, filePath string) error { - return mockedError - } - - mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) - actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false) - assert.Equal(t, "", actual) - assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error()) - }) -} - -func TestGetFileInfo(t *testing.T) { - th, _ := SetupTestHelper(t) - - t.Run("should return file info", func(t *testing.T) { - fileInfo := &mm_model.FileInfo{ - Id: "file_info_id", - Archived: false, - } - - th.Store.EXPECT().GetFileInfo("filename").Return(fileInfo, nil).Times(2) - - fetchedFileInfo, err := th.App.GetFileInfo("Afilename") - assert.NoError(t, err) - assert.Equal(t, "file_info_id", fetchedFileInfo.Id) - assert.False(t, fetchedFileInfo.Archived) - - fetchedFileInfo, err = th.App.GetFileInfo("Afilename.txt") - assert.NoError(t, err) - assert.Equal(t, "file_info_id", fetchedFileInfo.Id) - assert.False(t, fetchedFileInfo.Archived) - }) - - t.Run("should return archived file info", func(t *testing.T) { - fileInfo := &mm_model.FileInfo{ - Id: "file_info_id", - Archived: true, - } - - th.Store.EXPECT().GetFileInfo("filename").Return(fileInfo, nil) - - fetchedFileInfo, err := th.App.GetFileInfo("Afilename") - assert.NoError(t, err) - assert.Equal(t, "file_info_id", fetchedFileInfo.Id) - assert.True(t, fetchedFileInfo.Archived) - }) - - t.Run("should return archived file infoerror", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("filename").Return(nil, errDummy) - - fetchedFileInfo, err := th.App.GetFileInfo("Afilename") - assert.Error(t, err) - assert.Nil(t, fetchedFileInfo) - }) -} - -func TestGetFile(t *testing.T) { - th, _ := SetupTestHelper(t) - - t.Run("happy path, no errors", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ - Id: "fileInfoID", - Path: "/path/to/file/fileName.txt", - }, nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedReadCloseSeek := &mocks.ReadCloseSeeker{} - readerFunc := func(path string) filestore.ReadCloseSeeker { - return mockedReadCloseSeek - } - - readerErrorFunc := func(path string) error { - return nil - } - mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(readerFunc, readerErrorFunc) - mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil) - - fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") - assert.NoError(t, err) - assert.NotNil(t, fileInfo) - assert.NotNil(t, seeker) - }) - - t.Run("when GetFilePath() throws error", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, errDummy) - - fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") - assert.Error(t, err) - assert.Nil(t, fileInfo) - assert.Nil(t, seeker) - }) - - t.Run("when FileExists returns false", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ - Id: "fileInfoID", - Path: "/path/to/file/fileName.txt", - }, nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(false, nil) - - fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") - assert.Error(t, err) - assert.Nil(t, fileInfo) - assert.Nil(t, seeker) - }) - t.Run("when FileReader throws error", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ - Id: "fileInfoID", - Path: "/path/to/file/fileName.txt", - }, nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(nil, errDummy) - mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil) - - fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt") - assert.Error(t, err) - assert.Nil(t, fileInfo) - assert.Nil(t, seeker) - }) - -} - -func TestGetFilePath(t *testing.T) { - th, _ := SetupTestHelper(t) - - t.Run("when FileInfo exists", func(t *testing.T) { - path := "/path/to/file/fileName.txt" - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ - Id: "fileInfoID", - Path: path, - }, nil) - - fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") - assert.NoError(t, err) - assert.NotNil(t, fileInfo) - assert.Equal(t, path, filePath) - }) - - t.Run("when FileInfo doesn't exist", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) - - fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") - assert.NoError(t, err) - assert.Nil(t, fileInfo) - assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath) - }) - - t.Run("when FileInfo exists but FileInfo.Path is not set", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ - Id: "fileInfoID", - Path: "", - }, nil) - - fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") - assert.NoError(t, err) - assert.NotNil(t, fileInfo) - assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath) - }) -} - -func TestCopyCard(t *testing.T) { - th, _ := SetupTestHelper(t) - imageBlock := &model.Block{ - ID: "imageBlock", - ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", - CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - Schema: 1, - Type: "image", - Title: "", - Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, - CreateAt: 1680725585250, - UpdateAt: 1680725585250, - DeleteAt: 0, - BoardID: "boardID", - } - t.Run("Board doesn't exist", func(t *testing.T) { - th.Store.EXPECT().GetBoard("boardID").Return(nil, errDummy) - _, err := th.App.CopyCardFiles("boardID", []*model.Block{}, false) - assert.Error(t, err) - }) - - t.Run("Board exists, image block, with FileInfo", func(t *testing.T) { - path := "/path/to/file/fileName.txt" - fileInfo := &mm_model.FileInfo{ - Id: "imageBlock", - Path: path, - } - th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ - ID: "boardID", - IsTemplate: false, - }, nil) - th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) - th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) - - updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false) - assert.NoError(t, err) - assert.Equal(t, "7fileName.jpg", imageBlock.Fields["fileId"]) - assert.NotNil(t, updatedFileNames["7fileName.jpg"]) - assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) - }) - - t.Run("Board exists, attachment block, with FileInfo", func(t *testing.T) { - attachmentBlock := &model.Block{ - ID: "attachmentBlock", - ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", - CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - Schema: 1, - Type: "attachment", - Title: "", - Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, - CreateAt: 1680725585250, - UpdateAt: 1680725585250, - DeleteAt: 0, - BoardID: "boardID", - } - - path := "/path/to/file/fileName.txt" - fileInfo := &mm_model.FileInfo{ - Id: "attachmentBlock", - Path: path, - } - th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ - ID: "boardID", - IsTemplate: false, - }, nil) - th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) - th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) - - updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{attachmentBlock}, false) - assert.NoError(t, err) - assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) - }) - - t.Run("Board exists, image block, without FileInfo", func(t *testing.T) { - // path := "/path/to/file/fileName.txt" - // fileInfo := &mm_model.FileInfo{ - // Id: "imageBlock", - // Path: path, - // } - th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ - ID: "boardID", - IsTemplate: false, - }, nil) - th.Store.EXPECT().GetFileInfo(gomock.Any()).Return(nil, nil) - th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) - - updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false) - assert.NoError(t, err) - assert.NotNil(t, imageBlock.Fields["fileId"].(string)) - assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)]) - }) -} - -func TestCopyAndUpdateCardFiles(t *testing.T) { - th, _ := SetupTestHelper(t) - imageBlock := &model.Block{ - ID: "imageBlock", - ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", - CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - Schema: 1, - Type: "image", - Title: "", - Fields: map[string]interface{}{"fileId": "7fileName.jpg"}, - CreateAt: 1680725585250, - UpdateAt: 1680725585250, - DeleteAt: 0, - BoardID: "boardID", - } - - t.Run("Board exists, image block, with FileInfo", func(t *testing.T) { - path := "/path/to/file/fileName.txt" - fileInfo := &mm_model.FileInfo{ - Id: "imageBlock", - Path: path, - } - th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{ - ID: "boardID", - IsTemplate: false, - }, nil) - th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil) - th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil) - th.Store.EXPECT().PatchBlocks(gomock.Any(), "userID").Return(nil) - - mockedFileBackend := &mocks.FileBackend{} - th.App.filesBackend = mockedFileBackend - mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil) - - err := th.App.CopyAndUpdateCardFiles("boardID", "userID", []*model.Block{imageBlock}, false) - assert.NoError(t, err) - - assert.NotEqual(t, path, imageBlock.Fields["fileId"]) - }) -} diff --git a/server/boards/app/helper_test.go b/server/boards/app/helper_test.go deleted file mode 100644 index a4f862f9d9..0000000000 --- a/server/boards/app/helper_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package app - -import ( - "testing" - - "github.com/golang/mock/gomock" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/metrics" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mmpermissions" - mmpermissionsMocks "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mmpermissions/mocks" - permissionsMocks "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mocks" - "github.com/mattermost/mattermost/server/v8/boards/services/store/mockstore" - "github.com/mattermost/mattermost/server/v8/boards/services/webhook" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks" -) - -type TestHelper struct { - App *App - Store *mockstore.MockStore - FilesBackend *mocks.FileBackend - logger mlog.LoggerIFace - API *mmpermissionsMocks.MockAPI -} - -func SetupTestHelper(t *testing.T) (*TestHelper, func()) { - ctrl := gomock.NewController(t) - cfg := config.Configuration{} - store := mockstore.NewMockStore(ctrl) - filesBackend := &mocks.FileBackend{} - auth := auth.New(&cfg, store, nil) - logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) - sessionToken := "TESTTOKEN" - wsserver := ws.NewServer(auth, sessionToken, false, logger, store) - webhook := webhook.NewClient(&cfg, logger) - metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) - - mockStore := permissionsMocks.NewMockStore(ctrl) - mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) - permissions := mmpermissions.New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError)) - - appServices := Services{ - Auth: auth, - Store: store, - FilesBackend: filesBackend, - Webhook: webhook, - Metrics: metricsService, - Logger: logger, - SkipTemplateInit: true, - Permissions: permissions, - } - app2 := New(&cfg, wsserver, appServices) - - tearDown := func() { - app2.Shutdown() - if logger != nil { - _ = logger.Shutdown() - } - } - - return &TestHelper{ - App: app2, - Store: store, - FilesBackend: filesBackend, - logger: logger, - API: mockAPI, - }, tearDown -} diff --git a/server/boards/app/import.go b/server/boards/app/import.go deleted file mode 100644 index 1bd69794d8..0000000000 --- a/server/boards/app/import.go +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "bufio" - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "path" - "path/filepath" - "strings" - - "github.com/krolaw/zipstream" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - archiveVersion = 2 - legacyFileBegin = "{\"version\":1" -) - -var ( - errBlockIsNotABoard = errors.New("block is not a board") -) - -// ImportArchive imports an archive containing zero or more boards, plus all -// associated content, including cards, content blocks, views, and images. -// -// Archives are ZIP files containing a `version.json` file and zero or more -// directories, each containing a `board.jsonl` and zero or more image files. -func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { - // peek at the first bytes to see if this is a legacy archive format - br := bufio.NewReader(r) - peek, err := br.Peek(len(legacyFileBegin)) - if err == nil && string(peek) == legacyFileBegin { - a.logger.Debug("importing legacy archive") - _, errImport := a.ImportBoardJSONL(br, opt) - - return errImport - } - - zr := zipstream.NewReader(br) - - boardMap := make(map[string]*model.Board) // maps old board ids to new - fileMap := make(map[string]string) // maps old fileIds to new - - for { - hdr, err := zr.Next() - if err != nil { - if errors.Is(err, io.EOF) { - a.fixImagesAttachments(boardMap, fileMap, opt.TeamID, opt.ModifiedBy) - a.logger.Debug("import archive - done", mlog.Int("boards_imported", len(boardMap))) - return nil - } - return err - } - - dir, filename := filepath.Split(hdr.Name) - dir = path.Clean(dir) - - switch filename { - case "version.json": - ver, errVer := parseVersionFile(zr) - if errVer != nil { - return errVer - } - if ver != archiveVersion { - return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion) - } - case "board.jsonl": - board, err := a.ImportBoardJSONL(zr, opt) - if err != nil { - return fmt.Errorf("cannot import board %s: %w", dir, err) - } - boardMap[dir] = board - default: - // import file/image; dir is the old board id - board, ok := boardMap[dir] - if !ok { - a.logger.Warn("skipping orphan image in archive", - mlog.String("dir", dir), - mlog.String("filename", filename), - ) - continue - } - - newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate) - if err != nil { - return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err) - } - fileMap[filename] = newFileName - - a.logger.Debug("import archive file", - mlog.String("TeamID", opt.TeamID), - mlog.String("boardID", board.ID), - mlog.String("filename", filename), - mlog.String("newFileName", newFileName), - ) - } - } -} - -// Update image and attachment blocks -func (a *App) fixImagesAttachments(boardMap map[string]*model.Board, fileMap map[string]string, teamID string, userId string) { - blockIDs := make([]string, 0) - blockPatches := make([]model.BlockPatch, 0) - for _, board := range boardMap { - if board.IsTemplate { - continue - } - - opts := model.QueryBlocksOptions{ - BoardID: board.ID, - } - newBlocks, err := a.GetBlocks(opts) - if err != nil { - a.logger.Info("cannot retrieve imported blocks for board", mlog.String("BoardID", board.ID), mlog.Err(err)) - return - } - - for _, block := range newBlocks { - if block.Type == "image" || block.Type == "attachment" { - fieldName := "fileId" - oldId := block.Fields[fieldName] - blockIDs = append(blockIDs, block.ID) - - blockPatches = append(blockPatches, model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - fieldName: fileMap[oldId.(string)], - }, - }) - } - } - - blockPatchBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches} - a.PatchBlocks(teamID, &blockPatchBatch, userId) - } -} - -// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting -// board id is returned. -func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*model.Board, error) { - // TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks. - // We don't want to load the whole file in memory, even though it's a single board. - boardsAndBlocks := &model.BoardsAndBlocks{ - Blocks: make([]*model.Block, 0, 10), - Boards: make([]*model.Board, 0, 10), - } - lineReader := bufio.NewReader(r) - - userID := opt.ModifiedBy - if userID == model.SingleUser { - userID = "" - } - now := utils.GetMillis() - var boardID string - var boardMembers []*model.BoardMember - - lineNum := 1 - firstLine := true - for { - line, errRead := readLine(lineReader) - if len(line) != 0 { - var skip bool - if firstLine { - // first line might be a header tag (old archive format) - if strings.HasPrefix(string(line), legacyFileBegin) { - skip = true - } - } - - if !skip { - var archiveLine model.ArchiveLine - if err := json.Unmarshal(line, &archiveLine); err != nil { - return nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, err) - } - - // first line must be a board - if firstLine && archiveLine.Type == "block" { - archiveLine.Type = "board_block" - } - - switch archiveLine.Type { - case "board": - var board model.Board - if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil { - return nil, fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2) - } - board.ModifiedBy = userID - board.UpdateAt = now - board.TeamID = opt.TeamID - boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board) - boardID = board.ID - case "board_block": - // legacy archives encoded boards as blocks; we need to convert them to real boards. - var block *model.Block - if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil { - return nil, fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2) - } - block.ModifiedBy = userID - block.UpdateAt = now - board, err := a.blockToBoard(block, opt) - if err != nil { - return nil, fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err) - } - boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board) - boardID = board.ID - case "block": - var block *model.Block - if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil { - return nil, fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2) - } - block.ModifiedBy = userID - block.UpdateAt = now - block.BoardID = boardID - boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block) - case "boardMember": - var boardMember *model.BoardMember - if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil { - return nil, fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2) - } - boardMembers = append(boardMembers, boardMember) - default: - return nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type) - } - firstLine = false - } - } - - if errRead != nil { - if errors.Is(errRead, io.EOF) { - break - } - return nil, fmt.Errorf("error reading archive line %d: %w", lineNum, errRead) - } - lineNum++ - } - - // loop to remove the people how are not part of the team and system - for i := len(boardMembers) - 1; i >= 0; i-- { - if _, err := a.GetUser(boardMembers[i].UserID); err != nil { - boardMembers = append(boardMembers[:i], boardMembers[i+1:]...) - } - } - - a.fixBoardsandBlocks(boardsAndBlocks, opt) - - var err error - boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger) - if err != nil { - return nil, fmt.Errorf("error generating archive block IDs: %w", err) - } - - boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false) - if err != nil { - return nil, fmt.Errorf("error inserting archive blocks: %w", err) - } - - // add users to all the new boards (if not the fake system user). - for _, board := range boardsAndBlocks.Boards { - // make sure an admin user gets added - adminMember := &model.BoardMember{ - BoardID: board.ID, - UserID: opt.ModifiedBy, - SchemeAdmin: true, - } - if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil { - return nil, fmt.Errorf("cannot add adminMember to board: %w", err2) - } - for _, boardMember := range boardMembers { - bm := &model.BoardMember{ - BoardID: board.ID, - UserID: boardMember.UserID, - Roles: boardMember.Roles, - MinimumRole: boardMember.MinimumRole, - SchemeAdmin: boardMember.SchemeAdmin, - SchemeEditor: boardMember.SchemeEditor, - SchemeCommenter: boardMember.SchemeCommenter, - SchemeViewer: boardMember.SchemeViewer, - Synthetic: boardMember.Synthetic, - } - if _, err2 := a.AddMemberToBoard(bm); err2 != nil { - return nil, fmt.Errorf("cannot add member to board: %w", err2) - } - } - } - - // find new board id - for _, board := range boardsAndBlocks.Boards { - return board, nil - } - return nil, fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock) -} - -// fixBoardsandBlocks allows the caller of `ImportArchive` to modify or filters boards and blocks being -// imported via callbacks. -func (a *App) fixBoardsandBlocks(boardsAndBlocks *model.BoardsAndBlocks, opt model.ImportArchiveOptions) { - if opt.BlockModifier == nil && opt.BoardModifier == nil { - return - } - - modInfoCache := make(map[string]interface{}) - modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards)) - modBlocks := make([]*model.Block, 0, len(boardsAndBlocks.Blocks)) - - for _, board := range boardsAndBlocks.Boards { - b := *board - if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) { - a.logger.Debug("skipping insert board per board modifier", - mlog.String("boardID", board.ID), - ) - continue - } - modBoards = append(modBoards, &b) - } - - for _, block := range boardsAndBlocks.Blocks { - b := block - if opt.BlockModifier != nil && !opt.BlockModifier(b, modInfoCache) { - a.logger.Debug("skipping insert block per block modifier", - mlog.String("blockID", block.ID), - ) - continue - } - modBlocks = append(modBlocks, b) - } - - boardsAndBlocks.Boards = modBoards - boardsAndBlocks.Blocks = modBlocks -} - -// blockToBoard converts a `model.Block` to `model.Board`. Legacy archive formats encode boards as blocks -// and need conversion during import. -func (a *App) blockToBoard(block *model.Block, opt model.ImportArchiveOptions) (*model.Board, error) { - if block.Type != model.TypeBoard { - return nil, errBlockIsNotABoard - } - - board := &model.Board{ - ID: block.ID, - TeamID: opt.TeamID, - CreatedBy: block.CreatedBy, - ModifiedBy: block.ModifiedBy, - Type: model.BoardTypePrivate, - Title: block.Title, - CreateAt: block.CreateAt, - UpdateAt: block.UpdateAt, - DeleteAt: block.DeleteAt, - Properties: make(map[string]interface{}), - CardProperties: make([]map[string]interface{}, 0), - } - - if icon, ok := stringValue(block.Fields, "icon"); ok { - board.Icon = icon - } - if description, ok := stringValue(block.Fields, "description"); ok { - board.Description = description - } - if showDescription, ok := boolValue(block.Fields, "showDescription"); ok { - board.ShowDescription = showDescription - } - if isTemplate, ok := boolValue(block.Fields, "isTemplate"); ok { - board.IsTemplate = isTemplate - } - if templateVer, ok := intValue(block.Fields, "templateVer"); ok { - board.TemplateVersion = templateVer - } - if properties, ok := mapValue(block.Fields, "properties"); ok { - board.Properties = properties - } - if cardProperties, ok := arrayMapsValue(block.Fields, "cardProperties"); ok { - board.CardProperties = cardProperties - } - return board, nil -} - -func stringValue(m map[string]interface{}, key string) (string, bool) { - v, ok := m[key] - if !ok { - return "", false - } - s, ok := v.(string) - if !ok { - return "", false - } - return s, true -} - -func boolValue(m map[string]interface{}, key string) (bool, bool) { - v, ok := m[key] - if !ok { - return false, false - } - b, ok := v.(bool) - if !ok { - return false, false - } - return b, true -} - -func intValue(m map[string]interface{}, key string) (int, bool) { - v, ok := m[key] - if !ok { - return 0, false - } - i, ok := v.(int) - if !ok { - return 0, false - } - return i, true -} - -func mapValue(m map[string]interface{}, key string) (map[string]interface{}, bool) { - v, ok := m[key] - if !ok { - return nil, false - } - mm, ok := v.(map[string]interface{}) - if !ok { - return nil, false - } - return mm, true -} - -func arrayMapsValue(m map[string]interface{}, key string) ([]map[string]interface{}, bool) { - v, ok := m[key] - if !ok { - return nil, false - } - ai, ok := v.([]interface{}) - if !ok { - return nil, false - } - - arr := make([]map[string]interface{}, 0, len(ai)) - for _, mi := range ai { - mm, ok := mi.(map[string]interface{}) - if !ok { - return nil, false - } - arr = append(arr, mm) - } - return arr, true -} - -func parseVersionFile(r io.Reader) (int, error) { - file, err := io.ReadAll(r) - if err != nil { - return 0, fmt.Errorf("cannot read version.json: %w", err) - } - - var header model.ArchiveHeader - if err := json.Unmarshal(file, &header); err != nil { - return 0, fmt.Errorf("cannot parse version.json: %w", err) - } - return header.Version, nil -} - -func readLine(r *bufio.Reader) ([]byte, error) { - line, err := r.ReadBytes('\n') - line = bytes.TrimSpace(line) - return line, err -} diff --git a/server/boards/app/import_test.go b/server/boards/app/import_test.go deleted file mode 100644 index 2d1dc2e562..0000000000 --- a/server/boards/app/import_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "bytes" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestApp_ImportArchive(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - board := &model.Board{ - ID: "d14b9df9-1f31-4732-8a64-92bc7162cd28", - TeamID: "test-team", - Title: "Cross-Functional Project Plan", - } - - block := &model.Block{ - ID: "2c1873e0-1484-407d-8b2c-3c3b5a2a9f9e", - ParentID: board.ID, - Type: model.TypeView, - BoardID: board.ID, - } - - babs := &model.BoardsAndBlocks{ - Boards: []*model.Board{board}, - Blocks: []*model.Block{block}, - } - - boardMember := &model.BoardMember{ - BoardID: board.ID, - UserID: "user", - } - - t.Run("import asana archive", func(t *testing.T) { - r := bytes.NewReader([]byte(asana)) - opts := model.ImportArchiveOptions{ - TeamID: "test-team", - ModifiedBy: "user", - } - - th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil) - th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{boardMember}, nil) - th.Store.EXPECT().GetBoard(board.ID).Return(board, nil) - th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil) - th.Store.EXPECT().GetUserCategoryBoards("user", "test-team").Return([]model.CategoryBoards{ - { - Category: model.Category{ - Type: "default", - Name: "Boards", - ID: "boards_category_id", - }, - }, - }, nil) - th.Store.EXPECT().GetUserCategoryBoards("user", "test-team") - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user", "test-team", false).Return([]*model.Board{}, nil) - th.Store.EXPECT().GetMembersForUser("user").Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().AddUpdateCategoryBoard("user", utils.Anything, utils.Anything).Return(nil) - - err := th.App.ImportArchive(r, opts) - require.NoError(t, err, "import archive should not fail") - }) - - t.Run("import board archive", func(t *testing.T) { - r := bytes.NewReader([]byte(boardArchive)) - opts := model.ImportArchiveOptions{ - TeamID: "test-team", - ModifiedBy: "f1tydgc697fcbp8ampr6881jea", - } - - bm1 := &model.BoardMember{ - BoardID: board.ID, - UserID: "f1tydgc697fcbp8ampr6881jea", - } - - bm2 := &model.BoardMember{ - BoardID: board.ID, - UserID: "hxxzooc3ff8cubsgtcmpn8733e", - } - - bm3 := &model.BoardMember{ - BoardID: board.ID, - UserID: "nto73edn5ir6ifimo5a53y1dwa", - } - - user1 := &model.User{ - ID: "f1tydgc697fcbp8ampr6881jea", - } - - user2 := &model.User{ - ID: "hxxzooc3ff8cubsgtcmpn8733e", - } - - user3 := &model.User{ - ID: "nto73edn5ir6ifimo5a53y1dwa", - } - - th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "f1tydgc697fcbp8ampr6881jea").Return(babs, nil) - th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{bm1, bm2, bm3}, nil) - th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team").Return([]model.CategoryBoards{}, nil) - th.Store.EXPECT().GetUserCategoryBoards("f1tydgc697fcbp8ampr6881jea", "test-team").Return([]model.CategoryBoards{ - { - Category: model.Category{ - ID: "boards_category_id", - Name: "Boards", - Type: model.CategoryTypeSystem, - }, - }, - }, nil) - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category_id", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetMembersForUser("f1tydgc697fcbp8ampr6881jea").Return([]*model.BoardMember{}, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("f1tydgc697fcbp8ampr6881jea", "test-team", false).Return([]*model.Board{}, nil) - th.Store.EXPECT().AddUpdateCategoryBoard("f1tydgc697fcbp8ampr6881jea", utils.Anything, utils.Anything).Return(nil) - th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil) - th.Store.EXPECT().GetMemberForBoard(board.ID, "f1tydgc697fcbp8ampr6881jea").AnyTimes().Return(bm1, nil) - th.Store.EXPECT().GetMemberForBoard(board.ID, "hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(bm2, nil) - th.Store.EXPECT().GetMemberForBoard(board.ID, "nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(bm3, nil) - th.Store.EXPECT().GetUserByID("f1tydgc697fcbp8ampr6881jea").AnyTimes().Return(user1, nil) - th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil) - th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil) - - newBoard, err := th.App.ImportBoardJSONL(r, opts) - require.NoError(t, err, "import archive should not fail") - require.Equal(t, board.ID, newBoard.ID, "Board ID should be same") - }) - - t.Run("fix image and attachment", func(t *testing.T) { - boardMap := map[string]*model.Board{ - "test": board, - } - - fileMap := map[string]string{ - "oldFileName1.jpg": "newFileName1.jpg", - "oldFileName2.jpg": "newFileName2.jpg", - } - - imageBlock := &model.Block{ - ID: "blockID-1", - ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", - CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - Schema: 1, - Type: "image", - Title: "", - Fields: map[string]interface{}{"fileId": "oldFileName1.jpg"}, - CreateAt: 1680725585250, - UpdateAt: 1680725585250, - DeleteAt: 0, - BoardID: "board-id", - } - - attachmentBlock := &model.Block{ - ID: "blockID-2", - ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske", - CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh", - Schema: 1, - Type: "attachment", - Title: "", - Fields: map[string]interface{}{"fileId": "oldFileName2.jpg"}, - CreateAt: 1680725585250, - UpdateAt: 1680725585250, - DeleteAt: 0, - BoardID: "board-id", - } - - blockIDs := []string{"blockID-1", "blockID-2"} - - blockPatch := model.BlockPatch{ - UpdatedFields: map[string]interface{}{"fileId": "newFileName1.jpg"}, - } - - blockPatch2 := model.BlockPatch{ - UpdatedFields: map[string]interface{}{"fileId": "newFileName2.jpg"}, - } - - blockPatches := []model.BlockPatch{blockPatch, blockPatch2} - - blockPatchesBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches} - - opts := model.QueryBlocksOptions{ - BoardID: board.ID, - } - th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{imageBlock, attachmentBlock}, nil) - th.Store.EXPECT().GetBlocksByIDs(blockIDs).Return([]*model.Block{imageBlock, attachmentBlock}, nil) - th.Store.EXPECT().GetBlock(blockIDs[0]).Return(imageBlock, nil) - th.Store.EXPECT().GetBlock(blockIDs[1]).Return(attachmentBlock, nil) - th.Store.EXPECT().GetMembersForBoard("board-id").AnyTimes().Return([]*model.BoardMember{}, nil) - - th.Store.EXPECT().PatchBlocks(&blockPatchesBatch, "my-userid") - th.App.fixImagesAttachments(boardMap, fileMap, "test-team", "my-userid") - }) -} - -//nolint:lll -const asana = `{"version":1,"date":1614714686842} -{"type":"block","data":{"id":"d14b9df9-1f31-4732-8a64-92bc7162cd28","fields":{"icon":"","description":"","cardProperties":[{"id":"3bdcbaeb-bc78-4884-8531-a0323b74676a","name":"Section","type":"select","options":[{"id":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732","value":"Planning","color":"propColorGray"},{"id":"454559bb-b788-4ff6-873e-04def8491d2c","value":"Milestones","color":"propColorBrown"},{"id":"deaab476-c690-48df-828f-725b064dc476","value":"Next steps","color":"propColorOrange"},{"id":"2138305a-3157-461c-8bbe-f19ebb55846d","value":"Comms Plan","color":"propColorYellow"}]}]},"createAt":1614714686836,"updateAt":1614714686836,"deleteAt":0,"schema":1,"parentId":"","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"board","title":"Cross-Functional Project Plan"}} -{"type":"block","data":{"id":"2c1873e0-1484-407d-8b2c-3c3b5a2a9f9e","fields":{"sortOptions":[],"visiblePropertyIds":[],"visibleOptionIds":[],"hiddenOptionIds":[],"filter":{"operation":"and","filters":[]},"cardOrder":[],"columnWidths":{},"viewType":"board"},"createAt":1614714686840,"updateAt":1614714686840,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"view","title":"Board View"}} -{"type":"block","data":{"id":"520c332b-adf5-4a32-88ab-43655c8b6aa2","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732"},"contentOrder":["deb3966c-6d56-43b1-8e95-36806877ce81"]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[READ ME] - Instructions for using this template"}} -{"type":"block","data":{"id":"deb3966c-6d56-43b1-8e95-36806877ce81","fields":{},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"520c332b-adf5-4a32-88ab-43655c8b6aa2","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"text","title":"This project template is set up in List View with sections and Asana-created Custom Fields to help you track your team's work. We've provided some example content in this template to get you started, but you should add tasks, change task names, add more Custom Fields, and change any other info to make this project your own.\n\nSend feedback about this template: https://asa.na/templatesfeedback"}} -{"type":"block","data":{"id":"be791f66-a5e5-4408-82f6-cb1280f5bc45","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"d8d94ef1-5e74-40bb-8be5-fc0eb3f47732"},"contentOrder":["2688b31f-e7ff-4de1-87ae-d4b5570f8712"]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"Redesign the landing page of our website"}} -{"type":"block","data":{"id":"2688b31f-e7ff-4de1-87ae-d4b5570f8712","fields":{},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"be791f66-a5e5-4408-82f6-cb1280f5bc45","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"text","title":"Redesign the landing page to focus on the main persona."}} -{"type":"block","data":{"id":"98f74948-1700-4a3c-8cc2-8bb632499def","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"454559bb-b788-4ff6-873e-04def8491d2c"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Consider trying a new email marketing service"}} -{"type":"block","data":{"id":"142fba5d-05e6-4865-83d9-b3f54d9de96e","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"454559bb-b788-4ff6-873e-04def8491d2c"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Budget finalization"}} -{"type":"block","data":{"id":"ca6670b1-b034-4e42-8971-c659b478b9e0","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"deaab476-c690-48df-828f-725b064dc476"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Find a venue for the holiday party"}} -{"type":"block","data":{"id":"db1dd596-0999-4741-8b05-72ca8e438e31","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"deaab476-c690-48df-828f-725b064dc476"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Approve campaign copy"}} -{"type":"block","data":{"id":"16861c05-f31f-46af-8429-80a87b5aa93a","fields":{"icon":"","properties":{"3bdcbaeb-bc78-4884-8531-a0323b74676a":"2138305a-3157-461c-8bbe-f19ebb55846d"},"contentOrder":[]},"createAt":1614714686841,"updateAt":1614714686841,"deleteAt":0,"schema":1,"parentId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","rootId":"d14b9df9-1f31-4732-8a64-92bc7162cd28","modifiedBy":"","type":"card","title":"[EXAMPLE TASK] Send out updated attendee list"}} -` - -//nolint:lll -const boardArchive = `{"type":"board","data":{"id":"bfoi6yy6pa3yzika53spj7pq9ee","teamId":"wsmqbtwb5jb35jb3mtp85c8a9h","channelId":"","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","type":"P","minimumRole":"","title":"Custom","description":"","icon":"","showDescription":false,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"aonihehbifijmx56aqzu3cc7w1r","name":"Status","options":[],"type":"select"},{"id":"aohjkzt769rxhtcz1o9xcoce5to","name":"Person","options":[],"type":"person"}],"createAt":1672750481591,"updateAt":1672750481591,"deleteAt":0}} -{"type":"block","data":{"id":"ckpc3b1dp3pbw7bqntfryy9jbzo","parentId":"bjaqxtbyqz3bu7pgyddpgpms74a","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","schema":1,"type":"card","title":"Test","fields":{"contentOrder":[],"icon":"","isTemplate":false,"properties":{"aohjkzt769rxhtcz1o9xcoce5to":"hxxzooc3ff8cubsgtcmpn8733e"}},"createAt":1672750481612,"updateAt":1672845003530,"deleteAt":0,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}} -{"type":"block","data":{"id":"v7tdajwpm47r3u8duedk89bhxar","parentId":"bpypang3a3errqstj1agx9kuqay","createdBy":"nto73edn5ir6ifimo5a53y1dwa","modifiedBy":"nto73edn5ir6ifimo5a53y1dwa","schema":1,"type":"view","title":"Board view","fields":{"cardOrder":["crsyw7tbr3pnjznok6ppngmmyya","c5titiemp4pgaxbs4jksgybbj4y"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":[],"visiblePropertyIds":["aohjkzt769rxhtcz1o9xcoce5to"]},"createAt":1672750481626,"updateAt":1672750481626,"deleteAt":0,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}} -{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"f1tydgc697fcbp8ampr6881jea","roles":"","minimumRole":"","schemeAdmin":false,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":true,"synthetic":false}} -{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"hxxzooc3ff8cubsgtcmpn8733e","roles":"","minimumRole":"","schemeAdmin":false,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":true,"synthetic":false}} -{"type":"boardMember","data":{"boardId":"bfoi6yy6pa3yzika53spj7pq9ee","userId":"nto73edn5ir6ifimo5a53y1dwa","roles":"","minimumRole":"","schemeAdmin":true,"schemeEditor":false,"schemeCommenter":false,"schemeViewer":false,"synthetic":false}} -` diff --git a/server/boards/app/initialize.go b/server/boards/app/initialize.go deleted file mode 100644 index 9de547d5a3..0000000000 --- a/server/boards/app/initialize.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "context" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// initialize is called when the App is first created. -func (a *App) initialize(skipTemplateInit bool) { - if !skipTemplateInit { - if err := a.InitTemplates(); err != nil { - a.logger.Error(`InitializeTemplates failed`, mlog.Err(err)) - } - } -} - -func (a *App) Shutdown() { - if a.blockChangeNotifier != nil { - ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout) - defer cancel() - if !a.blockChangeNotifier.Shutdown(ctx) { - a.logger.Warn("blockChangeNotifier shutdown timed out") - } - } -} diff --git a/server/boards/app/insights.go b/server/boards/app/insights.go deleted file mode 100644 index 364b5e0566..0000000000 --- a/server/boards/app/insights.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/pkg/errors" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *App) GetTeamBoardsInsights(userID string, teamID string, opts *mm_model.InsightsOpts) (*model.BoardInsightsList, error) { - // check if server is properly licensed, and user is not a guest - userPermitted, err := insightPermissionGate(a, userID, false) - if err != nil { - return nil, err - } - if !userPermitted { - return nil, errors.New("User isn't authorized to access insights.") - } - boardIDs, err := getUserBoards(userID, teamID, a) - if err != nil { - return nil, err - } - return a.store.GetTeamBoardsInsights(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs) -} - -func (a *App) GetUserBoardsInsights(userID string, teamID string, opts *mm_model.InsightsOpts) (*model.BoardInsightsList, error) { - // check if server is properly licensed, and user is not a guest - userPermitted, err := insightPermissionGate(a, userID, true) - if err != nil { - return nil, err - } - if !userPermitted { - return nil, errors.New("User isn't authorized to access insights.") - } - boardIDs, err := getUserBoards(userID, teamID, a) - if err != nil { - return nil, err - } - return a.store.GetUserBoardsInsights(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, boardIDs) -} - -func insightPermissionGate(a *App, userID string, isMyInsights bool) (bool, error) { - licenseError := errors.New("invalid license/authorization to use insights API") - guestError := errors.New("guests aren't authorized to use insights API") - lic := a.store.GetLicense() - - user, err := a.store.GetUserByID(userID) - if err != nil { - return false, err - } - - if user.IsGuest { - return false, guestError - } - - if lic == nil && !isMyInsights { - a.logger.Debug("Deployment doesn't have a license") - return false, licenseError - } - - if !isMyInsights && (lic.SkuShortName != mm_model.LicenseShortSkuProfessional && lic.SkuShortName != mm_model.LicenseShortSkuEnterprise) { - return false, licenseError - } - - return true, nil -} - -func (a *App) GetUserTimezone(userID string) (string, error) { - return a.store.GetUserTimezone(userID) -} - -func getUserBoards(userID string, teamID string, a *App) ([]string, error) { - // get boards accessible by user and filter boardIDs - boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID, true) - if err != nil { - return nil, errors.New("error getting boards for user") - } - boardIDs := make([]string, 0, len(boards)) - - for _, board := range boards { - boardIDs = append(boardIDs, board.ID) - } - return boardIDs, nil -} diff --git a/server/boards/app/insights_test.go b/server/boards/app/insights_test.go deleted file mode 100644 index 36f67a34dc..0000000000 --- a/server/boards/app/insights_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -var mockInsightsBoards = []*model.Board{ - { - ID: "mock-user-workspace-id", - Title: "MockUserWorkspace", - }, -} - -var mockTeamInsights = []*model.BoardInsight{ - { - BoardID: "board-id-1", - }, - { - BoardID: "board-id-2", - }, -} - -var mockTeamInsightsList = &model.BoardInsightsList{ - InsightsListData: mm_model.InsightsListData{HasNext: false}, - Items: mockTeamInsights, -} - -type insightError struct { - msg string -} - -func (ie insightError) Error() string { - return ie.msg -} - -func TestGetTeamAndUserBoardsInsights(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("success query", func(t *testing.T) { - fakeLicense := &mm_model.License{Features: &mm_model.Features{}, SkuShortName: mm_model.LicenseShortSkuEnterprise} - th.Store.EXPECT().GetLicense().Return(fakeLicense).AnyTimes() - fakeUser := &model.User{ - ID: "user-id", - IsGuest: false, - } - th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes() - th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes() - th.Store.EXPECT(). - GetTeamBoardsInsights("team-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}). - Return(mockTeamInsightsList, nil) - results, err := th.App.GetTeamBoardsInsights("user-id", "team-id", &mm_model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10}) - require.NoError(t, err) - require.Len(t, results.Items, 2) - th.Store.EXPECT(). - GetUserBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}). - Return(mockTeamInsightsList, nil) - results, err = th.App.GetUserBoardsInsights("user-id", "team-id", &mm_model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10}) - require.NoError(t, err) - require.Len(t, results.Items, 2) - }) - - t.Run("fail query", func(t *testing.T) { - fakeLicense := &mm_model.License{Features: &mm_model.Features{}, SkuShortName: mm_model.LicenseShortSkuEnterprise} - th.Store.EXPECT().GetLicense().Return(fakeLicense).AnyTimes() - fakeUser := &model.User{ - ID: "user-id", - IsGuest: false, - } - th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes() - th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes() - th.Store.EXPECT(). - GetTeamBoardsInsights("team-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}). - Return(nil, insightError{"board-insight-error"}) - _, err := th.App.GetTeamBoardsInsights("user-id", "team-id", &mm_model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10}) - require.Error(t, err) - require.ErrorIs(t, err, insightError{"board-insight-error"}) - th.Store.EXPECT(). - GetUserBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}). - Return(nil, insightError{"board-insight-error"}) - _, err = th.App.GetUserBoardsInsights("user-id", "team-id", &mm_model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 10}) - require.Error(t, err) - require.ErrorIs(t, err, insightError{"board-insight-error"}) - }) -} diff --git a/server/boards/app/onboarding.go b/server/boards/app/onboarding.go deleted file mode 100644 index 48e1f7d805..0000000000 --- a/server/boards/app/onboarding.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -const ( - KeyOnboardingTourStarted = "onboardingTourStarted" - KeyOnboardingTourCategory = "tourCategory" - KeyOnboardingTourStep = "onboardingTourStep" - - ValueOnboardingFirstStep = "0" - ValueTourCategoryOnboarding = "onboarding" - - WelcomeBoardTitle = "Welcome to Boards!" -) - -var ( - errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks") - errCannotCreateBoard = errors.New("new board wasn't created") -) - -func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) { - // copy the welcome board into this workspace - boardID, err := a.createWelcomeBoard(userID, teamID) - if err != nil { - return "", "", err - } - - // set user's tour state to initial state - userPreferencesPatch := model.UserPreferencesPatch{ - UpdatedFields: map[string]string{ - KeyOnboardingTourStarted: "1", - KeyOnboardingTourStep: ValueOnboardingFirstStep, - KeyOnboardingTourCategory: ValueTourCategoryOnboarding, - }, - } - if _, err := a.store.PatchUserPreferences(userID, userPreferencesPatch); err != nil { - return "", "", err - } - - return teamID, boardID, nil -} - -func (a *App) getOnboardingBoardID() (string, error) { - boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "") - if err != nil { - return "", err - } - - var onboardingBoardID string - for _, block := range boards { - if block.Title == WelcomeBoardTitle && block.TeamID == model.GlobalTeamID { - onboardingBoardID = block.ID - break - } - } - - if onboardingBoardID == "" { - return "", errUnableToFindWelcomeBoard - } - - return onboardingBoardID, nil -} - -func (a *App) createWelcomeBoard(userID, teamID string) (string, error) { - onboardingBoardID, err := a.getOnboardingBoardID() - if err != nil { - return "", err - } - - bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false) - if err != nil { - return "", err - } - - if len(bab.Boards) != 1 { - 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 -} diff --git a/server/boards/app/onboarding_test.go b/server/boards/app/onboarding_test.go deleted file mode 100644 index 769310f8cf..0000000000 --- a/server/boards/app/onboarding_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -const ( - testTeamID = "team_id" -) - -func TestPrepareOnboardingTour(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - teamID := testTeamID - userID := "user_id_1" - welcomeBoard := model.Board{ - ID: "board_id_1", - Title: "Welcome to Boards!", - TeamID: "0", - IsTemplate: true, - } - - 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{ - { - ID: "board_id_2", - Title: "Welcome to Boards!", - TeamID: "0", - IsTemplate: true, - }, - }}, - nil, nil) - th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2) - th.Store.EXPECT().GetMembersForBoard("board_id_2").Return([]*model.BoardMember{}, nil).Times(1) - th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(2) - th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1) - th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil) - - 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_2", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil) - th.Store.EXPECT().GetMembersForUser("user_id_1").Return([]*model.BoardMember{}, nil) - - userPreferencesPatch := model.UserPreferencesPatch{ - UpdatedFields: map[string]string{ - KeyOnboardingTourStarted: "1", - KeyOnboardingTourStep: ValueOnboardingFirstStep, - KeyOnboardingTourCategory: ValueTourCategoryOnboarding, - }, - } - - th.Store.EXPECT().PatchUserPreferences(userID, userPreferencesPatch).Return(nil, nil) - th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{}, nil).Times(1) - - // when this is called the second time, the default category is created so we need to include that in the response list - th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "boards_category_id", Name: "Boards"}, - }, - }, nil).Times(2) - - th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil).Times(1) - th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ - ID: "boards_category", - Name: "Boards", - }, nil) - th.Store.EXPECT().GetBoardsForUserAndTeam("user_id_1", teamID, false).Return([]*model.Board{}, nil) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", []string{"board_id_2"}).Return(nil) - - teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID) - assert.NoError(t, err) - assert.Equal(t, testTeamID, teamID) - assert.NotEmpty(t, boardID) - }) -} - -func TestCreateWelcomeBoard(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - teamID := testTeamID - userID := "user_id_1" - welcomeBoard := model.Board{ - ID: "board_id_1", - Title: "Welcome to Boards!", - TeamID: "0", - IsTemplate: true, - } - 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}}, nil, nil) - th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(3) - th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).AnyTimes() - th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil) - - 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) - th.Store.EXPECT().GetUserCategoryBoards(userID, "team_id").Return([]model.CategoryBoards{ - { - Category: model.Category{ID: "boards_category_id", Name: "Boards"}, - }, - }, nil).Times(3) - th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "boards_category_id", []string{"board_id_1"}).Return(nil) - - boardID, err := th.App.createWelcomeBoard(userID, teamID) - assert.NoError(t, err) - assert.NotEmpty(t, boardID) - }) - - t.Run("template doesn't contain a board", func(t *testing.T) { - teamID := testTeamID - th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil) - boardID, err := th.App.createWelcomeBoard("user_id_1", teamID) - assert.Error(t, err) - assert.Empty(t, boardID) - }) - - t.Run("template doesn't contain the welcome board", func(t *testing.T) { - teamID := testTeamID - welcomeBoard := model.Board{ - ID: "board_id_1", - Title: "Other template", - TeamID: teamID, - IsTemplate: true, - } - th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) - boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") - assert.Error(t, err) - assert.Empty(t, boardID) - }) -} - -func TestGetOnboardingBoardID(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("base case", func(t *testing.T) { - welcomeBoard := model.Board{ - ID: "board_id_1", - Title: "Welcome to Boards!", - TeamID: "0", - IsTemplate: true, - } - th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) - - onboardingBoardID, err := th.App.getOnboardingBoardID() - assert.NoError(t, err) - assert.Equal(t, "board_id_1", onboardingBoardID) - }) - - t.Run("no blocks found", func(t *testing.T) { - th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{}, nil) - - onboardingBoardID, err := th.App.getOnboardingBoardID() - assert.Error(t, err) - assert.Empty(t, onboardingBoardID) - }) - - t.Run("onboarding board doesn't exists", func(t *testing.T) { - welcomeBoard := model.Board{ - ID: "board_id_1", - Title: "Other template", - TeamID: "0", - IsTemplate: true, - } - th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil) - - onboardingBoardID, err := th.App.getOnboardingBoardID() - assert.Error(t, err) - assert.Empty(t, onboardingBoardID) - }) -} diff --git a/server/boards/app/permissions.go b/server/boards/app/permissions.go deleted file mode 100644 index ac7f3ff2aa..0000000000 --- a/server/boards/app/permissions.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -func (a *App) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { - return a.permissions.HasPermissionToBoard(userID, boardID, permission) -} diff --git a/server/boards/app/server_metadata.go b/server/boards/app/server_metadata.go deleted file mode 100644 index ebd5217c69..0000000000 --- a/server/boards/app/server_metadata.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "runtime" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type ServerMetadata struct { - Version string `json:"version"` - BuildNumber string `json:"build_number"` - BuildDate string `json:"build_date"` - Commit string `json:"commit"` - Edition string `json:"edition"` - DBType string `json:"db_type"` - DBVersion string `json:"db_version"` - OSType string `json:"os_type"` - OSArch string `json:"os_arch"` - SKU string `json:"sku"` -} - -func (a *App) GetServerMetadata() *ServerMetadata { - var dbType string - var dbVersion string - if a != nil && a.store != nil { - dbType = a.store.DBType() - dbVersion = a.store.DBVersion() - } - - return &ServerMetadata{ - Version: model.CurrentVersion, - BuildNumber: model.BuildNumber, - BuildDate: model.BuildDate, - Commit: model.BuildHash, - Edition: model.Edition, - DBType: dbType, - DBVersion: dbVersion, - OSType: runtime.GOOS, - OSArch: runtime.GOARCH, - SKU: "personal_server", - } -} diff --git a/server/boards/app/server_metadata_test.go b/server/boards/app/server_metadata_test.go deleted file mode 100644 index 073da76376..0000000000 --- a/server/boards/app/server_metadata_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "reflect" - "runtime" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestGetServerMetadata(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().DBType().Return("TEST_DB_TYPE") - th.Store.EXPECT().DBVersion().Return("TEST_DB_VERSION") - - t.Run("Get Server Metadata", func(t *testing.T) { - got := th.App.GetServerMetadata() - want := &ServerMetadata{ - Version: model.CurrentVersion, - BuildNumber: model.BuildNumber, - BuildDate: model.BuildDate, - Commit: model.BuildHash, - Edition: model.Edition, - DBType: "TEST_DB_TYPE", - DBVersion: "TEST_DB_VERSION", - OSType: runtime.GOOS, - OSArch: runtime.GOARCH, - SKU: "personal_server", - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("got: %q, want: %q", got, want) - } - }) -} diff --git a/server/boards/app/sharing.go b/server/boards/app/sharing.go deleted file mode 100644 index 0331927663..0000000000 --- a/server/boards/app/sharing.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *App) GetSharing(boardID string) (*model.Sharing, error) { - sharing, err := a.store.GetSharing(boardID) - if err != nil { - return nil, err - } - return sharing, nil -} - -func (a *App) UpsertSharing(sharing model.Sharing) error { - return a.store.UpsertSharing(sharing) -} diff --git a/server/boards/app/sharing_test.go b/server/boards/app/sharing_test.go deleted file mode 100644 index 31c024f211..0000000000 --- a/server/boards/app/sharing_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql" - "testing" - - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestGetSharing(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - t.Run("should get a sharing successfully", func(t *testing.T) { - want := &model.Sharing{ - ID: utils.NewID(utils.IDTypeBlock), - Enabled: true, - Token: "token", - ModifiedBy: "otherid", - UpdateAt: utils.GetMillis(), - } - th.Store.EXPECT().GetSharing("test-id").Return(want, nil) - - result, err := th.App.GetSharing("test-id") - require.NoError(t, err) - - require.Equal(t, result, want) - require.NotNil(t, th.App) - }) - - t.Run("should fail to get a sharing", func(t *testing.T) { - th.Store.EXPECT().GetSharing("test-id").Return( - nil, - errors.New("sharing not found"), - ) - result, err := th.App.GetSharing("test-id") - - require.Nil(t, result) - require.Error(t, err) - require.Equal(t, "sharing not found", err.Error()) - }) - - t.Run("should return a not found error", func(t *testing.T) { - th.Store.EXPECT().GetSharing("test-id").Return( - nil, - sql.ErrNoRows, - ) - result, err := th.App.GetSharing("test-id") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, result) - }) -} - -func TestUpsertSharing(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - sharing := model.Sharing{ - ID: utils.NewID(utils.IDTypeBlock), - Enabled: true, - Token: "token", - ModifiedBy: "otherid", - UpdateAt: utils.GetMillis(), - } - - t.Run("should success to upsert sharing", func(t *testing.T) { - th.Store.EXPECT().UpsertSharing(sharing).Return(nil) - err := th.App.UpsertSharing(sharing) - - require.NoError(t, err) - }) - - t.Run("should fail to upsert a sharing", func(t *testing.T) { - th.Store.EXPECT().UpsertSharing(sharing).Return(errors.New("sharing not found")) - err := th.App.UpsertSharing(sharing) - - require.Error(t, err) - require.Equal(t, "sharing not found", err.Error()) - }) -} diff --git a/server/boards/app/subscriptions.go b/server/boards/app/subscriptions.go deleted file mode 100644 index 7af755cfa1..0000000000 --- a/server/boards/app/subscriptions.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { - sub, err := a.store.CreateSubscription(sub) - if err != nil { - return nil, err - } - a.notifySubscriptionChanged(sub) - - return sub, nil -} - -func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) { - sub, err := a.store.GetSubscription(blockID, subscriberID) - if err != nil { - return nil, err - } - if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil { - return nil, err - } - sub.DeleteAt = utils.GetMillis() - a.notifySubscriptionChanged(sub) - - return sub, nil -} - -func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) { - return a.store.GetSubscriptions(subscriberID) -} - -func (a *App) notifySubscriptionChanged(subscription *model.Subscription) { - if a.notifications == nil { - return - } - - board, err := a.getBoardForBlock(subscription.BlockID) - if err != nil { - a.logger.Error("Error notifying subscription change", - mlog.String("subscriber_id", subscription.SubscriberID), - mlog.String("block_id", subscription.BlockID), - mlog.Err(err), - ) - } - a.wsAdapter.BroadcastSubscriptionChange(board.TeamID, subscription) -} diff --git a/server/boards/app/teams.go b/server/boards/app/teams.go deleted file mode 100644 index 8d6c1a8b22..0000000000 --- a/server/boards/app/teams.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (a *App) GetRootTeam() (*model.Team, error) { - teamID := "0" - team, _ := a.store.GetTeam(teamID) - if team == nil { - team = &model.Team{ - ID: teamID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - err := a.store.UpsertTeamSignupToken(*team) - if err != nil { - a.logger.Error("Unable to initialize team", mlog.Err(err)) - return nil, err - } - - team, err = a.store.GetTeam(teamID) - if err != nil { - a.logger.Error("Unable to get initialized team", mlog.Err(err)) - return nil, err - } - - a.logger.Info("initialized team") - } - - return team, nil -} - -func (a *App) GetTeam(id string) (*model.Team, error) { - team, err := a.store.GetTeam(id) - if model.IsErrNotFound(err) { - return nil, nil - } - if err != nil { - return nil, err - } - return team, nil -} - -func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) { - return a.store.GetTeamsForUser(userID) -} - -func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool { - return a.auth.DoesUserHaveTeamAccess(userID, teamID) -} - -func (a *App) UpsertTeamSettings(team model.Team) error { - return a.store.UpsertTeamSettings(team) -} - -func (a *App) UpsertTeamSignupToken(team model.Team) error { - return a.store.UpsertTeamSignupToken(team) -} - -func (a *App) GetTeamCount() (int64, error) { - return a.store.GetTeamCount() -} diff --git a/server/boards/app/teams_test.go b/server/boards/app/teams_test.go deleted file mode 100644 index 9a37f76840..0000000000 --- a/server/boards/app/teams_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql" - "errors" - "testing" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -var errInvalidTeam = errors.New("invalid team id") - -var mockTeam = &model.Team{ - ID: "mock-team-id", - Title: "MockTeam", -} - -var errUpsertSignupToken = errors.New("upsert error") - -func TestGetRootTeam(t *testing.T) { - var newRootTeam = &model.Team{ - ID: "0", - Title: "NewRootTeam", - } - - testCases := []struct { - title string - teamToReturnBeforeUpsert *model.Team - teamToReturnAfterUpsert *model.Team - isError bool - }{ - { - "Success, Return new root team, when root team returned by mockstore is nil", - nil, - newRootTeam, - false, - }, - { - "Success, Return existing root team, when root team returned by mockstore is notnil", - newRootTeam, - nil, - false, - }, - { - "Fail, Return nil, when root team returned by mockstore is nil, and upsert new root team fails", - nil, - nil, - true, - }, - } - - for _, tc := range testCases { - t.Run(tc.title, func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnBeforeUpsert, nil) - if tc.teamToReturnBeforeUpsert == nil { - th.Store.EXPECT().UpsertTeamSignupToken(gomock.Any()).DoAndReturn( - func(arg0 model.Team) error { - if tc.isError { - return errUpsertSignupToken - } - th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnAfterUpsert, nil) - return nil - }) - } - rootTeam, err := th.App.GetRootTeam() - - if tc.isError { - require.Error(t, err) - } else { - assert.NotNil(t, rootTeam.ID) - assert.NotNil(t, rootTeam.SignupToken) - assert.Equal(t, "", rootTeam.ModifiedBy) - assert.Equal(t, int64(0), rootTeam.UpdateAt) - assert.Equal(t, "NewRootTeam", rootTeam.Title) - require.NoError(t, err) - require.NotNil(t, rootTeam) - } - }) - } -} - -func TestGetTeam(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - testCases := []struct { - title string - teamID string - isError bool - }{ - { - "Success, Return new root team, when team returned by mockstore is not nil", - "mock-team-id", - false, - }, - { - "Success, Return nil, when get team returns an sql error", - "team-not-available-id", - false, - }, - { - "Fail, Return nil, when get team by mockstore returns an error", - "invalid-team-id", - true, - }, - } - - th.Store.EXPECT().GetTeam("mock-team-id").Return(mockTeam, nil) - th.Store.EXPECT().GetTeam("invalid-team-id").Return(nil, errInvalidTeam) - th.Store.EXPECT().GetTeam("team-not-available-id").Return(nil, sql.ErrNoRows) - for _, tc := range testCases { - t.Run(tc.title, func(t *testing.T) { - t.Log(tc.title) - team, err := th.App.GetTeam(tc.teamID) - - if tc.isError { - require.Error(t, err) - } else if tc.teamID != "team-not-available-id" { - assert.NotNil(t, team.ID) - assert.NotNil(t, team.SignupToken) - assert.Equal(t, "mock-team-id", team.ID) - assert.Equal(t, "", team.ModifiedBy) - assert.Equal(t, int64(0), team.UpdateAt) - assert.Equal(t, "MockTeam", team.Title) - require.NoError(t, err) - require.NotNil(t, team) - } - }) - } -} - -func TestTeamOperations(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().UpsertTeamSettings(*mockTeam).Return(nil) - th.Store.EXPECT().UpsertTeamSignupToken(*mockTeam).Return(nil) - th.Store.EXPECT().GetTeamCount().Return(int64(10), nil) - - errUpsertTeamSettings := th.App.UpsertTeamSettings(*mockTeam) - assert.NoError(t, errUpsertTeamSettings) - - errUpsertTeamSignupToken := th.App.UpsertTeamSignupToken(*mockTeam) - assert.NoError(t, errUpsertTeamSignupToken) - - count, errGetTeamCount := th.App.GetTeamCount() - assert.NoError(t, errGetTeamCount) - assert.Equal(t, int64(10), count) -} diff --git a/server/boards/app/templates.boardarchive b/server/boards/app/templates.boardarchive deleted file mode 100644 index a6a1d90c07..0000000000 Binary files a/server/boards/app/templates.boardarchive and /dev/null differ diff --git a/server/boards/app/templates.go b/server/boards/app/templates.go deleted file mode 100644 index 5e2281cb3f..0000000000 --- a/server/boards/app/templates.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "bytes" - "fmt" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/assets" - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - defaultTemplateVersion = 6 // bump this number to force default templates to be re-imported -) - -func (a *App) InitTemplates() error { - _, err := a.initializeTemplates() - return err -} - -// initializeTemplates imports default templates if the boards table is empty. -func (a *App) initializeTemplates() (bool, error) { - boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "") - if err != nil { - return false, fmt.Errorf("cannot initialize templates: %w", err) - } - - a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards))) - - isNeeded, reason := a.isInitializationNeeded(boards) - if !isNeeded { - a.logger.Debug("Template import not needed, skipping") - return false, nil - } - - a.logger.Debug("Importing new default templates", - mlog.String("reason", reason), - mlog.Int("size", len(assets.DefaultTemplatesArchive)), - ) - - // Remove in case of newer Templates - if err = a.store.RemoveDefaultTemplates(boards); err != nil { - return false, fmt.Errorf("cannot remove old template boards: %w", err) - } - - r := bytes.NewReader(assets.DefaultTemplatesArchive) - - opt := model.ImportArchiveOptions{ - TeamID: model.GlobalTeamID, - ModifiedBy: model.SystemUserID, - BlockModifier: fixTemplateBlock, - BoardModifier: fixTemplateBoard, - } - if err = a.ImportArchive(r, opt); err != nil { - return false, fmt.Errorf("cannot initialize global templates for team %s: %w", model.GlobalTeamID, err) - } - return true, nil -} - -// isInitializationNeeded returns true if the blocks table contains no default templates, -// or contains at least one default template with an old version number. -func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) { - if len(boards) == 0 { - return true, "no default templates found" - } - - // look for any built-in template boards with the wrong version number (or no version #). - for _, board := range boards { - // if not built-in board...skip - if board.CreatedBy != model.SystemUserID { - continue - } - if board.TemplateVersion < defaultTemplateVersion { - return true, "template_version too old" - } - } - return false, "" -} - -// fixTemplateBlock fixes a block to be inserted as part of a template. -func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool { - // cache contains ids of skipped boards. Ensure their children are skipped as well. - if _, ok := cache[block.BoardID]; ok { - cache[block.ID] = struct{}{} - return false - } - - if _, ok := cache[block.ParentID]; ok { - cache[block.ID] = struct{}{} - return false - } - return true -} - -// fixTemplateBoard fixes a board to be inserted as part of a template. -func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool { - // filter out template blocks; we only want the non-template - // blocks which we will turn into default template blocks. - if board.IsTemplate { - cache[board.ID] = struct{}{} - return false - } - - // remove '(NEW)' from title & force template flag - board.Title = strings.ReplaceAll(board.Title, "(NEW)", "") - board.IsTemplate = true - board.TemplateVersion = defaultTemplateVersion - board.Type = model.BoardTypeOpen - return true -} diff --git a/server/boards/app/templates_test.go b/server/boards/app/templates_test.go deleted file mode 100644 index cd6c88208d..0000000000 --- a/server/boards/app/templates_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" -) - -func TestApp_initializeTemplates(t *testing.T) { - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: model.GlobalTeamID, - Type: model.BoardTypeOpen, - Title: "test board", - IsTemplate: true, - TemplateVersion: defaultTemplateVersion, - } - - block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - ParentID: board.ID, - BoardID: board.ID, - Type: model.TypeText, - Title: "test text", - } - - boardsAndBlocks := &model.BoardsAndBlocks{ - Boards: []*model.Board{board}, - Blocks: []*model.Block{block}, - } - - boardMember := &model.BoardMember{ - BoardID: board.ID, - UserID: "test-user", - } - - t.Run("Needs template init", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().GetTemplateBoards(model.GlobalTeamID, "").Return([]*model.Board{}, nil) - th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(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().GetBoard(board.ID).AnyTimes().Return(board, nil) - th.Store.EXPECT().GetMemberForBoard(gomock.Any(), gomock.Any()).AnyTimes().Return(boardMember, nil) - th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil).AnyTimes() - - th.FilesBackend.On("WriteFile", mock.Anything, mock.Anything).Return(int64(1), nil) - - done, err := th.App.initializeTemplates() - require.NoError(t, err, "initializeTemplates should not error") - require.True(t, done, "initialization was needed") - }) - - t.Run("Skip template init", func(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - - th.Store.EXPECT().GetTemplateBoards(model.GlobalTeamID, "").Return([]*model.Board{board}, nil) - - done, err := th.App.initializeTemplates() - require.NoError(t, err, "initializeTemplates should not error") - require.False(t, done, "initialization was not needed") - }) -} diff --git a/server/boards/app/user.go b/server/boards/app/user.go deleted file mode 100644 index 5a33bfc875..0000000000 --- a/server/boards/app/user.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, error) { - return a.store.GetUsersByTeam(teamID, asGuestID, a.config.ShowEmailAddress, a.config.ShowFullName) -} - -func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) { - users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName) - if err != nil { - return nil, err - } - - for i, u := range users { - if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) { - users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id) - } - if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) { - users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id) - } - } - return users, nil -} - -func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mm_model.Preference, error) { - updatedPreferences, err := a.store.PatchUserPreferences(userID, patch) - if err != nil { - return nil, err - } - - return updatedPreferences, nil -} - -func (a *App) GetUserPreferences(userID string) ([]mm_model.Preference, error) { - return a.store.GetUserPreferences(userID) -} - -func (a *App) UserIsGuest(userID string) (bool, error) { - user, err := a.store.GetUserByID(userID) - if err != nil { - return false, err - } - return user.IsGuest, nil -} - -func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) { - isGuest, err := a.UserIsGuest(seerUser) - if err != nil { - return false, err - } - if isGuest { - hasSharedChannels, err := a.store.CanSeeUser(seerUser, seenUser) - if err != nil { - return false, err - } - return hasSharedChannels, nil - } - return true, nil -} - -func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mm_model.Channel, error) { - channels, err := a.store.SearchUserChannels(teamID, userID, query) - if err != nil { - return nil, err - } - - var writeableChannels []*mm_model.Channel - for _, channel := range channels { - if a.permissions.HasPermissionToChannel(userID, channel.Id, model.PermissionCreatePost) { - writeableChannels = append(writeableChannels, channel) - } - } - return writeableChannels, nil -} - -func (a *App) GetChannel(teamID string, channelID string) (*mm_model.Channel, error) { - return a.store.GetChannel(teamID, channelID) -} diff --git a/server/boards/app/user_test.go b/server/boards/app/user_test.go deleted file mode 100644 index 200a1a5cde..0000000000 --- a/server/boards/app/user_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestSearchUsers(t *testing.T) { - th, tearDown := SetupTestHelper(t) - defer tearDown() - th.App.config.ShowEmailAddress = false - th.App.config.ShowFullName = false - - teamID := "team-id-1" - userID := "user-id-1" - - t.Run("return empty users", func(t *testing.T) { - th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{}, nil) - - users, err := th.App.SearchTeamUsers(teamID, "", "", true) - assert.NoError(t, err) - assert.Equal(t, 0, len(users)) - }) - - t.Run("return user", func(t *testing.T) { - th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1) - th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) - - users, err := th.App.SearchTeamUsers(teamID, "", "", true) - assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - assert.Equal(t, 0, len(users[0].Permissions)) - }) - - t.Run("return team admin", func(t *testing.T) { - th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) - th.App.config.ShowEmailAddress = false - th.App.config.ShowFullName = false - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) - th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1) - - users, err := th.App.SearchTeamUsers(teamID, "", "", true) - assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) - }) - - t.Run("return system admin", func(t *testing.T) { - th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil) - th.App.config.ShowEmailAddress = false - th.App.config.ShowFullName = false - th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1) - th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(true).Times(1) - - users, err := th.App.SearchTeamUsers(teamID, "", "", true) - assert.NoError(t, err) - assert.Equal(t, 1, len(users)) - assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id) - assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id) - }) - - t.Run("test user channels", func(t *testing.T) { - channelID := "Channel1" - th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mm_model.Channel{{Id: channelID}}, nil) - th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1) - - channels, err := th.App.SearchUserChannels(teamID, userID, "") - assert.NoError(t, err) - assert.Equal(t, 1, len(channels)) - }) - - t.Run("test user channels- no permissions", func(t *testing.T) { - channelID := "Channel1" - th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mm_model.Channel{{Id: channelID}}, nil) - th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1) - - channels, err := th.App.SearchUserChannels(teamID, userID, "") - assert.NoError(t, err) - assert.Equal(t, 0, len(channels)) - }) -} diff --git a/server/boards/assets/assets.go b/server/boards/assets/assets.go deleted file mode 100644 index 889a44afcb..0000000000 --- a/server/boards/assets/assets.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package assets - -import ( - _ "embed" -) - -// DefaultTemplatesArchive is an embedded archive file containing the default -// templates to be imported to team 0. -// This archive is generated with `make templates-archive` -// -//go:embed templates.boardarchive -var DefaultTemplatesArchive []byte diff --git a/server/boards/assets/build-template-archive/main.go b/server/boards/assets/build-template-archive/main.go deleted file mode 100644 index ed5cf0d318..0000000000 --- a/server/boards/assets/build-template-archive/main.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package main - -import ( - "archive/zip" - "encoding/json" - "flag" - "fmt" - "io" - "os" - "path/filepath" -) - -const ( - defArchiveFilename = "templates.boardarchive" - versionFilename = "version.json" - boardFilename = "board.jsonl" - minArchiveVersion = 2 - maxArchiveVersion = 2 -) - -type archiveVersion struct { - Version int `json:"version"` - Date int64 `json:"date"` -} - -type appConfig struct { - dir string - out string - verbose bool -} - -func main() { - cfg := appConfig{} - - flag.StringVar(&cfg.dir, "dir", "", "source directory of templates") - flag.StringVar(&cfg.out, "out", defArchiveFilename, "output filename") - flag.BoolVar(&cfg.verbose, "verbose", false, "enable verbose output") - flag.Parse() - - if cfg.dir == "" { - flag.Usage() - os.Exit(-1) - } - - var code int - if err := build(cfg); err != nil { - code = -1 - fmt.Fprintf(os.Stderr, "error creating archive: %v\n", err) - } else if cfg.verbose { - fmt.Fprintf(os.Stdout, "archive created: %s\n", cfg.out) - } - - os.Exit(code) -} - -func build(cfg appConfig) (err error) { - version, err := getVersionFile(cfg) - if err != nil { - return err - } - - // create the output archive zip file - archiveFile, err := os.Create(cfg.out) - if err != nil { - return fmt.Errorf("error creating %s: %w", cfg.out, err) - } - archiveZip := zip.NewWriter(archiveFile) - defer func() { - if err2 := archiveZip.Close(); err2 != nil { - if err == nil { - err = fmt.Errorf("error closing zip %s: %w", cfg.out, err2) - } - } - if err2 := archiveFile.Close(); err2 != nil { - if err == nil { - err = fmt.Errorf("error closing %s: %w", cfg.out, err2) - } - } - }() - - // write the version file - v, err := archiveZip.Create(versionFilename) - if err != nil { - return fmt.Errorf("error creating %s: %w", cfg.out, err) - } - if _, err = v.Write(version); err != nil { - return fmt.Errorf("error writing %s: %w", cfg.out, err) - } - - // each board is a subdirectory; write each to the archive - files, err := os.ReadDir(cfg.dir) - if err != nil { - return fmt.Errorf("error reading directory %s: %w", cfg.dir, err) - } - for _, f := range files { - if !f.IsDir() { - if f.Name() != versionFilename && cfg.verbose { - fmt.Fprintf(os.Stdout, "skipping non-directory %s\n", f.Name()) - } - continue - } - if err = writeBoard(archiveZip, f.Name(), cfg); err != nil { - return fmt.Errorf("error writing board %s: %w", f.Name(), err) - } - } - return nil -} - -func getVersionFile(cfg appConfig) ([]byte, error) { - path := filepath.Join(cfg.dir, versionFilename) - buf, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("cannot read %s: %w", path, err) - } - - var version archiveVersion - if err := json.Unmarshal(buf, &version); err != nil { - return nil, fmt.Errorf("cannot parse %s: %w", path, err) - } - - if version.Version < minArchiveVersion || version.Version > maxArchiveVersion { - return nil, errUnsupportedVersion{Min: minArchiveVersion, Max: maxArchiveVersion, Got: version.Version} - } - - return buf, nil -} - -func writeBoard(w *zip.Writer, boardID string, cfg appConfig) error { - // copy the board's jsonl file first. BoardID is also the directory name. - srcPath := filepath.Join(cfg.dir, boardID, boardFilename) - destPath := filepath.Join(boardID, boardFilename) - if err := writeFile(w, srcPath, destPath, cfg); err != nil { - return err - } - - boardPath := filepath.Join(cfg.dir, boardID) - files, err := os.ReadDir(boardPath) - if err != nil { - return fmt.Errorf("error reading board directory %s: %w", cfg.dir, err) - } - for _, f := range files { - if f.IsDir() { - if cfg.verbose { - fmt.Fprintf(os.Stdout, "skipping directory %s\n", f.Name()) - } - continue - } - if f.Name() == boardFilename { - continue - } - - srcPath = filepath.Join(cfg.dir, boardID, f.Name()) - destPath = filepath.Join(boardID, f.Name()) - if err = writeFile(w, srcPath, destPath, cfg); err != nil { - return fmt.Errorf("error writing %s: %w", destPath, err) - } - } - return nil -} - -func writeFile(w *zip.Writer, srcPath string, destPath string, cfg appConfig) (err error) { - inFile, err := os.Open(srcPath) - if err != nil { - return fmt.Errorf("error reading %s: %w", srcPath, err) - } - defer inFile.Close() - - outFile, err := w.Create(destPath) - if err != nil { - return fmt.Errorf("error creating %s: %w", destPath, err) - } - size, err := io.Copy(outFile, inFile) - if err != nil { - return fmt.Errorf("error writing %s: %w", destPath, err) - } - - if cfg.verbose { - fmt.Fprintf(os.Stdout, "%s written (%d bytes)\n", destPath, size) - } - - return nil -} - -type errUnsupportedVersion struct { - Min int - Max int - Got int -} - -func (e errUnsupportedVersion) Error() string { - return fmt.Sprintf("unsupported archive version; require between %d and %d inclusive, got %d", e.Min, e.Max, e.Got) -} diff --git a/server/boards/assets/build-template-archive/test.boardarchive b/server/boards/assets/build-template-archive/test.boardarchive deleted file mode 100644 index 4865f340f6..0000000000 Binary files a/server/boards/assets/build-template-archive/test.boardarchive and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/b7wnw9awd4pnefryhq51apbzb4c/board.jsonl b/server/boards/assets/templates-boardarchive/b7wnw9awd4pnefryhq51apbzb4c/board.jsonl deleted file mode 100644 index 865388e914..0000000000 --- a/server/boards/assets/templates-boardarchive/b7wnw9awd4pnefryhq51apbzb4c/board.jsonl +++ /dev/null @@ -1,36 +0,0 @@ -{"type":"block","data":{"id":"b7wnw9awd4pnefryhq51apbzb4c","parentId":"","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"board","title":"Meeting Agenda (NEW)","fields":{"cardProperties":[{"id":"d777ba3b-8728-40d1-87a6-59406bbbbfb0","name":"Status","options":[{"color":"propColorPink","id":"34eb9c25-d5bf-49d9-859e-f74f4e0030e7","value":"To Discuss 💬"},{"color":"propColorYellow","id":"d37a61f4-f332-4db9-8b2d-5e0a91aa20ed","value":"Revisit Later ⏳"},{"color":"propColorGreen","id":"dabadd9b-adf1-4d9f-8702-805ac6cef602","value":"Done / Archived 📦"}],"type":"select"},{"id":"4cf1568d-530f-4028-8ffd-bdc65249187e","name":"Priority","options":[{"color":"propColorRed","id":"8b05c83e-a44a-4d04-831e-97f01d8e2003","value":"1. High"},{"color":"propColorYellow","id":"b1abafbf-a038-4a19-8b68-56e0fd2319f7","value":"2. Medium"},{"color":"propColorGray","id":"2491ffaa-eb55-417b-8aff-4bd7d4136613","value":"3. Low"}],"type":"select"},{"id":"aw4w63xhet79y9gueqzzeiifdoe","name":"Created by","options":[],"type":"createdBy"},{"id":"a6ux19353xcwfqg9k1inqg5sg4w","name":"Created time","options":[],"type":"createdTime"}],"description":"Use this template for recurring meeting agendas, like team meetings and 1:1's. To use this board:\n* Participants queue new items to discuss under \"To Discuss\"\n* Go through items during the meeting\n* Move items to Done or Revisit Later as needed","icon":"🍩","isTemplate":false,"showDescription":true},"createAt":1641497047916,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cgwagmaw6gin7xcq7nwew8rsynr","parentId":"b7wnw9awd4pnefryhq51apbzb4c","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"card","title":"Team Schedule","fields":{"contentOrder":["a4t1p1pbxbtnnu8p8e538o8369a","7b7hsbkm6sifqfqi4gstxxaz7my","aoqz1pydxbtnzdcs4ehcuys6cuc","7b3njq5m3n78hdpe4bimzr34fic","73dzfgistnbgzuekc6c8irou9wy","7z4cjur4ybbfibgmydhfct4jdke"],"icon":"⏰","isTemplate":false,"properties":{"4cf1568d-530f-4028-8ffd-bdc65249187e":"8b05c83e-a44a-4d04-831e-97f01d8e2003","d777ba3b-8728-40d1-87a6-59406bbbbfb0":"34eb9c25-d5bf-49d9-859e-f74f4e0030e7"}},"createAt":1641497048246,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"chki1tsudciyiiffrkqbcmp71rh","parentId":"b7wnw9awd4pnefryhq51apbzb4c","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"card","title":"Video production","fields":{"contentOrder":["a9ti13dqo8jfmjdmg97f5umfdyw","717fa85sx3f8f8m81f771s9hmwr","a4se5s4ozx3ry8ec57w6z6jpk7y","7n37rxrn9uffdzrfi1xajotzjey","7ifofmuwjzbdzppfxgtuai4i47h","7cfc4fkpz53gn9frciz9kui4p1c"],"icon":"📹","isTemplate":false,"properties":{"4cf1568d-530f-4028-8ffd-bdc65249187e":"b1abafbf-a038-4a19-8b68-56e0fd2319f7","d777ba3b-8728-40d1-87a6-59406bbbbfb0":"34eb9c25-d5bf-49d9-859e-f74f4e0030e7"}},"createAt":1641497048092,"updateAt":1643788318629,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cmt5usr1mw3fom886t34ekjquay","parentId":"b7wnw9awd4pnefryhq51apbzb4c","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"card","title":"Offsite plans","fields":{"contentOrder":["aw53ugkfq8pyi9fjh9j6i4kdeiw","7ni9593iz3pnb7xitoz3guwq5gh","agjkcro3x7irbxedyxrn8iuerrr","75zkot1f3sjb7ifysuzijitw91y","7is5m8apdu3g53c8f6cz6sq7bmh","7xsmzscbqn3ftudzqbb4w1q7t7e"],"icon":"🚙","isTemplate":false,"properties":{"4cf1568d-530f-4028-8ffd-bdc65249187e":"8b05c83e-a44a-4d04-831e-97f01d8e2003","d777ba3b-8728-40d1-87a6-59406bbbbfb0":"dabadd9b-adf1-4d9f-8702-805ac6cef602"}},"createAt":1641497048336,"updateAt":1643788318629,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cnqsbzg4b7brfddtyh7fc66atrw","parentId":"b7wnw9awd4pnefryhq51apbzb4c","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"card","title":"Social Media Strategy","fields":{"contentOrder":["ao57n1fbtmt8q8bfk8ieqgzqt3a","76h9y996sdj8sbrbpqjo9d8cwto","aco8iu5jp7jbyzmzegwxkeusgzr","7y6zcyofmsfrbt899ts1ixr3iey","7hudywfzcwirkpcp1p5jhsfs83r","7jzw67ngdgtns8mstsg9g614oac"],"icon":"🎉","isTemplate":false,"properties":{"4cf1568d-530f-4028-8ffd-bdc65249187e":"b1abafbf-a038-4a19-8b68-56e0fd2319f7","d777ba3b-8728-40d1-87a6-59406bbbbfb0":"d37a61f4-f332-4db9-8b2d-5e0a91aa20ed"}},"createAt":1641497048417,"updateAt":1643788318629,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vfs8sj79dt7n75bomn46fybxmfo","parentId":"b7wnw9awd4pnefryhq51apbzb4c","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"view","title":"Discussion Items","fields":{"cardOrder":["cjpkiya33qsagr4f9hrdwhgiajc"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"d777ba3b-8728-40d1-87a6-59406bbbbfb0","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[{"propertyId":"4cf1568d-530f-4028-8ffd-bdc65249187e","reversed":false}],"viewType":"board","visibleOptionIds":[],"visiblePropertyIds":["4cf1568d-530f-4028-8ffd-bdc65249187e"]},"createAt":1641497048501,"updateAt":1643788318629,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"73dzfgistnbgzuekc6c8irou9wy","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586451774,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7b3njq5m3n78hdpe4bimzr34fic","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586448934,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7b7hsbkm6sifqfqi4gstxxaz7my","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641586358664,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7z4cjur4ybbfibgmydhfct4jdke","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586454130,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a4t1p1pbxbtnnu8p8e538o8369a","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n*[Add meeting notes here]*","fields":{},"createAt":1641586355777,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aoqz1pydxbtnzdcs4ehcuys6cuc","parentId":"cgwagmaw6gin7xcq7nwew8rsynr","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Action Items","fields":{},"createAt":1641586443526,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"766mkfhc4u7dxzcc36nhfpmm5fy","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641586677789,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76w5qigi5ufgktcmmnw9ze88w5w","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641497389096,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"79wi7osb3utd3mjt9x57h7wpqfa","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641497390990,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7un1ccdg7qi8j3gxmkx5y3d9nhr","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641497382984,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"as3orhrci6tnutp5etbh6bzbgdy","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"text","title":"# Action Items","fields":{},"createAt":1641497371429,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"axyitfq8ae38qictgcw34cmwueh","parentId":"ch798q5ucefyobf5bymgqjt4f3h","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"mweioqznbife7p7aee7dr4wcxo","modifiedBy":"mweioqznbife7p7aee7dr4wcxo","schema":1,"type":"text","title":"# Notes\n*[Add meeting notes here]*","fields":{},"createAt":1641497348992,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"717fa85sx3f8f8m81f771s9hmwr","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641586368705,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7cfc4fkpz53gn9frciz9kui4p1c","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586479058,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ifofmuwjzbdzppfxgtuai4i47h","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586476646,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7n37rxrn9uffdzrfi1xajotzjey","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586469805,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a4se5s4ozx3ry8ec57w6z6jpk7y","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Action Items","fields":{},"createAt":1641586462602,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a9ti13dqo8jfmjdmg97f5umfdyw","parentId":"chki1tsudciyiiffrkqbcmp71rh","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n*[Add meeting notes here]*","fields":{},"createAt":1641586365342,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"75zkot1f3sjb7ifysuzijitw91y","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586514173,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7is5m8apdu3g53c8f6cz6sq7bmh","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586516563,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ni9593iz3pnb7xitoz3guwq5gh","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641586383504,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7xsmzscbqn3ftudzqbb4w1q7t7e","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586518624,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"agjkcro3x7irbxedyxrn8iuerrr","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Action Items","fields":{},"createAt":1641586506048,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aw53ugkfq8pyi9fjh9j6i4kdeiw","parentId":"cmt5usr1mw3fom886t34ekjquay","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n*[Add meeting notes here]*","fields":{},"createAt":1641586380592,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76h9y996sdj8sbrbpqjo9d8cwto","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641586375619,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7hudywfzcwirkpcp1p5jhsfs83r","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586495344,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7jzw67ngdgtns8mstsg9g614oac","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586497433,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7y6zcyofmsfrbt899ts1ixr3iey","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"","fields":{"value":false},"createAt":1641586492877,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aco8iu5jp7jbyzmzegwxkeusgzr","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Action Items","fields":{},"createAt":1641586487881,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ao57n1fbtmt8q8bfk8ieqgzqt3a","parentId":"cnqsbzg4b7brfddtyh7fc66atrw","rootId":"b7wnw9awd4pnefryhq51apbzb4c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n*[Add meeting notes here]*","fields":{},"createAt":1641586373252,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/bbkpwdj8x17bdpdqd176n8ctoua/board.jsonl b/server/boards/assets/templates-boardarchive/bbkpwdj8x17bdpdqd176n8ctoua/board.jsonl deleted file mode 100644 index d591663833..0000000000 --- a/server/boards/assets/templates-boardarchive/bbkpwdj8x17bdpdqd176n8ctoua/board.jsonl +++ /dev/null @@ -1,94 +0,0 @@ -{"type":"board","data":{"id":"bbkpwdj8x17bdpdqd176n8ctoua","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"Sales Pipeline CRM","description":"Use this template to grow and keep track of your sales opportunities.","icon":"📈","showDescription":true,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"a5hwxjsmkn6bak6r7uea5bx1kwc","name":"Status","options":[{"color":"propColorGray","id":"akj61wc9yxdwyw3t6m8igyf9d5o","value":"Lead"},{"color":"propColorYellow","id":"aic89a5xox4wbppi6mbyx6ujsda","value":"Qualified"},{"color":"propColorBrown","id":"ah6ehh43rwj88jy4awensin8pcw","value":"Meeting"},{"color":"propColorPurple","id":"aprhd96zwi34o9cs4xyr3o9sf3c","value":"Proposal"},{"color":"propColorOrange","id":"axesd74yuxtbmw1sbk8ufax7z3a","value":"Negotiation"},{"color":"propColorRed","id":"a5txuiubumsmrs8gsd5jz5gc1oa","value":"Lost"},{"color":"propColorGreen","id":"acm9q494bcthyoqzmfogxxy5czy","value":"Closed 🏆"}],"type":"select"},{"id":"aoheuj1f3mu6eehygr45fxa144y","name":"Account Owner","options":[],"type":"multiPerson"},{"id":"aro91wme9kfaie5ceu9qasmtcnw","name":"Priority","options":[{"color":"propColorRed","id":"apjnaggwixchfxwiatfh7ey7uno","value":"High 🔥"},{"color":"propColorYellow","id":"apiswzj7uiwbh87z8dw8c6mturw","value":"Medium"},{"color":"propColorBrown","id":"auu9bfzqeuruyjwzzqgz7q8apuw","value":"Low"}],"type":"select"},{"id":"ainpw47babwkpyj77ic4b9zq9xr","name":"Company","options":[],"type":"text"},{"id":"ahf43e44h3y8ftanqgzno9z7q7w","name":"Estimated Value","options":[],"type":"number"},{"id":"amahgyn9n4twaapg3jyxb6y4jic","name":"Territory","options":[{"color":"propColorBrown","id":"ar6t1ttcumgfuqugg5o4g4mzrza","value":"Western US"},{"color":"propColorGreen","id":"asbwojkm7zb4ohrtij97jkdfgwe","value":"Mountain West / Central US"},{"color":"propColorGray","id":"aw8ppwtcrm8iwopdadje3ni196w","value":"Mid-Atlantic / Southeast"},{"color":"propColorBlue","id":"aafwyza5iwdcwcyfyj6bp7emufw","value":"Northeast US / Canada"},{"color":"propColorPink","id":"agw8rcb9uxyt3c7g6tq3r65fgqe","value":"Eastern Europe"},{"color":"propColorPurple","id":"as5bk6afoaaa7caewe1zc391sce","value":"Central Europe / Africa"},{"color":"propColorYellow","id":"a8fj94bka8z9t6p95qd3hn6t5re","value":"Middle East"},{"color":"propColorOrange","id":"arpxa3faaou9trt4zx5sh435gne","value":"UK"},{"color":"propColorRed","id":"azdidd5wze4kcxf8neefj3ctkyr","value":"Asia"},{"color":"propColorGray","id":"a4jn5mhqs3thknqf5opykntgsnc","value":"Australia"},{"color":"propColorBrown","id":"afjbgrecb7hp5owj7xh8u4w33tr","value":"Latin America"}],"type":"select"},{"id":"abru6tz8uebdxy4skheqidh7zxy","name":"Email","options":[],"type":"email"},{"id":"a1438fbbhjeffkexmcfhnx99o1h","name":"Phone","options":[],"type":"phone"},{"id":"auhf91pm85f73swwidi4wid8jqe","name":"Last Contact Date","options":[],"type":"date"},{"id":"adtf1151chornmihz4xbgbk9exa","name":"Expected Close","options":[],"type":"date"},{"id":"aejo5tcmq54bauuueem9wc4fw4y","name":"Close Probability","options":[],"type":"text"},{"id":"amba7ot98fh7hwsx8jdcfst5g7h","name":"Created Date","options":[],"type":"createdTime"}],"createAt":1667509277974,"updateAt":1667511890353,"deleteAt":0}} -{"type":"block","data":{"id":"v76ciioz6ujd49phimp5jzomsww","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"All Contacts","fields":{"cardOrder":["cyt3qdus94pg3fkxq4ojebyd5fr","chew1d7kc3py3pj51qyqaiz6ade","c91bktnpajfrrdpxs7ck1h7ziwh","c77c6z9k9oigdpbocg8kxi7h8ah","c9ciauq49ifdntc99rnehkkshpr"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":240,"a1438fbbhjeffkexmcfhnx99o1h":151,"a5hwxjsmkn6bak6r7uea5bx1kwc":132,"abru6tz8uebdxy4skheqidh7zxy":247,"adtf1151chornmihz4xbgbk9exa":125,"aejo5tcmq54bauuueem9wc4fw4y":127,"ahf43e44h3y8ftanqgzno9z7q7w":129,"ainpw47babwkpyj77ic4b9zq9xr":157,"amahgyn9n4twaapg3jyxb6y4jic":224,"amba7ot98fh7hwsx8jdcfst5g7h":171,"aoheuj1f3mu6eehygr45fxa144y":130,"auhf91pm85f73swwidi4wid8jqe":157},"defaultTemplateId":"cphg5tyix4irsipkcp9ujaj3gwh","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a5hwxjsmkn6bak6r7uea5bx1kwc","aoheuj1f3mu6eehygr45fxa144y","aro91wme9kfaie5ceu9qasmtcnw","ainpw47babwkpyj77ic4b9zq9xr","ahf43e44h3y8ftanqgzno9z7q7w","amahgyn9n4twaapg3jyxb6y4jic","abru6tz8uebdxy4skheqidh7zxy","a1438fbbhjeffkexmcfhnx99o1h","auhf91pm85f73swwidi4wid8jqe","adtf1151chornmihz4xbgbk9exa","aejo5tcmq54bauuueem9wc4fw4y","amba7ot98fh7hwsx8jdcfst5g7h"]},"createAt":1667513494864,"updateAt":1667513802156,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"va9qcbagmdbfwb8xq5hawbq1a4r","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Pipeline Tracker","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["akj61wc9yxdwyw3t6m8igyf9d5o","aic89a5xox4wbppi6mbyx6ujsda","ah6ehh43rwj88jy4awensin8pcw","aprhd96zwi34o9cs4xyr3o9sf3c","axesd74yuxtbmw1sbk8ufax7z3a","a5txuiubumsmrs8gsd5jz5gc1oa","acm9q494bcthyoqzmfogxxy5czy"],"visiblePropertyIds":["aro91wme9kfaie5ceu9qasmtcnw","amahgyn9n4twaapg3jyxb6y4jic"]},"createAt":1667513379646,"updateAt":1667513589086,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"c77c6z9k9oigdpbocg8kxi7h8ah","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Jonathan Frazier","fields":{"contentOrder":["a7hc9n8oz47gybkxj4ssnwgi7ky","a4bminunz1j8p3go9ixxdxpi4no","71ibw3rrac7gcmgr4f16st7fz1c","736fwfii9t7nafekshdjc6y4rge","78aiw1o1wzibzzbiuo4e78p4pdr","7ei858uzb9jye8yqo7j5nq1knaa","7bai1o5z5fibiuxs7i9i8tti87w","7cg1mxma4fjb67xmh1p7fyxekro","76ry4rpfhq7ykprpmbidxdjr33o","77gckzfpcmjb1bysnnqs7cnzseo","7biw71wn9nfdgxd7fbh9un68zrc","7iz6fjou66i8muqnhzb9pocff3e"],"icon":"🙎‍♂️","isTemplate":false,"properties":{"a1438fbbhjeffkexmcfhnx99o1h":"(999) 123-5678","a5hwxjsmkn6bak6r7uea5bx1kwc":"a5txuiubumsmrs8gsd5jz5gc1oa","abru6tz8uebdxy4skheqidh7zxy":"jonathan.frazier@email.com","aejo5tcmq54bauuueem9wc4fw4y":"0%","ahf43e44h3y8ftanqgzno9z7q7w":"$800,000","ainpw47babwkpyj77ic4b9zq9xr":"Ositions Inc.","amahgyn9n4twaapg3jyxb6y4jic":"as5bk6afoaaa7caewe1zc391sce","aro91wme9kfaie5ceu9qasmtcnw":"apiswzj7uiwbh87z8dw8c6mturw","auhf91pm85f73swwidi4wid8jqe":"{\"from\":1669118400000}"}},"createAt":1667513212844,"updateAt":1667513367839,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"c91bktnpajfrrdpxs7ck1h7ziwh","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Richard Guzman","fields":{"contentOrder":["a43kn6138w7boimnp5xe1khezjc","ab6q8dsqh7ifhmk14ow4m9ytj3e","73p7qyd8h13nq5fk54rqgbee7or","7sqafho6jofdtjk5byn3yskq5ry","7q6pi4f9dbtrpzbcm65hg9useso","7976uafbzbjrmjya983z5bweesy","7nu71kxnutbd6fdnmzjfbrczinw","7j3q9i5a337ym3fdnigx3ifrhoh","7jap7w5js9bfazgsa59skmocmhw","7mndskgucj3g18ys7c6wjpub78o","7kwmrfpx8pir5ieg5w8orbtq8ba","7mwxoycnpq7nhix7r5x3wtmqd3h"],"icon":"👨‍💼","isTemplate":false,"properties":{"a1438fbbhjeffkexmcfhnx99o1h":"(222) 123-1234","a5hwxjsmkn6bak6r7uea5bx1kwc":"axesd74yuxtbmw1sbk8ufax7z3a","abru6tz8uebdxy4skheqidh7zxy":"richard.guzman@email.com","adtf1151chornmihz4xbgbk9exa":"{\"from\":1681992000000}","aejo5tcmq54bauuueem9wc4fw4y":"80%","ahf43e44h3y8ftanqgzno9z7q7w":"$3,200,000","ainpw47babwkpyj77ic4b9zq9xr":"Afformance Ltd.","amahgyn9n4twaapg3jyxb6y4jic":"ar6t1ttcumgfuqugg5o4g4mzrza","aro91wme9kfaie5ceu9qasmtcnw":"apjnaggwixchfxwiatfh7ey7uno","auhf91pm85f73swwidi4wid8jqe":"{\"from\":1667476800000}"}},"createAt":1667512379637,"updateAt":1667512604683,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"c9ciauq49ifdntc99rnehkkshpr","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Byron Cole","fields":{"contentOrder":["a4wwsynhjafb4dgbubda18ho3fr","a1ag6b4hwkibbbbxdmse74cw3ur","767qdn4uhbbrb8gyq4x7w1rfcoc","7ad16jbhbcpro78cueumekyqjyy","7xbj8zr1jxfnxfkyfyccb84ddeo","7sggexapxebb1zk9oqta6gcwsda","7y3ncauhatfrg7nzyr67twe36wc","74ach4ckw53grfygwp8m6wbj4ya","7agc943grqtgidb3e49dkqumrce","7owy1izqn1if55r5hc3fgu8fada","7zcbwgrw5apd4frn6uxd386rktc","7zijtxs3enjy5frzc4zb6937b3w"],"icon":"🤵","isTemplate":false,"properties":{"a1438fbbhjeffkexmcfhnx99o1h":"(333) 123-1234","a5hwxjsmkn6bak6r7uea5bx1kwc":"acm9q494bcthyoqzmfogxxy5czy","abru6tz8uebdxy4skheqidh7zxy":"byron.cole@email.com","adtf1151chornmihz4xbgbk9exa":"{\"from\":1667563200000}","aejo5tcmq54bauuueem9wc4fw4y":"100%","ahf43e44h3y8ftanqgzno9z7q7w":"$500,000","ainpw47babwkpyj77ic4b9zq9xr":"Helx Industries","amahgyn9n4twaapg3jyxb6y4jic":"aafwyza5iwdcwcyfyj6bp7emufw","aro91wme9kfaie5ceu9qasmtcnw":"apjnaggwixchfxwiatfh7ey7uno","auhf91pm85f73swwidi4wid8jqe":"{\"from\":1667822400000}"}},"createAt":1667512692248,"updateAt":1667512904723,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"chew1d7kc3py3pj51qyqaiz6ade","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Caitlyn Russel","fields":{"contentOrder":["atgpetmwdubb5jkugcb6jm9pzyo","acod8woq6zjbzmc1hz8qkfxyi1h","7s47d7rzh4pnw5rcnpjysxg6duh","7s9smhppjff87tndwawqwdmfryo","7t3ib1amo7fgzbmhg4tkzqustcy","7s5bgtoajcbnd8rrc5bxzabdcyw","7nze85jfmobfm8j8xfmrbdwyrfa","7jrwix8rkbtb5bdek79mtat8w1c","7c1iwiqsi1iddpfzqisbkubjxhh","7tp1rgey147nnfjuose7418oioh","7ftxm79a1e7nuxpb913aqphoqbo","799cbodnfr3ydfjp53die7egd1e"],"icon":"🧑‍💼","isTemplate":false,"properties":{"a1438fbbhjeffkexmcfhnx99o1h":"(111) 123-1234","a5hwxjsmkn6bak6r7uea5bx1kwc":"ah6ehh43rwj88jy4awensin8pcw","abru6tz8uebdxy4skheqidh7zxy":"caitlyn.russel@email.com","adtf1151chornmihz4xbgbk9exa":"{\"from\":1689336000000}","aejo5tcmq54bauuueem9wc4fw4y":"20%","ahf43e44h3y8ftanqgzno9z7q7w":"$250,000","ainpw47babwkpyj77ic4b9zq9xr":"Liminary Corp.","amahgyn9n4twaapg3jyxb6y4jic":"aafwyza5iwdcwcyfyj6bp7emufw","aro91wme9kfaie5ceu9qasmtcnw":"apiswzj7uiwbh87z8dw8c6mturw","auhf91pm85f73swwidi4wid8jqe":"{\"from\":1668168000000}"}},"createAt":1667509567800,"updateAt":1667512683024,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"cphg5tyix4irsipkcp9ujaj3gwh","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"New Prospect","fields":{"contentOrder":["atw8pz7bqgp877pd5714jbpqrsh","azwoek6rwfpfqiruig13owyyagr","71735rqboe3rkxypssssddjykkc","7jgf4cownfiy7xpaznxdsnyze9a","74khjujy4hir4zmer4hkj1gcckh","768ut9xkqipgf9fk6ub146spu5e","7jryotoo5wig9bdt3kh1fmgm5qw","7p7hz5ky15jgrirb64533xzsquo","7c9cy5ohjd3b85xkee539zw9owh","7dsynp6qf8tdtjpcqsxfyuqyzmo","7kxzdhjtx8pdazm7bufusybwygo","7h1zyk7thz7gx3r5degq6qorjay"],"icon":"👤","isTemplate":true,"properties":{"a5hwxjsmkn6bak6r7uea5bx1kwc":"akj61wc9yxdwyw3t6m8igyf9d5o"}},"createAt":1667513652330,"updateAt":1667513749765,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"cyt3qdus94pg3fkxq4ojebyd5fr","parentId":"bbkpwdj8x17bdpdqd176n8ctoua","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Shelby Olson","fields":{"contentOrder":["ay46giuhekby5tmrnw4abnz97pw","awugzceoocjdnmn1ab6srb5cc6r","7bncmywm3h38s7ndpcu9sytfffy","7zmoekhdb4i848p313eh1okp78c","7xxy1eewp8jdxpcouho8jq7ed4w","7rgodjyks6jrtprbeizusduat4c","7u5qxs77u57bc8bd33ug5aa91rw","7jexsw3nutb8m3x6eyqio7gtcxr","7abw4xifxubn1urheakij9kjc5e","7r47h1d8fjfrpzkfzyxha44wrqe","7yq4oh69547rm9mp3eqg9zqzoxw","7yhpeqyesfif188x4pabwurnw4o"],"icon":"🙎‍♀️","isTemplate":false,"properties":{"a1438fbbhjeffkexmcfhnx99o1h":"(111) 321-5678","a5hwxjsmkn6bak6r7uea5bx1kwc":"akj61wc9yxdwyw3t6m8igyf9d5o","abru6tz8uebdxy4skheqidh7zxy":"shelby.olson@email.com","ahf43e44h3y8ftanqgzno9z7q7w":"$30,000","ainpw47babwkpyj77ic4b9zq9xr":"Kadera Global","amahgyn9n4twaapg3jyxb6y4jic":"ar6t1ttcumgfuqugg5o4g4mzrza","aro91wme9kfaie5ceu9qasmtcnw":"auu9bfzqeuruyjwzzqgz7q8apuw","auhf91pm85f73swwidi4wid8jqe":"{\"from\":1669291200000}"}},"createAt":1667512982640,"updateAt":1667513171727,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"vg19cqh9bnbfq5edwq4kep3ssxr","parentId":"bzwb99zf498tsm7mjqbiy7g81ze","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Open Deals","fields":{"cardOrder":["chew1d7kc3py3pj51qyqaiz6ade"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[{"condition":"includes","propertyId":"a5hwxjsmkn6bak6r7uea5bx1kwc","values":["akj61wc9yxdwyw3t6m8igyf9d5o","aic89a5xox4wbppi6mbyx6ujsda","ah6ehh43rwj88jy4awensin8pcw","aprhd96zwi34o9cs4xyr3o9sf3c","axesd74yuxtbmw1sbk8ufax7z3a"]}],"operation":"and"},"groupById":"aro91wme9kfaie5ceu9qasmtcnw","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["apjnaggwixchfxwiatfh7ey7uno","apiswzj7uiwbh87z8dw8c6mturw","auu9bfzqeuruyjwzzqgz7q8apuw",""],"visiblePropertyIds":[]},"createAt":1667509277984,"updateAt":1667513521431,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"71ibw3rrac7gcmgr4f16st7fz1c","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":true},"createAt":1667513212852,"updateAt":1667513212852,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"736fwfii9t7nafekshdjc6y4rge","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":true},"createAt":1667513212861,"updateAt":1667513341391,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"76ry4rpfhq7ykprpmbidxdjr33o","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{"value":true},"createAt":1667513212920,"updateAt":1667513348088,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"77gckzfpcmjb1bysnnqs7cnzseo","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667513212930,"updateAt":1667513212930,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"78aiw1o1wzibzzbiuo4e78p4pdr","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":true},"createAt":1667513212869,"updateAt":1667513342078,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7bai1o5z5fibiuxs7i9i8tti87w","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":true},"createAt":1667513212887,"updateAt":1667513344670,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7biw71wn9nfdgxd7fbh9un68zrc","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667513212939,"updateAt":1667513212939,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7cg1mxma4fjb67xmh1p7fyxekro","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{"value":true},"createAt":1667513212912,"updateAt":1667513345694,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7ei858uzb9jye8yqo7j5nq1knaa","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":true},"createAt":1667513212878,"updateAt":1667513343116,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7iz6fjou66i8muqnhzb9pocff3e","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667513212947,"updateAt":1667513212947,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"a4bminunz1j8p3go9ixxdxpi4no","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667513212903,"updateAt":1667513212903,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"a7hc9n8oz47gybkxj4ssnwgi7ky","parentId":"c77c6z9k9oigdpbocg8kxi7h8ah","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.","fields":{},"createAt":1667513212895,"updateAt":1667513212895,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"73p7qyd8h13nq5fk54rqgbee7or","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":true},"createAt":1667512379656,"updateAt":1667512968074,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7976uafbzbjrmjya983z5bweesy","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":true},"createAt":1667512379686,"updateAt":1667512970061,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7j3q9i5a337ym3fdnigx3ifrhoh","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{"value":true},"createAt":1667512379752,"updateAt":1667512975240,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7jap7w5js9bfazgsa59skmocmhw","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{"value":true},"createAt":1667512379772,"updateAt":1667512975857,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7kwmrfpx8pir5ieg5w8orbtq8ba","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667512379805,"updateAt":1667512379805,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7mndskgucj3g18ys7c6wjpub78o","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667512379792,"updateAt":1667512379792,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7mwxoycnpq7nhix7r5x3wtmqd3h","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667512379814,"updateAt":1667512379814,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7nu71kxnutbd6fdnmzjfbrczinw","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":true},"createAt":1667512379695,"updateAt":1667512973476,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7q6pi4f9dbtrpzbcm65hg9useso","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":true},"createAt":1667512379677,"updateAt":1667512969519,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7sqafho6jofdtjk5byn3yskq5ry","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":true},"createAt":1667512379668,"updateAt":1667512968798,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"a43kn6138w7boimnp5xe1khezjc","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.","fields":{},"createAt":1667512379704,"updateAt":1667512379704,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"ab6q8dsqh7ifhmk14ow4m9ytj3e","parentId":"c91bktnpajfrrdpxs7ck1h7ziwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667512379729,"updateAt":1667512379728,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"74ach4ckw53grfygwp8m6wbj4ya","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{"value":true},"createAt":1667512692319,"updateAt":1667512917248,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"767qdn4uhbbrb8gyq4x7w1rfcoc","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":true},"createAt":1667512692257,"updateAt":1667512911931,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7ad16jbhbcpro78cueumekyqjyy","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":true},"createAt":1667512692265,"updateAt":1667512912836,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7agc943grqtgidb3e49dkqumrce","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{"value":true},"createAt":1667512692327,"updateAt":1667512919194,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7owy1izqn1if55r5hc3fgu8fada","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{"value":true},"createAt":1667512692335,"updateAt":1667512920115,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7sggexapxebb1zk9oqta6gcwsda","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":true},"createAt":1667512692283,"updateAt":1667512914481,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7xbj8zr1jxfnxfkyfyccb84ddeo","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":true},"createAt":1667512692273,"updateAt":1667512913567,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7y3ncauhatfrg7nzyr67twe36wc","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":true},"createAt":1667512692292,"updateAt":1667512915496,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7zcbwgrw5apd4frn6uxd386rktc","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{"value":true},"createAt":1667512692344,"updateAt":1667512920721,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7zijtxs3enjy5frzc4zb6937b3w","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{"value":true},"createAt":1667512692353,"updateAt":1667512922687,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"a1ag6b4hwkibbbbxdmse74cw3ur","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667512692310,"updateAt":1667512692310,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"a4wwsynhjafb4dgbubda18ho3fr","parentId":"c9ciauq49ifdntc99rnehkkshpr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.","fields":{},"createAt":1667512692301,"updateAt":1667512692301,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"799cbodnfr3ydfjp53die7egd1e","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667512344379,"updateAt":1667512354748,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7c1iwiqsi1iddpfzqisbkubjxhh","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{},"createAt":1667512215518,"updateAt":1667512224971,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7ftxm79a1e7nuxpb913aqphoqbo","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667512251753,"updateAt":1667512267186,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7jrwix8rkbtb5bdek79mtat8w1c","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{},"createAt":1667512204105,"updateAt":1667512287236,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7nze85jfmobfm8j8xfmrbdwyrfa","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":true},"createAt":1667510597027,"updateAt":1667512961521,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7s47d7rzh4pnw5rcnpjysxg6duh","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":true},"createAt":1667510557630,"updateAt":1667512956967,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7s5bgtoajcbnd8rrc5bxzabdcyw","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":true},"createAt":1667510586823,"updateAt":1667512960547,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7s9smhppjff87tndwawqwdmfryo","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":true},"createAt":1667510564441,"updateAt":1667512958081,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7t3ib1amo7fgzbmhg4tkzqustcy","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":true},"createAt":1667510573106,"updateAt":1667512959302,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7tp1rgey147nnfjuose7418oioh","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667512225170,"updateAt":1667512251543,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"acod8woq6zjbzmc1hz8qkfxyi1h","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667512186838,"updateAt":1667512192833,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"atgpetmwdubb5jkugcb6jm9pzyo","parentId":"chew1d7kc3py3pj51qyqaiz6ade","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.","fields":{},"createAt":1667512110036,"updateAt":1667512180024,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"71735rqboe3rkxypssssddjykkc","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":false},"createAt":1667513652337,"updateAt":1667513703739,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"74khjujy4hir4zmer4hkj1gcckh","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":false},"createAt":1667513652354,"updateAt":1667513652354,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"768ut9xkqipgf9fk6ub146spu5e","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":false},"createAt":1667513652368,"updateAt":1667513652368,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7c9cy5ohjd3b85xkee539zw9owh","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{},"createAt":1667513652448,"updateAt":1667513652448,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7dsynp6qf8tdtjpcqsxfyuqyzmo","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667513652464,"updateAt":1667513652464,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7h1zyk7thz7gx3r5degq6qorjay","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667513652495,"updateAt":1667513652495,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7jgf4cownfiy7xpaznxdsnyze9a","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":false},"createAt":1667513652344,"updateAt":1667513652344,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7jryotoo5wig9bdt3kh1fmgm5qw","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":false},"createAt":1667513652384,"updateAt":1667513652384,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7kxzdhjtx8pdazm7bufusybwygo","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667513652486,"updateAt":1667513652486,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7p7hz5ky15jgrirb64533xzsquo","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{},"createAt":1667513652428,"updateAt":1667513652428,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"atw8pz7bqgp877pd5714jbpqrsh","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n[Enter notes here...]","fields":{},"createAt":1667513652402,"updateAt":1667513741067,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"azwoek6rwfpfqiruig13owyyagr","parentId":"cphg5tyix4irsipkcp9ujaj3gwh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667513652416,"updateAt":1667513652416,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"777h45bs9xffj3ecpe9ti9jqdar","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667513758151,"updateAt":1667513758151,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"78sze8whs5i8htgtsuwqc81agjr","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":false},"createAt":1667513758058,"updateAt":1667513758058,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7brzhoqztxpyg8jtqrd7c6dqtie","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667513758161,"updateAt":1667513758161,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7dejdqngn43rp3phmg64ditmyrr","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667513758142,"updateAt":1667513758142,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7dpbn45wo63r97fgb5356od9jyr","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":false},"createAt":1667513758067,"updateAt":1667513758067,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7gku1jfqfppb358a3q6y1b8sb7a","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":false},"createAt":1667513758086,"updateAt":1667513758086,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7p6xqywuxwifsfb67bc5zbhu1ny","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{},"createAt":1667513758133,"updateAt":1667513758133,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7qfj7p7xb4fgp9y4a8sp13ixiny","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{},"createAt":1667513758124,"updateAt":1667513758124,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7xrfub5nuxtb5pbuwrwtjbtekdw","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":false},"createAt":1667513758077,"updateAt":1667513758077,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7ynnwzqecx7f8t8yci1htkikude","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":false},"createAt":1667513758096,"updateAt":1667513758096,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"as8xstqmiobdr7ykjc4rb9pfcdh","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667513758114,"updateAt":1667513758114,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"azxp9y8hk33guugjq6iba7whj6h","parentId":"ct59gu9j4cpnrtjcpyn3a5okdqa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\n[Enter notes here...]","fields":{},"createAt":1667513758104,"updateAt":1667513758104,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7abw4xifxubn1urheakij9kjc5e","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send proposal","fields":{},"createAt":1667512982703,"updateAt":1667512982703,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7bncmywm3h38s7ndpcu9sytfffy","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send initial email","fields":{"value":true},"createAt":1667512982648,"updateAt":1667512982648,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7jexsw3nutb8m3x6eyqio7gtcxr","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow up after demo","fields":{},"createAt":1667512982697,"updateAt":1667512982697,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7r47h1d8fjfrpzkfzyxha44wrqe","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Finalize contract","fields":{},"createAt":1667512982712,"updateAt":1667512982712,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7rgodjyks6jrtprbeizusduat4c","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule follow-up sales call","fields":{"value":false},"createAt":1667512982669,"updateAt":1667513178427,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7u5qxs77u57bc8bd33ug5aa91rw","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule demo","fields":{"value":false},"createAt":1667512982675,"updateAt":1667513176256,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7xxy1eewp8jdxpcouho8jq7ed4w","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Schedule initial sales call","fields":{"value":false},"createAt":1667512982661,"updateAt":1667513177889,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7yhpeqyesfif188x4pabwurnw4o","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Post-sales follow up","fields":{},"createAt":1667512982725,"updateAt":1667512982725,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7yq4oh69547rm9mp3eqg9zqzoxw","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Hand-off to customer success","fields":{},"createAt":1667512982718,"updateAt":1667512982718,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"7zmoekhdb4i848p313eh1okp78c","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Send follow-up email","fields":{"value":false},"createAt":1667512982655,"updateAt":1667513179761,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"awugzceoocjdnmn1ab6srb5cc6r","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1667512982690,"updateAt":1667512982690,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} -{"type":"block","data":{"id":"ay46giuhekby5tmrnw4abnz97pw","parentId":"cyt3qdus94pg3fkxq4ojebyd5fr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Notes\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.","fields":{},"createAt":1667512982683,"updateAt":1667512982683,"deleteAt":0,"boardId":"bbkpwdj8x17bdpdqd176n8ctoua"}} diff --git a/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/76fwrj36hptg6dywka4k5mt3sph.png b/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/76fwrj36hptg6dywka4k5mt3sph.png deleted file mode 100644 index 21c40c3d5b..0000000000 Binary files a/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/76fwrj36hptg6dywka4k5mt3sph.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/board.jsonl b/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/board.jsonl deleted file mode 100644 index 82a8c080e1..0000000000 --- a/server/boards/assets/templates-boardarchive/bbn1888mprfrm5fjw9f1je9x3xo/board.jsonl +++ /dev/null @@ -1,22 +0,0 @@ -{"type":"block","data":{"id":"bbn1888mprfrm5fjw9f1je9x3xo","parentId":"","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Personal Tasks (NEW)","fields":{"cardProperties":[{"id":"a9zf59u8x1rf4ywctpcqama7tio","name":"Occurrence","options":[{"color":"propColorBlue","id":"an51dnkenmoog9cetapbc4uyt3y","value":"Daily"},{"color":"propColorOrange","id":"afpy8s7i45frggprmfsqngsocqh","value":"Weekly"},{"color":"propColorPurple","id":"aj4jyekqqssatjcq7r7chmy19ey","value":"Monthly"}],"type":"select"},{"id":"abthng7baedhhtrwsdodeuincqy","name":"Completed","options":[],"type":"checkbox"}],"description":"Use this template to organize your life and track your personal tasks.","icon":"✔️","isTemplate":false,"showDescription":true},"createAt":1640281433899,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c5xamko6rpibhje3bjreenon7ce","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Pay bills","fields":{"contentOrder":["7gwsf4uxtftgjt841zgwydxeere","7j6rbt87htj83bbssod76iumsja","7fjacjgfxjfrf3psxc46wwsgqdo"],"icon":"🔌","isTemplate":false,"properties":{"a9zf59u8x1rf4ywctpcqama7tio":"aj4jyekqqssatjcq7r7chmy19ey","abthng7baedhhtrwsdodeuincqy":"true"}},"createAt":1640366942078,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"co6a88h6og3dm3kkub64kyb71jw","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Buy groceries","fields":{"contentOrder":["amd9sbzwrkpdspkisato6ajmzby","7r749xjm5pfnuib18sefxwezc4o","7zhat99shridtfntr97ek5j7yho","7imjjx8fazty8fcjzkns464nupy","7cbjz6bszwprnby56gfgzqehexc","76x8gh63upjdnm8uso3nja7gjqh","7z6ho1e3dibg6mki7jug84yxpja"],"icon":"🛒","isTemplate":false,"properties":{"a9zf59u8x1rf4ywctpcqama7tio":"afpy8s7i45frggprmfsqngsocqh"}},"createAt":1640365957059,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cr7gz7sempbfqpq7sign4jaeyxc","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Go for a walk","fields":{"contentOrder":["a6b44enuiwpgszm1wt6og1mshqa","aumtoywd8wjy7udm4ntcib4ckpo","75gpszxg6difjmf1j3f5edj3w7a"],"icon":"👣","isTemplate":false,"properties":{"a9zf59u8x1rf4ywctpcqama7tio":"an51dnkenmoog9cetapbc4uyt3y"}},"createAt":1640281433950,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cx7cki81xppd3pdgnyktwbgtzer","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Feed Fluffy","fields":{"contentOrder":["as5kdrix3ibd3jrnqzz94dcqqba"],"icon":"🐱","isTemplate":false,"properties":{"a9zf59u8x1rf4ywctpcqama7tio":"an51dnkenmoog9cetapbc4uyt3y"}},"createAt":1640281433850,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"czowhma7rnpgb3eczbqo3t7fijo","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Gardening","fields":{"contentOrder":[],"icon":"🌳","isTemplate":false,"properties":{"a9zf59u8x1rf4ywctpcqama7tio":"afpy8s7i45frggprmfsqngsocqh"}},"createAt":1640281433750,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vjq4piq89kbds5x5zq39zww7joo","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"List View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":280},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a9zf59u8x1rf4ywctpcqama7tio","abthng7baedhhtrwsdodeuincqy"]},"createAt":1641247999081,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vyeipq97iqbfjtd6fgcbxg6xbme","parentId":"bbn1888mprfrm5fjw9f1je9x3xo","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Board View","fields":{"cardOrder":["co6a88h6og3dm3kkub64kyb71jw","c5xamko6rpibhje3bjreenon7ce","cr7gz7sempbfqpq7sign4jaeyxc","cx7cki81xppd3pdgnyktwbgtzer","czowhma7rnpgb3eczbqo3t7fijo"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"a9zf59u8x1rf4ywctpcqama7tio","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["an51dnkenmoog9cetapbc4uyt3y","afpy8s7i45frggprmfsqngsocqh","aj4jyekqqssatjcq7r7chmy19ey",""],"visiblePropertyIds":["a9zf59u8x1rf4ywctpcqama7tio"]},"createAt":1640281433698,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7fjacjgfxjfrf3psxc46wwsgqdo","parentId":"c5xamko6rpibhje3bjreenon7ce","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Utilities","fields":{"value":true},"createAt":1640367568655,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7gwsf4uxtftgjt841zgwydxeere","parentId":"c5xamko6rpibhje3bjreenon7ce","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Mobile phone","fields":{"value":true},"createAt":1640367517692,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7j6rbt87htj83bbssod76iumsja","parentId":"c5xamko6rpibhje3bjreenon7ce","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Internet","fields":{"value":true},"createAt":1640367560684,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76x8gh63upjdnm8uso3nja7gjqh","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Cereal","fields":{"value":false},"createAt":1640366017886,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7cbjz6bszwprnby56gfgzqehexc","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Butter","fields":{"value":false},"createAt":1640365985683,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7imjjx8fazty8fcjzkns464nupy","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Bread","fields":{"value":false},"createAt":1640365983209,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7r749xjm5pfnuib18sefxwezc4o","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Milk","fields":{"value":false},"createAt":1640365978720,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7z6ho1e3dibg6mki7jug84yxpja","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Bananas","fields":{"value":false},"createAt":1640367364568,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7zhat99shridtfntr97ek5j7yho","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Eggs","fields":{"value":false},"createAt":1640365980953,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"amd9sbzwrkpdspkisato6ajmzby","parentId":"co6a88h6og3dm3kkub64kyb71jw","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Grocery list","fields":{},"createAt":1640367228497,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"75gpszxg6difjmf1j3f5edj3w7a","parentId":"cr7gz7sempbfqpq7sign4jaeyxc","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"76fwrj36hptg6dywka4k5mt3sph.png"},"createAt":1640368278060,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a6b44enuiwpgszm1wt6og1mshqa","parentId":"cr7gz7sempbfqpq7sign4jaeyxc","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Goal\nWalk at least 10,000 steps every day.","fields":{},"createAt":1640367836067,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aumtoywd8wjy7udm4ntcib4ckpo","parentId":"cr7gz7sempbfqpq7sign4jaeyxc","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Route","fields":{},"createAt":1640368155600,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"as5kdrix3ibd3jrnqzz94dcqqba","parentId":"cx7cki81xppd3pdgnyktwbgtzer","rootId":"bbn1888mprfrm5fjw9f1je9x3xo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"","fields":{},"createAt":1640368933239,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/bc41mwxg9ybb69pn9j5zna6d36c/board.jsonl b/server/boards/assets/templates-boardarchive/bc41mwxg9ybb69pn9j5zna6d36c/board.jsonl deleted file mode 100644 index 8f3435f67b..0000000000 --- a/server/boards/assets/templates-boardarchive/bc41mwxg9ybb69pn9j5zna6d36c/board.jsonl +++ /dev/null @@ -1,52 +0,0 @@ -{"type":"block","data":{"id":"bc41mwxg9ybb69pn9j5zna6d36c","parentId":"","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Project Tasks (NEW)","fields":{"cardProperties":[{"id":"a972dc7a-5f4c-45d2-8044-8c28c69717f1","name":"Status","options":[{"color":"propColorBlue","id":"ayz81h9f3dwp7rzzbdebesc7ute","value":"Not Started"},{"color":"propColorYellow","id":"ar6b8m3jxr3asyxhr8iucdbo6yc","value":"In Progress"},{"color":"propColorRed","id":"afi4o5nhnqc3smtzs1hs3ij34dh","value":"Blocked"},{"color":"propColorGreen","id":"adeo5xuwne3qjue83fcozekz8ko","value":"Completed 🙌"},{"color":"propColorBrown","id":"ahpyxfnnrzynsw3im1psxpkgtpe","value":"Archived"}],"type":"select"},{"id":"d3d682bf-e074-49d9-8df5-7320921c2d23","name":"Priority","options":[{"color":"propColorRed","id":"d3bfb50f-f569-4bad-8a3a-dd15c3f60101","value":"1. High 🔥"},{"color":"propColorYellow","id":"87f59784-b859-4c24-8ebe-17c766e081dd","value":"2. Medium"},{"color":"propColorGray","id":"98a57627-0f76-471d-850d-91f3ed9fd213","value":"3. Low"}],"type":"select"},{"id":"axkhqa4jxr3jcqe4k87g8bhmary","name":"Assignee","options":[],"type":"person"},{"id":"a8daz81s4xjgke1ww6cwik5w7ye","name":"Estimated Hours","options":[],"type":"number"},{"id":"a3zsw7xs8sxy7atj8b6totp3mby","name":"Due Date","options":[],"type":"date"},{"id":"a7gdnz8ff8iyuqmzddjgmgo9ery","name":"Created By","options":[],"type":"createdBy"},{"id":"2a5da320-735c-4093-8787-f56e15cdfeed","name":"Date Created","options":[],"type":"createdTime"}],"description":"Use this template to stay on top of your project tasks and progress.","icon":"🎯","isTemplate":false,"showDescription":true},"createAt":1640281242611,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c68gyx34srjgjxmrs1z8pj7nbce","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Identify dependencies","fields":{"contentOrder":["akqkae666a7bnbgib4ykbexjjey","7b1h5q66pkig4mp948z635dejxy","aepujbmb347ye9j7uikbk3oajqh","76q9tmzey4byqdpimsdxeg1gx3h","79qbaadiuwjgujnz9tgqmmkaaqo","7msorzdb7r3rk3qjncmdxhpqz5o","7izro8efd1irwpepfph4uz56bgh"],"icon":"🔗","isTemplate":false,"properties":{"a8daz81s4xjgke1ww6cwik5w7ye":"16","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ayz81h9f3dwp7rzzbdebesc7ute","d3d682bf-e074-49d9-8df5-7320921c2d23":"98a57627-0f76-471d-850d-91f3ed9fd213"}},"createAt":1640364405240,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c6w7rxrootfdw7j4fsftc5gsyoo","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Define project scope","fields":{"contentOrder":["ags74nq3isiywmmkkg8h4tbxcfh","7q7rkcbuqwfffjgrk57yjkydnry","a66dncm7qppd4tjo9886d5bbsaa","7jy54jqerhbnj7r4efpuk3g4cda","716fy9hw4p38a5mf8rq5ap6txoo","7opf3hssh6pn9zyy6toh53r49iw","7g1qskptj9i8gimg1aynyqtnwka"],"icon":"🔬","isTemplate":false,"properties":{"a8daz81s4xjgke1ww6cwik5w7ye":"32","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ar6b8m3jxr3asyxhr8iucdbo6yc","d3d682bf-e074-49d9-8df5-7320921c2d23":"87f59784-b859-4c24-8ebe-17c766e081dd"}},"createAt":1640364532461,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cdwqxf4b3utbbxdrgbwtmk9y9eo","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Requirements sign-off","fields":{"contentOrder":["aags5e9sbbfnqtrtf39hoopbxme","7kriyyuos4pgg8k6t8fkcsa7bde","adw7awe3ucp8g781dfq7yw6kfur","7xk7xg6yonbn88fpkihigzn8whr","7b9uyiog56jr1zgonbutxfd7w3c","7r3ua3e7w3jrmpqdngzqs74i1go","76hsxtocpnbnrijxqcfccfkyo1e"],"icon":"🖋️","isTemplate":false,"properties":{"a8daz81s4xjgke1ww6cwik5w7ye":"8","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ayz81h9f3dwp7rzzbdebesc7ute","d3d682bf-e074-49d9-8df5-7320921c2d23":"d3bfb50f-f569-4bad-8a3a-dd15c3f60101"}},"createAt":1640281242441,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cfk8kwmuhcfd8m8qicz5aqw4mar","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Project budget approval","fields":{"contentOrder":["a9h4kfaurrprepefrw95i1raoxr","7btyuex8nji8jxn9yieaxgwoe6h","a34hy46bu8bngxcxpz9woui4afa","7ekrgkgq67fdofn9gskpe19bkrc","7ygi1kq3683ya5ydfttuc5rhasr","7qmjyww91rj8a38dsgu5b5wu7hr","7qmmpepfm4byqjqo9m16yp7m3no"],"icon":"💵","isTemplate":false,"properties":{"a8daz81s4xjgke1ww6cwik5w7ye":"16","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ayz81h9f3dwp7rzzbdebesc7ute","d3d682bf-e074-49d9-8df5-7320921c2d23":"d3bfb50f-f569-4bad-8a3a-dd15c3f60101"}},"createAt":1640281242677,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ckcntrrmcjbywpciau57gw5suoo","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Conduct market analysis","fields":{"contentOrder":["a6gowxxpgijgip8qzrsp5rmjwqy","771bq4ja3ejfwbgaq78cdpgmjih","asdoj8ffhcirh3x3iys3joeox9o","7k975b49ni7yrfn3nqg7q4x4wde","7e9aj57zouidozb8sf8e1wybywe","71dm4jiu43byubx7pukjiy19pay","719y6x4tkiigd9nwarn1e6ek7ic"],"icon":"📈","isTemplate":false,"properties":{"a8daz81s4xjgke1ww6cwik5w7ye":"40","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ar6b8m3jxr3asyxhr8iucdbo6yc","d3d682bf-e074-49d9-8df5-7320921c2d23":"87f59784-b859-4c24-8ebe-17c766e081dd"}},"createAt":1640281242851,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vcuoise4b8jn1ffzujfuacymmmr","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Project Priorities","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"d3d682bf-e074-49d9-8df5-7320921c2d23","hiddenOptionIds":[],"kanbanCalculations":{"":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"87f59784-b859-4c24-8ebe-17c766e081dd":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"98a57627-0f76-471d-850d-91f3ed9fd213":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"d3bfb50f-f569-4bad-8a3a-dd15c3f60101":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"}},"sortOptions":[],"viewType":"board","visibleOptionIds":["d3bfb50f-f569-4bad-8a3a-dd15c3f60101","87f59784-b859-4c24-8ebe-17c766e081dd","98a57627-0f76-471d-850d-91f3ed9fd213",""],"visiblePropertyIds":["a972dc7a-5f4c-45d2-8044-8c28c69717f1","a8daz81s4xjgke1ww6cwik5w7ye"]},"createAt":1640281242551,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vey61xzc6u38ptnpjqaik6ap91e","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Progress Tracker","fields":{"cardOrder":["cfk8kwmuhcfd8m8qicz5aqw4mar","cdwqxf4b3utbbxdrgbwtmk9y9eo","c68gyx34srjgjxmrs1z8pj7nbce","ckcntrrmcjbywpciau57gw5suoo","c6w7rxrootfdw7j4fsftc5gsyoo","coxnjt3ro1in19dd1e3awdt338r"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"a972dc7a-5f4c-45d2-8044-8c28c69717f1","hiddenOptionIds":[],"kanbanCalculations":{"":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"adeo5xuwne3qjue83fcozekz8ko":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"afi4o5nhnqc3smtzs1hs3ij34dh":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"ahpyxfnnrzynsw3im1psxpkgtpe":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"ar6b8m3jxr3asyxhr8iucdbo6yc":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"},"ayz81h9f3dwp7rzzbdebesc7ute":{"calculation":"sum","propertyId":"a8daz81s4xjgke1ww6cwik5w7ye"}},"sortOptions":[],"viewType":"board","visibleOptionIds":["ayz81h9f3dwp7rzzbdebesc7ute","ar6b8m3jxr3asyxhr8iucdbo6yc","afi4o5nhnqc3smtzs1hs3ij34dh","adeo5xuwne3qjue83fcozekz8ko","ahpyxfnnrzynsw3im1psxpkgtpe",""],"visiblePropertyIds":["d3d682bf-e074-49d9-8df5-7320921c2d23","a8daz81s4xjgke1ww6cwik5w7ye"]},"createAt":1640281242788,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vfztxwjnegbdh38nfccu3bq1auc","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Task Overview","fields":{"cardOrder":["c6w7rxrootfdw7j4fsftc5gsyoo","ckcntrrmcjbywpciau57gw5suoo","c68gyx34srjgjxmrs1z8pj7nbce","cfk8kwmuhcfd8m8qicz5aqw4mar","cdwqxf4b3utbbxdrgbwtmk9y9eo","cz8p8gofakfby8kzz83j97db8ph","ce1jm5q5i54enhuu4h3kkay1hcc"],"collapsedOptionIds":[],"columnCalculations":{"a8daz81s4xjgke1ww6cwik5w7ye":"sum"},"columnWidths":{"2a5da320-735c-4093-8787-f56e15cdfeed":196,"__title":280,"a8daz81s4xjgke1ww6cwik5w7ye":139,"a972dc7a-5f4c-45d2-8044-8c28c69717f1":141,"d3d682bf-e074-49d9-8df5-7320921c2d23":110},"defaultTemplateId":"czw9es1e89fdpjr7cqptr1xq7qh","filter":{"filters":[],"operation":"and"},"groupById":"","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a972dc7a-5f4c-45d2-8044-8c28c69717f1","d3d682bf-e074-49d9-8df5-7320921c2d23","2a5da320-735c-4093-8787-f56e15cdfeed","a3zsw7xs8sxy7atj8b6totp3mby","axkhqa4jxr3jcqe4k87g8bhmary","a7gdnz8ff8iyuqmzddjgmgo9ery","a8daz81s4xjgke1ww6cwik5w7ye"]},"createAt":1640281242734,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vi49i1138jpnbiqhyd81beme9zy","parentId":"bc41mwxg9ybb69pn9j5zna6d36c","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Task Calendar","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"a3zsw7xs8sxy7atj8b6totp3mby","defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1640361708030,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76q9tmzey4byqdpimsdxeg1gx3h","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247437494,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"79qbaadiuwjgujnz9tgqmmkaaqo","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247440946,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7b1h5q66pkig4mp948z635dejxy","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247334696,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7izro8efd1irwpepfph4uz56bgh","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247447937,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7msorzdb7r3rk3qjncmdxhpqz5o","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247445214,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aepujbmb347ye9j7uikbk3oajqh","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247378401,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"akqkae666a7bnbgib4ykbexjjey","parentId":"c68gyx34srjgjxmrs1z8pj7nbce","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247332262,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"716fy9hw4p38a5mf8rq5ap6txoo","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247170396,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7g1qskptj9i8gimg1aynyqtnwka","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247182126,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7jy54jqerhbnj7r4efpuk3g4cda","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247156773,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7opf3hssh6pn9zyy6toh53r49iw","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247176917,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7q7rkcbuqwfffjgrk57yjkydnry","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247131586,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a66dncm7qppd4tjo9886d5bbsaa","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247135038,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ags74nq3isiywmmkkg8h4tbxcfh","parentId":"c6w7rxrootfdw7j4fsftc5gsyoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247112211,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76hsxtocpnbnrijxqcfccfkyo1e","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247486848,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7b9uyiog56jr1zgonbutxfd7w3c","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247480724,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7kriyyuos4pgg8k6t8fkcsa7bde","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247352753,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7r3ua3e7w3jrmpqdngzqs74i1go","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247483695,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7xk7xg6yonbn88fpkihigzn8whr","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247478297,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aags5e9sbbfnqtrtf39hoopbxme","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247350239,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"adw7awe3ucp8g781dfq7yw6kfur","parentId":"cdwqxf4b3utbbxdrgbwtmk9y9eo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247399161,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7btyuex8nji8jxn9yieaxgwoe6h","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247342345,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ekrgkgq67fdofn9gskpe19bkrc","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247459230,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7qmjyww91rj8a38dsgu5b5wu7hr","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247464903,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7qmmpepfm4byqjqo9m16yp7m3no","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247468228,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ygi1kq3683ya5ydfttuc5rhasr","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247461754,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a34hy46bu8bngxcxpz9woui4afa","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247389505,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a9h4kfaurrprepefrw95i1raoxr","parentId":"cfk8kwmuhcfd8m8qicz5aqw4mar","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247339781,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"719y6x4tkiigd9nwarn1e6ek7ic","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247428974,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"71dm4jiu43byubx7pukjiy19pay","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247425545,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"771bq4ja3ejfwbgaq78cdpgmjih","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247327922,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7e9aj57zouidozb8sf8e1wybywe","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247421647,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7k975b49ni7yrfn3nqg7q4x4wde","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247417179,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a6gowxxpgijgip8qzrsp5rmjwqy","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247325247,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"asdoj8ffhcirh3x3iys3joeox9o","parentId":"ckcntrrmcjbywpciau57gw5suoo","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247365651,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"73a715h3xkiye9jj9px3daujgpa","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 3]","fields":{"value":false},"createAt":1641247243580,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"75afimcsuqby6xxq39wiae9obme","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 2]","fields":{"value":false},"createAt":1641247239940,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7dodh1pgw73yq78pgtmk3ckc9fr","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"divider","title":"","fields":{},"createAt":1641247212754,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ttgtruigcbfzdmxkhmzt6kp6dh","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"[Subtask 1]","fields":{"value":false},"createAt":1641247226415,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7u7mmiit57b8i8gsp6mc6x7h9he","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"...","fields":{"value":false},"createAt":1641247248372,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"adxx8y691qf8btg7w8mx6x78w9y","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*","fields":{},"createAt":1641247210152,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"afatxnq346jbcin9iisryo38grr","parentId":"czw9es1e89fdpjr7cqptr1xq7qh","rootId":"bc41mwxg9ybb69pn9j5zna6d36c","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Checklist","fields":{},"createAt":1641247215942,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/bcm39o11e4ib8tye8mt6iyuec9o/board.jsonl b/server/boards/assets/templates-boardarchive/bcm39o11e4ib8tye8mt6iyuec9o/board.jsonl deleted file mode 100644 index 8801780ed7..0000000000 --- a/server/boards/assets/templates-boardarchive/bcm39o11e4ib8tye8mt6iyuec9o/board.jsonl +++ /dev/null @@ -1,12 +0,0 @@ -{"type":"board","data":{"id":"bcm39o11e4ib8tye8mt6iyuec9o","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"Company Goals \u0026 OKRs","description":"Use this template to plan your company goals and OKRs more efficiently.","icon":"⛳","showDescription":true,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"a6amddgmrzakw66cidqzgk6p4ge","name":"Objective","options":[{"color":"propColorGreen","id":"auw3afh3kfhrfgmjr8muiz137jy","value":"Grow Revenue"},{"color":"propColorOrange","id":"apqfjst8massbjjhpcsjs3y1yqa","value":"Delight Customers"},{"color":"propColorPurple","id":"ao9b5pxyt7tkgdohzh9oaustdhr","value":"Drive Product Adoption"}],"type":"select"},{"id":"a17ryhi1jfsboxkwkztwawhmsxe","name":"Status","options":[{"color":"propColorGray","id":"a6robxx81diugpjq5jkezz3j1fo","value":"Not Started"},{"color":"propColorBlue","id":"a8nukezwwmknqwjsygg7eaxs9te","value":"In Progress"},{"color":"propColorYellow","id":"apnt1f7na9rzgk1rt49keg7xbiy","value":"At Risk"},{"color":"propColorRed","id":"axbz3m1amss335wzwf9s7pqjzxr","value":"Missed"},{"color":"propColorGreen","id":"abzfwnn6rmtfzyq5hg8uqmpsncy","value":"Complete 🙌"}],"type":"select"},{"id":"azzbawji5bksj69sekcs4srm1ky","name":"Department","options":[{"color":"propColorBrown","id":"aw5i7hmpadn6mbwbz955ubarhme","value":"Engineering"},{"color":"propColorBlue","id":"afkxpcjqjypu7hhar7banxau91h","value":"Product"},{"color":"propColorOrange","id":"aehoa17cz18rqnrf75g7dwhphpr","value":"Marketing"},{"color":"propColorGreen","id":"agrfeaoj7d8p5ianw5iaf3191ae","value":"Sales"},{"color":"propColorYellow","id":"agm9p6gcq15ueuzqq3wd4be39wy","value":"Support"},{"color":"propColorPink","id":"aucop7kw6xwodcix6zzojhxih6r","value":"Design"},{"color":"propColorPurple","id":"afust91f3g8ht368mkn5x9tgf1o","value":"Finance"},{"color":"propColorGray","id":"acocxxwjurud1jixhp7nowdig7y","value":"Human Resources"}],"type":"select"},{"id":"adp5ft3kgz7r5iqq3tnwg551der","name":"Priority","options":[{"color":"propColorRed","id":"a8zg3rjtf4swh7smsjxpsn743rh","value":"P1 🔥"},{"color":"propColorYellow","id":"as555ipyzopjjpfb5rjtssecw5e","value":"P2"},{"color":"propColorGray","id":"a1ts3ftyr8nocsicui98c89uxjy","value":"P3"}],"type":"select"},{"id":"aqxyzkdrs4egqf7yk866ixkaojc","name":"Quarter","options":[{"color":"propColorBlue","id":"ahfbn1jsmhydym33ygxwg5jt3kh","value":"Q1"},{"color":"propColorBrown","id":"awfu37js3fomfkkczm1zppac57a","value":"Q2"},{"color":"propColorGreen","id":"anruuoyez51r3yjxuoc8zoqnwaw","value":"Q3"},{"color":"propColorPurple","id":"acb6dqqs6yson7bbzx6jk9bghjh","value":"Q4"}],"type":"select"},{"id":"adu6mebzpibq6mgcswk69xxmnqe","name":"Due Date","options":[],"type":"date"},{"id":"asope3bddhm4gpsng5cfu4hf6rh","name":"Assignee","options":[],"type":"multiPerson"},{"id":"ajwxp866f9obs1kutfwaa5ru7fe","name":"Target","options":[],"type":"number"},{"id":"azqnyswk6s1boiwuthscm78qwuo","name":"Actual","options":[],"type":"number"},{"id":"ahz3fmjnaguec8hce7xq3h5cjdr","name":"Completion (%)","options":[],"type":"text"},{"id":"a17bfcgnzmkwhziwa4tr38kiw5r","name":"Note","options":[],"type":"text"}],"createAt":1667430124226,"updateAt":1667431508571,"deleteAt":0}} -{"type":"block","data":{"id":"vangk4cpd5fgpbr7635tx6oxg7c","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Quarter","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":452,"a17ryhi1jfsboxkwkztwawhmsxe":148,"a6amddgmrzakw66cidqzgk6p4ge":230,"azzbawji5bksj69sekcs4srm1ky":142},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"aqxyzkdrs4egqf7yk866ixkaojc","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a6amddgmrzakw66cidqzgk6p4ge","a17ryhi1jfsboxkwkztwawhmsxe","azzbawji5bksj69sekcs4srm1ky","adp5ft3kgz7r5iqq3tnwg551der","aqxyzkdrs4egqf7yk866ixkaojc","adu6mebzpibq6mgcswk69xxmnqe","asope3bddhm4gpsng5cfu4hf6rh","ajwxp866f9obs1kutfwaa5ru7fe","azqnyswk6s1boiwuthscm78qwuo","ahz3fmjnaguec8hce7xq3h5cjdr","a17bfcgnzmkwhziwa4tr38kiw5r"]},"createAt":1667431291178,"updateAt":1667431333436,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"vr1jnxkxi8pf9z83fhr4qbsbxao","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Objectives","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":387,"a17ryhi1jfsboxkwkztwawhmsxe":134,"a6amddgmrzakw66cidqzgk6p4ge":183,"aqxyzkdrs4egqf7yk866ixkaojc":100},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"a6amddgmrzakw66cidqzgk6p4ge","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a6amddgmrzakw66cidqzgk6p4ge","a17ryhi1jfsboxkwkztwawhmsxe","azzbawji5bksj69sekcs4srm1ky","adp5ft3kgz7r5iqq3tnwg551der","aqxyzkdrs4egqf7yk866ixkaojc","adu6mebzpibq6mgcswk69xxmnqe","asope3bddhm4gpsng5cfu4hf6rh","ajwxp866f9obs1kutfwaa5ru7fe","azqnyswk6s1boiwuthscm78qwuo","ahz3fmjnaguec8hce7xq3h5cjdr","a17bfcgnzmkwhziwa4tr38kiw5r"]},"createAt":1667431221976,"updateAt":1667431420460,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"c3m6mgymw978wjecydz16io868h","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Improve customer NPS score","fields":{"contentOrder":[],"icon":"💯","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"a8nukezwwmknqwjsygg7eaxs9te","a6amddgmrzakw66cidqzgk6p4ge":"apqfjst8massbjjhpcsjs3y1yqa","adp5ft3kgz7r5iqq3tnwg551der":"as555ipyzopjjpfb5rjtssecw5e","ahz3fmjnaguec8hce7xq3h5cjdr":"82%","ajwxp866f9obs1kutfwaa5ru7fe":"8.5","aqxyzkdrs4egqf7yk866ixkaojc":"anruuoyez51r3yjxuoc8zoqnwaw","azqnyswk6s1boiwuthscm78qwuo":"7","azzbawji5bksj69sekcs4srm1ky":"agm9p6gcq15ueuzqq3wd4be39wy"}},"createAt":1667430924551,"updateAt":1667430962900,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"ce9u86wofitrb5ns4qp5w1ij1nh","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Generate more Marketing Qualified Leads (MQLs)","fields":{"contentOrder":[],"icon":"🛣️","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"a8nukezwwmknqwjsygg7eaxs9te","a6amddgmrzakw66cidqzgk6p4ge":"auw3afh3kfhrfgmjr8muiz137jy","adp5ft3kgz7r5iqq3tnwg551der":"as555ipyzopjjpfb5rjtssecw5e","ahz3fmjnaguec8hce7xq3h5cjdr":"65%","ajwxp866f9obs1kutfwaa5ru7fe":"100","aqxyzkdrs4egqf7yk866ixkaojc":"ahfbn1jsmhydym33ygxwg5jt3kh","azqnyswk6s1boiwuthscm78qwuo":"65","azzbawji5bksj69sekcs4srm1ky":"aehoa17cz18rqnrf75g7dwhphpr"}},"createAt":1667430791375,"updateAt":1667430832892,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"cjkscjjex6fg8i8aa3umxof9wfc","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Increase customer retention","fields":{"contentOrder":[],"icon":"😀","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"a8nukezwwmknqwjsygg7eaxs9te","a6amddgmrzakw66cidqzgk6p4ge":"apqfjst8massbjjhpcsjs3y1yqa","adp5ft3kgz7r5iqq3tnwg551der":"a8zg3rjtf4swh7smsjxpsn743rh","ahz3fmjnaguec8hce7xq3h5cjdr":"66%","ajwxp866f9obs1kutfwaa5ru7fe":"90% customer retention rate","aqxyzkdrs4egqf7yk866ixkaojc":"acb6dqqs6yson7bbzx6jk9bghjh","azqnyswk6s1boiwuthscm78qwuo":"60%","azzbawji5bksj69sekcs4srm1ky":"afkxpcjqjypu7hhar7banxau91h"}},"createAt":1667430973987,"updateAt":1667431007817,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"ckxdhpf5bhf8i7n13fgbs4155ec","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Hit company global sales target","fields":{"contentOrder":[],"icon":"💰","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"a6robxx81diugpjq5jkezz3j1fo","a6amddgmrzakw66cidqzgk6p4ge":"auw3afh3kfhrfgmjr8muiz137jy","adp5ft3kgz7r5iqq3tnwg551der":"a8zg3rjtf4swh7smsjxpsn743rh","ahz3fmjnaguec8hce7xq3h5cjdr":"15%","ajwxp866f9obs1kutfwaa5ru7fe":"50MM","aqxyzkdrs4egqf7yk866ixkaojc":"awfu37js3fomfkkczm1zppac57a","azqnyswk6s1boiwuthscm78qwuo":"7.5MM","azzbawji5bksj69sekcs4srm1ky":"agrfeaoj7d8p5ianw5iaf3191ae"}},"createAt":1667430875599,"updateAt":1667430909496,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"cn1x4niym7tnpjg61jf1su67wcr","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Increase user signups by 30%","fields":{"contentOrder":[],"icon":"💳","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"a6robxx81diugpjq5jkezz3j1fo","a6amddgmrzakw66cidqzgk6p4ge":"ao9b5pxyt7tkgdohzh9oaustdhr","adp5ft3kgz7r5iqq3tnwg551der":"as555ipyzopjjpfb5rjtssecw5e","ahz3fmjnaguec8hce7xq3h5cjdr":"0%","ajwxp866f9obs1kutfwaa5ru7fe":"1,000","aqxyzkdrs4egqf7yk866ixkaojc":"acb6dqqs6yson7bbzx6jk9bghjh","azqnyswk6s1boiwuthscm78qwuo":"0","azzbawji5bksj69sekcs4srm1ky":"afkxpcjqjypu7hhar7banxau91h"}},"createAt":1667431085923,"updateAt":1667431132757,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"cpa534b5natgmunis8u1ixb55pw","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Add 10 new customers in the EU","fields":{"contentOrder":[],"icon":"🌍","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"apnt1f7na9rzgk1rt49keg7xbiy","a6amddgmrzakw66cidqzgk6p4ge":"auw3afh3kfhrfgmjr8muiz137jy","adp5ft3kgz7r5iqq3tnwg551der":"a1ts3ftyr8nocsicui98c89uxjy","ahz3fmjnaguec8hce7xq3h5cjdr":"30%","ajwxp866f9obs1kutfwaa5ru7fe":"10","aqxyzkdrs4egqf7yk866ixkaojc":"acb6dqqs6yson7bbzx6jk9bghjh","azqnyswk6s1boiwuthscm78qwuo":"3","azzbawji5bksj69sekcs4srm1ky":"agrfeaoj7d8p5ianw5iaf3191ae"}},"createAt":1667430190782,"updateAt":1667430844747,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"cq4krpnzqqfne3khfyhnn3c6r5r","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Launch 3 key features","fields":{"contentOrder":[],"icon":"🚀","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"apnt1f7na9rzgk1rt49keg7xbiy","a6amddgmrzakw66cidqzgk6p4ge":"ao9b5pxyt7tkgdohzh9oaustdhr","adp5ft3kgz7r5iqq3tnwg551der":"a8zg3rjtf4swh7smsjxpsn743rh","ahz3fmjnaguec8hce7xq3h5cjdr":"33%","ajwxp866f9obs1kutfwaa5ru7fe":"3","aqxyzkdrs4egqf7yk866ixkaojc":"anruuoyez51r3yjxuoc8zoqnwaw","azqnyswk6s1boiwuthscm78qwuo":"1","azzbawji5bksj69sekcs4srm1ky":"aw5i7hmpadn6mbwbz955ubarhme"}},"createAt":1667431144882,"updateAt":1667431177540,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"cugiq6j98utg1zdekbpjpufo51y","parentId":"bcm39o11e4ib8tye8mt6iyuec9o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Reduce bug backlog by 50%","fields":{"contentOrder":[],"icon":"🐞","isTemplate":false,"properties":{"a17ryhi1jfsboxkwkztwawhmsxe":"abzfwnn6rmtfzyq5hg8uqmpsncy","a6amddgmrzakw66cidqzgk6p4ge":"apqfjst8massbjjhpcsjs3y1yqa","adp5ft3kgz7r5iqq3tnwg551der":"a1ts3ftyr8nocsicui98c89uxjy","ahz3fmjnaguec8hce7xq3h5cjdr":"100%","ajwxp866f9obs1kutfwaa5ru7fe":"75","aqxyzkdrs4egqf7yk866ixkaojc":"awfu37js3fomfkkczm1zppac57a","azqnyswk6s1boiwuthscm78qwuo":"75","azzbawji5bksj69sekcs4srm1ky":"aw5i7hmpadn6mbwbz955ubarhme"}},"createAt":1667431018282,"updateAt":1667431070950,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} -{"type":"block","data":{"id":"vx4ng6gtakbntt8k98znkzszc1a","parentId":"bm4ubx56krp4zwyfcqh7nxiigbr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Departments","fields":{"cardOrder":["cpa534b5natgmunis8u1ixb55pw"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"azzbawji5bksj69sekcs4srm1ky","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["aw5i7hmpadn6mbwbz955ubarhme","afkxpcjqjypu7hhar7banxau91h","aehoa17cz18rqnrf75g7dwhphpr","agrfeaoj7d8p5ianw5iaf3191ae","agm9p6gcq15ueuzqq3wd4be39wy","aucop7kw6xwodcix6zzojhxih6r","afust91f3g8ht368mkn5x9tgf1o","acocxxwjurud1jixhp7nowdig7y"],"visiblePropertyIds":[]},"createAt":1667430124232,"updateAt":1667431286030,"deleteAt":0,"boardId":"bcm39o11e4ib8tye8mt6iyuec9o"}} diff --git a/server/boards/assets/templates-boardarchive/bd65qbzuqupfztpg31dgwgwm5ga/board.jsonl b/server/boards/assets/templates-boardarchive/bd65qbzuqupfztpg31dgwgwm5ga/board.jsonl deleted file mode 100644 index 45cbc5a0e5..0000000000 --- a/server/boards/assets/templates-boardarchive/bd65qbzuqupfztpg31dgwgwm5ga/board.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"type":"block","data":{"id":"bd65qbzuqupfztpg31dgwgwm5ga","parentId":"","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Personal Goals (NEW)","fields":{"cardProperties":[{"id":"af6fcbb8-ca56-4b73-83eb-37437b9a667d","name":"Status","options":[{"color":"propColorRed","id":"bf52bfe6-ac4c-4948-821f-83eaa1c7b04a","value":"To Do"},{"color":"propColorYellow","id":"77c539af-309c-4db1-8329-d20ef7e9eacd","value":"Doing"},{"color":"propColorGreen","id":"98bdea27-0cce-4cde-8dc6-212add36e63a","value":"Done 🙌"}],"type":"select"},{"id":"d9725d14-d5a8-48e5-8de1-6f8c004a9680","name":"Category","options":[{"color":"propColorPurple","id":"3245a32d-f688-463b-87f4-8e7142c1b397","value":"Life Skills"},{"color":"propColorGreen","id":"80be816c-fc7a-4928-8489-8b02180f4954","value":"Finance"},{"color":"propColorOrange","id":"ffb3f951-b47f-413b-8f1d-238666728008","value":"Health"}],"type":"select"},{"id":"d6b1249b-bc18-45fc-889e-bec48fce80ef","name":"Target","options":[{"color":"propColorBlue","id":"9a090e33-b110-4268-8909-132c5002c90e","value":"Q1"},{"color":"propColorBrown","id":"0a82977f-52bf-457b-841b-e2b7f76fb525","value":"Q2"},{"color":"propColorGreen","id":"6e7139e4-5358-46bb-8c01-7b029a57b80a","value":"Q3"},{"color":"propColorPurple","id":"d5371c63-66bf-4468-8738-c4dc4bea4843","value":"Q4"}],"type":"select"},{"id":"ajy6xbebzopojaenbnmfpgtdwso","name":"Due Date","options":[],"type":"date"}],"description":"Use this template to set and accomplish new personal goals.","icon":"⛰️","isTemplate":false,"showDescription":true},"createAt":1641246775089,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c76haqhzin78q5dkfko7kwhbjjh","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Start a daily journal","fields":{"contentOrder":[],"icon":"✍️","isTemplate":false,"properties":{"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"bf52bfe6-ac4c-4948-821f-83eaa1c7b04a","d6b1249b-bc18-45fc-889e-bec48fce80ef":"0a82977f-52bf-457b-841b-e2b7f76fb525","d9725d14-d5a8-48e5-8de1-6f8c004a9680":"3245a32d-f688-463b-87f4-8e7142c1b397"}},"createAt":1641246774828,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ca3byfg7iq3g8zjpg1t8hwa6ekh","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Run 3 times a week","fields":{"contentOrder":[],"icon":"🏃","isTemplate":false,"properties":{"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"bf52bfe6-ac4c-4948-821f-83eaa1c7b04a","d6b1249b-bc18-45fc-889e-bec48fce80ef":"6e7139e4-5358-46bb-8c01-7b029a57b80a","d9725d14-d5a8-48e5-8de1-6f8c004a9680":"ffb3f951-b47f-413b-8f1d-238666728008"}},"createAt":1641246775039,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ckng5n1ag5f8m5gfdifn7ijof9y","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Learn to paint","fields":{"contentOrder":[],"icon":"🎨","isTemplate":false,"properties":{"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"77c539af-309c-4db1-8329-d20ef7e9eacd","d6b1249b-bc18-45fc-889e-bec48fce80ef":"9a090e33-b110-4268-8909-132c5002c90e","d9725d14-d5a8-48e5-8de1-6f8c004a9680":"3245a32d-f688-463b-87f4-8e7142c1b397"}},"createAt":1641246774928,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cw9zofoi6dj8x7x8r6ypebpwpuc","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Open retirement account","fields":{"contentOrder":[],"icon":"🏦","isTemplate":false,"properties":{"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"bf52bfe6-ac4c-4948-821f-83eaa1c7b04a","d6b1249b-bc18-45fc-889e-bec48fce80ef":"0a82977f-52bf-457b-841b-e2b7f76fb525","d9725d14-d5a8-48e5-8de1-6f8c004a9680":"80be816c-fc7a-4928-8489-8b02180f4954"}},"createAt":1641246774987,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"v9sj7oekk1jr1pemtf9rps7fate","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Status","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"af6fcbb8-ca56-4b73-83eb-37437b9a667d","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["bf52bfe6-ac4c-4948-821f-83eaa1c7b04a","77c539af-309c-4db1-8329-d20ef7e9eacd","98bdea27-0cce-4cde-8dc6-212add36e63a",""],"visiblePropertyIds":["d9725d14-d5a8-48e5-8de1-6f8c004a9680","d6b1249b-bc18-45fc-889e-bec48fce80ef"]},"createAt":1641246774878,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vrpmc8r6nj7fcmdkp18cpcekzco","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Calendar View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"ajy6xbebzopojaenbnmfpgtdwso","defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1641247726340,"updateAt":1643788318630,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vw9mbn66j97dwb8jhqiq7zuum5e","parentId":"bd65qbzuqupfztpg31dgwgwm5ga","rootId":"bd65qbzuqupfztpg31dgwgwm5ga","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Date","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"d6b1249b-bc18-45fc-889e-bec48fce80ef","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["9a090e33-b110-4268-8909-132c5002c90e","0a82977f-52bf-457b-841b-e2b7f76fb525","6e7139e4-5358-46bb-8c01-7b029a57b80a","d5371c63-66bf-4468-8738-c4dc4bea4843",""],"visiblePropertyIds":["d9725d14-d5a8-48e5-8de1-6f8c004a9680"]},"createAt":1641246775139,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/7b9xk9boj3fbqfm3umeaaizp8qr.png b/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/7b9xk9boj3fbqfm3umeaaizp8qr.png deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/7tmfu5iqju3n1mdfwi5gru89qmw.png b/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/7tmfu5iqju3n1mdfwi5gru89qmw.png deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/board.jsonl b/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/board.jsonl deleted file mode 100644 index 2ba5c63073..0000000000 --- a/server/boards/assets/templates-boardarchive/bgi1yqiis8t8xdqxgnet8ebutky/board.jsonl +++ /dev/null @@ -1,32 +0,0 @@ -{"type":"board","data":{"id":"bgi1yqiis8t8xdqxgnet8ebutky","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"Sprint Planner ","description":"Use this template to plan your sprints and manage your releases more efficiently.","icon":"🗓️","showDescription":true,"isTemplate":false,"templateVersion":4,"properties":{},"cardProperties":[{"id":"50117d52-bcc7-4750-82aa-831a351c44a0","name":"Status","options":[{"color":"propColorGray","id":"aft5bzo7h9aspqgrx3jpy5tzrer","value":"Not Started"},{"color":"propColorOrange","id":"abrfos7e7eczk9rqw6y5abadm1y","value":"Next Up"},{"color":"propColorBlue","id":"ax8wzbka5ahs3zziji3pp4qp9mc","value":"In Progress"},{"color":"propColorYellow","id":"atabdfbdmjh83136d5e5oysxybw","value":"In Review"},{"color":"propColorPink","id":"ace1bzypd586kkyhcht5qqd9eca","value":"Approved"},{"color":"propColorRed","id":"aay656c9m1hzwxc9ch5ftymh3nw","value":"Blocked"},{"color":"propColorGreen","id":"a6ghze4iy441qhsh3eijnc8hwze","value":"Complete 🙌"}],"type":"select"},{"id":"20717ad3-5741-4416-83f1-6f133fff3d11","name":"Type","options":[{"color":"propColorYellow","id":"424ea5e3-9aa1-4075-8c5c-01b44b66e634","value":"Epic ⛰"},{"color":"propColorGray","id":"a5yxq8rbubrpnoommfwqmty138h","value":"Feature 🏗"},{"color":"propColorOrange","id":"apht1nt5ryukdmxkh6fkfn6rgoy","value":"User Story 📖"},{"color":"propColorGreen","id":"aiycbuo3dr5k4xxbfr7coem8ono","value":"Task ⛏"},{"color":"propColorRed","id":"aomnawq4551cbbzha9gxnmb3z5w","value":"Bug 🐞"}],"type":"select"},{"id":"60985f46-3e41-486e-8213-2b987440ea1c","name":"Sprint","options":[{"color":"propColorBrown","id":"c01676ca-babf-4534-8be5-cce2287daa6c","value":"Sprint 1"},{"color":"propColorPurple","id":"ed4a5340-460d-461b-8838-2c56e8ee59fe","value":"Sprint 2"},{"color":"propColorBlue","id":"14892380-1a32-42dd-8034-a0cea32bc7e6","value":"Sprint 3"}],"type":"select"},{"id":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","name":"Priority","options":[{"color":"propColorRed","id":"cb8ecdac-38be-4d36-8712-c4d58cc8a8e9","value":"P1 🔥"},{"color":"propColorYellow","id":"e6a7f297-4440-4783-8ab3-3af5ba62ca11","value":"P2"},{"color":"propColorGray","id":"c62172ea-5da7-4dec-8186-37267d8ee9a7","value":"P3"}],"type":"select"},{"id":"aphg37f7zbpuc3bhwhp19s1ribh","name":"Assignee","options":[],"type":"multiPerson"},{"id":"a4378omyhmgj3bex13sj4wbpfiy","name":"Due Date","options":[],"type":"date"},{"id":"ai7ajsdk14w7x5s8up3dwir77te","name":"Story Points","options":[],"type":"number"},{"id":"a1g6i613dpe9oryeo71ex3c86hy","name":"Design Link","options":[],"type":"url"},{"id":"aeomttrbhhsi8bph31jn84sto6h","name":"Created Time","options":[],"type":"createdTime"},{"id":"ax9f8so418s6s65hi5ympd93i6a","name":"Created By","options":[],"type":"createdBy"}],"createAt":1657660691136,"updateAt":1667496289175,"deleteAt":0}} -{"type":"block","data":{"id":"vdusd7mmojjy7dqtcews89kbawe","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Sprint","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{"ai7ajsdk14w7x5s8up3dwir77te":"count"},"columnWidths":{"20717ad3-5741-4416-83f1-6f133fff3d11":128,"50117d52-bcc7-4750-82aa-831a351c44a0":126,"__title":280,"a1g6i613dpe9oryeo71ex3c86hy":159,"aeomttrbhhsi8bph31jn84sto6h":141,"ax9f8so418s6s65hi5ympd93i6a":183,"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":100},"defaultTemplateId":"c9pwabyseiibumq71b9ykxsotqe","filter":{"filters":[],"operation":"and"},"groupById":"60985f46-3e41-486e-8213-2b987440ea1c","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["50117d52-bcc7-4750-82aa-831a351c44a0","20717ad3-5741-4416-83f1-6f133fff3d11","60985f46-3e41-486e-8213-2b987440ea1c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","aphg37f7zbpuc3bhwhp19s1ribh","a4378omyhmgj3bex13sj4wbpfiy","ai7ajsdk14w7x5s8up3dwir77te","a1g6i613dpe9oryeo71ex3c86hy","aeomttrbhhsi8bph31jn84sto6h","ax9f8so418s6s65hi5ympd93i6a"]},"createAt":1667495373961,"updateAt":1667507826320,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"c9pwabyseiibumq71b9ykxsotqe","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"User Story","fields":{"contentOrder":["anfmjd4qmxffj3bckd9nei61ioe"],"icon":"📖","isTemplate":true,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"apht1nt5ryukdmxkh6fkfn6rgoy","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer"}},"createAt":1667496557683,"updateAt":1667496593762,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"c9rh5kubchfy1tejtiwbsw6z5xr","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Horizontal scroll issue","fields":{"contentOrder":["aiazua9893f8tmgn5jcn476ieay","ayko7csybxpgg7ejnybqoimp6co","7n75owjmi1bfnbcdswmscqpon5r"],"icon":"〰️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"aomnawq4551cbbzha9gxnmb3z5w","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer","60985f46-3e41-486e-8213-2b987440ea1c":"ed4a5340-460d-461b-8838-2c56e8ee59fe","ai7ajsdk14w7x5s8up3dwir77te":"1","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1657660691350,"updateAt":1667495472233,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cc98t3whwhbnd5mx4qehmg43wpy","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Login screen not loading","fields":{"contentOrder":["ahaytdn7aajy63dsca6dhmzew6e","awcedibyeufyazxdy6x83wiqtne","73u5teq68rbrsfensjkigjfsk3h"],"icon":"🖥️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"aomnawq4551cbbzha9gxnmb3z5w","50117d52-bcc7-4750-82aa-831a351c44a0":"abrfos7e7eczk9rqw6y5abadm1y","60985f46-3e41-486e-8213-2b987440ea1c":"c01676ca-babf-4534-8be5-cce2287daa6c","ai7ajsdk14w7x5s8up3dwir77te":"1","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"cb8ecdac-38be-4d36-8712-c4d58cc8a8e9"}},"createAt":1657660691556,"updateAt":1667495472221,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cfb7jed1iz3ntx8rrcc5pphaixc","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Move cards across boards","fields":{"contentOrder":["aqm83zjjchi8a8nramuatg88cer"],"icon":"🚚","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"a5yxq8rbubrpnoommfwqmty138h","50117d52-bcc7-4750-82aa-831a351c44a0":"abrfos7e7eczk9rqw6y5abadm1y","60985f46-3e41-486e-8213-2b987440ea1c":"ed4a5340-460d-461b-8838-2c56e8ee59fe","a1g6i613dpe9oryeo71ex3c86hy":"https://mattermost.com/boards/","ai7ajsdk14w7x5s8up3dwir77te":"2","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1657660691670,"updateAt":1667495472240,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cg1ausqdw9bdpbgx1aaoas6umaa","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Cross-team collaboration","fields":{"contentOrder":["aanatt8ay8iyj7n4gxstxijiber"],"icon":"🤝","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"424ea5e3-9aa1-4075-8c5c-01b44b66e634","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer","60985f46-3e41-486e-8213-2b987440ea1c":"14892380-1a32-42dd-8034-a0cea32bc7e6","a1g6i613dpe9oryeo71ex3c86hy":"https://mattermost.com/boards/","ai7ajsdk14w7x5s8up3dwir77te":"3","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"c62172ea-5da7-4dec-8186-37267d8ee9a7"}},"createAt":1657660691791,"updateAt":1667495472239,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ci1zytcoz1jrj3qncas3fjc9ruo","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Bug","fields":{"contentOrder":["ajgyboqst3fy1zb989wkuqiaz5o","akzfz1eh8uj87mfkgucgmtdzwzw","7zbiceo9toidtbjya5xxo6fcsow"],"icon":"🐞","isTemplate":true,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"aomnawq4551cbbzha9gxnmb3z5w","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer"}},"createAt":1667507786809,"updateAt":1667507806029,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cs7rqsonyr7gofepxn84ui8niyy","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Standard properties","fields":{"contentOrder":["ax5npjmoqo7b87fzjo518ahfdkc"],"icon":"🏷️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"a5yxq8rbubrpnoommfwqmty138h","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer","60985f46-3e41-486e-8213-2b987440ea1c":"14892380-1a32-42dd-8034-a0cea32bc7e6","a1g6i613dpe9oryeo71ex3c86hy":"https://mattermost.com/boards/","ai7ajsdk14w7x5s8up3dwir77te":"3","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1657660691454,"updateAt":1667495472304,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cxi14orfaajfsjjpgok167kc78y","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Epic","fields":{"contentOrder":["aoer81hcfmt818d1awj3bnntkzh"],"icon":"🤝","isTemplate":true,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"424ea5e3-9aa1-4075-8c5c-01b44b66e634","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer","a1g6i613dpe9oryeo71ex3c86hy":"https://mattermost.com/boards/","ai7ajsdk14w7x5s8up3dwir77te":"3","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"c62172ea-5da7-4dec-8186-37267d8ee9a7"}},"createAt":1667496390689,"updateAt":1667496493419,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cxmfbp7wdoifdzdztkrurxe3pgh","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Global templates","fields":{"contentOrder":["a6r3jdde39ibbury8s8zib5prjy"],"icon":"🖼️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"a5yxq8rbubrpnoommfwqmty138h","50117d52-bcc7-4750-82aa-831a351c44a0":"a6ghze4iy441qhsh3eijnc8hwze","60985f46-3e41-486e-8213-2b987440ea1c":"c01676ca-babf-4534-8be5-cce2287daa6c","a1g6i613dpe9oryeo71ex3c86hy":"https://mattermost.com/boards/","ai7ajsdk14w7x5s8up3dwir77te":"2","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1657660691245,"updateAt":1667496491020,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"cxom5chmr5tna9ru4na34dbhmur","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Feature","fields":{"contentOrder":["ad9bf7wpdwbnwbebkptg3puwu4c"],"icon":"🏗️","isTemplate":true,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"a5yxq8rbubrpnoommfwqmty138h","50117d52-bcc7-4750-82aa-831a351c44a0":"aft5bzo7h9aspqgrx3jpy5tzrer"}},"createAt":1667496496593,"updateAt":1667496522591,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"v4fpda1kk3jgy8ctqyw9ey4fwye","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Status","fields":{"cardOrder":["cxmfbp7wdoifdzdztkrurxe3pgh","c9rh5kubchfy1tejtiwbsw6z5xr","cfb7jed1iz3ntx8rrcc5pphaixc","cc98t3whwhbnd5mx4qehmg43wpy","cs7rqsonyr7gofepxn84ui8niyy","cg1ausqdw9bdpbgx1aaoas6umaa"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"cidz4imnqhir48brz6e8hxhfrhy","filter":{"filters":[],"operation":"and"},"groupById":"50117d52-bcc7-4750-82aa-831a351c44a0","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[{"propertyId":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","reversed":false}],"viewType":"board","visibleOptionIds":["aft5bzo7h9aspqgrx3jpy5tzrer","abrfos7e7eczk9rqw6y5abadm1y","ax8wzbka5ahs3zziji3pp4qp9mc","atabdfbdmjh83136d5e5oysxybw","ace1bzypd586kkyhcht5qqd9eca","aay656c9m1hzwxc9ch5ftymh3nw","a6ghze4iy441qhsh3eijnc8hwze"],"visiblePropertyIds":["20717ad3-5741-4416-83f1-6f133fff3d11","60985f46-3e41-486e-8213-2b987440ea1c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1657660691994,"updateAt":1667496285840,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"vj3bern6637nt7c5edfx8qx6b6h","parentId":"bgi1yqiis8t8xdqxgnet8ebutky","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Type","fields":{"cardOrder":["cc98t3whwhbnd5mx4qehmg43wpy","cfb7jed1iz3ntx8rrcc5pphaixc","cs7rqsonyr7gofepxn84ui8niyy","c9rh5kubchfy1tejtiwbsw6z5xr","cxmfbp7wdoifdzdztkrurxe3pgh","cg1ausqdw9bdpbgx1aaoas6umaa"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"cidz4imnqhir48brz6e8hxhfrhy","filter":{"filters":[],"operation":"and"},"groupById":"20717ad3-5741-4416-83f1-6f133fff3d11","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[{"propertyId":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","reversed":false}],"viewType":"board","visibleOptionIds":["424ea5e3-9aa1-4075-8c5c-01b44b66e634","a5yxq8rbubrpnoommfwqmty138h","apht1nt5ryukdmxkh6fkfn6rgoy","aiycbuo3dr5k4xxbfr7coem8ono","aomnawq4551cbbzha9gxnmb3z5w"],"visiblePropertyIds":["20717ad3-5741-4416-83f1-6f133fff3d11","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1657660691890,"updateAt":1667496327144,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"anfmjd4qmxffj3bckd9nei61ioe","parentId":"c9pwabyseiibumq71b9ykxsotqe","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1667496557692,"updateAt":1667496557692,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"7n75owjmi1bfnbcdswmscqpon5r","parentId":"c9rh5kubchfy1tejtiwbsw6z5xr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7tmfu5iqju3n1mdfwi5gru89qmw.png"},"createAt":1657660690017,"updateAt":1657660690017,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aiazua9893f8tmgn5jcn476ieay","parentId":"c9rh5kubchfy1tejtiwbsw6z5xr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1657660691037,"updateAt":1657660691037,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ayko7csybxpgg7ejnybqoimp6co","parentId":"c9rh5kubchfy1tejtiwbsw6z5xr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1657660690831,"updateAt":1657660690831,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"73u5teq68rbrsfensjkigjfsk3h","parentId":"cc98t3whwhbnd5mx4qehmg43wpy","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7b9xk9boj3fbqfm3umeaaizp8qr.png"},"createAt":1657660690116,"updateAt":1657660690116,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ahaytdn7aajy63dsca6dhmzew6e","parentId":"cc98t3whwhbnd5mx4qehmg43wpy","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1657660690422,"updateAt":1657660690422,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"awcedibyeufyazxdy6x83wiqtne","parentId":"cc98t3whwhbnd5mx4qehmg43wpy","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1657660690318,"updateAt":1657660690318,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aqm83zjjchi8a8nramuatg88cer","parentId":"cfb7jed1iz3ntx8rrcc5pphaixc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1657660690521,"updateAt":1657660690521,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aumokx4tdmjrgxgy4o8s3jow8ha","parentId":"cfmk7771httynm8r7rm8cbrmrya","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1657729295838,"updateAt":1657729295838,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aydywea4hq3rytf3k7a9y4iqtbe","parentId":"cfmk7771httynm8r7rm8cbrmrya","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1657729295724,"updateAt":1657729295724,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aanatt8ay8iyj7n4gxstxijiber","parentId":"cg1ausqdw9bdpbgx1aaoas6umaa","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\n*[Brief description of what this epic is about]*\n\n## Motivation\n*[Brief description on why this is needed]*\n\n## Acceptance Criteria\n - *[Criteron 1]*\n - *[Criteron 2]*\n - ...\n\n## Personas\n - *[Persona A]*\n - *[Persona B]*\n - ...\n\n## Reference Materials\n - *[Links to other relevant documents as needed]*\n - ...","fields":{},"createAt":1657660690218,"updateAt":1657660690218,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"7zbiceo9toidtbjya5xxo6fcsow","parentId":"ci1zytcoz1jrj3qncas3fjc9ruo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7tmfu5iqju3n1mdfwi5gru89qmw.png"},"createAt":1667507786817,"updateAt":1667507786817,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ajgyboqst3fy1zb989wkuqiaz5o","parentId":"ci1zytcoz1jrj3qncas3fjc9ruo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1667507786830,"updateAt":1667507786830,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"akzfz1eh8uj87mfkgucgmtdzwzw","parentId":"ci1zytcoz1jrj3qncas3fjc9ruo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1667507786823,"updateAt":1667507786823,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ax5npjmoqo7b87fzjo518ahfdkc","parentId":"cs7rqsonyr7gofepxn84ui8niyy","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1657660690723,"updateAt":1657660690723,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"aoer81hcfmt818d1awj3bnntkzh","parentId":"cxi14orfaajfsjjpgok167kc78y","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\n*[Brief description of what this epic is about]*\n\n## Motivation\n*[Brief description on why this is needed]*\n\n## Acceptance Criteria\n - *[Criteron 1]*\n - *[Criteron 2]*\n - ...\n\n## Personas\n - *[Persona A]*\n - *[Persona B]*\n - ...\n\n## Reference Materials\n - *[Links to other relevant documents as needed]*\n - ...","fields":{},"createAt":1667496390699,"updateAt":1667496390699,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"a6r3jdde39ibbury8s8zib5prjy","parentId":"cxmfbp7wdoifdzdztkrurxe3pgh","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1657660690935,"updateAt":1657660690935,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} -{"type":"block","data":{"id":"ad9bf7wpdwbnwbebkptg3puwu4c","parentId":"cxom5chmr5tna9ru4na34dbhmur","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1667496496600,"updateAt":1667496496600,"deleteAt":0,"boardId":"bgi1yqiis8t8xdqxgnet8ebutky"}} diff --git a/server/boards/assets/templates-boardarchive/bh4pkixqsjift58e1qy6htrgeay/board.jsonl b/server/boards/assets/templates-boardarchive/bh4pkixqsjift58e1qy6htrgeay/board.jsonl deleted file mode 100644 index 37c52190f6..0000000000 --- a/server/boards/assets/templates-boardarchive/bh4pkixqsjift58e1qy6htrgeay/board.jsonl +++ /dev/null @@ -1,14 +0,0 @@ -{"type":"board","data":{"id":"bh4pkixqsjift58e1qy6htrgeay","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"User Research Sessions","description":"Use this template to manage and keep track of all your user research sessions.","icon":"🔬","showDescription":true,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"aaebj5fyx493eezx6ukxiwydgty","name":"Status","options":[{"color":"propColorGray","id":"af6hjb3ysuaxbwnfqpby4wwnkdr","value":"Backlog 📒"},{"color":"propColorYellow","id":"aotxum1p5bw3xuzqz3ctjw66yww","value":"Contacted 📞"},{"color":"propColorBlue","id":"a7yq89whddzob1futao4rxk3yzc","value":"Scheduled 📅"},{"color":"propColorRed","id":"aseqq9hrsua56r3s6nbuirj9eec","value":"Cancelled 🚫"},{"color":"propColorGreen","id":"ap93ysuzy1xa7z818r6myrn4h4y","value":"Completed ✔️"}],"type":"select"},{"id":"akrxgi7p7w14fym3gbynb98t9fh","name":"Interview Date","options":[],"type":"date"},{"id":"atg9qu6oe4bjm8jczzsn71ff5me","name":"Product Area","options":[{"color":"propColorGreen","id":"ahn89mqg9u4igk6pdm7333t8i5h","value":"Desktop App"},{"color":"propColorPurple","id":"aehc83ffays3gh8myz16a8j7k4e","value":"Web App"},{"color":"propColorBlue","id":"a1sxagjgaadym5yrjak6tcup1oa","value":"Mobile App"}],"type":"select"},{"id":"acjq4t5ymytu8x1f68wkggm7ypc","name":"Email","options":[],"type":"email"},{"id":"aphio1s5gkmpdbwoxynim7acw3e","name":"Interviewer","options":[],"type":"multiPerson"},{"id":"aqafzdeekpyncwz7m7i54q3iqqy","name":"Recording URL","options":[],"type":"url"},{"id":"aify3r761b9w43bqjtskrzi68tr","name":"Passcode","options":[],"type":"text"}],"createAt":1667410119064,"updateAt":1667504168497,"deleteAt":0}} -{"type":"block","data":{"id":"vtibhoxpq67f1xmh8a8kxh39nka","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"All Users","fields":{"cardOrder":["ccsa77z7ubbbhbd3jq8xyx4hq8r"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":280,"aaebj5fyx493eezx6ukxiwydgty":146,"acjq4t5ymytu8x1f68wkggm7ypc":222,"akrxgi7p7w14fym3gbynb98t9fh":131,"atg9qu6oe4bjm8jczzsn71ff5me":131},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"akrxgi7p7w14fym3gbynb98t9fh","reversed":false}],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["aaebj5fyx493eezx6ukxiwydgty","akrxgi7p7w14fym3gbynb98t9fh","atg9qu6oe4bjm8jczzsn71ff5me","acjq4t5ymytu8x1f68wkggm7ypc","aphio1s5gkmpdbwoxynim7acw3e","aqafzdeekpyncwz7m7i54q3iqqy","aify3r761b9w43bqjtskrzi68tr"]},"createAt":1667410162023,"updateAt":1667410900827,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"ccsa77z7ubbbhbd3jq8xyx4hq8r","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Frank Nash","fields":{"contentOrder":["aiqaqwzhe1tn9umunms95414kzo"],"icon":"👨‍💼","isTemplate":false,"properties":{"aaebj5fyx493eezx6ukxiwydgty":"ap93ysuzy1xa7z818r6myrn4h4y","acjq4t5ymytu8x1f68wkggm7ypc":"frank.nash@email.com","aify3r761b9w43bqjtskrzi68tr":"Password123","akrxgi7p7w14fym3gbynb98t9fh":"{\"from\":1669896000000}","aqafzdeekpyncwz7m7i54q3iqqy":"https://user-images.githubusercontent.com/46905241/121941290-ee355280-cd03-11eb-9b9f-f6f524e4103e.gif","atg9qu6oe4bjm8jczzsn71ff5me":"aehc83ffays3gh8myz16a8j7k4e"}},"createAt":1667410176348,"updateAt":1667410539559,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"cdus79hea7ib6tb6nhic4bzcjbc","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Richard Parsons","fields":{"contentOrder":["aodtggcnfuby6fq8ehxg4koafgr"],"icon":"👨‍🦱","isTemplate":false,"properties":{"aaebj5fyx493eezx6ukxiwydgty":"a7yq89whddzob1futao4rxk3yzc","acjq4t5ymytu8x1f68wkggm7ypc":"richard.parsons@email.com","aify3r761b9w43bqjtskrzi68tr":"Password123","akrxgi7p7w14fym3gbynb98t9fh":"{\"from\":1671019200000}","aqafzdeekpyncwz7m7i54q3iqqy":"https://user-images.githubusercontent.com/46905241/121941290-ee355280-cd03-11eb-9b9f-f6f524e4103e.gif","atg9qu6oe4bjm8jczzsn71ff5me":"a1sxagjgaadym5yrjak6tcup1oa"}},"createAt":1667410640657,"updateAt":1667417523845,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"cewmyr3nbybdombzmi83arq3koo","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Claire Hart","fields":{"contentOrder":["ahkkrf9xn8tfq9y98to8pbt6qnw"],"icon":"👩‍🦰","isTemplate":false,"properties":{"aaebj5fyx493eezx6ukxiwydgty":"aseqq9hrsua56r3s6nbuirj9eec","acjq4t5ymytu8x1f68wkggm7ypc":"claire.hart@email.com","aify3r761b9w43bqjtskrzi68tr":"Password123","akrxgi7p7w14fym3gbynb98t9fh":"{\"from\":1670500800000}","aqafzdeekpyncwz7m7i54q3iqqy":"https://user-images.githubusercontent.com/46905241/121941290-ee355280-cd03-11eb-9b9f-f6f524e4103e.gif","atg9qu6oe4bjm8jczzsn71ff5me":"ahn89mqg9u4igk6pdm7333t8i5h"}},"createAt":1667410785750,"updateAt":1667410805030,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"cix9xfgh48ir55y7fdjftuje3za","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Olivia Alsop","fields":{"contentOrder":["a8xz4ead8k7budxknwhjxm9n3uc"],"icon":"👩‍💼","isTemplate":false,"properties":{"aaebj5fyx493eezx6ukxiwydgty":"a7yq89whddzob1futao4rxk3yzc","acjq4t5ymytu8x1f68wkggm7ypc":"olivia.alsop@email.com","aify3r761b9w43bqjtskrzi68tr":"Password123","akrxgi7p7w14fym3gbynb98t9fh":"{\"from\":1671192000000}","aqafzdeekpyncwz7m7i54q3iqqy":"https://user-images.githubusercontent.com/46905241/121941290-ee355280-cd03-11eb-9b9f-f6f524e4103e.gif","atg9qu6oe4bjm8jczzsn71ff5me":"a1sxagjgaadym5yrjak6tcup1oa"}},"createAt":1667410730577,"updateAt":1667410775912,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"cn3skudjd9tbp5md9bocifnazpw","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Bernadette Powell","fields":{"contentOrder":["au67hjd7es7y6jkumo4ysrudwfa"],"icon":"🧑‍💼","isTemplate":false,"properties":{"aaebj5fyx493eezx6ukxiwydgty":"af6hjb3ysuaxbwnfqpby4wwnkdr","acjq4t5ymytu8x1f68wkggm7ypc":"bernadette.powell@email.com"}},"createAt":1667410584181,"updateAt":1667410629860,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"vqi9zpn3h43bkbfc8c8jc7ci1hr","parentId":"bh4pkixqsjift58e1qy6htrgeay","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Date","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"akrxgi7p7w14fym3gbynb98t9fh","defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1667410845935,"updateAt":1667410849497,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"vdbpwgay6bbn8581n39yjiyxrxo","parentId":"bixohg18tt11in4qbtinimk974y","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Status","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["af6hjb3ysuaxbwnfqpby4wwnkdr","aotxum1p5bw3xuzqz3ctjw66yww","a7yq89whddzob1futao4rxk3yzc","aseqq9hrsua56r3s6nbuirj9eec","ap93ysuzy1xa7z818r6myrn4h4y"],"visiblePropertyIds":[]},"createAt":1667410119073,"updateAt":1667417470776,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"aiqaqwzhe1tn9umunms95414kzo","parentId":"ccsa77z7ubbbhbd3jq8xyx4hq8r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Interview Notes\n- ...\n- ...\n- ... ","fields":{},"createAt":1667410410824,"updateAt":1667410422875,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"aodtggcnfuby6fq8ehxg4koafgr","parentId":"cdus79hea7ib6tb6nhic4bzcjbc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Interview Notes\n- ...\n- ...\n- ... ","fields":{},"createAt":1667410640663,"updateAt":1667410640663,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"ahkkrf9xn8tfq9y98to8pbt6qnw","parentId":"cewmyr3nbybdombzmi83arq3koo","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Interview Notes\n- ...\n- ...\n- ... ","fields":{},"createAt":1667410785755,"updateAt":1667410785755,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"a8xz4ead8k7budxknwhjxm9n3uc","parentId":"cix9xfgh48ir55y7fdjftuje3za","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Interview Notes\n- ...\n- ...\n- ... ","fields":{},"createAt":1667410730582,"updateAt":1667410730582,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} -{"type":"block","data":{"id":"au67hjd7es7y6jkumo4ysrudwfa","parentId":"cn3skudjd9tbp5md9bocifnazpw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Interview Notes\n- ...\n- ...\n- ... ","fields":{},"createAt":1667410584187,"updateAt":1667410584187,"deleteAt":0,"boardId":"bh4pkixqsjift58e1qy6htrgeay"}} diff --git a/server/boards/assets/templates-boardarchive/bkqk6hpfx7pbsucue7jan5n1o1o/board.jsonl b/server/boards/assets/templates-boardarchive/bkqk6hpfx7pbsucue7jan5n1o1o/board.jsonl deleted file mode 100644 index a325c07bab..0000000000 --- a/server/boards/assets/templates-boardarchive/bkqk6hpfx7pbsucue7jan5n1o1o/board.jsonl +++ /dev/null @@ -1,13 +0,0 @@ -{"type":"board","data":{"id":"bkqk6hpfx7pbsucue7jan5n1o1o","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"Competitive Analysis","description":"Use this template to track and stay ahead of the competition.","icon":"🗂️","showDescription":true,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"ahzspe59iux8wigra8bg6cg18nc","name":"Website","options":[],"type":"url"},{"id":"aozntq4go4nkab688j1s7stqtfc","name":"Location","options":[],"type":"text"},{"id":"aiefo7nh9jwisn8b4cgakowithy","name":"Revenue","options":[],"type":"text"},{"id":"a6cwaq79b1pdpb97wkanmeyy4er","name":"Employees","options":[],"type":"number"},{"id":"an1eerzscfxn6awdfajbg41uz3h","name":"Founded","options":[],"type":"text"},{"id":"a1semdhszu1rq17d7et5ydrqqio","name":"Market Position","options":[{"color":"propColorYellow","id":"arfjpz9by5car71tz3behba8yih","value":"Leader"},{"color":"propColorRed","id":"abajmr34b8g1916w495xjb35iko","value":"Challenger"},{"color":"propColorBlue","id":"abt79uxg5edqojsrrefcnr4eruo","value":"Follower"},{"color":"propColorBrown","id":"aipf3qfgjtkheiayjuxrxbpk9wa","value":"Nicher"}],"type":"select"},{"id":"aapogff3xoa8ym7xf56s87kysda","name":"Last updated time","options":[],"type":"updatedTime"},{"id":"az3jkw3ynd3mqmart7edypey15e","name":"Last updated by","options":[],"type":"updatedBy"}],"createAt":1667337304886,"updateAt":1667352513150,"deleteAt":0}} -{"type":"block","data":{"id":"vfzq8kedf3bnt7qkrsom658j6io","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Competitor List","fields":{"cardOrder":["c96bjeqk6zjrm5qtyoenexh3f8e","chg7cdun9hjbf5pue6zc1gxm8rw","cn9chs8a4zjyqzqez7qor63s8uc","ctkqr4ce3zjrzur3q4mn47eeuuc","cnam59x954idsxpbbfp8bmsigtr"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":210,"a1semdhszu1rq17d7et5ydrqqio":121,"aapogff3xoa8ym7xf56s87kysda":194,"ahzspe59iux8wigra8bg6cg18nc":156,"aiefo7nh9jwisn8b4cgakowithy":155,"aozntq4go4nkab688j1s7stqtfc":151,"az3jkw3ynd3mqmart7edypey15e":145},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["ahzspe59iux8wigra8bg6cg18nc","aozntq4go4nkab688j1s7stqtfc","aiefo7nh9jwisn8b4cgakowithy","a6cwaq79b1pdpb97wkanmeyy4er","an1eerzscfxn6awdfajbg41uz3h","a1semdhszu1rq17d7et5ydrqqio","aapogff3xoa8ym7xf56s87kysda","az3jkw3ynd3mqmart7edypey15e"]},"createAt":1667339411936,"updateAt":1667399926321,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"vr158bbbsetn5ffm1gebhduhx5a","parentId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Market Position","fields":{"cardOrder":["cip8b4jcomfr7by9gtizebikfke","cacs91js1hb887ds41r6dwnd88c","ca3u8edwrof89i8obxffnz4xw3a"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["arfjpz9by5car71tz3behba8yih","abajmr34b8g1916w495xjb35iko","abt79uxg5edqojsrrefcnr4eruo","aipf3qfgjtkheiayjuxrxbpk9wa"],"visiblePropertyIds":[]},"createAt":1667351648812,"updateAt":1667352684324,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"c96bjeqk6zjrm5qtyoenexh3f8e","parentId":"bkqk6hpfx7pbsucue7jan5n1o1o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Liminary Corp.","fields":{"contentOrder":["ainmysbw6xpyczm3p5xayocpm3e"],"icon":"🌧","isTemplate":false,"properties":{"a1semdhszu1rq17d7et5ydrqqio":"abt79uxg5edqojsrrefcnr4eruo","a6cwaq79b1pdpb97wkanmeyy4er":"300","ahzspe59iux8wigra8bg6cg18nc":"liminarycorp.com","aiefo7nh9jwisn8b4cgakowithy":"$25,000,000","an1eerzscfxn6awdfajbg41uz3h":"2017","aozntq4go4nkab688j1s7stqtfc":"Toronto, Canada"}},"createAt":1667338157613,"updateAt":1667351630721,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"chg7cdun9hjbf5pue6zc1gxm8rw","parentId":"bkqk6hpfx7pbsucue7jan5n1o1o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Helx Industries","fields":{"contentOrder":["an5k8g7ntz7bgppfx9cuk9oyaja"],"icon":"📦","isTemplate":false,"properties":{"a1semdhszu1rq17d7et5ydrqqio":"abt79uxg5edqojsrrefcnr4eruo","a6cwaq79b1pdpb97wkanmeyy4er":"650","ahzspe59iux8wigra8bg6cg18nc":"helxindustries.com","aiefo7nh9jwisn8b4cgakowithy":"$50,000,000","an1eerzscfxn6awdfajbg41uz3h":"2009","aozntq4go4nkab688j1s7stqtfc":"New York, NY"}},"createAt":1667338444580,"updateAt":1667351626493,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"cn9chs8a4zjyqzqez7qor63s8uc","parentId":"bkqk6hpfx7pbsucue7jan5n1o1o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Kadera Global","fields":{"contentOrder":["aup87xiwr9bye8fpshibuh156ih"],"icon":"🛡","isTemplate":false,"properties":{"a1semdhszu1rq17d7et5ydrqqio":"aipf3qfgjtkheiayjuxrxbpk9wa","a6cwaq79b1pdpb97wkanmeyy4er":"150","ahzspe59iux8wigra8bg6cg18nc":"kaderaglobal.com","aiefo7nh9jwisn8b4cgakowithy":"$12,000,000","an1eerzscfxn6awdfajbg41uz3h":"2015","aozntq4go4nkab688j1s7stqtfc":"Seattle, OR"}},"createAt":1667338227718,"updateAt":1667351623847,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"cnam59x954idsxpbbfp8bmsigtr","parentId":"bkqk6hpfx7pbsucue7jan5n1o1o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Ositions Inc.","fields":{"contentOrder":["aukdnjj7mw3ggudoe88wmmpgore"],"icon":"🌃","isTemplate":false,"properties":{"a1semdhszu1rq17d7et5ydrqqio":"abajmr34b8g1916w495xjb35iko","a6cwaq79b1pdpb97wkanmeyy4er":"2,700","ahzspe59iux8wigra8bg6cg18nc":"ositionsinc.com","aiefo7nh9jwisn8b4cgakowithy":"$125,000,000","an1eerzscfxn6awdfajbg41uz3h":"2004","aozntq4go4nkab688j1s7stqtfc":"Berlin, Germany"}},"createAt":1667337634942,"updateAt":1667351619186,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"ctkqr4ce3zjrzur3q4mn47eeuuc","parentId":"bkqk6hpfx7pbsucue7jan5n1o1o","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Afformance Ltd.","fields":{"contentOrder":["a1c857fph4byaxdqf88kw1wrwyo"],"icon":"⚡","isTemplate":false,"properties":{"a1semdhszu1rq17d7et5ydrqqio":"arfjpz9by5car71tz3behba8yih","a6cwaq79b1pdpb97wkanmeyy4er":"1,800","ahzspe59iux8wigra8bg6cg18nc":"afformanceltd.com","aiefo7nh9jwisn8b4cgakowithy":"$200,000,000","an1eerzscfxn6awdfajbg41uz3h":"2002","aozntq4go4nkab688j1s7stqtfc":"Palo Alto, CA"}},"createAt":1667338608746,"updateAt":1667351615526,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"ainmysbw6xpyczm3p5xayocpm3e","parentId":"c96bjeqk6zjrm5qtyoenexh3f8e","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.\n## Strengths\n- ...\n- ...\n## Weaknesses\n- ...\n- ...\n## Opportunities\n- ...\n- ...\n## Threats\n- ...\n- ...","fields":{},"createAt":1667339042969,"updateAt":1667340672945,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"an5k8g7ntz7bgppfx9cuk9oyaja","parentId":"chg7cdun9hjbf5pue6zc1gxm8rw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.\n## Strengths\n- ...\n- ...\n## Weaknesses\n- ...\n- ...\n## Opportunities\n- ...\n- ...\n## Threats\n- ...\n- ...","fields":{},"createAt":1667339057076,"updateAt":1667340666993,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"aup87xiwr9bye8fpshibuh156ih","parentId":"cn9chs8a4zjyqzqez7qor63s8uc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.\n\n## Strengths\n- ...\n- ...\n## Weaknesses\n- ...\n- ...\n## Opportunities\n- ...\n- ...\n## Threats\n- ...\n- ...","fields":{},"createAt":1667339054835,"updateAt":1667340086915,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"aukdnjj7mw3ggudoe88wmmpgore","parentId":"cnam59x954idsxpbbfp8bmsigtr","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.\n## Strengths\n- ...\n- ...\n\n## Weaknesses\n- ...\n- ...\n\n## Opportunities\n- ...\n- ...\n\n## Threats\n- ...\n- ...","fields":{},"createAt":1667339032796,"updateAt":1667340679120,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} -{"type":"block","data":{"id":"a1c857fph4byaxdqf88kw1wrwyo","parentId":"ctkqr4ce3zjrzur3q4mn47eeuuc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis fermentum aliquet massa in ornare. Pellentesque mollis nisl efficitur, eleifend nisi congue, scelerisque nunc. Aliquam lorem quam, commodo id nunc nec, congue bibendum velit. Vivamus sed mattis libero, et iaculis diam. Suspendisse euismod hendrerit nisl, quis ornare ipsum gravida in.\n## Strengths\n- ...\n- ...\n## Weaknesses\n- ...\n- ...\n## Opportunities\n- ...\n- ...\n## Threats\n- ...\n- ...","fields":{},"createAt":1667339061925,"updateAt":1667340648719,"deleteAt":0,"boardId":"bkqk6hpfx7pbsucue7jan5n1o1o"}} diff --git a/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/7y5kr8x8ybpnwdykjfuz57rggrh.png b/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/7y5kr8x8ybpnwdykjfuz57rggrh.png deleted file mode 100644 index 4b4d5152dc..0000000000 Binary files a/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/7y5kr8x8ybpnwdykjfuz57rggrh.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/board.jsonl b/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/board.jsonl deleted file mode 100644 index 5288b120f3..0000000000 --- a/server/boards/assets/templates-boardarchive/brs9cdimfw7fodyi7erqt747rhc/board.jsonl +++ /dev/null @@ -1,18 +0,0 @@ -{"type":"block","data":{"id":"brs9cdimfw7fodyi7erqt747rhc","parentId":"","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Content Calendar (NEW)","fields":{"cardProperties":[{"id":"ae9ar615xoknd8hw8py7mbyr7zo","name":"Status","options":[{"color":"propColorGray","id":"awna1nuarjca99m9s4uiy9kwj5h","value":"Idea 💡"},{"color":"propColorOrange","id":"a9ana1e9w673o5cp8md4xjjwfto","value":"Draft"},{"color":"propColorPurple","id":"apy9dcd7zmand615p3h53zjqxjh","value":"In Review"},{"color":"propColorBlue","id":"acri4cm3bmay55f7ksztphmtnga","value":"Ready to Publish"},{"color":"propColorGreen","id":"amsowcd9a8e1kid317r7ttw6uzh","value":"Published 🎉"}],"type":"select"},{"id":"aysx3atqexotgwp5kx6h5i5ancw","name":"Type","options":[{"color":"propColorOrange","id":"aywiofmmtd3ofgzj95ysky4pjga","value":"Press Release"},{"color":"propColorGreen","id":"apqdgjrmsmx8ngmp7zst51647de","value":"Sponsored Post"},{"color":"propColorPurple","id":"a3woynbjnb7j16e74uw3pubrytw","value":"Customer Story"},{"color":"propColorRed","id":"aq36k5pkpfcypqb3idw36xdi1fh","value":"Product Release"},{"color":"propColorGray","id":"azn66pmk34adygnizjqhgiac4ia","value":"Partnership"},{"color":"propColorBlue","id":"aj8y675weso8kpb6eceqbpj4ruw","value":"Feature Announcement"},{"color":"propColorYellow","id":"a3xky7ygn14osr1mokerbfah5cy","value":"Article"}],"type":"select"},{"id":"ab6mbock6styfe6htf815ph1mhw","name":"Channel","options":[{"color":"propColorBrown","id":"a8xceonxiu4n3c43szhskqizicr","value":"Website"},{"color":"propColorGreen","id":"a3pdzi53kpbd4okzdkz6khi87zo","value":"Blog"},{"color":"propColorOrange","id":"a3d9ux4fmi3anyd11kyipfbhwde","value":"Email"},{"color":"propColorRed","id":"a8cbbfdwocx73zn3787cx6gacsh","value":"Podcast"},{"color":"propColorPink","id":"aigjtpcaxdp7d6kmctrwo1ztaia","value":"Print"},{"color":"propColorBlue","id":"af1wsn13muho59e7ghwaogxy5ey","value":"Facebook"},{"color":"propColorGray","id":"a47zajfxwhsg6q8m7ppbiqs7jge","value":"LinkedIn"},{"color":"propColorYellow","id":"az8o8pfe9hq6s7xaehoqyc3wpyc","value":"Twitter"}],"type":"multiSelect"},{"id":"ao44fz8nf6z6tuj1x31t9yyehcc","name":"Assignee","options":[],"type":"person"},{"id":"a39x5cybshwrbjpc3juaakcyj6e","name":"Due Date","options":[],"type":"date"},{"id":"agqsoiipowmnu9rdwxm57zrehtr","name":"Publication Date","options":[],"type":"date"},{"id":"ap4e7kdg7eip7j3c3oyiz39eaoc","name":"Link","options":[],"type":"url"}],"description":"Use this template to plan and organize your editorial content.","icon":"📅","isTemplate":false,"showDescription":true},"createAt":1641618112737,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c3pxiqf156fnhjfazwwpo79rt6w","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"New Project and Workflow Management Solutions for Developers","fields":{"contentOrder":["71qhnzuec6esdi6fnynwpze4xya","aianjmrimwfyr7jiiju1oi77kiw"],"icon":"🎯","isTemplate":false,"properties":{"a39x5cybshwrbjpc3juaakcyj6e":"{\"from\":1645790400000}","ab6mbock6styfe6htf815ph1mhw":["a8xceonxiu4n3c43szhskqizicr","a3pdzi53kpbd4okzdkz6khi87zo","a3d9ux4fmi3anyd11kyipfbhwde"],"ae9ar615xoknd8hw8py7mbyr7zo":"awna1nuarjca99m9s4uiy9kwj5h","ap4e7kdg7eip7j3c3oyiz39eaoc":"https://mattermost.com/newsroom/press-releases/mattermost-launches-new-project-and-workflow-management-solutions-for-developers/","aysx3atqexotgwp5kx6h5i5ancw":"aywiofmmtd3ofgzj95ysky4pjga"}},"createAt":1641618113009,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cemyj9s9nwtgzieowpufrd1oo5h","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"[Tweet] Mattermost v6.1 includes card @-mention notifications in Boards","fields":{"contentOrder":["7i96m7nbsdsex8n6hzuzrmdfjuy","7ed5bwp3gr8yax3mhtuwiaa9gjy","a8egmu8gsqp8dzfk9pgpq5mm4ta","awyawmyjtj3nfffu4aphaqy9bgy","abdasiyq4k7ndtfrdadrias8sjy","71ppnm4bcmbrbpn73nefjkao17r"],"icon":"🐤","isTemplate":false,"properties":{"a39x5cybshwrbjpc3juaakcyj6e":"{\"from\":1639051200000}","ab6mbock6styfe6htf815ph1mhw":["az8o8pfe9hq6s7xaehoqyc3wpyc"],"ae9ar615xoknd8hw8py7mbyr7zo":"a9ana1e9w673o5cp8md4xjjwfto","agqsoiipowmnu9rdwxm57zrehtr":"{\"from\":1637668800000}","ap4e7kdg7eip7j3c3oyiz39eaoc":"https://twitter.com/Mattermost/status/1463145633162969097?s=20","aysx3atqexotgwp5kx6h5i5ancw":"aj8y675weso8kpb6eceqbpj4ruw"}},"createAt":1641618112896,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cp963ioyx63rz98q8gs19nxxm7w","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Top 10 Must-Have DevOps Tools in 2021","fields":{"contentOrder":["7fo1utqc8x1z1z6hzg33hes1ktc","ajm6ykd3633dbxdq6j76wtthbia"],"icon":"🛠️","isTemplate":false,"properties":{"a39x5cybshwrbjpc3juaakcyj6e":"{\"from\":1636113600000}","ab6mbock6styfe6htf815ph1mhw":["a8xceonxiu4n3c43szhskqizicr"],"ae9ar615xoknd8hw8py7mbyr7zo":"a9ana1e9w673o5cp8md4xjjwfto","agqsoiipowmnu9rdwxm57zrehtr":"{\"from\":1637323200000}","ap4e7kdg7eip7j3c3oyiz39eaoc":"https://www.toolbox.com/tech/devops/articles/best-devops-tools/","aysx3atqexotgwp5kx6h5i5ancw":"a3xky7ygn14osr1mokerbfah5cy"}},"createAt":1641618112796,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"crrwzx9z4dfbsiki6suzwj3mqfw","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Unblocking Workflows: The Guide to Developer Productivity","fields":{"contentOrder":["77tz16jtz5x73ncs3dxc3fp1d7h","asmp1ztc1gjyh3k8og8yyizu5jy"],"icon":"💻","isTemplate":false,"properties":{"a39x5cybshwrbjpc3juaakcyj6e":"{\"from\":1638532800000}","ab6mbock6styfe6htf815ph1mhw":["a3pdzi53kpbd4okzdkz6khi87zo"],"ae9ar615xoknd8hw8py7mbyr7zo":"apy9dcd7zmand615p3h53zjqxjh","agqsoiipowmnu9rdwxm57zrehtr":"{\"from\":1639483200000}","ap4e7kdg7eip7j3c3oyiz39eaoc":"https://mattermost.com/newsroom/press-releases/mattermost-unveils-definitive-report-on-the-state-of-developer-productivity-unblocking-workflows-the-guide-to-developer-productivity-2022-edition/","aysx3atqexotgwp5kx6h5i5ancw":"a3xky7ygn14osr1mokerbfah5cy"}},"createAt":1641618112846,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vaiuu5bg4ofdn8j4whttdgtus4w","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"By Status","fields":{"cardOrder":[null,"cdbfkd15d6iy18rgx1tskmfsr6c","cn8yofg9rtkgmzgmb5xdi56p3ic","csgsnnywpuqzs5jgq87snk9x17e","cqwaytore5y487wdu8zffppqnea",null],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"cff1jmrxfrirgbeebhr9qd7nida","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["awna1nuarjca99m9s4uiy9kwj5h","a9ana1e9w673o5cp8md4xjjwfto","apy9dcd7zmand615p3h53zjqxjh","acri4cm3bmay55f7ksztphmtnga","amsowcd9a8e1kid317r7ttw6uzh",""],"visiblePropertyIds":["ab6mbock6styfe6htf815ph1mhw"]},"createAt":1641618113176,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vgbzazskupjrq7gnrwqqk51adsh","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Due Date Calendar","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"a39x5cybshwrbjpc3juaakcyj6e","defaultTemplateId":"cff1jmrxfrirgbeebhr9qd7nida","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1641618113068,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vkk4dm1tnzb8fbmr5gxhibr63te","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Publication Calendar","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"agqsoiipowmnu9rdwxm57zrehtr","defaultTemplateId":"cff1jmrxfrirgbeebhr9qd7nida","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1641618113123,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vpsefkithi7gq3rfyignqxa9cze","parentId":"brs9cdimfw7fodyi7erqt747rhc","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Content List","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":322,"ab6mbock6styfe6htf815ph1mhw":229,"aysx3atqexotgwp5kx6h5i5ancw":208},"defaultTemplateId":"cff1jmrxfrirgbeebhr9qd7nida","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"a39x5cybshwrbjpc3juaakcyj6e","reversed":false}],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["ae9ar615xoknd8hw8py7mbyr7zo","aysx3atqexotgwp5kx6h5i5ancw","ab6mbock6styfe6htf815ph1mhw","ao44fz8nf6z6tuj1x31t9yyehcc","a39x5cybshwrbjpc3juaakcyj6e","agqsoiipowmnu9rdwxm57zrehtr","ap4e7kdg7eip7j3c3oyiz39eaoc"]},"createAt":1641618243042,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aianjmrimwfyr7jiiju1oi77kiw","parentId":"c3pxiqf156fnhjfazwwpo79rt6w","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618141074,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"71ppnm4bcmbrbpn73nefjkao17r","parentId":"cemyj9s9nwtgzieowpufrd1oo5h","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7y5kr8x8ybpnwdykjfuz57rggrh.png"},"createAt":1641618185785,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a8egmu8gsqp8dzfk9pgpq5mm4ta","parentId":"cemyj9s9nwtgzieowpufrd1oo5h","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618157625,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"awyawmyjtj3nfffu4aphaqy9bgy","parentId":"cemyj9s9nwtgzieowpufrd1oo5h","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Media","fields":{},"createAt":1641618160634,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a4uyug1msrtrkdfy5fwu8shf7so","parentId":"cff1jmrxfrirgbeebhr9qd7nida","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618338368,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"abztjcgndkffd3gybef6phr14so","parentId":"cff1jmrxfrirgbeebhr9qd7nida","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n- ...\n\n## Notes\n- ...\n- ...\n- ...","fields":{},"createAt":1641618112322,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"azczyg4pfj3ysjpxf4hjtu666ne","parentId":"cff1jmrxfrirgbeebhr9qd7nida","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618112527,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ajm6ykd3633dbxdq6j76wtthbia","parentId":"cp963ioyx63rz98q8gs19nxxm7w","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618208454,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"asmp1ztc1gjyh3k8og8yyizu5jy","parentId":"crrwzx9z4dfbsiki6suzwj3mqfw","rootId":"brs9cdimfw7fodyi7erqt747rhc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Research\n- ...\n- ...\n\n## Plan\n- ...\n- ...\n\n## Notes\n- ...\n- ...","fields":{},"createAt":1641618224780,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/bsjd59qtpbf888mqez3ge77domw/board.jsonl b/server/boards/assets/templates-boardarchive/bsjd59qtpbf888mqez3ge77domw/board.jsonl deleted file mode 100644 index 3d826fe7f9..0000000000 --- a/server/boards/assets/templates-boardarchive/bsjd59qtpbf888mqez3ge77domw/board.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"type":"board","data":{"id":"bsjd59qtpbf888mqez3ge77domw","teamId":"qghzt68dq7bopgqamcnziq69ao","channelId":"","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","type":"P","minimumRole":"","title":"Team Retrospective","description":"Use this template at the end of your project or sprint to identify what worked well and what can be improved for the future.","icon":"🧭","showDescription":true,"isTemplate":false,"templateVersion":0,"properties":{},"cardProperties":[{"id":"adjckpdotpgkz7c6wixzw9ipb1e","name":"Category","options":[{"color":"propColorGray","id":"aok6pgecm85qe9k5kcphzoe63ma","value":"To Discuss 📣"},{"color":"propColorGreen","id":"aq1dwbf661yx337hjcd5q3sbxwa","value":"Went Well 👍"},{"color":"propColorRed","id":"ar87yh5xmsswqkxmjq1ipfftfpc","value":"Didn't Go Well 🚫"},{"color":"propColorBlue","id":"akj3fkmxq7idma55mdt8sqpumyw","value":"Action Items ✅"}],"type":"select"},{"id":"aspaay76a5wrnuhtqgm97tt3rer","name":"Details","options":[],"type":"text"},{"id":"arzsm76s376y7suuhao3tu6efoc","name":"Created By","options":[],"type":"createdBy"},{"id":"a8anbe5fpa668sryatcdsuuyh8a","name":"Created Time","options":[],"type":"createdTime"}],"createAt":1667494395151,"updateAt":1667508014713,"deleteAt":0}} -{"type":"block","data":{"id":"v56n9ixhhbpn79m6eb4xwpo6dih","parentId":"bjbhs6bos3m8zjouf78xceg9nqw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Board view","fields":{"cardOrder":["cniwb8xwcqtbstbcm3sdfrr854h","cs4qwpzr65fgttd7364dicskanh","c9s78pzbdg3g4jkcdjqahtnfejc","c8utmazns878jtfgtf7exyi9pee","cnobejmb6bf8e3c1w7em5z4pwyh"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["aok6pgecm85qe9k5kcphzoe63ma","aq1dwbf661yx337hjcd5q3sbxwa","ar87yh5xmsswqkxmjq1ipfftfpc","akj3fkmxq7idma55mdt8sqpumyw"],"visiblePropertyIds":["aspaay76a5wrnuhtqgm97tt3rer"]},"createAt":1667494395162,"updateAt":1667508040536,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} -{"type":"block","data":{"id":"c8utmazns878jtfgtf7exyi9pee","parentId":"bsjd59qtpbf888mqez3ge77domw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Tight deadline","fields":{"contentOrder":[],"icon":"📅","isTemplate":false,"properties":{"adjckpdotpgkz7c6wixzw9ipb1e":"ar87yh5xmsswqkxmjq1ipfftfpc"}},"createAt":1667495008197,"updateAt":1667495012284,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} -{"type":"block","data":{"id":"c9s78pzbdg3g4jkcdjqahtnfejc","parentId":"bsjd59qtpbf888mqez3ge77domw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Team communication","fields":{"contentOrder":[],"icon":"💬","isTemplate":false,"properties":{"adjckpdotpgkz7c6wixzw9ipb1e":"aq1dwbf661yx337hjcd5q3sbxwa"}},"createAt":1667494992121,"updateAt":1667494995438,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} -{"type":"block","data":{"id":"cniwb8xwcqtbstbcm3sdfrr854h","parentId":"bsjd59qtpbf888mqez3ge77domw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Reschedule planning meeting","fields":{"contentOrder":[],"icon":"🗓️","isTemplate":false,"properties":{"adjckpdotpgkz7c6wixzw9ipb1e":"aok6pgecm85qe9k5kcphzoe63ma"}},"createAt":1667494810631,"updateAt":1667495048934,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} -{"type":"block","data":{"id":"cnobejmb6bf8e3c1w7em5z4pwyh","parentId":"bsjd59qtpbf888mqez3ge77domw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Schedule more time for testing","fields":{"contentOrder":[],"icon":"🧪","isTemplate":false,"properties":{"adjckpdotpgkz7c6wixzw9ipb1e":"akj3fkmxq7idma55mdt8sqpumyw"}},"createAt":1667495025505,"updateAt":1667495032672,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} -{"type":"block","data":{"id":"cs4qwpzr65fgttd7364dicskanh","parentId":"bsjd59qtpbf888mqez3ge77domw","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Positive user feedback","fields":{"contentOrder":[],"icon":"🥰","isTemplate":false,"properties":{"adjckpdotpgkz7c6wixzw9ipb1e":"aq1dwbf661yx337hjcd5q3sbxwa"}},"createAt":1667494972061,"updateAt":1667494978637,"deleteAt":0,"boardId":"bsjd59qtpbf888mqez3ge77domw"}} diff --git a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/77pe9r4ckbin438ph3f18bpatua.png b/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/77pe9r4ckbin438ph3f18bpatua.png deleted file mode 100644 index 1ef77a1f53..0000000000 Binary files a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/77pe9r4ckbin438ph3f18bpatua.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/7pbp4qg415pbstc6enzeicnu3qh.png b/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/7pbp4qg415pbstc6enzeicnu3qh.png deleted file mode 100644 index 7c0bc0bd36..0000000000 Binary files a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/7pbp4qg415pbstc6enzeicnu3qh.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/board.jsonl b/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/board.jsonl deleted file mode 100644 index 0197ba2371..0000000000 --- a/server/boards/assets/templates-boardarchive/bui5izho7dtn77xg3thkiqprc9r/board.jsonl +++ /dev/null @@ -1,30 +0,0 @@ -{"type":"block","data":{"id":"bui5izho7dtn77xg3thkiqprc9r","parentId":"","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Roadmap (NEW)","fields":{"cardProperties":[{"id":"50117d52-bcc7-4750-82aa-831a351c44a0","name":"Status","options":[{"color":"propColorGray","id":"8c557f69-b0ed-46ec-83a3-8efab9d47ef5","value":"Not Started"},{"color":"propColorYellow","id":"ec6d2bc5-df2b-4f77-8479-e59ceb039946","value":"In Progress"},{"color":"propColorGreen","id":"849766ba-56a5-48d1-886f-21672f415395","value":"Complete 🙌"}],"type":"select"},{"id":"20717ad3-5741-4416-83f1-6f133fff3d11","name":"Type","options":[{"color":"propColorYellow","id":"424ea5e3-9aa1-4075-8c5c-01b44b66e634","value":"Epic ⛰"},{"color":"propColorGreen","id":"6eea96c9-4c61-4968-8554-4b7537e8f748","value":"Task 🔨"},{"color":"propColorRed","id":"1fdbb515-edd2-4af5-80fc-437ed2211a49","value":"Bug 🐞"}],"type":"select"},{"id":"60985f46-3e41-486e-8213-2b987440ea1c","name":"Sprint","options":[{"color":"propColorBrown","id":"c01676ca-babf-4534-8be5-cce2287daa6c","value":"Sprint 1"},{"color":"propColorPurple","id":"ed4a5340-460d-461b-8838-2c56e8ee59fe","value":"Sprint 2"},{"color":"propColorBlue","id":"14892380-1a32-42dd-8034-a0cea32bc7e6","value":"Sprint 3"}],"type":"select"},{"id":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","name":"Priority","options":[{"color":"propColorRed","id":"cb8ecdac-38be-4d36-8712-c4d58cc8a8e9","value":"P1 🔥"},{"color":"propColorYellow","id":"e6a7f297-4440-4783-8ab3-3af5ba62ca11","value":"P2"},{"color":"propColorGray","id":"c62172ea-5da7-4dec-8186-37267d8ee9a7","value":"P3"}],"type":"select"},{"id":"aphg37f7zbpuc3bhwhp19s1ribh","name":"Assignee","options":[],"type":"person"},{"id":"a4378omyhmgj3bex13sj4wbpfiy","name":"Due Date","options":[],"type":"date"},{"id":"a36o9q1yik6nmar6ri4q4uca7ey","name":"Created Date","options":[],"type":"createdTime"},{"id":"ai7ajsdk14w7x5s8up3dwir77te","name":"Design Link","options":[],"type":"url"}],"description":"Use this template to plan your roadmap and manage your releases more efficiently.","icon":"🗺️","isTemplate":false,"showDescription":true},"createAt":1640363551156,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c3jawn6e4fbr3jctthy9xxkdsqe","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"App crashing","fields":{"contentOrder":["79t7rkiuspeneqi9xurou9tqzwh","a4d68ftemrbfsfykur6eh6nrogh","ae54fbyywubnbtr3s4yhgns4nye","7o9ktgofg37yc7gma9s3jd9bd3a"],"icon":"📉","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"1fdbb515-edd2-4af5-80fc-437ed2211a49","50117d52-bcc7-4750-82aa-831a351c44a0":"ec6d2bc5-df2b-4f77-8479-e59ceb039946","60985f46-3e41-486e-8213-2b987440ea1c":"c01676ca-babf-4534-8be5-cce2287daa6c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"cb8ecdac-38be-4d36-8712-c4d58cc8a8e9"}},"createAt":1641589357560,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c5trb4319wi8n3x4r4f7f83ytdc","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Calendar view","fields":{"contentOrder":["7df11783ny67mdnognqae31ax6y","ag9rxpgbwqid1mm5hgg8b9yhf6o"],"icon":"📆","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"6eea96c9-4c61-4968-8554-4b7537e8f748","50117d52-bcc7-4750-82aa-831a351c44a0":"849766ba-56a5-48d1-886f-21672f415395","60985f46-3e41-486e-8213-2b987440ea1c":"c01676ca-babf-4534-8be5-cce2287daa6c","ai7ajsdk14w7x5s8up3dwir77te":"https://mattermost.com/boards/","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1641590072588,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c9p4bdasriifc7qgihzhjm63ugy","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Standard templates","fields":{"contentOrder":["7uonmjk41nipnrsi6tz8wau5ssh","afz66z155b7fhik9p6opysjneha"],"icon":"🗺️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"6eea96c9-4c61-4968-8554-4b7537e8f748","50117d52-bcc7-4750-82aa-831a351c44a0":"ec6d2bc5-df2b-4f77-8479-e59ceb039946","60985f46-3e41-486e-8213-2b987440ea1c":"ed4a5340-460d-461b-8838-2c56e8ee59fe","ai7ajsdk14w7x5s8up3dwir77te":"https://mattermost.com/boards/","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1641589960934,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"chfrdo1nb3p8ofnbftyinr6949o","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Import / Export","fields":{"contentOrder":["aw66wjze7qfr1ukqs8gw53qa5qw"],"icon":"🚢","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"6eea96c9-4c61-4968-8554-4b7537e8f748","50117d52-bcc7-4750-82aa-831a351c44a0":"ec6d2bc5-df2b-4f77-8479-e59ceb039946","60985f46-3e41-486e-8213-2b987440ea1c":"c01676ca-babf-4534-8be5-cce2287daa6c","ai7ajsdk14w7x5s8up3dwir77te":"https://mattermost.com/boards/","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1640363550923,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cp1m1wrpfatdxikhwkf58oo5k3o","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Review API design","fields":{"contentOrder":["ahsamufik97nsfxjgx9cs6cmzme"],"icon":"🛣️","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"424ea5e3-9aa1-4075-8c5c-01b44b66e634","50117d52-bcc7-4750-82aa-831a351c44a0":"8c557f69-b0ed-46ec-83a3-8efab9d47ef5","60985f46-3e41-486e-8213-2b987440ea1c":"14892380-1a32-42dd-8034-a0cea32bc7e6","ai7ajsdk14w7x5s8up3dwir77te":"https://mattermost.com/boards/","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"c62172ea-5da7-4dec-8186-37267d8ee9a7"}},"createAt":1640363550754,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cqfy6g434pigk3p7j3gq55trq9o","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Icons don't display","fields":{"contentOrder":["axfkn6tuy4igubj3ka99tbymb8o","acbpep9wxdtyg8gg3fi6h1hgoro","7tedfdyq4p7g77dmkrebryh4jor"],"icon":"💻","isTemplate":false,"properties":{"20717ad3-5741-4416-83f1-6f133fff3d11":"1fdbb515-edd2-4af5-80fc-437ed2211a49","50117d52-bcc7-4750-82aa-831a351c44a0":"8c557f69-b0ed-46ec-83a3-8efab9d47ef5","60985f46-3e41-486e-8213-2b987440ea1c":"ed4a5340-460d-461b-8838-2c56e8ee59fe","ai7ajsdk14w7x5s8up3dwir77te":"https://mattermost.com/boards/","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e":"e6a7f297-4440-4783-8ab3-3af5ba62ca11"}},"createAt":1640363550868,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"v1uubwdzrw7fsxnd6pss1dyhh5e","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Calendar View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"a4378omyhmgj3bex13sj4wbpfiy","defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1640379248049,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"v7n4sc9cre7gsbq9yydsuekpg8a","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Board: Sprints","fields":{"cardOrder":["c3jawn6e4fbr3jctthy9xxkdsqe","c5trb4319wi8n3x4r4f7f83ytdc","c9p4bdasriifc7qgihzhjm63ugy","cqfy6g434pigk3p7j3gq55trq9o","chfrdo1nb3p8ofnbftyinr6949o","cp1m1wrpfatdxikhwkf58oo5k3o"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"60985f46-3e41-486e-8213-2b987440ea1c","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","reversed":false}],"viewType":"board","visibleOptionIds":["c01676ca-babf-4534-8be5-cce2287daa6c","ed4a5340-460d-461b-8838-2c56e8ee59fe","14892380-1a32-42dd-8034-a0cea32bc7e6",""],"visiblePropertyIds":["20717ad3-5741-4416-83f1-6f133fff3d11","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1640363550811,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"v8sa3mo81d38rbmd8bz4n6dg7qc","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"List: Tasks 🔨","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"50117d52-bcc7-4750-82aa-831a351c44a0":139,"__title":280},"defaultTemplateId":"","filter":{"filters":[{"condition":"includes","propertyId":"20717ad3-5741-4416-83f1-6f133fff3d11","values":["6eea96c9-4c61-4968-8554-4b7537e8f748"]}],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"50117d52-bcc7-4750-82aa-831a351c44a0","reversed":true}],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["50117d52-bcc7-4750-82aa-831a351c44a0","20717ad3-5741-4416-83f1-6f133fff3d11","60985f46-3e41-486e-8213-2b987440ea1c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1640363550980,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vi43bqxsho3fmjbu1oa8qafwo4c","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Board: Status","fields":{"cardOrder":["c3jawn6e4fbr3jctthy9xxkdsqe","cm4w7cc3aac6s9jdcujbs4j8f4r","c6egh6cpnj137ixdoitsoxq17oo","cct9u78utsdyotmejbmwwg66ihr","cmft87it1q7yebbd51ij9k65xbw","c9fe77j9qcruxf4itzib7ag6f1c","coup7afjknqnzbdwghiwbsq541w","c5ex1hndz8qyc8gx6ofbfeksftc"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"cidz4imnqhir48brz6e8hxhfrhy","filter":{"filters":[],"operation":"and"},"groupById":"50117d52-bcc7-4750-82aa-831a351c44a0","hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","reversed":false}],"viewType":"board","visibleOptionIds":["8c557f69-b0ed-46ec-83a3-8efab9d47ef5","ec6d2bc5-df2b-4f77-8479-e59ceb039946","849766ba-56a5-48d1-886f-21672f415395",""],"visiblePropertyIds":["20717ad3-5741-4416-83f1-6f133fff3d11","60985f46-3e41-486e-8213-2b987440ea1c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1640363551099,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vod5de87tz7nxpji31oou4ine3c","parentId":"bui5izho7dtn77xg3thkiqprc9r","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"List: Bugs 🐞","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"50117d52-bcc7-4750-82aa-831a351c44a0":145,"__title":280},"defaultTemplateId":"","filter":{"filters":[{"condition":"includes","propertyId":"20717ad3-5741-4416-83f1-6f133fff3d11","values":["1fdbb515-edd2-4af5-80fc-437ed2211a49"]}],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[{"propertyId":"f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e","reversed":false}],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["50117d52-bcc7-4750-82aa-831a351c44a0","20717ad3-5741-4416-83f1-6f133fff3d11","60985f46-3e41-486e-8213-2b987440ea1c","f7f3ad42-b31a-4ac2-81f0-28ea80c5b34e"]},"createAt":1640363550690,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7o9ktgofg37yc7gma9s3jd9bd3a","parentId":"c3jawn6e4fbr3jctthy9xxkdsqe","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"77pe9r4ckbin438ph3f18bpatua.png"},"createAt":1641589687567,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a4d68ftemrbfsfykur6eh6nrogh","parentId":"c3jawn6e4fbr3jctthy9xxkdsqe","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1641589386414,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ae54fbyywubnbtr3s4yhgns4nye","parentId":"c3jawn6e4fbr3jctthy9xxkdsqe","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1641589472988,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ag9rxpgbwqid1mm5hgg8b9yhf6o","parentId":"c5trb4319wi8n3x4r4f7f83ytdc","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1641590081840,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"afz66z155b7fhik9p6opysjneha","parentId":"c9p4bdasriifc7qgihzhjm63ugy","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1641589969935,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"73dpuy7r9qpfymrp67c9n3krrsc","parentId":"cfefgwjke6bbxpjpig618g9bpte","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7pbp4qg415pbstc6enzeicnu3qh.png"},"createAt":1640379104209,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aabek71yr1trxmjudty7efncp3r","parentId":"cfefgwjke6bbxpjpig618g9bpte","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\nIf applicable, add screenshots to elaborate on the problem.","fields":{},"createAt":1640379104369,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"asqzoizq31b81dpyzm1tnm8wyxc","parentId":"cfefgwjke6bbxpjpig618g9bpte","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n\nA clear and concise description of what you expected to happen.\n\n## Edition and Platform\n\n - Edition: Personal Desktop / Personal Server / Mattermost plugin\n - Version: [e.g. v0.9.0]\n - Browser and OS: [e.g. Chrome 91 on macOS, Edge 93 on Windows]\n\n## Additional context\n\nAdd any other context about the problem here.","fields":{},"createAt":1640379104459,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"auefo9xa6sffatbeqzya56bhebo","parentId":"cfefgwjke6bbxpjpig618g9bpte","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n\n*[A clear and concise description of what you expected to happen.]*\n\n## Screenshots\n\n*[If applicable, add screenshots to elaborate on the problem.]*\n\n## Edition and Platform\n\n - Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n - Version: *[e.g. v0.9.0]*\n - Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n\n*[Add any other context about the problem here.]*","fields":{},"createAt":1640379139361,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aw66wjze7qfr1ukqs8gw53qa5qw","parentId":"chfrdo1nb3p8ofnbftyinr6949o","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1640380216220,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"anppzrbx3i7b47n17b6jje6e1yc","parentId":"cidz4imnqhir48brz6e8hxhfrhy","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Description\n*[Brief description of this task]*\n\n## Requirements\n- *[Requirement 1]*\n- *[Requirement 2]*\n- ...","fields":{},"createAt":1640380239894,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"azyfnyszy6jb9iys9izfz1bhbdw","parentId":"cidz4imnqhir48brz6e8hxhfrhy","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Requirements\n- [Requirement 1]\n- [Requirement 2]\n- ...","fields":{},"createAt":1640380231316,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ahsamufik97nsfxjgx9cs6cmzme","parentId":"cp1m1wrpfatdxikhwkf58oo5k3o","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\n*[Brief description of what this epic is about]*\n\n## Motivation\n*[Brief description on why this is needed]*\n\n## Acceptance Criteria\n - *[Criteron 1]*\n - *[Criteron 2]*\n - ...\n\n## Personas\n - *[Persona A]*\n - *[Persona B]*\n - ...\n\n## Reference Materials\n - *[Links to other relevant documents as needed]*\n - ...","fields":{},"createAt":1640380010492,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7tedfdyq4p7g77dmkrebryh4jor","parentId":"cqfy6g434pigk3p7j3gq55trq9o","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7pbp4qg415pbstc6enzeicnu3qh.png"},"createAt":1640379056342,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"acbpep9wxdtyg8gg3fi6h1hgoro","parentId":"cqfy6g434pigk3p7j3gq55trq9o","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Screenshots\n*[If applicable, add screenshots to elaborate on the problem.]*","fields":{},"createAt":1640378826029,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"axfkn6tuy4igubj3ka99tbymb8o","parentId":"cqfy6g434pigk3p7j3gq55trq9o","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Steps to reproduce the behavior\n1. Go to ...\n2. Select ...\n3. Scroll down to ...\n4. See error\n\n## Expected behavior\n*[A clear and concise description of what you expected to happen.]*\n\n## Edition and Platform\n- Edition: *[e.g. Personal Desktop / Personal Server / Mattermost plugin]*\n- Version: *[e.g. v0.9.0]*\n- Browser and OS: *[e.g. Chrome 91 on macOS, Edge 93 on Windows]*\n\n## Additional context\n*[Add any other context about the problem here.]*","fields":{},"createAt":1640378803642,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a58i6xsb3abdhm87oezaum6ehhc","parentId":"cwrq9ag3p5pgzzy98nfd3wwra1w","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\n*[Brief description of what this epic is about]*\n## Motivation\n*[Brief description on why this is needed]*\n## Acceptance Criteria\n- *[Criteron 1]*\n- *[Criteron 2]*\n- ...\n## Personas\n- *[Persona A]*\n- *[Persona B]*\n- ...\n## Reference Materials\n- *[Links to other relevant documents as needed]*\n- ...","fields":{},"createAt":1640380125209,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a799597ibbjb17yxy1c3zjias1w","parentId":"cwrq9ag3p5pgzzy98nfd3wwra1w","rootId":"bui5izho7dtn77xg3thkiqprc9r","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"## Summary\n[Brief description of what this epic is about]\n\n## Motivation\n[Brief description on why this is needed]\n\n## Acceptance Criteria\n - [Criteron 1]\n - [Criteron 2]\n - ...\n\n## Personas\n - [Persona A]\n - [Persona B]\n - ...\n\n## Reference Materials\n - [Links to other relevant documents as needed]\n - ...","fields":{},"createAt":1640380118322,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74nt9eqzea3ydjjpgjtsxcjgrxc.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74nt9eqzea3ydjjpgjtsxcjgrxc.gif deleted file mode 100644 index 4999604861..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74nt9eqzea3ydjjpgjtsxcjgrxc.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74uia99m9btr8peydw7oexn37tw.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74uia99m9btr8peydw7oexn37tw.gif deleted file mode 100644 index 003c84166c..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/74uia99m9btr8peydw7oexn37tw.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/78jws5m1myf8pufewzkaa6i11sc.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/78jws5m1myf8pufewzkaa6i11sc.gif deleted file mode 100644 index 7cdd8a0eb3..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/78jws5m1myf8pufewzkaa6i11sc.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7d6hrtig3zt8f9cnbo1um5oxx3y.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7d6hrtig3zt8f9cnbo1um5oxx3y.gif deleted file mode 100644 index ec25b5f21e..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7d6hrtig3zt8f9cnbo1um5oxx3y.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7dybb6t8fj3nrdft7nerhuf784y.png b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7dybb6t8fj3nrdft7nerhuf784y.png deleted file mode 100644 index 6ec86c8c3a..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7dybb6t8fj3nrdft7nerhuf784y.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7ek6wbpp19jfoujs1goh6kttbby.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7ek6wbpp19jfoujs1goh6kttbby.gif deleted file mode 100644 index 8c0a21cadb..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7ek6wbpp19jfoujs1goh6kttbby.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7iw4rxx7jj7bypmdotd9z469cyh.png b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7iw4rxx7jj7bypmdotd9z469cyh.png deleted file mode 100644 index fc13b461d8..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7iw4rxx7jj7bypmdotd9z469cyh.png and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7knxbyuiedtdafcgmropgkrtybr.gif b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7knxbyuiedtdafcgmropgkrtybr.gif deleted file mode 100644 index 7f08d2af37..0000000000 Binary files a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/7knxbyuiedtdafcgmropgkrtybr.gif and /dev/null differ diff --git a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/board.jsonl b/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/board.jsonl deleted file mode 100644 index 54e98ab6d4..0000000000 --- a/server/boards/assets/templates-boardarchive/buixxjic3xjfkieees4iafdrznc/board.jsonl +++ /dev/null @@ -1,47 +0,0 @@ -{"type":"block","data":{"id":"buixxjic3xjfkieees4iafdrznc","parentId":"","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"board","title":"Welcome to Boards!","fields":{"cardProperties":[{"id":"a972dc7a-5f4c-45d2-8044-8c28c69717f1","name":"Status","options":[{"color":"propColorRed","id":"amm6wfhnbuxojwssyftgs9dipqe","value":"To do 🔥"},{"color":"propColorYellow","id":"af3p8ztcyxgn8wd9z4az7o9tjeh","value":"Next up"},{"color":"propColorPurple","id":"ajurey3xkocs1nwx8di5zx6oe7o","value":"Later"},{"color":"propColorGreen","id":"agkinkjy5983wsk6kppsujajxqw","value":"Completed 🙌"}],"type":"select"},{"id":"acypkejeb5yfujhj9te57p9kaxw","name":"Priority","options":[{"color":"propColorOrange","id":"aanaehcw3m13jytujsjk5hpf6ry","value":"1. High"},{"color":"propColorBrown","id":"ascd7nm9r491ayot8i86g1gmgqw","value":"2. Medium"},{"color":"propColorGray","id":"aq6ukoiciyfctgwyhwzpfss8ghe","value":"3. Low"}],"type":"select"},{"id":"aqh13jabwexjkzr3jqsz1i1syew","name":"Assignee","options":[],"type":"person"},{"id":"acmg7mz1rr1eykfug4hcdpb1y1o","name":"Due Date","options":[],"type":"date"},{"id":"amewjwfjrtpu8ha73xsrdmxazxr","name":"Reviewed","options":[],"type":"checkbox"},{"id":"attzzboqaz6m1sdti5xa7gjnk1e","name":"Last updated time","options":[],"type":"updatedTime"},{"id":"a4nfnb5xr3txr5xq7y9ho7kyz6c","name":"Reference","options":[],"type":"url"},{"id":"a9gzwi3dt5n55nddej6zcbhxaeh","name":"Created by","options":[],"type":"createdBy"}],"description":"Mattermost Boards is an open source project management tool that helps you organize, track, and manage work across teams. Select a card to learn more.","icon":"👋","isTemplate":false,"showDescription":true},"createAt":1640034759040,"updateAt":1643788318628,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c5ay4q3t1hf8cdcschejip7ybpc","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Drag cards","fields":{"contentOrder":["apktbgtee5jb8xrnqy3ibiujxew","aefratgmk6j8nzj5fngfrf4k8hw"],"icon":"🤏","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/working-with-boards.html#dragging-cards","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ajurey3xkocs1nwx8di5zx6oe7o","acypkejeb5yfujhj9te57p9kaxw":"aq6ukoiciyfctgwyhwzpfss8ghe"}},"createAt":1640034759400,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"c9h4wpgh1ajyzfdqoyotohtj6oy","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Manage tasks with cards","fields":{"contentOrder":["a778mcixrm7byzb4mxrixjtrwwa","7mgy47rzyxpdm5c5eod9x5nypea","7tuw1my7b7fnxd8sfyzpz6dd1sc","784uu3ufcgb878ky7hyugmf6xcw","77msur4yswfn65d8qycdyfpfawe","7dh4oncxngj8jb8n59sefmsynac","7nkegq1zimifpmxcrq8ntyothoe","7nb8y7jyoetro8cd36qcju53z8c","7exhjmek1ctbexxt95w5cy1cuwo","7peuyuzgkc3fmzczfjuzseg9ksa","76nwb9tqfsid5jx46yw34itqima","7dy3mcgzgybf1ifa3emgewkzj7e","a5ca6tii33bfw8ba36y1rswq3he","7876od6xhffr6fy69zeogag7eyw","7x7bq9awkatbm5x4docbh5gaw4y","7ghpx9qff43dgtke1rwidmge1ho","7nb8y7jyoetro8cd36qcju53z8c","7hdyxemhbytfm3m83g88djq9nhr","7pgnejxokubbe9kdrxj6g9qa41e","7hw9z6qtx8jyizkmm9g5yq3gxcy","7gk6ooz6npbb8by5rgp9aig7tua","7ayruwskq4b8rte64fiwz493kjo"],"icon":"☑️","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/work-with-cards.html","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"amm6wfhnbuxojwssyftgs9dipqe","acypkejeb5yfujhj9te57p9kaxw":"aanaehcw3m13jytujsjk5hpf6ry"}},"createAt":1640034759460,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cfkikng8egbr878ryaztmpkno4w","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Create your own board","fields":{"contentOrder":["apedf7fbrspgt3cg8e5worq1gqa","as7511u6t1pdc7fe7zrbzdfg51y","7r9my1yuddbn45dojrfht3neg8c","7eir5gdjxgjbsxpbyp3df4npcze","7cux9rwr1b3rjmxakbipeoxky6h"],"icon":"📋","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/working-with-boards.html#adding-new-boards","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"amm6wfhnbuxojwssyftgs9dipqe","acypkejeb5yfujhj9te57p9kaxw":"aanaehcw3m13jytujsjk5hpf6ry"}},"createAt":1640034759557,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cm8yz355wbtfd7rtpgs655wbr4e","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Share a board","fields":{"contentOrder":["a5z5po5apabfibkgmkq53dxe9dw","ag791jfbc47gobroeo9ie1afcdo","7r7asyew8d7fyunf4sow8e5iyoc","ad8j3n8tp77bppee3ipjt6odgpe","7w935usqt6pby8qz9x5pxaj7iow","7ogbs8h6q4j8z7ngy1m7eag63nw","7z1jau5qy3jfcxdp5cgq3duk6ne","7hkn59merfbf38gzxf7sabewuma"],"icon":"📤","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/sharing-boards.html","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ajurey3xkocs1nwx8di5zx6oe7o","acypkejeb5yfujhj9te57p9kaxw":"aq6ukoiciyfctgwyhwzpfss8ghe"}},"createAt":1640034759139,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cmjtfip8a738nbr33shzmgk559o","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Create a new card","fields":{"contentOrder":["aykjshfjrxpd9zngqruenqn5s7h","adhsx4h5ss7rqdcjt8xyam6xtqc","auow16g4f4tf4z89qrxbg3btxba","7me9p46gbqiyfmfnapi7dyxb5br","76bqrrm8dobr37kttya6jhznjih"],"icon":"📝","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/working-with-boards.html#adding-cards","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"amm6wfhnbuxojwssyftgs9dipqe","acypkejeb5yfujhj9te57p9kaxw":"aanaehcw3m13jytujsjk5hpf6ry"}},"createAt":1640034759755,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cq1gmiwhx4jgd7q9ad9c1icasqr","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Share cards on Channels","fields":{"contentOrder":["arpj7spx9op8jumm6yfdsxwpeuw","a4z7htb6catgaue5npinux4tmrc","a3qa1n69wc7d1u8krumz9ogcidy","7y5hxcb9zzprzzrqeu675rtnpae"],"icon":"📮","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/work-with-cards.html#share-card-previews","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"af3p8ztcyxgn8wd9z4az7o9tjeh","acypkejeb5yfujhj9te57p9kaxw":"ascd7nm9r491ayot8i86g1gmgqw"}},"createAt":1641487149480,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cse6a9d81tfyd7e34cbmfttbgte","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Filter and sort cards","fields":{"contentOrder":["a4fz9kcfs9ibj8puk9mux7ac94c","ad9fecctco7ggjjeo9usfpwkfpa","78i8aqjmqtibr7x4okhz6uqquqr","7oz9pp3bkopgfpycph3oqgze8uw"],"icon":"🎛️","isTemplate":false,"properties":{"a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ajurey3xkocs1nwx8di5zx6oe7o","acypkejeb5yfujhj9te57p9kaxw":"aq6ukoiciyfctgwyhwzpfss8ghe"}},"createAt":1640034759298,"updateAt":1643788318631,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cstm8jadnmbds9kooz4tr8sy5wr","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Create a new view","fields":{"contentOrder":["aozbezukpgif3jpbsq7tahmmp5e","a538fji6kcp89fyhmgaoko7wk6c","7owai1ux3h3gtf8byynfk6hyx1c","7n8jq1dizyfgotby3o91arf1hxh","77y4wffj1ctg7xmm9bx45qn6q6o","7te5jcsrym3by7yxq4um8usj7ao"],"icon":"👓","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/working-with-boards.html#adding-new-views","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"af3p8ztcyxgn8wd9z4az7o9tjeh","acypkejeb5yfujhj9te57p9kaxw":"ascd7nm9r491ayot8i86g1gmgqw"}},"createAt":1640034759508,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cut8jasi4etbd7mpqpn36fna9ba","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"Add new properties","fields":{"contentOrder":["afb3pntrwgtrwidfeo1f1dsonqy","ayhk11qsuz789fk8bqae4oz8mro","7gc3z8cf8rirgfyutwoke9nn6jy","76cinqnb6k3dzmfbm9fnc8eofny","79yggmhcyhbgdiej5w4re3k4ssy"],"icon":"🏷️","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/work-with-cards.html#add-and-manage-properties","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"af3p8ztcyxgn8wd9z4az7o9tjeh","acypkejeb5yfujhj9te57p9kaxw":"ascd7nm9r491ayot8i86g1gmgqw"}},"createAt":1640034759239,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"cwf9xw6d6zbgfbbgd6atr336uco","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"card","title":"@mention teammates","fields":{"contentOrder":["akbz4abecginpjgz1etweynd7io","ab6ygnyg757bc9cpm9bkp8aam8e","7mbw9t71hjbrydgzgkqqaoh8usr","78mzk7qpof7ybxpfjn9j9an9gkh"],"icon":"🔔","isTemplate":false,"properties":{"a4nfnb5xr3txr5xq7y9ho7kyz6c":"https://docs.mattermost.com/boards/work-with-cards.html#mention-people","a972dc7a-5f4c-45d2-8044-8c28c69717f1":"ajurey3xkocs1nwx8di5zx6oe7o","acypkejeb5yfujhj9te57p9kaxw":"aq6ukoiciyfctgwyhwzpfss8ghe"}},"createAt":1640034759348,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vkm63rwrg5p8q5mrrk6ghzk3q1r","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Preview: Table View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{"__title":280,"a972dc7a-5f4c-45d2-8044-8c28c69717f1":100,"acypkejeb5yfujhj9te57p9kaxw":169},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"table","visibleOptionIds":[],"visiblePropertyIds":["a972dc7a-5f4c-45d2-8044-8c28c69717f1","aqh13jabwexjkzr3jqsz1i1syew","acmg7mz1rr1eykfug4hcdpb1y1o","acypkejeb5yfujhj9te57p9kaxw"]},"createAt":1640034759909,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vmu9ebyngpjb1mkc887m3pfocfa","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Preview: Calendar View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"dateDisplayPropertyId":"acmg7mz1rr1eykfug4hcdpb1y1o","defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"calendar","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1641796830370,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vssisypn6sfbctxy8si7oj3y9io","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Preview: Gallery View","fields":{"cardOrder":[],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"hiddenOptionIds":[],"kanbanCalculations":{},"sortOptions":[],"viewType":"gallery","visibleOptionIds":[],"visiblePropertyIds":["__title"]},"createAt":1641791749185,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"vzou1dc41ntn73mkyymka9yrese","parentId":"buixxjic3xjfkieees4iafdrznc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"view","title":"Onboarding","fields":{"cardOrder":["cmjtfip8a738nbr33shzmgk559o","c9h4wpgh1ajyzfdqoyotohtj6oy","cfkikng8egbr878ryaztmpkno4w","cq1gmiwhx4jgd7q9ad9c1icasqr","cut8jasi4etbd7mpqpn36fna9ba","cstm8jadnmbds9kooz4tr8sy5wr","cwf9xw6d6zbgfbbgd6atr336uco","c5ay4q3t1hf8cdcschejip7ybpc","cm8yz355wbtfd7rtpgs655wbr4e","cse6a9d81tfyd7e34cbmfttbgte"],"collapsedOptionIds":[],"columnCalculations":{},"columnWidths":{},"defaultTemplateId":"","filter":{"filters":[],"operation":"and"},"groupById":"a972dc7a-5f4c-45d2-8044-8c28c69717f1","hiddenOptionIds":[""],"kanbanCalculations":{},"sortOptions":[],"viewType":"board","visibleOptionIds":["aqb5x3pt87dcc9stbk4ofodrpoy","a1mtm777bkagq3iuu7xo9b13qfr","auxbwzptiqzkii5r61uz3ndsy1r","aj9386k1bx8qwmepeuxg3b7z4pw"],"visiblePropertyIds":[]},"createAt":1640034759964,"updateAt":1643788318632,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aefratgmk6j8nzj5fngfrf4k8hw","parentId":"c5ay4q3t1hf8cdcschejip7ybpc","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Mattermost Boards makes it easy for you to update certain properties on cards through our drag and drop functionality. Simply drag this card from the **Later** column to the **Completed** column to automatically update the status and mark this task as complete.","fields":{},"createAt":1640035135582,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"77msur4yswfn65d8qycdyfpfawe","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Assign tasks to teammates","fields":{"value":false},"createAt":1641787609114,"updateAt":1643788318633,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7ayruwskq4b8rte64fiwz493kjo","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Create and manage checklists, like this one... :)","fields":{"value":false},"createAt":1641788123369,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7dh4oncxngj8jb8n59sefmsynac","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Add and update descriptions with Markdown","fields":{"value":false},"createAt":1641793564533,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7exhjmek1ctbexxt95w5cy1cuwo","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Follow cards to get notified on the latest updates","fields":{"value":false},"createAt":1641787466530,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7mgy47rzyxpdm5c5eod9x5nypea","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Set priorities and update statuses","fields":{"value":false},"createAt":1641787424191,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7nkegq1zimifpmxcrq8ntyothoe","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Provide feedback and ask questions via comments","fields":{"value":false},"createAt":1642012664514,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7peuyuzgkc3fmzczfjuzseg9ksa","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"@mention teammates so they can follow, and collaborate on, comments and descriptions","fields":{"value":false},"createAt":1641787459538,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7tuw1my7b7fnxd8sfyzpz6dd1sc","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"checkbox","title":"Manage deadlines and milestones","fields":{"value":false},"createAt":1641787583043,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a778mcixrm7byzb4mxrixjtrwwa","parentId":"c9h4wpgh1ajyzfdqoyotohtj6oy","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Cards allow your entire team to manage and collaborate on a task in one place. Within a card, your team can:","fields":{},"createAt":1641786489535,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7cux9rwr1b3rjmxakbipeoxky6h","parentId":"cfkikng8egbr878ryaztmpkno4w","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"74uia99m9btr8peydw7oexn37tw.gif"},"createAt":1643638662994,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"apedf7fbrspgt3cg8e5worq1gqa","parentId":"cfkikng8egbr878ryaztmpkno4w","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"A board helps you manage your project, organize tasks, and collaborate with your team all in one place.","fields":{},"createAt":1641797295263,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"as7511u6t1pdc7fe7zrbzdfg51y","parentId":"cfkikng8egbr878ryaztmpkno4w","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"To create your own board, select the \"+\" on the top of the left hand sidebar. Choose from one of our standard templates and see how they can help you get started with your next project:\n\n- **Project Tasks**: Stay on top of your project tasks, track progress, and set priorities. \n- **Meeting Agenda**: Set your meeting agendas for recurring team meetings and 1:1s.\n- **Roadmap**: Plan your roadmap and manage your releases more efficiently.\n- **Personal Tasks**: Organize your life and track your personal tasks.\n- **Content Calendar**: Plan your editorial content, assign work, and track deadlines.\n- **Personal Goals**: Set and accomplish new personal goals and milestones.","fields":{},"createAt":1641788827589,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7hkn59merfbf38gzxf7sabewuma","parentId":"cm8yz355wbtfd7rtpgs655wbr4e","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7knxbyuiedtdafcgmropgkrtybr.gif"},"createAt":1643638846658,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a5z5po5apabfibkgmkq53dxe9dw","parentId":"cm8yz355wbtfd7rtpgs655wbr4e","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Keep stakeholders and customers up-to-date on project progress by sharing your board.","fields":{},"createAt":1642198459390,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ag791jfbc47gobroeo9ie1afcdo","parentId":"cm8yz355wbtfd7rtpgs655wbr4e","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"To share a board, select **Share** at the top right of the Board view. Copy the link to share the board internally with your team or generate public link that can be accessed by anyone externally.","fields":{},"createAt":1642199464341,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"76bqrrm8dobr37kttya6jhznjih","parentId":"cmjtfip8a738nbr33shzmgk559o","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7iw4rxx7jj7bypmdotd9z469cyh.png"},"createAt":1643143198631,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"auow16g4f4tf4z89qrxbg3btxba","parentId":"cmjtfip8a738nbr33shzmgk559o","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"To create a new card, simply do any of the following:\n- Select \"**New**\" on the top right header\n- Select \"**+ New**\" below any column\n- Select \"**+**\" to the right of any columnn header","fields":{},"createAt":1640034820888,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"aykjshfjrxpd9zngqruenqn5s7h","parentId":"cmjtfip8a738nbr33shzmgk559o","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Mattermost Boards helps you manage and track all your project tasks with **Cards**.","fields":{},"createAt":1641504858080,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7y5hxcb9zzprzzrqeu675rtnpae","parentId":"cq1gmiwhx4jgd7q9ad9c1icasqr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7ek6wbpp19jfoujs1goh6kttbby.gif"},"createAt":1643638818402,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a3qa1n69wc7d1u8krumz9ogcidy","parentId":"cq1gmiwhx4jgd7q9ad9c1icasqr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"After you've copied the link, paste it into any channel or Direct Message to share the card. A preview of the card will display within the channel with a link back to the card on Boards.","fields":{},"createAt":1642195798726,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a4z7htb6catgaue5npinux4tmrc","parentId":"cq1gmiwhx4jgd7q9ad9c1icasqr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"To share a card, you'll need to copy the card link first. You can:\n\n- Open a card and select the options menu button at the top right of the card.\n- Open the board view and hover your mouse over any card to access the options menu button.","fields":{},"createAt":1642193170054,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"arpj7spx9op8jumm6yfdsxwpeuw","parentId":"cq1gmiwhx4jgd7q9ad9c1icasqr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Cards can be linked and shared with teammates directly on Channels. Card previews are displayed when shared on Channels, so your team can discuss work items and get the relevant context without having to switch over to Boards.","fields":{},"createAt":1642193162587,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7oz9pp3bkopgfpycph3oqgze8uw","parentId":"cse6a9d81tfyd7e34cbmfttbgte","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7dybb6t8fj3nrdft7nerhuf784y.png"},"createAt":1643142381491,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ad9fecctco7ggjjeo9usfpwkfpa","parentId":"cse6a9d81tfyd7e34cbmfttbgte","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Organize and find the cards you're looking for with our filter, sort, and grouping options. From the Board header, you can quickly toggle on different properties, change the group display, set filters, and change how the cards are sorted.","fields":{},"createAt":1640034870185,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"7te5jcsrym3by7yxq4um8usj7ao","parentId":"cstm8jadnmbds9kooz4tr8sy5wr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"78jws5m1myf8pufewzkaa6i11sc.gif"},"createAt":1643638721400,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"a538fji6kcp89fyhmgaoko7wk6c","parentId":"cstm8jadnmbds9kooz4tr8sy5wr","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Views allow your team to visualize the same cards and data from different perspectives, so they can stay up-to-date in the way that works best for them. To add a new view, go to **Add a new view** from the view drop-down, then select from any of the following views:\n\n- **Board**: Adds a Kanban board, similar to this one, that allows your team to organize cards in swimlanes grouped by any property of your choosing. This view helps you visualize your project progress.\n- **Table**: Displays cards in a table format with rows and columns. Use this view to get an overview of all your project tasks. Easily view and compare the state of all properties across all cards without needing to open individual cards.\n- **Gallery**: Displays cards in a gallery format, so you can manage and organize cards with image attachments.\n- **Calendar**: Adds a calendar view to easily visualize your cards by dates and keep track of deadlines.","fields":{},"createAt":1640035911505,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"79yggmhcyhbgdiej5w4re3k4ssy","parentId":"cut8jasi4etbd7mpqpn36fna9ba","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"7d6hrtig3zt8f9cnbo1um5oxx3y.gif"},"createAt":1643638613909,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"afb3pntrwgtrwidfeo1f1dsonqy","parentId":"cut8jasi4etbd7mpqpn36fna9ba","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Customize cards to fit your needs and track the information most important to you. Boards supports a wide range of fully customizable property types. For example, you can:\n- Use the **Date** property for things like deadlines or milestones.\n- Assign owners to tasks with the **Person** property.\n- Define statuses and priorities with the **Select** property.\n- Create tags with the **Multi Select** property.\n- Link cards to webpages with the **URL** property.","fields":{},"createAt":1641611832288,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"78mzk7qpof7ybxpfjn9j9an9gkh","parentId":"cwf9xw6d6zbgfbbgd6atr336uco","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"image","title":"","fields":{"fileId":"74nt9eqzea3ydjjpgjtsxcjgrxc.gif"},"createAt":1643638766210,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"ab6ygnyg757bc9cpm9bkp8aam8e","parentId":"cwf9xw6d6zbgfbbgd6atr336uco","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"To mention a teammate use the **@ symbol with their username** in the comments or description section. They'll get a Direct Message notification via Channels and also be added as a [follower](https://docs.mattermost.com/boards/work-with-cards.html#receive-updates) to the card. \n\nWhenever any changes are made to the card, they'll automatically get notified on Channels.","fields":{},"createAt":1642177456022,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} -{"type":"block","data":{"id":"akbz4abecginpjgz1etweynd7io","parentId":"cwf9xw6d6zbgfbbgd6atr336uco","rootId":"buixxjic3xjfkieees4iafdrznc","createdBy":"edrkkih4cinzf8ueeszh6rmfoo","modifiedBy":"edrkkih4cinzf8ueeszh6rmfoo","schema":1,"type":"text","title":"Collaborate with teammates directly on each card using @mentions and have all the relevant context in one place.","fields":{},"createAt":1642177002644,"updateAt":1643788318634,"deleteAt":0,"workspaceId":"855b3j34ojn5p8f36yhu8336fe"}} diff --git a/server/boards/assets/templates-boardarchive/version.json b/server/boards/assets/templates-boardarchive/version.json deleted file mode 100644 index b33ba34f87..0000000000 --- a/server/boards/assets/templates-boardarchive/version.json +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"date":1643788318636} diff --git a/server/boards/assets/templates.boardarchive b/server/boards/assets/templates.boardarchive deleted file mode 100644 index a88819fd0a..0000000000 Binary files a/server/boards/assets/templates.boardarchive and /dev/null differ diff --git a/server/boards/auth/auth.go b/server/boards/auth/auth.go deleted file mode 100644 index c929e2614b..0000000000 --- a/server/boards/auth/auth.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockauth_interface.go -package mocks . AuthInterface -package auth - -import ( - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -type AuthInterface interface { - GetSession(token string) (*model.Session, error) - IsValidReadToken(boardID string, readToken string) (bool, error) - DoesUserHaveTeamAccess(userID string, teamID string) bool -} - -// Auth authenticates sessions. -type Auth struct { - config *config.Configuration - store store.Store - permissions permissions.PermissionsService -} - -// New returns a new Auth. -func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth { - return &Auth{config: config, store: store, permissions: permissions} -} - -// GetSession Get a user active session and refresh the session if needed. -func (a *Auth) GetSession(token string) (*model.Session, error) { - if len(token) < 1 { - return nil, errors.New("no session token") - } - - session, err := a.store.GetSession(token, a.config.SessionExpireTime) - if err != nil { - return nil, errors.Wrap(err, "unable to get the session for the token") - } - if session.UpdateAt < (utils.GetMillis() - utils.SecondsToMillis(a.config.SessionRefreshTime)) { - _ = a.store.RefreshSession(session) - } - return session, nil -} - -// IsValidReadToken validates the read token for a board. -func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) { - sharing, err := a.store.GetSharing(boardID) - if model.IsErrNotFound(err) { - return false, nil - } - if err != nil { - return false, err - } - - if !a.config.EnablePublicSharedBoards { - return false, errors.New("public shared boards disabled") - } - - if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) { - return true, nil - } - - return false, nil -} - -func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool { - return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) -} diff --git a/server/boards/auth/auth_test.go b/server/boards/auth/auth_test.go deleted file mode 100644 index d5393ccbf2..0000000000 --- a/server/boards/auth/auth_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import ( - "testing" - - "github.com/golang/mock/gomock" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/localpermissions" - mockpermissions "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mocks" - "github.com/mattermost/mattermost/server/v8/boards/services/store/mockstore" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type TestHelper struct { - Auth *Auth - Session model.Session - Store *mockstore.MockStore -} - -var mockSession = &model.Session{ - ID: utils.NewID(utils.IDTypeSession), - Token: "goodToken", - UserID: "12345", - CreateAt: utils.GetMillis() - utils.SecondsToMillis(2000), - UpdateAt: utils.GetMillis() - utils.SecondsToMillis(2000), -} - -func setupTestHelper(t *testing.T) *TestHelper { - ctrl := gomock.NewController(t) - ctrlPermissions := gomock.NewController(t) - cfg := config.Configuration{} - mockStore := mockstore.NewMockStore(ctrl) - mockPermissions := mockpermissions.NewMockStore(ctrlPermissions) - logger, err := mlog.NewLogger() - require.NoError(t, err) - newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger)) - - // called during default template setup for every test - mockStore.EXPECT().GetTemplateBoards("0", "").AnyTimes() - mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes() - mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes() - - return &TestHelper{ - Auth: newAuth, - Session: *mockSession, - Store: mockStore, - } -} - -func TestGetSession(t *testing.T) { - th := setupTestHelper(t) - - testcases := []struct { - title string - token string - refreshTime int64 - isError bool - }{ - {"fail, no token", "", 0, true}, - {"fail, invalid username", "badToken", 0, true}, - {"success, good token", "goodToken", 1000, false}, - } - - th.Store.EXPECT().GetSession("badToken", gomock.Any()).Return(nil, errors.New("Invalid Token")) - th.Store.EXPECT().GetSession("goodToken", gomock.Any()).Return(mockSession, nil) - th.Store.EXPECT().RefreshSession(gomock.Any()).Return(nil) - - for _, test := range testcases { - t.Run(test.title, func(t *testing.T) { - if test.refreshTime > 0 { - th.Auth.config.SessionRefreshTime = test.refreshTime - } - - session, err := th.Auth.GetSession(test.token) - if test.isError { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotNil(t, session) - } - }) - } -} - -func TestIsValidReadToken(t *testing.T) { - // ToDo: reimplement - - // th := setupTestHelper(t) - - // validBlockID := "testBlockID" - // mockContainer := store.Container{ - // TeamID: "testTeamID", - // } - // validReadToken := "testReadToken" - // mockSharing := model.Sharing{ - // ID: "testRootID", - // Enabled: true, - // Token: validReadToken, - // } - - // testcases := []struct { - // title string - // container store.Container - // blockID string - // readToken string - // isError bool - // isSuccess bool - // }{ - // {"fail, error GetRootID", mockContainer, "badBlock", "", true, false}, - // {"fail, rootID not found", mockContainer, "goodBlockID", "", false, false}, - // {"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false}, - // {"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false}, - // {"success", mockContainer, validBlockID, validReadToken, false, true}, - // } - - // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block")) - // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil) - // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil) - // th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2) - // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows) - // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error")) - // th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2) - - // for _, test := range testcases { - // t.Run(test.title, func(t *testing.T) { - // success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken) - // if test.isError { - // require.Error(t, err) - // } else { - // require.NoError(t, err) - // } - // if test.isSuccess { - // require.True(t, success) - // } else { - // require.False(t, success) - // } - // }) - // } -} diff --git a/server/boards/auth/mocks/mockauth_interface.go b/server/boards/auth/mocks/mockauth_interface.go deleted file mode 100644 index c9370f1fa2..0000000000 --- a/server/boards/auth/mocks/mockauth_interface.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/auth (interfaces: AuthInterface) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// MockAuthInterface is a mock of AuthInterface interface. -type MockAuthInterface struct { - ctrl *gomock.Controller - recorder *MockAuthInterfaceMockRecorder -} - -// MockAuthInterfaceMockRecorder is the mock recorder for MockAuthInterface. -type MockAuthInterfaceMockRecorder struct { - mock *MockAuthInterface -} - -// NewMockAuthInterface creates a new mock instance. -func NewMockAuthInterface(ctrl *gomock.Controller) *MockAuthInterface { - mock := &MockAuthInterface{ctrl: ctrl} - mock.recorder = &MockAuthInterfaceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAuthInterface) EXPECT() *MockAuthInterfaceMockRecorder { - return m.recorder -} - -// DoesUserHaveTeamAccess mocks base method. -func (m *MockAuthInterface) DoesUserHaveTeamAccess(arg0, arg1 string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DoesUserHaveTeamAccess", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// DoesUserHaveTeamAccess indicates an expected call of DoesUserHaveTeamAccess. -func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveTeamAccess(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveTeamAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveTeamAccess), arg0, arg1) -} - -// GetSession mocks base method. -func (m *MockAuthInterface) GetSession(arg0 string) (*model.Session, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSession", arg0) - ret0, _ := ret[0].(*model.Session) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSession indicates an expected call of GetSession. -func (mr *MockAuthInterfaceMockRecorder) GetSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockAuthInterface)(nil).GetSession), arg0) -} - -// IsValidReadToken mocks base method. -func (m *MockAuthInterface) IsValidReadToken(arg0, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsValidReadToken indicates an expected call of IsValidReadToken. -func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1) -} diff --git a/server/boards/client/client.go b/server/boards/client/client.go deleted file mode 100644 index 3c8541d74c..0000000000 --- a/server/boards/client/client.go +++ /dev/null @@ -1,1071 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/api" - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -const ( - APIURLSuffix = "/api/v2" -) - -type RequestReaderError struct { - buf []byte -} - -func (rre RequestReaderError) Error() string { - return "payload: " + string(rre.buf) -} - -type Response struct { - StatusCode int - Error error - Header http.Header -} - -func BuildResponse(r *http.Response) *Response { - return &Response{ - StatusCode: r.StatusCode, - Header: r.Header, - } -} - -func BuildErrorResponse(r *http.Response, err error) *Response { - statusCode := 0 - header := make(http.Header) - if r != nil { - statusCode = r.StatusCode - header = r.Header - } - - return &Response{ - StatusCode: statusCode, - Error: err, - Header: header, - } -} - -func closeBody(r *http.Response) { - if r.Body != nil { - _, _ = io.Copy(io.Discard, r.Body) - _ = r.Body.Close() - } -} - -func toJSON(v interface{}) string { - b, _ := json.Marshal(v) - return string(b) -} - -type Client struct { - URL string - APIURL string - HTTPClient *http.Client - HTTPHeader map[string]string - // Token if token is empty indicate client is not login yet - Token string -} - -func NewClient(url, sessionToken string) *Client { - url = strings.TrimRight(url, "/") - - headers := map[string]string{ - "X-Requested-With": "XMLHttpRequest", - } - - return &Client{url, url + APIURLSuffix, &http.Client{}, headers, sessionToken} -} - -func (c *Client) DoAPIGet(url, etag string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodGet, c.APIURL+url, "", etag) -} - -func (c *Client) DoAPIPost(url, data string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodPost, c.APIURL+url, data, "") -} - -func (c *Client) DoAPIPatch(url, data string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodPatch, c.APIURL+url, data, "") -} - -func (c *Client) DoAPIPut(url, data string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "") -} - -func (c *Client) DoAPIDelete(url string, data string) (*http.Response, error) { - return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, data, "") -} - -func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, error) { - return c.doAPIRequestReader(method, url, strings.NewReader(data), etag) -} - -type requestOption func(r *http.Request) - -func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* etag */ string, opts ...requestOption) (*http.Response, error) { - rq, err := http.NewRequest(method, url, data) - if err != nil { - return nil, err - } - - for _, opt := range opts { - opt(rq) - } - - if c.HTTPHeader != nil && len(c.HTTPHeader) > 0 { - for k, v := range c.HTTPHeader { - rq.Header.Set(k, v) - } - } - - if c.Token != "" { - rq.Header.Set("Authorization", "Bearer "+c.Token) - } - - rp, err := c.HTTPClient.Do(rq) - if err != nil || rp == nil { - return nil, err - } - - if rp.StatusCode == http.StatusNotModified { - return rp, nil - } - - if rp.StatusCode >= http.StatusMultipleChoices { - defer closeBody(rp) - b, err := io.ReadAll(rp.Body) - if err != nil { - return rp, fmt.Errorf("error when parsing response with code %d: %w", rp.StatusCode, err) - } - return rp, RequestReaderError{b} - } - - return rp, nil -} - -func (c *Client) GetTeamRoute(teamID string) string { - return fmt.Sprintf("%s/%s", c.GetTeamsRoute(), teamID) -} - -func (c *Client) GetTeamsRoute() string { - return "/teams" -} - -func (c *Client) GetBlockRoute(boardID, blockID string) string { - return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID) -} - -func (c *Client) GetBoardsRoute() string { - return "/boards" -} - -func (c *Client) GetBoardRoute(boardID string) string { - return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID) -} - -func (c *Client) GetBoardMetadataRoute(boardID string) string { - return fmt.Sprintf("%s/%s/metadata", c.GetBoardsRoute(), boardID) -} - -func (c *Client) GetJoinBoardRoute(boardID string) string { - 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 { - return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID)) -} - -func (c *Client) GetAllBlocksRoute(boardID string) string { - return fmt.Sprintf("%s/blocks?all=true", c.GetBoardRoute(boardID)) -} - -func (c *Client) GetBoardsAndBlocksRoute() string { - return "/boards-and-blocks" -} - -func (c *Client) GetCardsRoute() string { - return "/cards" -} - -func (c *Client) GetCardRoute(cardID string) string { - return fmt.Sprintf("%s/%s", c.GetCardsRoute(), cardID) -} - -func (c *Client) GetTeam(teamID string) (*model.Team, *Response) { - r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.TeamFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) GetTeamBoardsInsights(teamID string, userID string, timeRange string, page int, perPage int) (*model.BoardInsightsList, *Response) { - query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage) - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/insights"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var boardInsightsList *model.BoardInsightsList - if jsonErr := json.NewDecoder(r.Body).Decode(&boardInsightsList); jsonErr != nil { - return nil, BuildErrorResponse(r, jsonErr) - } - return boardInsightsList, BuildResponse(r) -} - -func (c *Client) GetUserBoardsInsights(teamID string, userID string, timeRange string, page int, perPage int) (*model.BoardInsightsList, *Response) { - query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v&team_id=%v", timeRange, page, perPage, teamID) - r, err := c.DoAPIGet(c.GetMeRoute()+"/boards/insights"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var boardInsightsList *model.BoardInsightsList - if jsonErr := json.NewDecoder(r.Body).Decode(&boardInsightsList); jsonErr != nil { - return nil, BuildErrorResponse(r, jsonErr) - } - return boardInsightsList, BuildResponse(r) -} - -func (c *Client) GetBlocksForBoard(boardID string) ([]*model.Block, *Response) { - r, err := c.DoAPIGet(c.GetBlocksRoute(boardID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BlocksFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) GetAllBlocksForBoard(boardID string) ([]*model.Block, *Response) { - r, err := c.DoAPIGet(c.GetAllBlocksRoute(boardID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BlocksFromJSON(r.Body), BuildResponse(r) -} - -const disableNotifyQueryParam = "disable_notify=true" - -func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch, disableNotify bool) (bool, *Response) { - var queryParams string - if disableNotify { - queryParams = "?" + disableNotifyQueryParam - } - r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID)+queryParams, toJSON(blockPatch)) - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) DuplicateBoard(boardID string, asTemplate bool, teamID string) (*model.BoardsAndBlocks, *Response) { - queryParams := "?asTemplate=false&" - if asTemplate { - queryParams = "?asTemplate=true" - } - if teamID != "" { - queryParams = queryParams + "&toTeam=" + teamID - } - r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/duplicate"+queryParams, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DuplicateBlock(boardID, blockID string, asTemplate bool) (bool, *Response) { - queryParams := "?asTemplate=false" - if asTemplate { - queryParams = "?asTemplate=true" - } - r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/duplicate"+queryParams, "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) { - r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/undelete", "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) InsertBlocks(boardID string, blocks []*model.Block, disableNotify bool) ([]*model.Block, *Response) { - var queryParams string - if disableNotify { - queryParams = "?" + disableNotifyQueryParam - } - r, err := c.DoAPIPost(c.GetBlocksRoute(boardID)+queryParams, toJSON(blocks)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BlocksFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DeleteBlock(boardID, blockID string, disableNotify bool) (bool, *Response) { - var queryParams string - if disableNotify { - queryParams = "?" + disableNotifyQueryParam - } - r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID)+queryParams, "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -// -// Cards -// - -func (c *Client) CreateCard(boardID string, card *model.Card, disableNotify bool) (*model.Card, *Response) { - var queryParams string - if disableNotify { - queryParams = "?" + disableNotifyQueryParam - } - r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/cards"+queryParams, toJSON(card)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var cardNew *model.Card - if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil { - return nil, BuildErrorResponse(r, err) - } - - return cardNew, BuildResponse(r) -} - -func (c *Client) GetCards(boardID string, page int, perPage int) ([]*model.Card, *Response) { - url := fmt.Sprintf("%s/cards?page=%d&per_page=%d", c.GetBoardRoute(boardID), page, perPage) - r, err := c.DoAPIGet(url, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var cards []*model.Card - if err := json.NewDecoder(r.Body).Decode(&cards); err != nil { - return nil, BuildErrorResponse(r, err) - } - - return cards, BuildResponse(r) -} - -func (c *Client) PatchCard(cardID string, cardPatch *model.CardPatch, disableNotify bool) (*model.Card, *Response) { - var queryParams string - if disableNotify { - queryParams = "?" + disableNotifyQueryParam - } - r, err := c.DoAPIPatch(c.GetCardRoute(cardID)+queryParams, toJSON(cardPatch)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var cardNew *model.Card - if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil { - return nil, BuildErrorResponse(r, err) - } - - return cardNew, BuildResponse(r) -} - -func (c *Client) GetCard(cardID string) (*model.Card, *Response) { - r, err := c.DoAPIGet(c.GetCardRoute(cardID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var card *model.Card - if err := json.NewDecoder(r.Body).Decode(&card); err != nil { - return nil, BuildErrorResponse(r, err) - } - - return card, BuildResponse(r) -} - -// -// Boards and blocks. -// - -func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { - r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) CreateCategory(category model.Category) (*model.Category, *Response) { - r, err := c.DoAPIPost(c.GetTeamRoute(category.TeamID)+"/categories", toJSON(category)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.CategoryFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DeleteCategory(teamID, categoryID string) *Response { - r, err := c.DoAPIDelete(c.GetTeamRoute(teamID)+"/categories/"+categoryID, "") - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} - -func (c *Client) UpdateCategoryBoard(teamID, categoryID, boardID string) *Response { - r, err := c.DoAPIPost(fmt.Sprintf("%s/categories/%s/boards/%s", c.GetTeamRoute(teamID), categoryID, boardID), "") - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} - -func (c *Client) GetUserCategoryBoards(teamID string) ([]model.CategoryBoards, *Response) { - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/categories", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var categoryBoards []model.CategoryBoards - _ = json.NewDecoder(r.Body).Decode(&categoryBoards) - return categoryBoards, BuildResponse(r) -} - -func (c *Client) ReorderCategories(teamID string, newOrder []string) ([]string, *Response) { - r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/reorder", toJSON(newOrder)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var updatedCategoryOrder []string - _ = json.NewDecoder(r.Body).Decode(&updatedCategoryOrder) - return updatedCategoryOrder, BuildResponse(r) -} - -func (c *Client) ReorderCategoryBoards(teamID, categoryID string, newOrder []string) ([]string, *Response) { - r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/reorder", toJSON(newOrder)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var updatedBoardsOrder []string - _ = json.NewDecoder(r.Body).Decode(&updatedBoardsOrder) - return updatedBoardsOrder, BuildResponse(r) -} - -func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { - r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks) (bool, *Response) { - r, err := c.DoAPIDelete(c.GetBoardsAndBlocksRoute(), toJSON(dbab)) - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -// Sharing - -func (c *Client) GetSharingRoute(boardID string) string { - return fmt.Sprintf("%s/sharing", c.GetBoardRoute(boardID)) -} - -func (c *Client) GetSharing(boardID string) (*model.Sharing, *Response) { - r, err := c.DoAPIGet(c.GetSharingRoute(boardID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - sharing := model.SharingFromJSON(r.Body) - return &sharing, BuildResponse(r) -} - -func (c *Client) PostSharing(sharing *model.Sharing) (bool, *Response) { - r, err := c.DoAPIPost(c.GetSharingRoute(sharing.ID), toJSON(sharing)) - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetRegisterRoute() string { - return "/register" -} - -func (c *Client) Register(request *model.RegisterRequest) (bool, *Response) { - r, err := c.DoAPIPost(c.GetRegisterRoute(), toJSON(&request)) - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetLoginRoute() string { - return "/login" -} - -func (c *Client) Login(request *model.LoginRequest) (*model.LoginResponse, *Response) { - r, err := c.DoAPIPost(c.GetLoginRoute(), toJSON(&request)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - data, err := model.LoginResponseFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - if data.Token != "" { - c.Token = data.Token - } - - return data, BuildResponse(r) -} - -func (c *Client) GetMeRoute() string { - return "/users/me" -} - -func (c *Client) GetMe() (*model.User, *Response) { - r, err := c.DoAPIGet(c.GetMeRoute(), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - me, err := model.UserFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - return me, BuildResponse(r) -} - -func (c *Client) GetUserID() string { - me, _ := c.GetMe() - if me == nil { - return "" - } - return me.ID -} - -func (c *Client) GetUserRoute(id string) string { - return fmt.Sprintf("/users/%s", id) -} - -func (c *Client) GetUser(id string) (*model.User, *Response) { - r, err := c.DoAPIGet(c.GetUserRoute(id), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - user, err := model.UserFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - return user, BuildResponse(r) -} - -func (c *Client) GetUserChangePasswordRoute(id string) string { - return fmt.Sprintf("/users/%s/changepassword", id) -} - -func (c *Client) UserChangePassword(id string, data *model.ChangePasswordRequest) (bool, *Response) { - r, err := c.DoAPIPost(c.GetUserChangePasswordRoute(id), toJSON(&data)) - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) CreateBoard(board *model.Board) (*model.Board, *Response) { - r, err := c.DoAPIPost(c.GetBoardsRoute(), toJSON(board)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) PatchBoard(boardID string, patch *model.BoardPatch) (*model.Board, *Response) { - r, err := c.DoAPIPatch(c.GetBoardRoute(boardID), toJSON(patch)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DeleteBoard(boardID string) (bool, *Response) { - r, err := c.DoAPIDelete(c.GetBoardRoute(boardID), "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) UndeleteBoard(boardID string) (bool, *Response) { - r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/undelete", "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetBoard(boardID, readToken string) (*model.Board, *Response) { - url := c.GetBoardRoute(boardID) - if readToken != "" { - url += fmt.Sprintf("?read_token=%s", readToken) - } - - r, err := c.DoAPIGet(url, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) GetBoardMetadata(boardID, readToken string) (*model.BoardMetadata, *Response) { - url := c.GetBoardMetadataRoute(boardID) - if readToken != "" { - url += fmt.Sprintf("?read_token=%s", readToken) - } - - r, err := c.DoAPIGet(url, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardMetadataFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) GetBoardsForTeam(teamID string) ([]*model.Board, *Response) { - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) SearchBoardsForUser(teamID, term string, field model.BoardSearchField) ([]*model.Board, *Response) { - query := fmt.Sprintf("q=%s&field=%s", term, field) - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) SearchBoardsForTeam(teamID, term string) ([]*model.Board, *Response) { - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?q="+term, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) GetMembersForBoard(boardID string) ([]*model.BoardMember, *Response) { - r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/members", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardMembersFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, *Response) { - r, err := c.DoAPIPost(c.GetBoardRoute(member.BoardID)+"/members", toJSON(member)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardMemberFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) JoinBoard(boardID string) (*model.BoardMember, *Response) { - r, err := c.DoAPIPost(c.GetJoinBoardRoute(boardID), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(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) { - r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardMemberFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) DeleteBoardMember(member *model.BoardMember) (bool, *Response) { - r, err := c.DoAPIDelete(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetTeamUploadFileRoute(teamID, boardID string) string { - return fmt.Sprintf("%s/%s/files", c.GetTeamRoute(teamID), boardID) -} - -func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.FileUploadResponse, *Response) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile(api.UploadFormFileKey, "file") - if err != nil { - return nil, &Response{Error: err} - } - if _, err = io.Copy(part, data); err != nil { - return nil, &Response{Error: err} - } - writer.Close() - - opt := func(r *http.Request) { - r.Header.Add("Content-Type", writer.FormDataContentType()) - } - - r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamUploadFileRoute(teamID, boardID), body, "", opt) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - fileUploadResponse, err := api.FileUploadResponseFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return fileUploadResponse, BuildResponse(r) -} - -func (c *Client) TeamUploadFileInfo(teamID, boardID string, fileName string) (*mm_model.FileInfo, *Response) { - r, err := c.DoAPIGet(fmt.Sprintf("/files/teams/%s/%s/%s/info", teamID, boardID, fileName), "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - fileInfoResponse, err := api.FileInfoResponseFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - return fileInfoResponse, BuildResponse(r) -} - -func (c *Client) GetSubscriptionsRoute() string { - return "/subscriptions" -} - -func (c *Client) CreateSubscription(sub *model.Subscription) (*model.Subscription, *Response) { - r, err := c.DoAPIPost(c.GetSubscriptionsRoute(), toJSON(&sub)) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - subNew, err := model.SubscriptionFromJSON(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - return subNew, BuildResponse(r) -} - -func (c *Client) DeleteSubscription(blockID string, subscriberID string) *Response { - url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(), blockID, subscriberID) - - r, err := c.DoAPIDelete(url, "") - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} - -func (c *Client) GetSubscriptions(subscriberID string) ([]*model.Subscription, *Response) { - url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(), subscriberID) - - r, err := c.DoAPIGet(url, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var subs []*model.Subscription - err = json.NewDecoder(r.Body).Decode(&subs) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return subs, BuildResponse(r) -} - -func (c *Client) GetTemplatesForTeam(teamID string) ([]*model.Board, *Response) { - r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/templates", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return model.BoardsFromJSON(r.Body), BuildResponse(r) -} - -func (c *Client) ExportBoardArchive(boardID string) ([]byte, *Response) { - r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/archive/export", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - buf, err := io.ReadAll(r.Body) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - return buf, BuildResponse(r) -} - -func (c *Client) ImportArchive(teamID string, data io.Reader) *Response { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile(api.UploadFormFileKey, "file") - if err != nil { - return &Response{Error: err} - } - if _, err = io.Copy(part, data); err != nil { - return &Response{Error: err} - } - writer.Close() - - opt := func(r *http.Request) { - r.Header.Add("Content-Type", writer.FormDataContentType()) - } - - r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamRoute(teamID)+"/archive/import", body, "", opt) - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} - -func (c *Client) GetLimits() (*model.BoardsCloudLimits, *Response) { - r, err := c.DoAPIGet("/limits", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var limits *model.BoardsCloudLimits - err = json.NewDecoder(r.Body).Decode(&limits) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return limits, BuildResponse(r) -} - -func (c *Client) MoveContentBlock(srcBlockID string, dstBlockID string, where string, userID string) (bool, *Response) { - r, err := c.DoAPIPost("/content-blocks/"+srcBlockID+"/moveto/"+where+"/"+dstBlockID, "") - if err != nil { - return false, BuildErrorResponse(r, err) - } - defer closeBody(r) - - return true, BuildResponse(r) -} - -func (c *Client) GetStatistics() (*model.BoardsStatistics, *Response) { - r, err := c.DoAPIGet("/statistics", "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var stats *model.BoardsStatistics - err = json.NewDecoder(r.Body).Decode(&stats) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return stats, BuildResponse(r) -} - -func (c *Client) GetBoardsForCompliance(teamID string, page, perPage int) (*model.BoardsComplianceResponse, *Response) { - query := fmt.Sprintf("?team_id=%s&page=%d&per_page=%d", teamID, page, perPage) - r, err := c.DoAPIGet("/admin/boards"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var res *model.BoardsComplianceResponse - err = json.NewDecoder(r.Body).Decode(&res) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return res, BuildResponse(r) -} - -func (c *Client) GetBoardsComplianceHistory( - modifiedSince int64, includeDeleted bool, teamID string, page, perPage int) (*model.BoardsComplianceHistoryResponse, *Response) { - query := fmt.Sprintf("?modified_since=%d&include_deleted=%t&team_id=%s&page=%d&per_page=%d", - modifiedSince, includeDeleted, teamID, page, perPage) - r, err := c.DoAPIGet("/admin/boards_history"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var res *model.BoardsComplianceHistoryResponse - err = json.NewDecoder(r.Body).Decode(&res) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return res, BuildResponse(r) -} - -func (c *Client) GetBlocksComplianceHistory( - modifiedSince int64, includeDeleted bool, teamID, boardID string, page, perPage int) (*model.BlocksComplianceHistoryResponse, *Response) { - query := fmt.Sprintf("?modified_since=%d&include_deleted=%t&team_id=%s&board_id=%s&page=%d&per_page=%d", - modifiedSince, includeDeleted, teamID, boardID, page, perPage) - r, err := c.DoAPIGet("/admin/blocks_history"+query, "") - if err != nil { - return nil, BuildErrorResponse(r, err) - } - defer closeBody(r) - - var res *model.BlocksComplianceHistoryResponse - err = json.NewDecoder(r.Body).Decode(&res) - if err != nil { - return nil, BuildErrorResponse(r, err) - } - - return res, BuildResponse(r) -} - -func (c *Client) HideBoard(teamID, categoryID, boardID string) *Response { - r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/boards/"+boardID+"/hide", "") - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} - -func (c *Client) UnhideBoard(teamID, categoryID, boardID string) *Response { - r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/boards/"+boardID+"/unhide", "") - if err != nil { - return BuildErrorResponse(r, err) - } - defer closeBody(r) - - return BuildResponse(r) -} diff --git a/server/boards/integrationtests/blocks_test.go b/server/boards/integrationtests/blocks_test.go deleted file mode 100644 index 2edce3b20d..0000000000 --- a/server/boards/integrationtests/blocks_test.go +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func TestGetBlocks(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - - initialID1 := utils.NewID(utils.IDTypeBlock) - initialID2 := utils.NewID(utils.IDTypeBlock) - newBlocks := []*model.Block{ - { - ID: initialID1, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - }, - { - ID: initialID2, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - }, - } - newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 2) - blockID1 := newBlocks[0].ID - blockID2 := newBlocks[1].ID - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 2) - - blockIDs := make([]string, len(blocks)) - for i, b := range blocks { - blockIDs[i] = b.ID - } - require.Contains(t, blockIDs, blockID1) - require.Contains(t, blockIDs, blockID2) -} - -func TestPostBlock(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - - var blockID1 string - var blockID2 string - var blockID3 string - - t.Run("Create a single block", func(t *testing.T) { - initialID1 := utils.NewID(utils.IDTypeBlock) - block := &model.Block{ - ID: initialID1, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - Title: "New title", - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, []*model.Block{block}, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - blockID1 = newBlocks[0].ID - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 1) - - blockIDs := make([]string, len(blocks)) - for i, b := range blocks { - blockIDs[i] = b.ID - } - require.Contains(t, blockIDs, blockID1) - }) - - t.Run("Create a couple of blocks in the same call", func(t *testing.T) { - initialID2 := utils.NewID(utils.IDTypeBlock) - initialID3 := utils.NewID(utils.IDTypeBlock) - newBlocks := []*model.Block{ - { - ID: initialID2, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - }, - { - ID: initialID3, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - }, - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 2) - blockID2 = newBlocks[0].ID - blockID3 = newBlocks[1].ID - require.NotEqual(t, initialID2, blockID2) - require.NotEqual(t, initialID3, blockID3) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 3) - - blockIDs := make([]string, len(blocks)) - for i, b := range blocks { - blockIDs[i] = b.ID - } - require.Contains(t, blockIDs, blockID1) - require.Contains(t, blockIDs, blockID2) - require.Contains(t, blockIDs, blockID3) - }) - - t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) { - block := &model.Block{ - ID: blockID1, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 20, - Type: model.TypeCard, - Title: "Updated title", - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, []*model.Block{block}, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - blockID4 := newBlocks[0].ID - require.NotEqual(t, blockID1, blockID4) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 4) - - var block4 *model.Block - for _, b := range blocks { - if b.ID == blockID4 { - block4 = b - } - } - require.NotNil(t, block4) - require.Equal(t, "Updated title", block4.Title) - }) -} - -func TestPatchBlock(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - initialID := utils.NewID(utils.IDTypeBlock) - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - time.Sleep(10 * time.Millisecond) - - block := &model.Block{ - ID: initialID, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - Title: "New title", - Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"}, - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, []*model.Block{block}, false) - th.CheckOK(resp) - require.Len(t, newBlocks, 1) - blockID := newBlocks[0].ID - - t.Run("Patch a block basic field", func(t *testing.T) { - newTitle := "Updated title" - blockPatch := &model.BlockPatch{ - Title: &newTitle, - } - - _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 1) - - var updatedBlock *model.Block - for _, b := range blocks { - if b.ID == blockID { - updatedBlock = b - } - } - require.NotNil(t, updatedBlock) - require.Equal(t, "Updated title", updatedBlock.Title) - }) - - t.Run("Patch a block custom fields", func(t *testing.T) { - blockPatch := &model.BlockPatch{ - UpdatedFields: map[string]interface{}{ - "test": "new test value", - "test3": "new field", - }, - } - - _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 1) - - var updatedBlock *model.Block - for _, b := range blocks { - if b.ID == blockID { - updatedBlock = b - } - } - require.NotNil(t, updatedBlock) - require.Equal(t, "new test value", updatedBlock.Fields["test"]) - require.Equal(t, "new field", updatedBlock.Fields["test3"]) - }) - - t.Run("Patch a block to remove custom fields", func(t *testing.T) { - blockPatch := &model.BlockPatch{ - DeletedFields: []string{"test", "test3", "test100"}, - } - - _, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 1) - - var updatedBlock *model.Block - for _, b := range blocks { - if b.ID == blockID { - updatedBlock = b - } - } - require.NotNil(t, updatedBlock) - require.Equal(t, nil, updatedBlock.Fields["test"]) - require.Equal(t, "test value 2", updatedBlock.Fields["test2"]) - require.Equal(t, nil, updatedBlock.Fields["test3"]) - }) -} - -func TestDeleteBlock(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - time.Sleep(10 * time.Millisecond) - - var blockID string - t.Run("Create a block", func(t *testing.T) { - initialID := utils.NewID(utils.IDTypeBlock) - block := &model.Block{ - ID: initialID, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - Title: "New title", - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, []*model.Block{block}, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - require.NotZero(t, newBlocks[0].ID) - require.NotEqual(t, initialID, newBlocks[0].ID) - blockID = newBlocks[0].ID - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, 1) - - blockIDs := make([]string, len(blocks)) - for i, b := range blocks { - blockIDs[i] = b.ID - } - require.Contains(t, blockIDs, blockID) - }) - - t.Run("Delete a block", func(t *testing.T) { - // this avoids triggering uniqueness constraint of - // id,insert_at on block history - time.Sleep(10 * time.Millisecond) - - _, resp := th.Client.DeleteBlock(board.ID, blockID, false) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Empty(t, blocks) - }) -} - -func TestUndeleteBlock(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - initialCount := len(blocks) - - var blockID string - t.Run("Create a block", func(t *testing.T) { - initialID := utils.NewID(utils.IDTypeBoard) - block := &model.Block{ - ID: initialID, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeBoard, - Title: "New title", - } - - newBlocks, resp := th.Client.InsertBlocks(board.ID, []*model.Block{block}, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - require.NotZero(t, newBlocks[0].ID) - require.NotEqual(t, initialID, newBlocks[0].ID) - blockID = newBlocks[0].ID - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+1) - - blockIDs := make([]string, len(blocks)) - for i, b := range blocks { - blockIDs[i] = b.ID - } - require.Contains(t, blockIDs, blockID) - }) - - t.Run("Delete a block", func(t *testing.T) { - // this avoids triggering uniqueness constraint of - // id,insert_at on block history - time.Sleep(10 * time.Millisecond) - - _, resp := th.Client.DeleteBlock(board.ID, blockID, false) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) - }) - - t.Run("Undelete a block", func(t *testing.T) { - // this avoids triggering uniqueness constraint of - // id,insert_at on block history - time.Sleep(10 * time.Millisecond) - - _, resp := th.Client.UndeleteBlock(board.ID, blockID) - require.NoError(t, resp.Error) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount+1) - }) - - t.Run("Try to undelete a block without permissions", func(t *testing.T) { - // this avoids triggering uniqueness constraint of - // id,insert_at on block history - time.Sleep(10 * time.Millisecond) - - _, resp := th.Client.DeleteBlock(board.ID, blockID, false) - require.NoError(t, resp.Error) - - _, resp = th.Client2.UndeleteBlock(board.ID, blockID) - th.CheckForbidden(resp) - - blocks, resp := th.Client.GetBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, blocks, initialCount) - }) -} diff --git a/server/boards/integrationtests/board_test.go b/server/boards/integrationtests/board_test.go deleted file mode 100644 index 280d1841e1..0000000000 --- a/server/boards/integrationtests/board_test.go +++ /dev/null @@ -1,2247 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "encoding/json" - "sort" - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/client" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func TestGetBoards(t *testing.T) { - t.Run("a non authenticated client should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - teamID := "0" - newBoard := &model.Board{ - TeamID: teamID, - Type: model.BoardTypeOpen, - } - - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - require.NotNil(t, board) - - boards, resp := th.Client.GetBoardsForTeam(teamID) - th.CheckUnauthorized(resp) - require.Nil(t, boards) - }) - - t.Run("should only return the boards that the user is a member of", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := "0" - otherTeamID := "other-team-id" - user1 := th.GetUser1() - user2 := th.GetUser2() - - board1 := &model.Board{ - TeamID: teamID, - Type: model.BoardTypeOpen, - Title: "Board 1", - } - rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true) - require.NoError(t, err) - require.NotNil(t, rBoard1) - - board2 := &model.Board{ - TeamID: teamID, - Type: model.BoardTypeOpen, - Title: "Board 2", - } - rBoard2, err := th.Server.App().CreateBoard(board2, user2.ID, false) - require.NoError(t, err) - require.NotNil(t, rBoard2) - - board3 := &model.Board{ - TeamID: teamID, - Type: model.BoardTypePrivate, - Title: "Board 3", - } - rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true) - require.NoError(t, err) - require.NotNil(t, rBoard3) - - board4 := &model.Board{ - TeamID: teamID, - Type: model.BoardTypePrivate, - Title: "Board 4", - } - rBoard4, err := th.Server.App().CreateBoard(board4, user1.ID, false) - require.NoError(t, err) - require.NotNil(t, rBoard4) - - 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, - Type: model.BoardTypeOpen, - } - rBoard6, err := th.Server.App().CreateBoard(board6, user1.ID, true) - require.NoError(t, err) - require.NotNil(t, rBoard6) - - boards, resp := th.Client.GetBoardsForTeam(teamID) - th.CheckOK(resp) - require.NotNil(t, boards) - require.ElementsMatch(t, []*model.Board{ - rBoard1, - rBoard2, - rBoard3, - }, boards) - - boardsFromOtherTeam, resp := th.Client.GetBoardsForTeam(otherTeamID) - th.CheckOK(resp) - require.NotNil(t, boardsFromOtherTeam) - require.Len(t, boardsFromOtherTeam, 1) - require.Equal(t, rBoard6.ID, boardsFromOtherTeam[0].ID) - }) -} - -func TestCreateBoard(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - newBoard := &model.Board{ - Title: "board title", - Type: model.BoardTypeOpen, - TeamID: testTeamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckUnauthorized(resp) - require.Nil(t, board) - }) - - t.Run("create public board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "board title 1" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - t.Run("creating a board should make the creator an admin", func(t *testing.T) { - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, board.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("creator should be able to access the public board and its blocks", func(t *testing.T) { - rbBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client.GetBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - }) - - t.Run("A non-member user should be able to access the public board but not its blocks", func(t *testing.T) { - rbBoard, resp := th.Client2.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client2.GetBlocksForBoard(board.ID) - th.CheckForbidden(resp) - require.Nil(t, rBlocks) - }) - }) - - t.Run("create private board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "private board title" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypePrivate, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - t.Run("creating a board should make the creator an admin", func(t *testing.T) { - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, board.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("creator should be able to access the private board and its blocks", func(t *testing.T) { - rbBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client.GetBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - }) - - t.Run("unauthorized user should not be able to access the private board or its blocks", func(t *testing.T) { - rbBoard, resp := th.Client2.GetBoard(board.ID, "") - th.CheckForbidden(resp) - require.Nil(t, rbBoard) - - rBlocks, resp := th.Client2.GetBlocksForBoard(board.ID) - th.CheckForbidden(resp) - require.Nil(t, rBlocks) - }) - }) - - t.Run("create invalid board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - title := "invalid board title" - teamID := testTeamID - user1 := th.GetUser1() - - t.Run("invalid board type", func(t *testing.T) { - var invalidBoardType model.BoardType = "invalid" - newBoard := &model.Board{ - Title: title, - TeamID: testTeamID, - Type: invalidBoardType, - } - - board, resp := th.Client.CreateBoard(newBoard) - th.CheckBadRequest(resp) - require.Nil(t, board) - - boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true) - require.NoError(t, err) - require.Empty(t, boards) - }) - - t.Run("no type", func(t *testing.T) { - newBoard := &model.Board{ - Title: title, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckBadRequest(resp) - require.Nil(t, board) - - boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true) - require.NoError(t, err) - require.Empty(t, boards) - }) - - t.Run("no team ID", func(t *testing.T) { - newBoard := &model.Board{ - Title: title, - } - board, resp := th.Client.CreateBoard(newBoard) - // the request is unauthorized because the permission - // check fails on an empty teamID - th.CheckForbidden(resp) - require.Nil(t, board) - - boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true) - require.NoError(t, err) - require.Empty(t, boards) - }) - }) -} - -func TestCreateBoardTemplate(t *testing.T) { - t.Run("create public board template", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "board template 1" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - IsTemplate: true, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - t.Run("creating a board template should make the creator an admin", func(t *testing.T) { - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, board.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("creator should be able to access the public board template and its blocks", func(t *testing.T) { - rbBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client.GetBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - }) - - t.Run("another user should be able to access the public board template and its blocks", func(t *testing.T) { - rbBoard, resp := th.Client2.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client2.GetBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - }) - }) - - t.Run("create private board template", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "private board template title" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypePrivate, - TeamID: teamID, - IsTemplate: true, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypePrivate, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - t.Run("creating a board template should make the creator an admin", func(t *testing.T) { - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, board.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("creator should be able to access the private board template and its blocks", func(t *testing.T) { - rbBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rbBoard) - require.Equal(t, board, rbBoard) - - rBlocks, resp := th.Client.GetBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - }) - - t.Run("unauthorized user should not be able to access the private board template or its blocks", func(t *testing.T) { - rbBoard, resp := th.Client2.GetBoard(board.ID, "") - th.CheckForbidden(resp) - require.Nil(t, rbBoard) - - rBlocks, resp := th.Client2.GetBlocksForBoard(board.ID) - th.CheckForbidden(resp) - require.Nil(t, rBlocks) - }) - }) -} - -func TestGetAllBlocksForBoard(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - board := th.CreateBoard("board-id", model.BoardTypeOpen) - - parentBlockID := utils.NewID(utils.IDTypeBlock) - childBlockID1 := utils.NewID(utils.IDTypeBlock) - childBlockID2 := utils.NewID(utils.IDTypeBlock) - - t.Run("Create the block structure", func(t *testing.T) { - newBlocks := []*model.Block{ - { - ID: parentBlockID, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - }, - { - ID: childBlockID1, - BoardID: board.ID, - ParentID: parentBlockID, - CreateAt: 2, - UpdateAt: 2, - Type: model.TypeCard, - }, - { - ID: childBlockID2, - BoardID: board.ID, - ParentID: parentBlockID, - CreateAt: 2, - UpdateAt: 2, - Type: model.TypeCard, - }, - } - - insertedBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, insertedBlocks, len(newBlocks)) - - insertedBlockIDs := make([]string, len(insertedBlocks)) - for i, b := range insertedBlocks { - insertedBlockIDs[i] = b.ID - } - - fetchedBlocks, resp := th.Client.GetAllBlocksForBoard(board.ID) - require.NoError(t, resp.Error) - require.Len(t, fetchedBlocks, len(newBlocks)) - - fetchedblockIDs := make([]string, len(fetchedBlocks)) - for i, b := range fetchedBlocks { - fetchedblockIDs[i] = b.ID - } - - sort.Strings(insertedBlockIDs) - sort.Strings(fetchedblockIDs) - - require.Equal(t, insertedBlockIDs, fetchedblockIDs) - }) -} - -func TestSearchBoards(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - boards, resp := th.Client.SearchBoardsForTeam(testTeamID, "term") - th.CheckUnauthorized(resp) - require.Nil(t, boards) - }) - - t.Run("all the matching private boards that the user is a member of and all matching public boards should be returned", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := testTeamID - user1 := th.GetUser1() - - board1 := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true) - require.NoError(t, err) - - board2 := &model.Board{ - Title: "public board where user1 is not member", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard2, err := th.Server.App().CreateBoard(board2, user1.ID, false) - require.NoError(t, err) - - board3 := &model.Board{ - Title: "private board where user1 is admin", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true) - require.NoError(t, err) - - board4 := &model.Board{ - Title: "private board where user1 is not member", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - _, err = th.Server.App().CreateBoard(board4, user1.ID, false) - require.NoError(t, err) - - board5 := &model.Board{ - Title: "private board where user1 is admin, but in other team", - Type: model.BoardTypePrivate, - TeamID: "other-team-id", - } - rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true) - require.NoError(t, err) - - testCases := []struct { - Name string - Client *client.Client - Term string - ExpectedIDs []string - }{ - { - Name: "should return all boards where user1 is member or that are public", - Client: th.Client, - Term: "board", - ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID, rBoard5.ID}, - }, - { - Name: "matching a full word", - Client: th.Client, - Term: "admin", - ExpectedIDs: []string{rBoard1.ID, rBoard3.ID, rBoard5.ID}, - }, - { - Name: "matching part of the word", - Client: th.Client, - Term: "ubli", - ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, - }, - { - Name: "case insensitive", - Client: th.Client, - Term: "UBLI", - ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, - }, - { - Name: "user2 can only see the public boards, as he's not a member of any", - Client: th.Client2, - Term: "board", - ExpectedIDs: []string{rBoard1.ID, rBoard2.ID}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - boards, resp := tc.Client.SearchBoardsForTeam(teamID, tc.Term) - th.CheckOK(resp) - - boardIDs := []string{} - for _, board := range boards { - boardIDs = append(boardIDs, board.ID) - } - - require.ElementsMatch(t, tc.ExpectedIDs, boardIDs) - }) - } - }) -} - -func TestGetBoard(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - board, resp := th.Client.GetBoard("boar-id", "") - th.CheckUnauthorized(resp) - require.Nil(t, board) - }) - - t.Run("valid read token should be enough to get the board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Server.Config().EnablePublicSharedBoards = true - - teamID := testTeamID - sharingToken := utils.NewID(utils.IDTypeToken) - - board := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard, err := th.Server.App().CreateBoard(board, th.GetUser1().ID, true) - require.NoError(t, err) - - sharing := &model.Sharing{ - ID: rBoard.ID, - Enabled: true, - Token: sharingToken, - UpdateAt: 1, - } - - success, resp := th.Client.PostSharing(sharing) - th.CheckOK(resp) - require.True(t, success) - - // the client logs out - th.Logout(th.Client) - - // we make sure that the client cannot currently retrieve the - // board with no session - board, resp = th.Client.GetBoard(rBoard.ID, "") - th.CheckUnauthorized(resp) - require.Nil(t, board) - - // it should be able to retrieve it with the read token - board, resp = th.Client.GetBoard(rBoard.ID, sharingToken) - th.CheckOK(resp) - require.NotNil(t, board) - }) - - t.Run("nonexisting board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board, resp := th.Client.GetBoard("nonexistent board", "") - th.CheckNotFound(resp) - require.Nil(t, board) - }) - - t.Run("a user that doesn't have permissions to a private board cannot retrieve it", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := testTeamID - newBoard := &model.Board{ - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, false) - require.NoError(t, err) - - rBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckForbidden(resp) - require.Nil(t, rBoard) - }) - - t.Run("a user that has permissions to a private board can retrieve it", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := testTeamID - newBoard := &model.Board{ - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - rBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rBoard) - }) - - t.Run("a user that doesn't have permissions to a public board but have them to its team can retrieve it", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := testTeamID - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, false) - require.NoError(t, err) - - rBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rBoard) - }) -} - -func TestGetBoardMetadata(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelperWithLicense(t, LicenseEnterprise).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - boardMetadata, resp := th.Client.GetBoardMetadata("boar-id", "") - th.CheckUnauthorized(resp) - require.Nil(t, boardMetadata) - }) - - t.Run("getBoardMetadata query is correct", func(t *testing.T) { - th := SetupTestHelperWithLicense(t, LicenseEnterprise).InitBasic() - defer th.TearDown() - th.Server.Config().EnablePublicSharedBoards = true - - teamID := testTeamID - - board := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard, err := th.Server.App().CreateBoard(board, th.GetUser1().ID, true) - require.NoError(t, err) - - // Check metadata - boardMetadata, resp := th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckOK(resp) - require.NotNil(t, boardMetadata) - - require.Equal(t, rBoard.CreatedBy, boardMetadata.CreatedBy) - require.Equal(t, rBoard.CreateAt, boardMetadata.DescendantFirstUpdateAt) - require.Equal(t, rBoard.UpdateAt, boardMetadata.DescendantLastUpdateAt) - require.Equal(t, rBoard.ModifiedBy, boardMetadata.LastModifiedBy) - - // Insert card1 - card1 := &model.Block{ - ID: "card1", - BoardID: rBoard.ID, - Title: "Card 1", - } - time.Sleep(20 * time.Millisecond) - require.NoError(t, th.Server.App().InsertBlock(card1, th.GetUser2().ID)) - rCard1, err := th.Server.App().GetBlockByID(card1.ID) - require.NoError(t, err) - - // Check updated metadata - boardMetadata, resp = th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckOK(resp) - require.NotNil(t, boardMetadata) - - require.Equal(t, rBoard.CreatedBy, boardMetadata.CreatedBy) - require.Equal(t, rBoard.CreateAt, boardMetadata.DescendantFirstUpdateAt) - require.Equal(t, rCard1.UpdateAt, boardMetadata.DescendantLastUpdateAt) - require.Equal(t, rCard1.ModifiedBy, boardMetadata.LastModifiedBy) - - // Insert card2 - card2 := &model.Block{ - ID: "card2", - BoardID: rBoard.ID, - Title: "Card 2", - } - time.Sleep(20 * time.Millisecond) - require.NoError(t, th.Server.App().InsertBlock(card2, th.GetUser1().ID)) - rCard2, err := th.Server.App().GetBlockByID(card2.ID) - require.NoError(t, err) - - // Check updated metadata - boardMetadata, resp = th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckOK(resp) - require.NotNil(t, boardMetadata) - require.Equal(t, rBoard.CreatedBy, boardMetadata.CreatedBy) - require.Equal(t, rBoard.CreateAt, boardMetadata.DescendantFirstUpdateAt) - require.Equal(t, rCard2.UpdateAt, boardMetadata.DescendantLastUpdateAt) - require.Equal(t, rCard2.ModifiedBy, boardMetadata.LastModifiedBy) - - t.Run("After delete board", func(t *testing.T) { - // Delete board - time.Sleep(20 * time.Millisecond) - require.NoError(t, th.Server.App().DeleteBoard(rBoard.ID, th.GetUser1().ID)) - - // Check updated metadata - boardMetadata, resp = th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckOK(resp) - require.NotNil(t, boardMetadata) - require.Equal(t, rBoard.CreatedBy, boardMetadata.CreatedBy) - require.Equal(t, rBoard.CreateAt, boardMetadata.DescendantFirstUpdateAt) - require.Greater(t, boardMetadata.DescendantLastUpdateAt, rCard2.UpdateAt) - require.Equal(t, th.GetUser1().ID, boardMetadata.LastModifiedBy) - }) - }) - - t.Run("getBoardMetadata should fail with no license", func(t *testing.T) { - th := SetupTestHelperWithLicense(t, LicenseNone).InitBasic() - defer th.TearDown() - th.Server.Config().EnablePublicSharedBoards = true - - teamID := testTeamID - - board := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard, err := th.Server.App().CreateBoard(board, th.GetUser1().ID, true) - require.NoError(t, err) - - // Check metadata - boardMetadata, resp := th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckNotImplemented(resp) - require.Nil(t, boardMetadata) - }) - - t.Run("getBoardMetadata should fail on Professional license", func(t *testing.T) { - th := SetupTestHelperWithLicense(t, LicenseProfessional).InitBasic() - defer th.TearDown() - th.Server.Config().EnablePublicSharedBoards = true - - teamID := testTeamID - - board := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard, err := th.Server.App().CreateBoard(board, th.GetUser1().ID, true) - require.NoError(t, err) - - // Check metadata - boardMetadata, resp := th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckNotImplemented(resp) - require.Nil(t, boardMetadata) - }) - - t.Run("valid read token should not get the board metadata", func(t *testing.T) { - th := SetupTestHelperWithLicense(t, LicenseEnterprise).InitBasic() - defer th.TearDown() - th.Server.Config().EnablePublicSharedBoards = true - - teamID := testTeamID - sharingToken := utils.NewID(utils.IDTypeToken) - userID := th.GetUser1().ID - - board := &model.Board{ - Title: "public board where user1 is admin", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - rBoard, err := th.Server.App().CreateBoard(board, userID, true) - require.NoError(t, err) - - sharing := &model.Sharing{ - ID: rBoard.ID, - Enabled: true, - Token: sharingToken, - UpdateAt: 1, - } - - success, resp := th.Client.PostSharing(sharing) - th.CheckOK(resp) - require.True(t, success) - - // the client logs out - th.Logout(th.Client) - - // we make sure that the client cannot currently retrieve the - // board with no session - boardMetadata, resp := th.Client.GetBoardMetadata(rBoard.ID, "") - th.CheckUnauthorized(resp) - require.Nil(t, boardMetadata) - - // it should not be able to retrieve it with the read token either - boardMetadata, resp = th.Client.GetBoardMetadata(rBoard.ID, sharingToken) - th.CheckUnauthorized(resp) - require.Nil(t, boardMetadata) - }) -} - -func TestPatchBoard(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - initialTitle := "title 1" - newBoard := &model.Board{ - Title: initialTitle, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - - newTitle := "a new title 1" - patch := &model.BoardPatch{Title: &newTitle} - - rBoard, resp := th.Client.PatchBoard(board.ID, patch) - th.CheckUnauthorized(resp) - require.Nil(t, rBoard) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.NoError(t, err) - require.Equal(t, initialTitle, dbBoard.Title) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newTitle := "a new title 2" - patch := &model.BoardPatch{Title: &newTitle} - - board, resp := th.Client.PatchBoard("non-existing-board", patch) - th.CheckNotFound(resp) - require.Nil(t, board) - }) - - t.Run("invalid patch on a board with permissions", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - user1 := th.GetUser1() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) - require.NoError(t, err) - - var invalidPatchType model.BoardType = "invalid" - patch := &model.BoardPatch{Type: &invalidPatchType} - - rBoard, resp := th.Client.PatchBoard(board.ID, patch) - th.CheckBadRequest(resp) - require.Nil(t, rBoard) - }) - - t.Run("valid patch on a board with permissions", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - user1 := th.GetUser1() - - initialTitle := "title" - newBoard := &model.Board{ - Title: initialTitle, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) - require.NoError(t, err) - - newTitle := "a new title" - patch := &model.BoardPatch{Title: &newTitle} - - rBoard, resp := th.Client.PatchBoard(board.ID, patch) - th.CheckOK(resp) - require.NotNil(t, rBoard) - require.Equal(t, newTitle, rBoard.Title) - }) - - t.Run("valid patch on a board without permissions", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - user1 := th.GetUser1() - - initialTitle := "title" - newBoard := &model.Board{ - Title: initialTitle, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, user1.ID, false) - require.NoError(t, err) - - newTitle := "a new title" - patch := &model.BoardPatch{Title: &newTitle} - - rBoard, resp := th.Client.PatchBoard(board.ID, patch) - th.CheckForbidden(resp) - require.Nil(t, rBoard) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.NoError(t, err) - require.Equal(t, initialTitle, dbBoard.Title) - }) -} - -func TestDeleteBoard(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - - success, resp := th.Client.DeleteBoard(board.ID) - th.CheckUnauthorized(resp) - require.False(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.NoError(t, err) - require.NotNil(t, dbBoard) - }) - - t.Run("a user without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "some-user-id", false) - require.NoError(t, err) - - success, resp := th.Client.DeleteBoard(board.ID) - th.CheckForbidden(resp) - require.False(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.NoError(t, err) - require.NotNil(t, dbBoard) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - success, resp := th.Client.DeleteBoard("non-existing-board") - th.CheckNotFound(resp) - require.False(t, success) - }) - - t.Run("an existing board should be correctly deleted", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - success, resp := th.Client.DeleteBoard(board.ID) - th.CheckOK(resp) - require.True(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, dbBoard) - }) -} - -func TestUndeleteBoard(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - - time.Sleep(1 * time.Millisecond) - err = th.Server.App().DeleteBoard(newBoard.ID, "user-id") - require.NoError(t, err) - - success, resp := th.Client.UndeleteBoard(board.ID) - th.CheckUnauthorized(resp) - require.False(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, dbBoard) - }) - - t.Run("a user without membership should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "some-user-id", false) - require.NoError(t, err) - - time.Sleep(1 * time.Millisecond) - err = th.Server.App().DeleteBoard(newBoard.ID, "some-user-id") - require.NoError(t, err) - - success, resp := th.Client.UndeleteBoard(board.ID) - th.CheckForbidden(resp) - require.False(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, dbBoard) - }) - - t.Run("a user with membership but without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "some-user-id", false) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: "user-id", - BoardID: board.ID, - SchemeEditor: true, - } - _, err = th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - - time.Sleep(1 * time.Millisecond) - err = th.Server.App().DeleteBoard(newBoard.ID, "some-user-id") - require.NoError(t, err) - - success, resp := th.Client.UndeleteBoard(board.ID) - th.CheckForbidden(resp) - require.False(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, dbBoard) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - success, resp := th.Client.UndeleteBoard("non-existing-board") - th.CheckForbidden(resp) - require.False(t, success) - }) - - t.Run("an existing deleted board should be correctly undeleted", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - time.Sleep(1 * time.Millisecond) - err = th.Server.App().DeleteBoard(newBoard.ID, "user-id") - require.NoError(t, err) - - success, resp := th.Client.UndeleteBoard(board.ID) - th.CheckOK(resp) - require.True(t, success) - - dbBoard, err := th.Server.App().GetBoard(board.ID) - require.NoError(t, err) - require.NotNil(t, dbBoard) - }) -} - -func TestGetMembersForBoard(t *testing.T) { - teamID := testTeamID - - createBoardWithUsers := func(th *TestHelper) *model.Board { - user1 := th.GetUser1() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, user1.ID, true) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - require.NotNil(t, user2Member) - - return board - } - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - board := createBoardWithUsers(th) - th.Logout(th.Client) - - members, resp := th.Client.GetMembersForBoard(board.ID) - th.CheckUnauthorized(resp) - require.Empty(t, members) - }) - - t.Run("a user without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - board := createBoardWithUsers(th) - - _ = th.Server.App().DeleteBoardMember(board.ID, th.GetUser2().ID) - - members, resp := th.Client2.GetMembersForBoard(board.ID) - th.CheckForbidden(resp) - require.Empty(t, members) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - members, resp := th.Client.GetMembersForBoard("non-existing-board") - th.CheckForbidden(resp) - require.Empty(t, members) - }) - - t.Run("should correctly return board members for a valid board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - board := createBoardWithUsers(th) - - members, resp := th.Client.GetMembersForBoard(board.ID) - th.CheckOK(resp) - require.Len(t, members, 2) - }) -} - -func TestAddMember(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: "user1", - BoardID: board.ID, - SchemeEditor: true, - } - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckUnauthorized(resp) - require.Nil(t, member) - }) - - t.Run("a user without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, "user-id", false) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: "user1", - BoardID: board.ID, - SchemeEditor: true, - } - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckForbidden(resp) - require.Nil(t, member) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newMember := &model.BoardMember{ - UserID: "user1", - BoardID: "non-existing-board-id", - SchemeEditor: true, - } - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckNotFound(resp) - require.Nil(t, member) - }) - - t.Run("should correctly add a new member for a valid board", func(t *testing.T) { - t.Run("a private board through an admin user", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckOK(resp) - require.Equal(t, newMember.UserID, member.UserID) - 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) - }) - - t.Run("a public board through a user that is not yet a member", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - - member, resp := th.Client2.AddMemberToBoard(newMember) - th.CheckForbidden(resp) - require.Nil(t, member) - - 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.CheckOK(resp) - require.NotNil(t, member) - - members, resp = th.Client2.GetMembersForBoard(board.ID) - th.CheckOK(resp) - require.Len(t, members, 2) - }) - - t.Run("should always add a new member as given board role", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeAdmin: false, - SchemeEditor: false, - SchemeCommenter: true, - } - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckOK(resp) - require.Equal(t, newMember.UserID, member.UserID) - require.Equal(t, newMember.BoardID, member.BoardID) - require.False(t, member.SchemeAdmin) - require.False(t, member.SchemeEditor) - require.True(t, member.SchemeCommenter) - }) - }) - - t.Run("should do nothing if the member already exists", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newMember := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - SchemeAdmin: false, - SchemeEditor: true, - } - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.True(t, members[0].SchemeAdmin) - require.True(t, members[0].SchemeEditor) - - member, resp := th.Client.AddMemberToBoard(newMember) - th.CheckOK(resp) - require.True(t, member.SchemeAdmin) - require.True(t, member.SchemeEditor) - - members, err = th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.True(t, members[0].SchemeAdmin) - require.True(t, members[0].SchemeEditor) - }) -} - -func TestUpdateMember(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - updatedMember := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - SchemeEditor: true, - } - - th.Logout(th.Client) - member, resp := th.Client.UpdateBoardMember(updatedMember) - th.CheckUnauthorized(resp) - require.Nil(t, member) - }) - - t.Run("a user without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - updatedMember := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - SchemeEditor: true, - } - - member, resp := th.Client2.UpdateBoardMember(updatedMember) - th.CheckForbidden(resp) - require.Nil(t, member) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - updatedMember := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: "non-existent-board-id", - SchemeEditor: true, - } - - member, resp := th.Client.UpdateBoardMember(updatedMember) - th.CheckForbidden(resp) - require.Nil(t, member) - }) - - t.Run("should correctly update a member for a valid board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - require.NotNil(t, user2Member) - require.False(t, user2Member.SchemeAdmin) - require.True(t, user2Member.SchemeEditor) - - memberUpdate := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeAdmin: true, - SchemeEditor: true, - } - - updatedUser2Member, resp := th.Client.UpdateBoardMember(memberUpdate) - th.CheckOK(resp) - require.True(t, updatedUser2Member.SchemeAdmin) - require.True(t, updatedUser2Member.SchemeEditor) - }) - - t.Run("should not update a member if that means that a board will not have any admin", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - memberUpdate := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - SchemeEditor: true, - } - - updatedUser1Member, resp := th.Client.UpdateBoardMember(memberUpdate) - th.CheckBadRequest(resp) - require.Nil(t, updatedUser1Member) - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("should always disable the admin role on update member if the user is a guest", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, userAdmin, true) - require.NoError(t, err) - - newGuestMember := &model.BoardMember{ - UserID: userGuest, - BoardID: board.ID, - SchemeViewer: true, - SchemeCommenter: true, - SchemeEditor: true, - SchemeAdmin: false, - } - guestMember, err := th.Server.App().AddMemberToBoard(newGuestMember) - require.NoError(t, err) - require.NotNil(t, guestMember) - require.True(t, guestMember.SchemeViewer) - require.True(t, guestMember.SchemeCommenter) - require.True(t, guestMember.SchemeEditor) - require.False(t, guestMember.SchemeAdmin) - - memberUpdate := &model.BoardMember{ - UserID: userGuest, - BoardID: board.ID, - SchemeAdmin: true, - SchemeViewer: true, - SchemeCommenter: true, - SchemeEditor: true, - } - - updatedGuestMember, resp := clients.Admin.UpdateBoardMember(memberUpdate) - th.CheckOK(resp) - require.True(t, updatedGuestMember.SchemeViewer) - require.True(t, updatedGuestMember.SchemeCommenter) - require.True(t, updatedGuestMember.SchemeEditor) - require.False(t, updatedGuestMember.SchemeAdmin) - }) -} - -func TestDeleteMember(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - member := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - } - - th.Logout(th.Client) - success, resp := th.Client.DeleteBoardMember(member) - th.CheckUnauthorized(resp) - require.False(t, success) - }) - - t.Run("a user without permissions should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - member := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - } - - success, resp := th.Client2.DeleteBoardMember(member) - th.CheckForbidden(resp) - require.False(t, success) - }) - - t.Run("non existing board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - updatedMember := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: "non-existent-board-id", - } - - success, resp := th.Client.DeleteBoardMember(updatedMember) - th.CheckNotFound(resp) - require.False(t, success) - }) - - t.Run("should correctly delete a member for a valid board", func(t *testing.T) { - //nolint:dupl - t.Run("admin removing a user", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - require.NotNil(t, user2Member) - require.False(t, user2Member.SchemeAdmin) - require.True(t, user2Member.SchemeEditor) - - memberToDelete := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - } - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - - success, resp := th.Client.DeleteBoardMember(memberToDelete) - th.CheckOK(resp) - require.True(t, success) - - members, err = th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - }) - - //nolint:dupl - t.Run("user removing themselves", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - require.NotNil(t, user2Member) - require.False(t, user2Member.SchemeAdmin) - require.True(t, user2Member.SchemeEditor) - - memberToDelete := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - } - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - - // Should fail - must call leave to leave a board - success, resp := th.Client2.DeleteBoardMember(memberToDelete) - th.CheckForbidden(resp) - require.False(t, success) - - members, err = th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - }) - - //nolint:dupl - t.Run("a non admin user should not be able to remove another user", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - newUser2Member := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - user2Member, err := th.Server.App().AddMemberToBoard(newUser2Member) - require.NoError(t, err) - require.NotNil(t, user2Member) - require.False(t, user2Member.SchemeAdmin) - require.True(t, user2Member.SchemeEditor) - - memberToDelete := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - } - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - - success, resp := th.Client2.DeleteBoardMember(memberToDelete) - th.CheckForbidden(resp) - require.False(t, success) - - members, err = th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - }) - }) - - t.Run("should not delete a member if that means that a board will not have any admin", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBoard := &model.Board{ - Title: "title", - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - - memberToDelete := &model.BoardMember{ - UserID: th.GetUser1().ID, - BoardID: board.ID, - } - - success, resp := th.Client.DeleteBoardMember(memberToDelete) - th.CheckBadRequest(resp) - require.False(t, success) - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 1) - require.True(t, members[0].SchemeAdmin) - }) -} - -func TestGetTemplates(t *testing.T) { - t.Run("should be able to retrieve built-in templates", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - err := th.Server.App().InitTemplates() - require.NoError(t, err, "InitTemplates should not fail") - - teamID := "my-team-id" - rBoards, resp := th.Client.GetTemplatesForTeam("0") - th.CheckOK(resp) - require.NotNil(t, rBoards) - require.GreaterOrEqual(t, len(rBoards), 6) - - t.Log("\n\n") - for _, board := range rBoards { - t.Logf("Test get template: %s - %s\n", board.Title, board.ID) - rBoard, resp := th.Client.GetBoard(board.ID, "") - th.CheckOK(resp) - require.NotNil(t, rBoard) - require.Equal(t, board, rBoard) - - rBlocks, resp := th.Client.GetAllBlocksForBoard(board.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks) - require.Greater(t, len(rBlocks), 0) - t.Logf("Got %d block(s)\n", len(rBlocks)) - - rBoardsAndBlock, resp := th.Client.DuplicateBoard(board.ID, false, teamID) - th.CheckOK(resp) - require.NotNil(t, rBoardsAndBlock) - require.Greater(t, len(rBoardsAndBlock.Boards), 0) - require.Greater(t, len(rBoardsAndBlock.Blocks), 0) - - rBoard2 := rBoardsAndBlock.Boards[0] - require.Contains(t, board.Title, rBoard2.Title) - require.False(t, rBoard2.IsTemplate) - - t.Logf("Duplicate template: %s - %s, %d block(s)\n", rBoard2.Title, rBoard2.ID, len(rBoardsAndBlock.Blocks)) - rBoard3, resp := th.Client.GetBoard(rBoard2.ID, "") - th.CheckOK(resp) - require.NotNil(t, rBoard3) - require.Equal(t, rBoard2, rBoard3) - - rBlocks2, resp := th.Client.GetAllBlocksForBoard(rBoard2.ID) - th.CheckOK(resp) - require.NotNil(t, rBlocks2) - require.Equal(t, len(rBoardsAndBlock.Blocks), len(rBlocks2)) - } - t.Log("\n\n") - }) -} - -func TestDuplicateBoard(t *testing.T) { - t.Run("create and duplicate public board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "Public board" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - newBlocks := []*model.Block{ - { - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Title: "View 1", - Type: model.TypeView, - }, - } - - newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - - newUserMember := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - th.Client.AddMemberToBoard(newUserMember) - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - - // Duplicate the board - rBoardsAndBlock, resp := th.Client.DuplicateBoard(board.ID, false, teamID) - th.CheckOK(resp) - require.NotNil(t, rBoardsAndBlock) - require.Equal(t, len(rBoardsAndBlock.Boards), 1) - require.Equal(t, len(rBoardsAndBlock.Blocks), 1) - duplicateBoard := rBoardsAndBlock.Boards[0] - require.Equal(t, duplicateBoard.Type, model.BoardTypePrivate, "Duplicated board should be private") - - members, err = th.Server.App().GetMembersForBoard(duplicateBoard.ID) - require.NoError(t, err) - require.Len(t, members, 1, "Duplicated board should only have one member") - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, duplicateBoard.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - }) - - t.Run("create and duplicate public board from a custom category", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - teamID := testTeamID - - category := model.Category{ - Name: "My Category", - UserID: me.ID, - TeamID: teamID, - } - createdCategory, resp := th.Client.CreateCategory(category) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, createdCategory) - require.Equal(t, "My Category", createdCategory.Name) - require.Equal(t, me.ID, createdCategory.UserID) - require.Equal(t, teamID, createdCategory.TeamID) - - title := "Public board" - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - // move board to custom category - resp = th.Client.UpdateCategoryBoard(teamID, createdCategory.ID, board.ID) - th.CheckOK(resp) - require.NoError(t, resp.Error) - - newBlocks := []*model.Block{ - { - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Title: "View 1", - Type: model.TypeView, - }, - } - - newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 1) - - newUserMember := &model.BoardMember{ - UserID: th.GetUser2().ID, - BoardID: board.ID, - SchemeEditor: true, - } - th.Client.AddMemberToBoard(newUserMember) - - members, err := th.Server.App().GetMembersForBoard(board.ID) - require.NoError(t, err) - require.Len(t, members, 2) - - // Duplicate the board - rBoardsAndBlock, resp := th.Client.DuplicateBoard(board.ID, false, teamID) - th.CheckOK(resp) - require.NotNil(t, rBoardsAndBlock) - require.Equal(t, len(rBoardsAndBlock.Boards), 1) - require.Equal(t, len(rBoardsAndBlock.Blocks), 1) - - duplicateBoard := rBoardsAndBlock.Boards[0] - require.Equal(t, duplicateBoard.Type, model.BoardTypePrivate, "Duplicated board should be private") - require.Equal(t, "Public board copy", duplicateBoard.Title) - - members, err = th.Server.App().GetMembersForBoard(duplicateBoard.ID) - require.NoError(t, err) - require.Len(t, members, 1, "Duplicated board should only have one member") - require.Equal(t, me.ID, members[0].UserID) - require.Equal(t, duplicateBoard.ID, members[0].BoardID) - require.True(t, members[0].SchemeAdmin) - - // verify duplicated board is in the same custom category - userCategoryBoards, resp := th.Client.GetUserCategoryBoards(teamID) - th.CheckOK(resp) - require.NotNil(t, rBoardsAndBlock) - - var duplicateBoardCategoryID string - for _, categoryBoard := range userCategoryBoards { - for _, boardMetadata := range categoryBoard.BoardMetadata { - if boardMetadata.BoardID == duplicateBoard.ID { - duplicateBoardCategoryID = categoryBoard.Category.ID - } - } - } - require.Equal(t, createdCategory.ID, duplicateBoardCategoryID) - }) -} - -func TestJoinBoard(t *testing.T) { - t.Run("create and join public board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "Test Public board" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - require.Equal(t, model.BoardRoleNone, board.MinimumRole) - - 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) - - s, _ := json.MarshalIndent(member, "", "\t") - t.Log(string(s)) - }) - - t.Run("create and join public board should match the minimumRole in the membership", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "Public board for commenters" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - MinimumRole: model.BoardRoleCommenter, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - 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) - require.False(t, member.SchemeAdmin, "new member should not be admin") - require.False(t, member.SchemeEditor, "new member should not be editor") - require.True(t, member.SchemeCommenter, "new member should be commenter") - require.False(t, member.SchemeViewer, "new member should not be viewer") - - s, _ := json.MarshalIndent(member, "", "\t") - t.Log(string(s)) - }) - - t.Run("create and join public board should match editor role in the membership when MinimumRole is empty", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "Public board for editors" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypeOpen, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - 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) - require.False(t, member.SchemeAdmin, "new member should not be admin") - require.True(t, member.SchemeEditor, "new member should be editor") - require.False(t, member.SchemeCommenter, "new member should not be commenter") - require.False(t, member.SchemeViewer, "new member should not be viewer") - - s, _ := json.MarshalIndent(member, "", "\t") - t.Log(string(s)) - }) - - t.Run("create and join private board (should not succeed)", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - me := th.GetUser1() - - title := "Private board" - teamID := testTeamID - newBoard := &model.Board{ - Title: title, - Type: model.BoardTypePrivate, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, board) - require.NotNil(t, board.ID) - require.Equal(t, title, board.Title) - require.Equal(t, model.BoardTypePrivate, board.Type) - require.Equal(t, teamID, board.TeamID) - require.Equal(t, me.ID, board.CreatedBy) - require.Equal(t, me.ID, board.ModifiedBy) - - member, resp := th.Client2.JoinBoard(board.ID) - th.CheckForbidden(resp) - require.Nil(t, member) - }) - - t.Run("join invalid board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - member, resp := th.Client2.JoinBoard("nonexistent-board-ID") - th.CheckNotFound(resp) - require.Nil(t, member) - }) -} diff --git a/server/boards/integrationtests/boards_and_blocks_test.go b/server/boards/integrationtests/boards_and_blocks_test.go deleted file mode 100644 index 4246da22fc..0000000000 --- a/server/boards/integrationtests/boards_and_blocks_test.go +++ /dev/null @@ -1,825 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/stretchr/testify/require" -) - -func TestCreateBoardsAndBlocks(t *testing.T) { - teamID := testTeamID - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{}, - Blocks: []*model.Block{}, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckUnauthorized(resp) - require.Nil(t, bab) - }) - - t.Run("invalid boards and blocks", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - t.Run("no boards", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{}, - Blocks: []*model.Block{ - {ID: "block-id", BoardID: "board-id", Type: model.TypeCard}, - }, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("no blocks", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{}, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("blocks from nonexistent boards", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{ - {ID: "block-id", BoardID: "nonexistent-board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - }, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("boards with no IDs", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, - {TeamID: teamID, Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{ - {ID: "block-id", BoardID: "board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - }, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("boards from different teams", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", TeamID: "team-id-1", Type: model.BoardTypePrivate}, - {ID: "board-id-2", TeamID: "team-id-2", Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{ - {ID: "block-id", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - }, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("creating boards and blocks", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - {ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - }, - } - - bab, resp := th.Client.CreateBoardsAndBlocks(newBab) - th.CheckOK(resp) - require.NotNil(t, bab) - - require.Len(t, bab.Boards, 2) - require.Len(t, bab.Blocks, 2) - - // board 1 should have been created with a new ID, and its - // block should be there too - boardsTermPublic, resp := th.Client.SearchBoardsForTeam(teamID, "public") - th.CheckOK(resp) - require.Len(t, boardsTermPublic, 1) - board1 := boardsTermPublic[0] - require.Equal(t, "public board", board1.Title) - require.Equal(t, model.BoardTypeOpen, board1.Type) - require.NotEqual(t, "board-id-1", board1.ID) - blocks1, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: board1.ID}) - require.NoError(t, err) - require.Len(t, blocks1, 1) - require.Equal(t, "block 1", blocks1[0].Title) - - // board 1 should have been created with a new ID, and its - // block should be there too - boardsTermPrivate, resp := th.Client.SearchBoardsForTeam(teamID, "private") - th.CheckOK(resp) - require.Len(t, boardsTermPrivate, 1) - board2 := boardsTermPrivate[0] - require.Equal(t, "private board", board2.Title) - require.Equal(t, model.BoardTypePrivate, board2.Type) - require.NotEqual(t, "board-id-2", board2.ID) - blocks2, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: board2.ID}) - require.NoError(t, err) - require.Len(t, blocks2, 1) - require.Equal(t, "block 2", blocks2[0].Title) - - // user should be an admin of both newly created boards - user1 := th.GetUser1() - members1, err := th.Server.App().GetMembersForBoard(board1.ID) - require.NoError(t, err) - require.Len(t, members1, 1) - require.Equal(t, user1.ID, members1[0].UserID) - members2, err := th.Server.App().GetMembersForBoard(board2.ID) - require.NoError(t, err) - require.Len(t, members2, 1) - require.Equal(t, user1.ID, members2[0].UserID) - }) - }) -} - -func TestPatchBoardsAndBlocks(t *testing.T) { - teamID := "team-id" - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - pbab := &model.PatchBoardsAndBlocks{} - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckUnauthorized(resp) - require.Nil(t, bab) - }) - - t.Run("invalid patch boards and blocks", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title 1" - newTitle := "new title 1" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - t.Run("no board IDs", func(t *testing.T) { - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("missmatch board IDs and patches", func(t *testing.T) { - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("no block IDs", func(t *testing.T) { - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("missmatch block IDs and patches", func(t *testing.T) { - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("block that doesn't belong to any board", func(t *testing.T) { - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - }) - - t.Run("if the user doesn't have permissions for one of the boards, nothing should be updated", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title 2" - newTitle := "new title 2" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckForbidden(resp) - require.Nil(t, bab) - }) - - t.Run("boards belonging to different teams should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title 3" - newTitle := "new title 3" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: "different-team-id", - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, "board-id-2"}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("patches should be rejected if one is invalid", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title 4" - newTitle := "new title 4" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - var invalidPatchType model.BoardType = "invalid" - invalidPatch := &model.BoardPatch{Type: &invalidPatchType} - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - invalidPatch, - }, - BlockIDs: []string{block1.ID, "board-id-2"}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("patches should be rejected if there is a block that doesn't belong to the boards being patched", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title" - newTitle := "new patched title" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckBadRequest(resp) - require.Nil(t, bab) - }) - - t.Run("patches should be applied if they're valid and they're related", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - userID := th.GetUser1().ID - initialTitle := "initial title" - newTitle := "new other title" - - newBoard1 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - newBoard2 := &model.Board{ - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) - require.NoError(t, err) - require.NotNil(t, board2) - - newBlock1 := &model.Block{ - ID: "block-id-1", - BoardID: board1.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - newBlock2 := &model.Block{ - ID: "block-id-2", - BoardID: board2.ID, - Title: initialTitle, - } - require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{board1.ID, board2.ID}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - BlockIDs: []string{block1.ID, block2.ID}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - bab, resp := th.Client.PatchBoardsAndBlocks(pbab) - th.CheckOK(resp) - require.NotNil(t, bab) - require.Len(t, bab.Boards, 2) - require.Len(t, bab.Blocks, 2) - - // ensure that the entities have been updated - rBoard1, err := th.Server.App().GetBoard(board1.ID) - require.NoError(t, err) - require.Equal(t, newTitle, rBoard1.Title) - rBlock1, err := th.Server.App().GetBlockByID(block1.ID) - require.NoError(t, err) - require.Equal(t, newTitle, rBlock1.Title) - - rBoard2, err := th.Server.App().GetBoard(board2.ID) - require.NoError(t, err) - require.Equal(t, newTitle, rBoard2.Title) - rBlock2, err := th.Server.App().GetBlockByID(block2.ID) - require.NoError(t, err) - require.Equal(t, newTitle, rBlock2.Title) - }) -} - -func TestDeleteBoardsAndBlocks(t *testing.T) { - teamID := "team-id" - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - dbab := &model.DeleteBoardsAndBlocks{} - - success, resp := th.Client.DeleteBoardsAndBlocks(dbab) - th.CheckUnauthorized(resp) - require.False(t, success) - }) - - t.Run("invalid delete boards and blocks", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - // a board and a block are required for the permission checks - newBoard := &model.Board{ - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - require.NotNil(t, board) - - newBlock := &model.Block{ - ID: "block-id-1", - BoardID: board.ID, - Title: "title", - } - require.NoError(t, th.Server.App().InsertBlock(newBlock, th.GetUser1().ID)) - block, err := th.Server.App().GetBlockByID(newBlock.ID) - require.NoError(t, err) - require.NotNil(t, block) - - t.Run("no boards", func(t *testing.T) { - dbab := &model.DeleteBoardsAndBlocks{ - Blocks: []string{block.ID}, - } - - success, resp := th.Client.DeleteBoardsAndBlocks(dbab) - th.CheckBadRequest(resp) - require.False(t, success) - }) - - t.Run("boards from different teams", func(t *testing.T) { - newOtherTeamsBoard := &model.Board{ - TeamID: "another-team-id", - Type: model.BoardTypeOpen, - } - otherTeamsBoard, err := th.Server.App().CreateBoard(newOtherTeamsBoard, th.GetUser1().ID, true) - require.NoError(t, err) - require.NotNil(t, board) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board.ID, otherTeamsBoard.ID}, - Blocks: []string{"block-id-1"}, - } - - success, resp := th.Client.DeleteBoardsAndBlocks(dbab) - th.CheckBadRequest(resp) - require.False(t, success) - }) - }) - - t.Run("if the user has no permissions to one of the boards, nothing should be deleted", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - // the user is an admin of the first board - newBoard1 := &model.Board{ - Type: model.BoardTypeOpen, - TeamID: "team_id_1", - } - board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true) - require.NoError(t, err) - require.NotNil(t, board1) - - // but not of the second - newBoard2 := &model.Board{ - Type: model.BoardTypeOpen, - TeamID: "team_id_1", - } - board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false) - require.NoError(t, err) - require.NotNil(t, board2) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board1.ID, board2.ID}, - Blocks: []string{"block-id-1"}, - } - - success, resp := th.Client.DeleteBoardsAndBlocks(dbab) - th.CheckForbidden(resp) - require.False(t, success) - }) - - t.Run("all boards and blocks should be deleted if the request is correct", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - {ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, - }, - } - - bab, err := th.Server.App().CreateBoardsAndBlocks(newBab, th.GetUser1().ID, true) - require.NoError(t, err) - require.Len(t, bab.Boards, 2) - require.Len(t, bab.Blocks, 2) - - // ensure that the entities have been successfully created - board1, err := th.Server.App().GetBoard("board-id-1") - require.NoError(t, err) - require.NotNil(t, board1) - block1, err := th.Server.App().GetBlockByID("block-id-1") - require.NoError(t, err) - require.NotNil(t, block1) - - board2, err := th.Server.App().GetBoard("board-id-2") - require.NoError(t, err) - require.NotNil(t, board2) - block2, err := th.Server.App().GetBlockByID("block-id-2") - require.NoError(t, err) - require.NotNil(t, block2) - - // call the API to delete boards and blocks - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{"board-id-1", "board-id-2"}, - Blocks: []string{"block-id-1", "block-id-2"}, - } - - success, resp := th.Client.DeleteBoardsAndBlocks(dbab) - th.CheckOK(resp) - require.True(t, success) - - // ensure that the entities have been successfully deleted - board1, err = th.Server.App().GetBoard("board-id-1") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, board1) - block1, err = th.Server.App().GetBlockByID("block-id-1") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, block1) - - board2, err = th.Server.App().GetBoard("board-id-2") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, board2) - block2, err = th.Server.App().GetBlockByID("block-id-2") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, block2) - }) -} diff --git a/server/boards/integrationtests/boardsapp_test.go b/server/boards/integrationtests/boardsapp_test.go deleted file mode 100644 index c011ab1454..0000000000 --- a/server/boards/integrationtests/boardsapp_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/server" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func TestSetConfiguration(t *testing.T) { - boolTrue := true - stringRef := "" - - baseFeatureFlags := &model.FeatureFlags{} - basePluginSettings := &model.PluginSettings{ - Directory: &stringRef, - } - driverName := "testDriver" - dataSource := "testDirectory" - baseSQLSettings := &model.SqlSettings{ - DriverName: &driverName, - DataSource: &dataSource, - } - - directory := "testDirectory" - baseFileSettings := &model.FileSettings{ - DriverName: &driverName, - Directory: &directory, - MaxFileSize: model.NewInt64(1024 * 1024), - } - - days := 365 - baseDataRetentionSettings := &model.DataRetentionSettings{ - BoardsRetentionDays: &days, - } - usernameRef := "username" - baseTeamSettings := &model.TeamSettings{ - TeammateNameDisplay: &usernameRef, - } - - falseRef := false - basePrivacySettings := &model.PrivacySettings{ - ShowEmailAddress: &falseRef, - ShowFullName: &falseRef, - } - - baseConfig := &model.Config{ - FeatureFlags: baseFeatureFlags, - PluginSettings: *basePluginSettings, - SqlSettings: *baseSQLSettings, - FileSettings: *baseFileSettings, - DataRetentionSettings: *baseDataRetentionSettings, - TeamSettings: *baseTeamSettings, - PrivacySettings: *basePrivacySettings, - } - - t.Run("test enable telemetry", func(t *testing.T) { - logSettings := &model.LogSettings{ - EnableDiagnostics: &boolTrue, - } - mmConfig := baseConfig - mmConfig.LogSettings = *logSettings - - config := server.CreateBoardsConfig(*mmConfig, "", "testId") - assert.Equal(t, true, config.Telemetry) - assert.Equal(t, "testId", config.TelemetryID) - }) - - t.Run("test boards feature flags", func(t *testing.T) { - featureFlags := &model.FeatureFlags{ - TestFeature: "test", - TestBoolFeature: boolTrue, - BoardsFeatureFlags: "hello_world-myTest", - } - - mmConfig := baseConfig - mmConfig.FeatureFlags = featureFlags - - config := server.CreateBoardsConfig(*mmConfig, "", "") - assert.Equal(t, "true", config.FeatureFlags["TestBoolFeature"]) - assert.Equal(t, "test", config.FeatureFlags["TestFeature"]) - - assert.Equal(t, "true", config.FeatureFlags["hello_world"]) - assert.Equal(t, "true", config.FeatureFlags["myTest"]) - }) -} - -func TestServeHTTP(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - - b := server.NewBoardsServiceForTest(th.Server, &FakePluginAdapter{}, nil, mlog.CreateConsoleTestLogger(true, mlog.LvlError)) - - assert := assert.New(t) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/hello", nil) - - b.ServeHTTP(nil, w, r) - - result := w.Result() - assert.NotNil(result) - defer result.Body.Close() - bodyBytes, err := io.ReadAll(result.Body) - assert.Nil(err) - bodyString := string(bodyBytes) - - assert.Equal("Hello", bodyString) -} diff --git a/server/boards/integrationtests/cards_test.go b/server/boards/integrationtests/cards_test.go deleted file mode 100644 index de748d510d..0000000000 --- a/server/boards/integrationtests/cards_test.go +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "fmt" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestCreateCard(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := th.CreateBoard(testTeamID, model.BoardTypeOpen) - th.Logout(th.Client) - - card := &model.Card{ - Title: "basic card", - } - cardNew, resp := th.Client.CreateCard(board.ID, card, false) - th.CheckUnauthorized(resp) - require.Nil(t, cardNew) - }) - - t.Run("good", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := th.CreateBoard(testTeamID, model.BoardTypeOpen) - contentOrder := []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)} - - card := &model.Card{ - Title: "test card 1", - Icon: "😱", - ContentOrder: contentOrder, - } - - cardNew, resp := th.Client.CreateCard(board.ID, card, false) - require.NoError(t, resp.Error) - th.CheckOK(resp) - require.NotNil(t, cardNew) - - require.Equal(t, board.ID, cardNew.BoardID) - require.Equal(t, "test card 1", cardNew.Title) - require.Equal(t, "😱", cardNew.Icon) - require.Equal(t, contentOrder, cardNew.ContentOrder) - }) - - t.Run("invalid card", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := th.CreateBoard(testTeamID, model.BoardTypeOpen) - - card := &model.Card{ - Title: "too many emoji's", - Icon: "😱😱😱😱", - } - - cardNew, resp := th.Client.CreateCard(board.ID, card, false) - require.Error(t, resp.Error) - require.Nil(t, cardNew) - }) -} - -func TestGetCards(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := th.CreateBoard(testTeamID, model.BoardTypeOpen) - userID := th.GetUser1().ID - - const cardCount = 25 - - // make some cards with content - for i := 0; i < cardCount; i++ { - card := &model.Card{ - BoardID: board.ID, - CreatedBy: userID, - ModifiedBy: userID, - Title: fmt.Sprintf("%d", i), - } - cardNew, resp := th.Client.CreateCard(board.ID, card, true) - th.CheckOK(resp) - - blocks := make([]*model.Block, 0, 3) - for j := 0; j < 3; j++ { - now := model.GetMillis() - block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - ParentID: cardNew.ID, - CreatedBy: userID, - ModifiedBy: userID, - CreateAt: now, - UpdateAt: now, - Schema: 1, - Type: model.TypeText, - Title: fmt.Sprintf("text %d for card %d", j, i), - BoardID: board.ID, - } - blocks = append(blocks, block) - } - _, resp = th.Client.InsertBlocks(board.ID, blocks, true) - th.CheckOK(resp) - } - - t.Run("fetch all cards", func(t *testing.T) { - cards, resp := th.Client.GetCards(board.ID, 0, -1) - th.CheckOK(resp) - assert.Len(t, cards, cardCount) - }) - - t.Run("fetch with pagination", func(t *testing.T) { - cardNums := make(map[int]struct{}) - - // return first 10 - cards, resp := th.Client.GetCards(board.ID, 0, 10) - th.CheckOK(resp) - assert.Len(t, cards, 10) - for _, card := range cards { - cardNum, err := strconv.Atoi(card.Title) - require.NoError(t, err) - cardNums[cardNum] = struct{}{} - } - - // return second 10 - cards, resp = th.Client.GetCards(board.ID, 1, 10) - th.CheckOK(resp) - assert.Len(t, cards, 10) - for _, card := range cards { - cardNum, err := strconv.Atoi(card.Title) - require.NoError(t, err) - cardNums[cardNum] = struct{}{} - } - - // return remaining 5 - cards, resp = th.Client.GetCards(board.ID, 2, 10) - th.CheckOK(resp) - assert.Len(t, cards, 5) - for _, card := range cards { - cardNum, err := strconv.Atoi(card.Title) - require.NoError(t, err) - cardNums[cardNum] = struct{}{} - } - - // make sure all card numbers were returned - assert.Len(t, cardNums, cardCount) - for i := 0; i < cardCount; i++ { - _, ok := cardNums[i] - assert.True(t, ok) - } - }) - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th.Logout(th.Client) - - cards, resp := th.Client.GetCards(board.ID, 0, 10) - th.CheckUnauthorized(resp) - require.Nil(t, cards) - }) -} - -func TestPatchCard(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - _, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1) - card := cards[0] - - th.Logout(th.Client) - - newTitle := "another title" - patch := &model.CardPatch{ - Title: &newTitle, - } - - patchedCard, resp := th.Client.PatchCard(card.ID, patch, false) - th.CheckUnauthorized(resp) - require.Nil(t, patchedCard) - }) - - t.Run("good", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1) - card := cards[0] - - // Patch the card - newTitle := "another title" - newIcon := "🐿" - newContentOrder := reverse(card.ContentOrder) - updatedProps := modifyCardProps(card.Properties) - patch := &model.CardPatch{ - Title: &newTitle, - Icon: &newIcon, - ContentOrder: &newContentOrder, - UpdatedProperties: updatedProps, - } - - patchedCard, resp := th.Client.PatchCard(card.ID, patch, false) - - th.CheckOK(resp) - require.NotNil(t, patchedCard) - require.Equal(t, board.ID, patchedCard.BoardID) - require.Equal(t, newTitle, patchedCard.Title) - require.Equal(t, newIcon, patchedCard.Icon) - require.NotEqual(t, card.ContentOrder, patchedCard.ContentOrder) - require.ElementsMatch(t, card.ContentOrder, patchedCard.ContentOrder) - require.EqualValues(t, updatedProps, patchedCard.Properties) - }) - - t.Run("invalid card patch", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - _, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1) - card := cards[0] - - // Bad patch (too many emoji) - newIcon := "🐿🐿🐿" - patch := &model.CardPatch{ - Icon: &newIcon, - } - - cardNew, resp := th.Client.PatchCard(card.ID, patch, false) - require.Error(t, resp.Error) - require.Nil(t, cardNew) - }) -} - -func TestGetCard(t *testing.T) { - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - _, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1) - card := cards[0] - - th.Logout(th.Client) - - cardFetched, resp := th.Client.GetCard(card.ID) - th.CheckUnauthorized(resp) - require.Nil(t, cardFetched) - }) - - t.Run("good", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 1) - card := cards[0] - - cardFetched, resp := th.Client.GetCard(card.ID) - - th.CheckOK(resp) - require.NotNil(t, cardFetched) - require.Equal(t, board.ID, cardFetched.BoardID) - require.Equal(t, card.Title, cardFetched.Title) - require.Equal(t, card.Icon, cardFetched.Icon) - require.Equal(t, card.ContentOrder, cardFetched.ContentOrder) - require.EqualValues(t, card.Properties, cardFetched.Properties) - }) -} - -// Helpers. -func reverse(src []string) []string { - out := make([]string, 0, len(src)) - for i := len(src) - 1; i >= 0; i-- { - out = append(out, src[i]) - } - return out -} - -func modifyCardProps(m map[string]any) map[string]any { - out := make(map[string]any) - for k := range m { - out[k] = utils.NewID(utils.IDTypeBlock) - } - return out -} diff --git a/server/boards/integrationtests/clienttestlib.go b/server/boards/integrationtests/clienttestlib.go deleted file mode 100644 index 8e90339a4d..0000000000 --- a/server/boards/integrationtests/clienttestlib.go +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package integrationtests - -import ( - "errors" - "fmt" - "net/http" - "os" - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/client" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/server" - "github.com/mattermost/mattermost/server/v8/boards/services/auth" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/localpermissions" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mmpermissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/stretchr/testify/require" -) - -const ( - user1Username = "user1" - user2Username = "user2" - password = "Pa$$word" - testTeamID = "team-id" -) - -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" - userGuest string = "guest" -) - -var ( - userAnonID = userAnon - userNoTeamMemberID = userNoTeamMember - userTeamMemberID = userTeamMember - userViewerID = userViewer - userCommenterID = userCommenter - userEditorID = userEditor - userAdminID = userAdmin - userGuestID = userGuest -) - -type LicenseType int - -const ( - LicenseNone LicenseType = iota // 0 - LicenseProfessional // 1 - LicenseEnterprise // 2 -) - -type TestHelper struct { - T *testing.T - Server *server.Server - Client *client.Client - Client2 *client.Client - - origEnvUnitTesting string -} - -type FakePermissionPluginAPI struct{} - -func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mm_model.Permission) bool { - return userID == userAdmin -} - -func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mm_model.Permission) bool { - if permission.Id == model.PermissionManageTeam.Id { - return false - } - if userID == userNoTeamMember { - return false - } - if teamID == "empty-team" { - return false - } - return true -} - -func (*FakePermissionPluginAPI) HasPermissionToChannel(userID string, channelID string, permission *mm_model.Permission) bool { - return channelID == "valid-channel-id" || channelID == "valid-channel-id-2" -} - -func GetTestConfig(t *testing.T) *config.Configuration { - driver := os.Getenv("MM_SQLSETTINGS_DRIVERNAME") - if driver == "" { - driver = model.PostgresDBType - } - - storeType := sqlstore.NewStoreType(driver, driver, true) - storeType.Store.Shutdown() - storeType.Logger.Shutdown() - - logging := ` - { - "testing": { - "type": "console", - "options": { - "out": "stdout" - }, - "format": "plain", - "format_options": { - "delim": " " - }, - "levels": [ - {"id": 5, "name": "debug"}, - {"id": 4, "name": "info"}, - {"id": 3, "name": "warn"}, - {"id": 2, "name": "error", "stacktrace": true}, - {"id": 1, "name": "fatal", "stacktrace": true}, - {"id": 0, "name": "panic", "stacktrace": true} - ] - } - }` - - return &config.Configuration{ - ServerRoot: "http://localhost:8888", - Port: 8888, - DBType: driver, - DBConfigString: storeType.ConnString, - DBTablePrefix: "test_", - WebPath: "./pack", - FilesDriver: "local", - FilesPath: "./files", - LoggingCfgJSON: logging, - SessionExpireTime: int64(30 * time.Second), - AuthMode: "native", - } -} - -func newTestServer(t *testing.T, singleUserToken string) *server.Server { - return newTestServerWithLicense(t, singleUserToken, LicenseNone) -} - -func newTestServerWithLicense(t *testing.T, singleUserToken string, licenseType LicenseType) *server.Server { - cfg := GetTestConfig(t) - - logger, _ := mlog.NewLogger() - err := logger.Configure("", cfg.LoggingCfgJSON, nil) - require.NoError(t, err) - - singleUser := singleUserToken != "" - innerStore, err := server.NewStore(cfg, singleUser, logger) - require.NoError(t, err) - - var db store.Store - - switch licenseType { - case LicenseProfessional: - db = NewTestProfessionalStore(innerStore) - case LicenseEnterprise: - db = NewTestEnterpriseStore(innerStore) - case LicenseNone: - fallthrough - default: - db = innerStore - } - - permissionsService := localpermissions.New(db, logger) - - params := server.Params{ - Cfg: cfg, - SingleUserToken: singleUserToken, - DBStore: db, - Logger: logger, - PermissionsService: permissionsService, - } - - srv, err := server.New(params) - require.NoError(t, err) - - return srv -} - -func NewTestServerPluginMode(t *testing.T) *server.Server { - cfg := GetTestConfig(t) - - 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, false, logger) - if err != nil { - panic(err) - } - - db := NewPluginTestStore(innerStore) - - permissionsService := mmpermissions.New(db, &FakePermissionPluginAPI{}, logger) - - params := server.Params{ - Cfg: cfg, - DBStore: db, - Logger: logger, - PermissionsService: permissionsService, - } - - srv, err := server.New(params) - if err != nil { - panic(err) - } - - return srv -} - -func newTestServerLocalMode(t *testing.T) *server.Server { - cfg := GetTestConfig(t) - cfg.EnablePublicSharedBoards = true - - logger, _ := mlog.NewLogger() - err := logger.Configure("", cfg.LoggingCfgJSON, nil) - require.NoError(t, err) - - db, err := server.NewStore(cfg, false, logger) - require.NoError(t, err) - - permissionsService := localpermissions.New(db, logger) - - params := server.Params{ - Cfg: cfg, - DBStore: db, - Logger: logger, - PermissionsService: permissionsService, - } - - srv, err := server.New(params) - require.NoError(t, err) - - // Reduce password has strength for unit tests to dramatically speed up account creation and login - auth.PasswordHashStrength = 4 - - return srv -} - -func SetupTestHelperWithToken(t *testing.T) *TestHelper { - origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING") - os.Setenv("FOCALBOARD_UNIT_TESTING", "1") - - sessionToken := "TESTTOKEN" - - th := &TestHelper{ - T: t, - origEnvUnitTesting: origUnitTesting, - } - - th.Server = newTestServer(t, sessionToken) - th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken) - th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken) - return th -} - -func SetupTestHelper(t *testing.T) *TestHelper { - return SetupTestHelperWithLicense(t, LicenseNone) -} - -func SetupTestHelperPluginMode(t *testing.T) *TestHelper { - origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING") - os.Setenv("FOCALBOARD_UNIT_TESTING", "1") - - th := &TestHelper{ - T: t, - origEnvUnitTesting: origUnitTesting, - } - - th.Server = NewTestServerPluginMode(t) - th.Start() - return th -} - -func SetupTestHelperLocalMode(t *testing.T) *TestHelper { - origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING") - os.Setenv("FOCALBOARD_UNIT_TESTING", "1") - - th := &TestHelper{ - T: t, - origEnvUnitTesting: origUnitTesting, - } - - th.Server = newTestServerLocalMode(t) - th.Start() - return th -} - -func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper { - origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING") - os.Setenv("FOCALBOARD_UNIT_TESTING", "1") - - th := &TestHelper{ - T: t, - origEnvUnitTesting: origUnitTesting, - } - - th.Server = newTestServerWithLicense(t, "", licenseType) - th.Client = client.NewClient(th.Server.Config().ServerRoot, "") - th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "") - return th -} - -// Start starts the test server and ensures that it's correctly -// responding to requests before returning. -func (th *TestHelper) Start() *TestHelper { - go func() { - if err := th.Server.Start(); err != nil { - panic(err) - } - }() - - for { - URL := th.Server.Config().ServerRoot - th.Server.Logger().Info("Polling server", mlog.String("url", URL)) - resp, err := http.Get(URL) //nolint:gosec - if err != nil { - th.Server.Logger().Error("Polling failed", mlog.Err(err)) - time.Sleep(100 * time.Millisecond) - continue - } - resp.Body.Close() - - // Currently returns 404 - // if resp.StatusCode != http.StatusOK { - // th.Server.Logger().Error("Not OK", mlog.Int("statusCode", resp.StatusCode)) - // continue - // } - - // Reached this point: server is up and running! - th.Server.Logger().Info("Server ping OK", mlog.Int("statusCode", resp.StatusCode)) - - break - } - - return th -} - -// InitBasic starts the test server and initializes the clients of the -// helper, registering them and logging them into the system. -func (th *TestHelper) InitBasic() *TestHelper { - // Reduce password has strength for unit tests to dramatically speed up account creation and login - auth.PasswordHashStrength = 4 - - th.Start() - - // user1 - th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "") - - // get token - team, resp := th.Client.GetTeam(model.GlobalTeamID) - th.CheckOK(resp) - require.NotNil(th.T, team) - require.NotNil(th.T, team.SignupToken) - - // user2 - th.RegisterAndLogin(th.Client2, user2Username, "user2@sample.com", password, team.SignupToken) - - return th -} - -var ErrRegisterFail = errors.New("register failed") - -func (th *TestHelper) TearDown() { - os.Setenv("FOCALBOARD_UNIT_TESTING", th.origEnvUnitTesting) - - logger := th.Server.Logger() - - if l, ok := logger.(*mlog.Logger); ok { - defer func() { _ = l.Shutdown() }() - } - - err := th.Server.Shutdown() - if err != nil { - panic(err) - } - - err = th.Server.Store().Shutdown() - if err != nil { - panic(err) - } - - os.RemoveAll(th.Server.Config().FilesPath) - - if err := os.Remove(th.Server.Config().DBConfigString); err == nil { - logger.Debug("Removed test database", mlog.String("file", th.Server.Config().DBConfigString)) - } -} - -func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) { - req := &model.RegisterRequest{ - Username: username, - Email: email, - Password: password, - Token: token, - } - - success, resp := th.Client.Register(req) - th.CheckOK(resp) - require.True(th.T, success) - - th.Login(client, username, password) -} - -func (th *TestHelper) Login(client *client.Client, username, password string) { - req := &model.LoginRequest{ - Type: "normal", - Username: username, - Password: password, - } - data, resp := client.Login(req) - th.CheckOK(resp) - require.NotNil(th.T, data) -} - -func (th *TestHelper) Login1() { - th.Login(th.Client, user1Username, password) -} - -func (th *TestHelper) Login2() { - th.Login(th.Client2, user2Username, password) -} - -func (th *TestHelper) Logout(client *client.Client) { - client.Token = "" -} - -func (th *TestHelper) Me(client *client.Client) *model.User { - user, resp := client.GetMe() - th.CheckOK(resp) - require.NotNil(th.T, user) - return user -} - -func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *model.Board { - newBoard := &model.Board{ - TeamID: teamID, - Type: boardType, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - return board -} - -func (th *TestHelper) CreateBoards(teamID string, boardType model.BoardType, count int) []*model.Board { - boards := make([]*model.Board, 0, count) - - for i := 0; i < count; i++ { - board := th.CreateBoard(teamID, boardType) - boards = append(boards, board) - } - return boards -} - -func (th *TestHelper) CreateCategory(category model.Category) *model.Category { - cat, resp := th.Client.CreateCategory(category) - th.CheckOK(resp) - return cat -} - -func (th *TestHelper) UpdateCategoryBoard(teamID, categoryID, boardID string) { - response := th.Client.UpdateCategoryBoard(teamID, categoryID, boardID) - th.CheckOK(response) -} - -func (th *TestHelper) CreateBoardAndCards(teamdID string, boardType model.BoardType, numCards int) (*model.Board, []*model.Card) { - board := th.CreateBoard(teamdID, boardType) - cards := make([]*model.Card, 0, numCards) - for i := 0; i < numCards; i++ { - card := &model.Card{ - Title: fmt.Sprintf("test card %d", i+1), - ContentOrder: []string{utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock), utils.NewID(utils.IDTypeBlock)}, - Icon: "😱", - Properties: th.MakeCardProps(5), - } - newCard, resp := th.Client.CreateCard(board.ID, card, true) - th.CheckOK(resp) - cards = append(cards, newCard) - } - return board, cards -} - -func (th *TestHelper) MakeCardProps(count int) map[string]any { - props := make(map[string]any) - for i := 0; i < count; i++ { - props[utils.NewID(utils.IDTypeBlock)] = utils.NewID(utils.IDTypeBlock) - } - return props -} - -func (th *TestHelper) GetUserCategoryBoards(teamID string) []model.CategoryBoards { - categoryBoards, response := th.Client.GetUserCategoryBoards(teamID) - th.CheckOK(response) - return categoryBoards -} - -func (th *TestHelper) DeleteCategory(teamID, categoryID string) { - response := th.Client.DeleteCategory(teamID, categoryID) - th.CheckOK(response) -} - -func (th *TestHelper) GetUser1() *model.User { - return th.Me(th.Client) -} - -func (th *TestHelper) GetUser2() *model.User { - return th.Me(th.Client2) -} - -func (th *TestHelper) CheckOK(r *client.Response) { - require.Equal(th.T, http.StatusOK, r.StatusCode) - require.NoError(th.T, r.Error) -} - -func (th *TestHelper) CheckBadRequest(r *client.Response) { - require.Equal(th.T, http.StatusBadRequest, r.StatusCode) - require.Error(th.T, r.Error) -} - -func (th *TestHelper) CheckNotFound(r *client.Response) { - require.Equal(th.T, http.StatusNotFound, r.StatusCode) - require.Error(th.T, r.Error) -} - -func (th *TestHelper) CheckUnauthorized(r *client.Response) { - require.Equal(th.T, http.StatusUnauthorized, r.StatusCode) - require.Error(th.T, r.Error) -} - -func (th *TestHelper) CheckForbidden(r *client.Response) { - require.Equal(th.T, http.StatusForbidden, r.StatusCode) - require.Error(th.T, r.Error) -} - -func (th *TestHelper) CheckRequestEntityTooLarge(r *client.Response) { - require.Equal(th.T, http.StatusRequestEntityTooLarge, r.StatusCode) - require.Error(th.T, r.Error) -} - -func (th *TestHelper) CheckNotImplemented(r *client.Response) { - require.Equal(th.T, http.StatusNotImplemented, r.StatusCode) - require.Error(th.T, r.Error) -} diff --git a/server/boards/integrationtests/compliance_test.go b/server/boards/integrationtests/compliance_test.go deleted file mode 100644 index 9cbabd4f35..0000000000 --- a/server/boards/integrationtests/compliance_test.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "math" - "os" - "strconv" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var ( - OneHour int64 = 360000 - OneDay = OneHour * 24 - OneYear = OneDay * 365 -) - -func setupTestHelperForCompliance(t *testing.T, complianceLicense bool) (*TestHelper, Clients) { - os.Setenv("FOCALBOARD_UNIT_TESTING_COMPLIANCE", strconv.FormatBool(complianceLicense)) - - th := SetupTestHelperPluginMode(t) - clients := setupClients(th) - - th.Client = clients.TeamMember - th.Client2 = clients.TeamMember - - return th, clients -} - -func TestGetBoardsForCompliance(t *testing.T) { - t.Run("missing Features.Compliance license should fail", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, false) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, 0, 0) - - th.CheckNotImplemented(resp) - require.Nil(t, bcr) - }) - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - th.Logout(th.Client) - - bcr, resp := clients.Anon.GetBoardsForCompliance(testTeamID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bcr) - }) - - t.Run("a user without manage_system permission should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bcr, resp := clients.TeamMember.GetBoardsForCompliance(testTeamID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bcr) - }) - - t.Run("good call", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 10 - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count) - - bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, 0, 0) - th.CheckOK(resp) - require.False(t, bcr.HasNext) - require.Len(t, bcr.Results, count) - }) - - t.Run("pagination", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 20 - const perPage = 3 - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count) - - boards := make([]*model.Board, 0, count) - page := 0 - for { - bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, page, perPage) - page++ - th.CheckOK(resp) - boards = append(boards, bcr.Results...) - if !bcr.HasNext { - break - } - } - require.Len(t, boards, count) - require.Equal(t, int(math.Floor((count/perPage)+1)), page) - }) - - t.Run("invalid teamID", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bcr, resp := clients.Admin.GetBoardsForCompliance(utils.NewID(utils.IDTypeTeam), 0, 0) - - th.CheckBadRequest(resp) - require.Nil(t, bcr) - }) -} - -func TestGetBoardsComplianceHistory(t *testing.T) { - t.Run("missing Features.Compliance license should fail", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, false) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0) - - th.CheckNotImplemented(resp) - require.Nil(t, bchr) - }) - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - th.Logout(th.Client) - - bchr, resp := clients.Anon.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bchr) - }) - - t.Run("a user without manage_system permission should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.TeamMember.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bchr) - }) - - t.Run("good call, exclude deleted", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 10 - boards := th.CreateBoards(testTeamID, model.BoardTypeOpen, count) - - deleted, resp := th.Client.DeleteBoard(boards[0].ID) - th.CheckOK(resp) - require.True(t, deleted) - - deleted, resp = th.Client.DeleteBoard(boards[1].ID) - th.CheckOK(resp) - require.True(t, deleted) - - bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, false, testTeamID, 0, 0) - th.CheckOK(resp) - require.False(t, bchr.HasNext) - require.Len(t, bchr.Results, count-2) // two boards deleted - }) - - t.Run("good call, include deleted", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 10 - boards := th.CreateBoards(testTeamID, model.BoardTypeOpen, count) - - deleted, resp := th.Client.DeleteBoard(boards[0].ID) - th.CheckOK(resp) - require.True(t, deleted) - - deleted, resp = th.Client.DeleteBoard(boards[1].ID) - th.CheckOK(resp) - require.True(t, deleted) - - bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0) - th.CheckOK(resp) - require.False(t, bchr.HasNext) - require.Len(t, bchr.Results, count+2) // both deleted boards have 2 history records each - }) - - t.Run("pagination", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 20 - const perPage = 3 - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count) - - boardHistory := make([]*model.BoardHistory, 0, count) - page := 0 - for { - bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, page, perPage) - page++ - th.CheckOK(resp) - boardHistory = append(boardHistory, bchr.Results...) - if !bchr.HasNext { - break - } - } - require.Len(t, boardHistory, count) - require.Equal(t, int(math.Floor((count/perPage)+1)), page) - }) - - t.Run("invalid teamID", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, utils.NewID(utils.IDTypeTeam), 0, 0) - - th.CheckBadRequest(resp) - require.Nil(t, bchr) - }) -} - -func TestGetBlocksComplianceHistory(t *testing.T) { - t.Run("missing Features.Compliance license should fail", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, false) - defer th.TearDown() - - board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0) - - th.CheckNotImplemented(resp) - require.Nil(t, bchr) - }) - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Anon.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bchr) - }) - - t.Run("a user without manage_system permission should be rejected", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.TeamMember.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0) - - th.CheckUnauthorized(resp) - require.Nil(t, bchr) - }) - - t.Run("good call, exclude deleted", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 10 - board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count) - - deleted, resp := th.Client.DeleteBlock(board.ID, cards[0].ID, true) - th.CheckOK(resp) - require.True(t, deleted) - - deleted, resp = th.Client.DeleteBlock(board.ID, cards[1].ID, true) - th.CheckOK(resp) - require.True(t, deleted) - - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, false, testTeamID, board.ID, 0, 0) - th.CheckOK(resp) - require.False(t, bchr.HasNext) - require.Len(t, bchr.Results, count-2) // 2 blocks deleted - }) - - t.Run("good call, include deleted", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 10 - board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count) - - deleted, resp := th.Client.DeleteBlock(board.ID, cards[0].ID, true) - th.CheckOK(resp) - require.True(t, deleted) - - deleted, resp = th.Client.DeleteBlock(board.ID, cards[1].ID, true) - th.CheckOK(resp) - require.True(t, deleted) - - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0) - th.CheckOK(resp) - require.False(t, bchr.HasNext) - require.Len(t, bchr.Results, count+2) // both deleted boards have 2 history records each - }) - - t.Run("pagination", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - const count = 20 - const perPage = 3 - board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count) - - blockHistory := make([]*model.BlockHistory, 0, count) - page := 0 - for { - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, page, perPage) - page++ - th.CheckOK(resp) - blockHistory = append(blockHistory, bchr.Results...) - if !bchr.HasNext { - break - } - } - require.Len(t, blockHistory, count) - require.Equal(t, int(math.Floor((count/perPage)+1)), page) - }) - - t.Run("invalid teamID", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, utils.NewID(utils.IDTypeTeam), board.ID, 0, 0) - - th.CheckBadRequest(resp) - require.Nil(t, bchr) - }) - - t.Run("invalid boardID", func(t *testing.T) { - th, clients := setupTestHelperForCompliance(t, true) - defer th.TearDown() - - _, _ = th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2) - - bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, utils.NewID(utils.IDTypeBoard), 0, 0) - - th.CheckBadRequest(resp) - require.Nil(t, bchr) - }) -} diff --git a/server/boards/integrationtests/configuration_test.go b/server/boards/integrationtests/configuration_test.go deleted file mode 100644 index a1b6a33dda..0000000000 --- a/server/boards/integrationtests/configuration_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - - "github.com/golang/mock/gomock" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/server" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - mockservicesapi "github.com/mattermost/mattermost/server/v8/boards/model/mocks" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/stretchr/testify/assert" -) - -func TestConfigurationNullConfiguration(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - - logger := mlog.CreateConsoleTestLogger(true, mlog.LvlError) - boardsApp := server.NewBoardsServiceForTest(th.Server, &FakePluginAdapter{}, nil, logger) - - assert.NotNil(t, boardsApp.Config()) -} - -func TestOnConfigurationChange(t *testing.T) { - stringRef := "" - - basePlugins := make(map[string]map[string]interface{}) - basePlugins[server.PluginName] = make(map[string]interface{}) - basePlugins[server.PluginName][server.SharedBoardsName] = true - - baseFeatureFlags := &mm_model.FeatureFlags{ - BoardsFeatureFlags: "Feature1-Feature2", - } - basePluginSettings := &mm_model.PluginSettings{ - Directory: &stringRef, - Plugins: basePlugins, - } - intRef := 365 - baseDataRetentionSettings := &mm_model.DataRetentionSettings{ - BoardsRetentionDays: &intRef, - } - usernameRef := "username" - baseTeamSettings := &mm_model.TeamSettings{ - TeammateNameDisplay: &usernameRef, - } - - falseRef := false - basePrivacySettings := &mm_model.PrivacySettings{ - ShowEmailAddress: &falseRef, - ShowFullName: &falseRef, - } - - baseConfig := &mm_model.Config{ - FeatureFlags: baseFeatureFlags, - PluginSettings: *basePluginSettings, - DataRetentionSettings: *baseDataRetentionSettings, - TeamSettings: *baseTeamSettings, - PrivacySettings: *basePrivacySettings, - } - - t.Run("Test Load Plugin Success", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - - ctrl := gomock.NewController(t) - api := mockservicesapi.NewMockServicesAPI(ctrl) - api.EXPECT().GetConfig().Return(baseConfig) - - b := server.NewBoardsServiceForTest(th.Server, &FakePluginAdapter{}, api, mlog.CreateConsoleTestLogger(true, mlog.LvlError)) - - err := b.OnConfigurationChange() - assert.NoError(t, err) - assert.Equal(t, 1, count) - - // make sure both App and Server got updated - assert.True(t, b.Config().EnablePublicSharedBoards) - assert.True(t, b.ClientConfig().EnablePublicSharedBoards) - - assert.Equal(t, "true", b.Config().FeatureFlags["Feature1"]) - assert.Equal(t, "true", b.Config().FeatureFlags["Feature2"]) - assert.Equal(t, "", b.Config().FeatureFlags["Feature3"]) - }) -} - -var count = 0 - -type FakePluginAdapter struct { - ws.PluginAdapter -} - -func (c *FakePluginAdapter) BroadcastConfigChange(clientConfig model.ClientConfig) { - count++ -} diff --git a/server/boards/integrationtests/content_blocks_test.go b/server/boards/integrationtests/content_blocks_test.go deleted file mode 100644 index 6aafa45008..0000000000 --- a/server/boards/integrationtests/content_blocks_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "fmt" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func TestMoveContentBlock(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - board := th.CreateBoard("team-id", model.BoardTypeOpen) - - cardID1 := utils.NewID(utils.IDTypeBlock) - cardID2 := utils.NewID(utils.IDTypeBlock) - contentBlockID1 := utils.NewID(utils.IDTypeBlock) - contentBlockID2 := utils.NewID(utils.IDTypeBlock) - contentBlockID3 := utils.NewID(utils.IDTypeBlock) - contentBlockID4 := utils.NewID(utils.IDTypeBlock) - contentBlockID5 := utils.NewID(utils.IDTypeBlock) - contentBlockID6 := utils.NewID(utils.IDTypeBlock) - - card1 := &model.Block{ - ID: cardID1, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - Fields: map[string]interface{}{ - "contentOrder": []string{contentBlockID1, contentBlockID2, contentBlockID3}, - }, - } - card2 := &model.Block{ - ID: cardID2, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - Fields: map[string]interface{}{ - "contentOrder": []string{contentBlockID4, contentBlockID5, contentBlockID6}, - }, - } - - contentBlock1 := &model.Block{ - ID: contentBlockID1, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID1, - } - contentBlock2 := &model.Block{ - ID: contentBlockID2, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID1, - } - contentBlock3 := &model.Block{ - ID: contentBlockID3, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID1, - } - contentBlock4 := &model.Block{ - ID: contentBlockID4, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID2, - } - contentBlock5 := &model.Block{ - ID: contentBlockID5, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID2, - } - contentBlock6 := &model.Block{ - ID: contentBlockID6, - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - ParentID: cardID2, - } - - newBlocks := []*model.Block{ - contentBlock1, - contentBlock2, - contentBlock3, - contentBlock4, - contentBlock5, - contentBlock6, - card1, - card2, - } - createdBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false) - require.NoError(t, resp.Error) - require.Len(t, newBlocks, 8) - - contentBlock1.ID = createdBlocks[0].ID - contentBlock2.ID = createdBlocks[1].ID - contentBlock3.ID = createdBlocks[2].ID - contentBlock4.ID = createdBlocks[3].ID - contentBlock5.ID = createdBlocks[4].ID - contentBlock6.ID = createdBlocks[5].ID - card1.ID = createdBlocks[6].ID - card2.ID = createdBlocks[7].ID - - ttCases := []struct { - name string - srcBlockID string - dstBlockID string - where string - userID string - errorMessage string - expectedContentOrder []interface{} - }{ - { - name: "not matching parents", - srcBlockID: contentBlock1.ID, - dstBlockID: contentBlock4.ID, - where: "after", - userID: "user-id", - errorMessage: fmt.Sprintf("payload: {\"error\":\"not matching parent %s and %s\",\"errorCode\":400}", card1.ID, card2.ID), - expectedContentOrder: []interface{}{contentBlock1.ID, contentBlock2.ID, contentBlock3.ID}, - }, - { - name: "valid request with not real change", - srcBlockID: contentBlock2.ID, - dstBlockID: contentBlock1.ID, - where: "after", - userID: "user-id", - errorMessage: "", - expectedContentOrder: []interface{}{contentBlock1.ID, contentBlock2.ID, contentBlock3.ID}, - }, - { - name: "valid request changing order with before", - srcBlockID: contentBlock2.ID, - dstBlockID: contentBlock1.ID, - where: "before", - userID: "user-id", - errorMessage: "", - expectedContentOrder: []interface{}{contentBlock2.ID, contentBlock1.ID, contentBlock3.ID}, - }, - { - name: "valid request changing order with after", - srcBlockID: contentBlock1.ID, - dstBlockID: contentBlock2.ID, - where: "after", - userID: "user-id", - errorMessage: "", - expectedContentOrder: []interface{}{contentBlock2.ID, contentBlock1.ID, contentBlock3.ID}, - }, - } - - for _, tc := range ttCases { - t.Run(tc.name, func(t *testing.T) { - _, resp := th.Client.MoveContentBlock(tc.srcBlockID, tc.dstBlockID, tc.where, tc.userID) - if tc.errorMessage == "" { - require.NoError(t, resp.Error) - } else { - require.EqualError(t, resp.Error, tc.errorMessage) - } - - parent, err := th.Server.App().GetBlockByID(card1.ID) - require.NoError(t, err) - require.Equal(t, parent.Fields["contentOrder"], tc.expectedContentOrder) - }) - } -} diff --git a/server/boards/integrationtests/export_test.go b/server/boards/integrationtests/export_test.go deleted file mode 100644 index 4da8e78f0c..0000000000 --- a/server/boards/integrationtests/export_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestExportBoard(t *testing.T) { - t.Run("export single board", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: "test-team", - Title: "Export Test Board", - CreatedBy: th.GetUser1().ID, - Type: model.BoardTypeOpen, - CreateAt: utils.GetMillis(), - UpdateAt: utils.GetMillis(), - } - - block := &model.Block{ - ID: utils.NewID(utils.IDTypeCard), - ParentID: board.ID, - Type: model.TypeCard, - BoardID: board.ID, - Title: "Test card # for export", - CreatedBy: th.GetUser1().ID, - CreateAt: utils.GetMillis(), - UpdateAt: utils.GetMillis(), - } - - babs := &model.BoardsAndBlocks{ - Boards: []*model.Board{board}, - Blocks: []*model.Block{block}, - } - - babs, resp := th.Client.CreateBoardsAndBlocks(babs) - th.CheckOK(resp) - - // export the board to an in-memory archive file - buf, resp := th.Client.ExportBoardArchive(babs.Boards[0].ID) - th.CheckOK(resp) - require.NotNil(t, buf) - - // import the archive file to team 0 - resp = th.Client.ImportArchive(model.GlobalTeamID, bytes.NewReader(buf)) - th.CheckOK(resp) - require.NoError(t, resp.Error) - - // check for test card - boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID, true) - require.NoError(t, err) - require.Len(t, boardsImported, 1) - boardImported := boardsImported[0] - blocksImported, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: boardImported.ID}) - require.NoError(t, err) - require.Len(t, blocksImported, 1) - require.Equal(t, block.Title, blocksImported[0].Title) - }) -} diff --git a/server/boards/integrationtests/file_test.go b/server/boards/integrationtests/file_test.go deleted file mode 100644 index 3e6cbbc4e8..0000000000 --- a/server/boards/integrationtests/file_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "bytes" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/stretchr/testify/require" -) - -func TestUploadFile(t *testing.T) { - const ( - testTeamID = "team-id" - ) - - t.Run("a non authenticated user should be rejected", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - th.Logout(th.Client) - - file, resp := th.Client.TeamUploadFile(testTeamID, "test-board-id", bytes.NewBuffer([]byte("test"))) - th.CheckUnauthorized(resp) - require.Nil(t, file) - }) - - t.Run("upload a file to an existing team and board without permissions", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - file, resp := th.Client.TeamUploadFile(testTeamID, "not-valid-board", bytes.NewBuffer([]byte("test"))) - th.CheckForbidden(resp) - require.Nil(t, file) - }) - - t.Run("upload a file to an existing team and board with permissions", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - testBoard := th.CreateBoard(testTeamID, model.BoardTypeOpen) - file, resp := th.Client.TeamUploadFile(testTeamID, testBoard.ID, bytes.NewBuffer([]byte("test"))) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, file) - require.NotNil(t, file.FileID) - }) - - t.Run("upload a file to an existing team and board with permissions but reaching the MaxFileLimit", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - testBoard := th.CreateBoard(testTeamID, model.BoardTypeOpen) - - config := th.Server.App().GetConfig() - config.MaxFileSize = 1 - th.Server.App().SetConfig(config) - - file, resp := th.Client.TeamUploadFile(testTeamID, testBoard.ID, bytes.NewBuffer([]byte("test"))) - th.CheckRequestEntityTooLarge(resp) - require.Nil(t, file) - - config.MaxFileSize = 100000 - th.Server.App().SetConfig(config) - - file, resp = th.Client.TeamUploadFile(testTeamID, testBoard.ID, bytes.NewBuffer([]byte("test"))) - th.CheckOK(resp) - require.NoError(t, resp.Error) - require.NotNil(t, file) - require.NotNil(t, file.FileID) - }) -} - -func TestFileInfo(t *testing.T) { - const ( - testTeamID = "team-id" - ) - - t.Run("Retrieving file info", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - testBoard := th.CreateBoard(testTeamID, model.BoardTypeOpen) - - fileInfo, resp := th.Client.TeamUploadFileInfo(testTeamID, testBoard.ID, "test") - th.CheckOK(resp) - require.NotNil(t, fileInfo) - require.NotNil(t, fileInfo.Id) - }) -} diff --git a/server/boards/integrationtests/permissions_test.go b/server/boards/integrationtests/permissions_test.go deleted file mode 100644 index eb43eb0e65..0000000000 --- a/server/boards/integrationtests/permissions_test.go +++ /dev/null @@ -1,3955 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//nolint:dupl -package integrationtests - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/client" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type Clients struct { - Anon *client.Client - NoTeamMember *client.Client - TeamMember *client.Client - Viewer *client.Client - Commenter *client.Client - Editor *client.Client - Admin *client.Client - Guest *client.Client -} - -const ( - methodPost = "POST" - methodGet = "GET" - methodPut = "PUT" - methodDelete = "DELETE" - methodPatch = "PATCH" -) - -type TestCase struct { - url string - method string - body string - userRole string // userAnon, userNoTeamMember, userTeamMember, userViewer, userCommenter, userEditor, userAdmin or userGuest - expectedStatusCode int - totalResults int -} - -func (tt TestCase) identifier() string { - return fmt.Sprintf( - "url: %s method: %s body: %s userRoles: %s expectedStatusCode: %d totalResults: %d", - tt.url, - tt.method, - tt.body, - tt.userRole, - tt.expectedStatusCode, - tt.totalResults, - ) -} - -func setupClients(th *TestHelper) Clients { - // user1 - clients := Clients{ - Anon: client.NewClient(th.Server.Config().ServerRoot, ""), - NoTeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), - TeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), - Viewer: client.NewClient(th.Server.Config().ServerRoot, ""), - Commenter: client.NewClient(th.Server.Config().ServerRoot, ""), - Editor: client.NewClient(th.Server.Config().ServerRoot, ""), - Admin: client.NewClient(th.Server.Config().ServerRoot, ""), - Guest: client.NewClient(th.Server.Config().ServerRoot, ""), - } - - clients.NoTeamMember.HTTPHeader["Mattermost-User-Id"] = userNoTeamMember - clients.TeamMember.HTTPHeader["Mattermost-User-Id"] = userTeamMember - clients.Viewer.HTTPHeader["Mattermost-User-Id"] = userViewer - clients.Commenter.HTTPHeader["Mattermost-User-Id"] = userCommenter - clients.Editor.HTTPHeader["Mattermost-User-Id"] = userEditor - clients.Admin.HTTPHeader["Mattermost-User-Id"] = userAdmin - clients.Guest.HTTPHeader["Mattermost-User-Id"] = userGuest - - // For plugin tests, the userID = username - userAnonID = userAnon - userNoTeamMemberID = userNoTeamMember - userTeamMemberID = userTeamMember - userViewerID = userViewer - userCommenterID = userCommenter - userEditorID = userEditor - userAdminID = userAdmin - userGuestID = userGuest - - return clients -} - -func setupLocalClients(th *TestHelper) Clients { - th.Client = client.NewClient(th.Server.Config().ServerRoot, "") - th.RegisterAndLogin(th.Client, "sysadmin", "sysadmin@sample.com", password, "") - - clients := Clients{ - Anon: client.NewClient(th.Server.Config().ServerRoot, ""), - NoTeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), - TeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), - Viewer: client.NewClient(th.Server.Config().ServerRoot, ""), - Commenter: client.NewClient(th.Server.Config().ServerRoot, ""), - Editor: client.NewClient(th.Server.Config().ServerRoot, ""), - Admin: client.NewClient(th.Server.Config().ServerRoot, ""), - Guest: nil, - } - - // get token - team, resp := th.Client.GetTeam(model.GlobalTeamID) - th.CheckOK(resp) - require.NotNil(th.T, team) - require.NotNil(th.T, team.SignupToken) - - th.RegisterAndLogin(clients.NoTeamMember, userNoTeamMember, userNoTeamMember+"@sample.com", password, team.SignupToken) - userNoTeamMemberID = clients.NoTeamMember.GetUserID() - - th.RegisterAndLogin(clients.TeamMember, userTeamMember, userTeamMember+"@sample.com", password, team.SignupToken) - userTeamMemberID = clients.TeamMember.GetUserID() - - th.RegisterAndLogin(clients.Viewer, userViewer, userViewer+"@sample.com", password, team.SignupToken) - userViewerID = clients.Viewer.GetUserID() - - th.RegisterAndLogin(clients.Commenter, userCommenter, userCommenter+"@sample.com", password, team.SignupToken) - userCommenterID = clients.Commenter.GetUserID() - - th.RegisterAndLogin(clients.Editor, userEditor, userEditor+"@sample.com", password, team.SignupToken) - userEditorID = clients.Editor.GetUserID() - - th.RegisterAndLogin(clients.Admin, userAdmin, userAdmin+"@sample.com", password, team.SignupToken) - userAdminID = clients.Admin.GetUserID() - - return clients -} - -func toJSON(t *testing.T, obj interface{}) string { - result, err := json.Marshal(obj) - require.NoError(t, err) - return string(result) -} - -type TestData struct { - publicBoard *model.Board - privateBoard *model.Board - publicTemplate *model.Board - privateTemplate *model.Board -} - -func setupData(t *testing.T, th *TestHelper) TestData { - customTemplate1, err := th.Server.App().CreateBoard( - &model.Board{Title: "Custom template 1", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypeOpen, MinimumRole: "viewer"}, - userAdminID, - true, - ) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-1", Title: "Test", Type: "card", BoardID: customTemplate1.ID, Fields: map[string]interface{}{}}, userAdminID) - require.NoError(t, err) - customTemplate2, err := th.Server.App().CreateBoard( - &model.Board{Title: "Custom template 2", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypePrivate, MinimumRole: "viewer"}, - userAdminID, - true) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-2", Title: "Test", Type: "card", BoardID: customTemplate2.ID, Fields: map[string]interface{}{}}, userAdminID) - require.NoError(t, err) - - board1, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 1", TeamID: "test-team", Type: model.BoardTypeOpen, MinimumRole: "viewer"}, userAdminID, true) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-3", Title: "Test", Type: "card", BoardID: board1.ID, Fields: map[string]interface{}{}}, userAdminID) - require.NoError(t, err) - board2, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 2", TeamID: "test-team", Type: model.BoardTypePrivate, MinimumRole: "viewer"}, userAdminID, true) - require.NoError(t, err) - - rBoard2, err := th.Server.App().GetBoard(board2.ID) - require.NoError(t, err) - require.NotNil(t, rBoard2) - require.Equal(t, rBoard2, board2) - - boardMember, err := th.Server.App().GetMemberForBoard(board2.ID, userAdminID) - require.NoError(t, err) - require.NotNil(t, boardMember) - require.Equal(t, boardMember.UserID, userAdminID) - require.Equal(t, boardMember.BoardID, board2.ID) - - err = th.Server.App().InsertBlock(&model.Block{ID: "block-4", Title: "Test", Type: "card", BoardID: board2.ID, Fields: map[string]interface{}{}}, userAdminID) - require.NoError(t, err) - - err = th.Server.App().UpsertSharing(model.Sharing{ID: board2.ID, Enabled: true, Token: "valid", ModifiedBy: userAdminID, UpdateAt: model.GetMillis()}) - require.NoError(t, err) - - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userViewerID, SchemeViewer: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userViewerID, SchemeViewer: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userCommenterID, SchemeCommenter: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userCommenterID, SchemeCommenter: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userEditorID, SchemeEditor: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userEditorID, SchemeEditor: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userViewerID, SchemeViewer: true}) - require.NoError(t, err) - - boardMember, err = th.Server.App().GetMemberForBoard(board1.ID, userViewerID) - require.NoError(t, err) - require.NotNil(t, boardMember) - require.Equal(t, boardMember.UserID, userViewerID) - require.Equal(t, boardMember.BoardID, board1.ID) - - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userViewerID, SchemeViewer: true}) - require.NoError(t, err) - - boardMember, err = th.Server.App().GetMemberForBoard(board2.ID, userViewerID) - require.NoError(t, err) - require.NotNil(t, boardMember) - require.Equal(t, boardMember.UserID, userViewerID) - require.Equal(t, boardMember.BoardID, board2.ID) - - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userCommenterID, SchemeCommenter: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userCommenterID, SchemeCommenter: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userEditorID, SchemeEditor: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userEditorID, SchemeEditor: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userGuestID, SchemeViewer: true}) - require.NoError(t, err) - - return TestData{ - publicBoard: board1, - privateBoard: board2, - publicTemplate: customTemplate1, - privateTemplate: customTemplate2, - } -} - -func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients Clients) { - for _, tc := range ttCases { - t.Run(tc.userRole+": "+tc.method+" "+tc.url, func(t *testing.T) { - reqClient := clients.Anon - switch tc.userRole { - case userAnon: - reqClient = clients.Anon - case userNoTeamMember: - reqClient = clients.NoTeamMember - case userTeamMember: - reqClient = clients.TeamMember - case userViewer: - reqClient = clients.Viewer - case userCommenter: - reqClient = clients.Commenter - case userEditor: - reqClient = clients.Editor - case userAdmin: - reqClient = clients.Admin - case userGuest: - if clients.Guest == nil { - return - } - reqClient = clients.Guest - } - - url := strings.ReplaceAll(tc.url, "{PRIVATE_BOARD_ID}", testData.privateBoard.ID) - url = strings.ReplaceAll(url, "{PUBLIC_BOARD_ID}", testData.publicBoard.ID) - url = strings.ReplaceAll(url, "{PUBLIC_TEMPLATE_ID}", testData.publicTemplate.ID) - url = strings.ReplaceAll(url, "{PRIVATE_TEMPLATE_ID}", testData.privateTemplate.ID) - url = strings.ReplaceAll(url, "{USER_ANON_ID}", userAnonID) - url = strings.ReplaceAll(url, "{USER_NO_TEAM_MEMBER_ID}", userNoTeamMemberID) - url = strings.ReplaceAll(url, "{USER_TEAM_MEMBER_ID}", userTeamMemberID) - url = strings.ReplaceAll(url, "{USER_VIEWER_ID}", userViewerID) - url = strings.ReplaceAll(url, "{USER_COMMENTER_ID}", userCommenterID) - url = strings.ReplaceAll(url, "{USER_EDITOR_ID}", userEditorID) - url = strings.ReplaceAll(url, "{USER_ADMIN_ID}", userAdminID) - url = strings.ReplaceAll(url, "{USER_GUEST_ID}", userGuestID) - - if strings.Contains(url, "{") || strings.Contains(url, "}") { - require.Fail(t, "Unreplaced tokens in url", url, tc.identifier()) - } - - var response *http.Response - var err error - switch tc.method { - case methodGet: - response, err = reqClient.DoAPIGet(url, "") - defer response.Body.Close() - case methodPost: - response, err = reqClient.DoAPIPost(url, tc.body) - defer response.Body.Close() - case methodPatch: - response, err = reqClient.DoAPIPatch(url, tc.body) - defer response.Body.Close() - case methodPut: - response, err = reqClient.DoAPIPut(url, tc.body) - defer response.Body.Close() - case methodDelete: - response, err = reqClient.DoAPIDelete(url, tc.body) - defer response.Body.Close() - } - - require.Equal(t, tc.expectedStatusCode, response.StatusCode, tc.identifier()) - if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { - require.NoError(t, err, tc.identifier()) - } - if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { - body, err := io.ReadAll(response.Body) - if err != nil { - require.Fail(t, err.Error(), tc.identifier()) - } - if strings.HasPrefix(string(body), "[") { - var data []interface{} - err = json.Unmarshal(body, &data) - if err != nil { - require.Fail(t, err.Error(), tc.identifier()) - } - require.Len(t, data, tc.totalResults, tc.identifier()) - } else { - if tc.totalResults > 0 { - require.Equal(t, 1, tc.totalResults) - require.Greater(t, len(string(body)), 2, tc.identifier()) - } else { - require.Len(t, string(body), 2, tc.identifier()) - } - } - } - }) - } -} - -func TestPermissionsGetTeamBoards(t *testing.T) { - ttCases := []TestCase{ - {"/teams/test-team/boards", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/boards", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/boards", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/boards", methodGet, "", userViewer, http.StatusOK, 2}, - {"/teams/test-team/boards", methodGet, "", userCommenter, http.StatusOK, 2}, - {"/teams/test-team/boards", methodGet, "", userEditor, http.StatusOK, 2}, - {"/teams/test-team/boards", methodGet, "", userAdmin, http.StatusOK, 2}, - {"/teams/test-team/boards", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsSearchTeamBoards(t *testing.T) { - ttCases := []TestCase{ - // Search boards - {"/teams/test-team/boards/search?q=b", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userViewer, http.StatusOK, 2}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userCommenter, http.StatusOK, 2}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userEditor, http.StatusOK, 2}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userAdmin, http.StatusOK, 2}, - {"/teams/test-team/boards/search?q=b", methodGet, "", userGuest, http.StatusOK, 1}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsSearchTeamLinkableBoards(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - // Search boards - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userTeamMember, http.StatusOK, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userViewer, http.StatusOK, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userCommenter, http.StatusOK, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userEditor, http.StatusOK, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userAdmin, http.StatusOK, 2}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - // Search boards - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userNoTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userViewer, http.StatusNotImplemented, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userCommenter, http.StatusNotImplemented, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userEditor, http.StatusNotImplemented, 0}, - {"/teams/test-team/boards/search/linkable?q=b", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetTeamTemplates(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper) { - err := th.Server.App().InitTemplates() - require.NoError(t, err, "InitTemplates should succeed") - } - - builtInTemplateCount := 13 - - ttCases := []TestCase{ - // Get Team Boards - {"/teams/test-team/templates", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/templates", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/templates", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/templates", methodGet, "", userViewer, http.StatusOK, 2}, - {"/teams/test-team/templates", methodGet, "", userCommenter, http.StatusOK, 2}, - {"/teams/test-team/templates", methodGet, "", userEditor, http.StatusOK, 2}, - {"/teams/test-team/templates", methodGet, "", userAdmin, http.StatusOK, 2}, - {"/teams/test-team/templates", methodGet, "", userGuest, http.StatusForbidden, 0}, - // Built-in templates - {"/teams/0/templates", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/0/templates", methodGet, "", userNoTeamMember, http.StatusOK, builtInTemplateCount}, - {"/teams/0/templates", methodGet, "", userTeamMember, http.StatusOK, builtInTemplateCount}, - {"/teams/0/templates", methodGet, "", userViewer, http.StatusOK, builtInTemplateCount}, - {"/teams/0/templates", methodGet, "", userCommenter, http.StatusOK, builtInTemplateCount}, - {"/teams/0/templates", methodGet, "", userEditor, http.StatusOK, builtInTemplateCount}, - {"/teams/0/templates", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateBoard(t *testing.T) { - publicBoard := toJSON(t, model.Board{Title: "Board To Create", TeamID: "test-team", Type: model.BoardTypeOpen}) - privateBoard := toJSON(t, model.Board{Title: "Board To Create", TeamID: "test-team", Type: model.BoardTypeOpen}) - - ttCases := []TestCase{ - // Create Public boards - {"/boards", methodPost, publicBoard, userAnon, http.StatusUnauthorized, 0}, - {"/boards", methodPost, publicBoard, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards", methodPost, publicBoard, userGuest, http.StatusForbidden, 0}, - {"/boards", methodPost, publicBoard, userTeamMember, http.StatusOK, 1}, - - // Create private boards - {"/boards", methodPost, privateBoard, userAnon, http.StatusUnauthorized, 0}, - {"/boards", methodPost, privateBoard, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards", methodPost, privateBoard, userGuest, http.StatusForbidden, 0}, - {"/boards", methodPost, privateBoard, userTeamMember, http.StatusOK, 1}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - ttCases[5].expectedStatusCode = http.StatusOK - ttCases[5].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetBoard(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userGuest, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[9].expectedStatusCode = http.StatusOK - ttCases[9].totalResults = 1 - ttCases[25].expectedStatusCode = http.StatusOK - ttCases[25].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetBoardPublic(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userAnon, http.StatusUnauthorized, 1}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userTeamMember, http.StatusForbidden, 1}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - cfg := th.Server.Config() - cfg.EnablePublicSharedBoards = false - th.Server.UpdateAppConfig() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - cfg := th.Server.Config() - cfg.EnablePublicSharedBoards = false - th.Server.UpdateAppConfig() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoard(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoardType(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoardMinimumRole(t *testing.T) { - patch := toJSON(t, map[string]model.BoardRole{"minimumRole": model.BoardRoleViewer}) - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoardChannelId(t *testing.T) { - patch := toJSON(t, map[string]string{"channelId": "valid-channel-id"}) - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteBoard(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDuplicateBoard(t *testing.T) { - // In same team - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - t.Run("plugin-same-team", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local-same-team", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[25].expectedStatusCode = http.StatusOK - ttCases[25].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) - - // In other team - ttCases = []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - t.Run("plugin-other-team", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local-other-team", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[25].expectedStatusCode = http.StatusOK - ttCases[25].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) - - // In empty team - ttCases = []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=empty-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=empty-team", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - t.Run("plugin-empty-team", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetBoardBlocks(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userGuest, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[25].expectedStatusCode = http.StatusOK - ttCases[25].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateBoardBlocks(t *testing.T) { - ttCasesF := func(testData TestData) []TestCase { - counter := 0 - newBlockJSON := func(boardID string) string { - counter++ - return toJSON(t, []*model.Block{{ - ID: fmt.Sprintf("%d", counter), - Title: "Board To Create", - BoardID: boardID, - Type: "card", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }}) - } - - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userGuest, http.StatusForbidden, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateBoardComments(t *testing.T) { - ttCasesF := func(testData TestData) []TestCase { - counter := 0 - newBlockJSON := func(boardID string) string { - counter++ - return toJSON(t, []*model.Block{{ - ID: fmt.Sprintf("%d", counter), - Title: "Comment to create", - BoardID: boardID, - Type: model.TypeComment, - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }}) - } - - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoardBlocks(t *testing.T) { - newBlocksPatchJSON := func(blockID string) string { - newTitle := "New Patch Block Title" - return toJSON(t, model.BlockPatchBatch{ - BlockIDs: []string{blockID}, - BlockPatches: []model.BlockPatch{ - {Title: &newTitle}, - }, - }) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsPatchBoardBlock(t *testing.T) { - newTitle := "New Patch Title" - patchJSON := toJSON(t, model.BlockPatch{Title: &newTitle}) - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/blockID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3", methodPatch, patchJSON, userAdmin, http.StatusNotFound, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteBoardBlock(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - err := th.Server.App().InsertBlock(&model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-7", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/blockID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3", methodDelete, "", userAdmin, http.StatusNotFound, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUndeleteBoardBlock(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - err := th.Server.App().InsertBlock(&model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-1", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-2", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-3", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-4", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-5", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-6", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-7", userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBlock("block-8", userAdmin) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/blockID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/undelete", methodPost, "", userAdmin, http.StatusNotFound, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsMoveContentBlock(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - err := th.Server.App().InsertBlock(&model.Block{ID: "content-1-1", Title: "Test", Type: "text", BoardID: testData.publicTemplate.ID, ParentID: "block-1"}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-1-2", Title: "Test", Type: "text", BoardID: testData.publicTemplate.ID, ParentID: "block-1"}, userAdmin) - require.NoError(t, err) - _, err = th.Server.App().PatchBlock("block-1", &model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": []string{"content-1-1", "content-1-2"}}}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-2-1", Title: "Test", Type: "text", BoardID: testData.privateTemplate.ID, ParentID: "block-2"}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-2-2", Title: "Test", Type: "text", BoardID: testData.privateTemplate.ID, ParentID: "block-2"}, userAdmin) - require.NoError(t, err) - _, err = th.Server.App().PatchBlock("block-2", &model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": []string{"content-2-1", "content-2-2"}}}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-3-1", Title: "Test", Type: "text", BoardID: testData.publicBoard.ID, ParentID: "block-3"}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-3-2", Title: "Test", Type: "text", BoardID: testData.publicBoard.ID, ParentID: "block-3"}, userAdmin) - require.NoError(t, err) - _, err = th.Server.App().PatchBlock("block-3", &model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": []string{"content-3-1", "content-3-2"}}}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-4-1", Title: "Test", Type: "text", BoardID: testData.privateBoard.ID, ParentID: "block-4"}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "content-4-2", Title: "Test", Type: "text", BoardID: testData.privateBoard.ID, ParentID: "block-4"}, userAdmin) - require.NoError(t, err) - _, err = th.Server.App().PatchBlock("block-4", &model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": []string{"content-4-1", "content-4-2"}}}, userAdmin) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userAnon, http.StatusUnauthorized, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userNoTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userViewer, http.StatusForbidden, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userCommenter, http.StatusForbidden, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userEditor, http.StatusOK, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userAdmin, http.StatusOK, 0}, - {"/content-blocks/content-4-1/moveto/after/content-4-2", methodPost, "{}", userGuest, http.StatusForbidden, 0}, - - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userAnon, http.StatusUnauthorized, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userNoTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userViewer, http.StatusForbidden, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userCommenter, http.StatusForbidden, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userEditor, http.StatusOK, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userAdmin, http.StatusOK, 0}, - {"/content-blocks/content-3-1/moveto/after/content-3-2", methodPost, "{}", userGuest, http.StatusForbidden, 0}, - - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userAnon, http.StatusUnauthorized, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userNoTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userViewer, http.StatusForbidden, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userCommenter, http.StatusForbidden, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userEditor, http.StatusOK, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userAdmin, http.StatusOK, 0}, - {"/content-blocks/content-2-1/moveto/after/content-2-2", methodPost, "{}", userGuest, http.StatusForbidden, 0}, - - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userAnon, http.StatusUnauthorized, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userNoTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userTeamMember, http.StatusForbidden, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userViewer, http.StatusForbidden, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userCommenter, http.StatusForbidden, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userEditor, http.StatusOK, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userAdmin, http.StatusOK, 0}, - {"/content-blocks/content-1-1/moveto/after/content-1-2", methodPost, "{}", userGuest, http.StatusForbidden, 0}, - - // Invalid srcBlockID/dstBlockID combination - {"/content-blocks/content-1-1/moveto/after/content-2-1", methodPost, "{}", userAdmin, http.StatusBadRequest, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUndeleteBoard(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - err := th.Server.App().DeleteBoard(testData.publicBoard.ID, userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBoard(testData.privateBoard.ID, userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBoard(testData.publicTemplate.ID, userAdmin) - require.NoError(t, err) - err = th.Server.App().DeleteBoard(testData.privateTemplate.ID, userAdmin) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/undelete", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDuplicateBoardBlock(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - err := th.Server.App().InsertBlock(&model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) - require.NoError(t, err) - err = th.Server.App().InsertBlock(&model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/blockID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetBoardMembers(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userViewer, http.StatusOK, 5}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userCommenter, http.StatusOK, 5}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userEditor, http.StatusOK, 5}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userAdmin, http.StatusOK, 5}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userGuest, http.StatusOK, 5}, - - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateBoardMembers(t *testing.T) { - ttCasesF := func(testData TestData) []TestCase { - boardMemberJSON := func(boardID string) string { - return toJSON(t, model.BoardMember{ - BoardID: boardID, - UserID: userTeamMemberID, - SchemeEditor: true, - }) - } - - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userGuest, http.StatusForbidden, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUpdateBoardMember(t *testing.T) { - ttCasesF := func(testData TestData) []TestCase { - boardMemberJSON := func(boardID string) string { - return toJSON(t, model.BoardMember{ - BoardID: boardID, - UserID: userTeamMember, - SchemeEditor: false, - SchemeViewer: true, - }) - } - - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicBoard.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.privateTemplate.ID), userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_VIEWER_ID}", methodPut, boardMemberJSON(testData.publicTemplate.ID), userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/memberID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodPut, "", userAdmin, http.StatusBadRequest, 0}, - - // Invalid boardID - {"/boards/invalid/members/{USER_VIEWER_ID}", methodPut, "", userAdmin, http.StatusBadRequest, 0}, - - // Invalid memberID - {"/boards/{PUBLIC_TEMPLATE_ID}/members/invalid", methodPut, "", userAdmin, http.StatusBadRequest, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteBoardMember(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: userTeamMemberID, SchemeViewer: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: userTeamMemberID, SchemeViewer: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: userTeamMemberID, SchemeViewer: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: userTeamMemberID, SchemeViewer: true}) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userGuest, http.StatusForbidden, 0}, - - // Invalid boardID/memberID combination - {"/boards/{PUBLIC_TEMPLATE_ID}/members/{USER_TEAM_MEMBER_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - - // Invalid boardID - {"/boards/invalid/members/{USER_VIEWER_ID}", methodDelete, "", userAdmin, http.StatusNotFound, 0}, - - // Invalid memberID - {"/boards/{PUBLIC_TEMPLATE_ID}/members/invalid", methodDelete, "", userAdmin, http.StatusOK, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsJoinBoardAsMember(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - - // Do we want to forbid already existing members to join to the board or simply return the current membership? - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userAdmin, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[9].expectedStatusCode = http.StatusOK - ttCases[9].totalResults = 1 - ttCases[25].expectedStatusCode = http.StatusOK - ttCases[25].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsLeaveBoardAsMember(t *testing.T) { - extraSetup := func(t *testing.T, th *TestHelper, testData TestData) { - _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: "not-real-user", SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: "not-real-user", SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: "not-real-user", SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: "not-real-user", SchemeAdmin: true}) - require.NoError(t, err) - } - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userGuest, http.StatusOK, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - - // Last admin leave should fail - extraSetup = func(t *testing.T, th *TestHelper, testData TestData) { - _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: userAdminID, SchemeAdmin: true}) - require.NoError(t, err) - - require.NoError(t, th.Server.App().DeleteBoardMember(testData.publicBoard.ID, "not-real-user")) - require.NoError(t, th.Server.App().DeleteBoardMember(testData.privateBoard.ID, "not-real-user")) - require.NoError(t, th.Server.App().DeleteBoardMember(testData.publicTemplate.ID, "not-real-user")) - require.NoError(t, th.Server.App().DeleteBoardMember(testData.privateTemplate.ID, "not-real-user")) - } - - ttCases = []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, - {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsShareBoard(t *testing.T) { - sharing := toJSON(t, model.Sharing{Enabled: true, Token: "test-token"}) - - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetSharedBoardInfo(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - - clients.Admin.PostSharing(&model.Sharing{ID: testData.publicBoard.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.privateBoard.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.publicTemplate.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.privateTemplate.ID, Enabled: true, Token: "test-token"}) - - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - - clients.Admin.PostSharing(&model.Sharing{ID: testData.publicBoard.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.privateBoard.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.publicTemplate.ID, Enabled: true, Token: "test-token"}) - clients.Admin.PostSharing(&model.Sharing{ID: testData.privateTemplate.ID, Enabled: true, Token: "test-token"}) - - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsListTeams(t *testing.T) { - ttCases := []TestCase{ - {"/teams", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams", methodGet, "", userNoTeamMember, http.StatusOK, 0}, - {"/teams", methodGet, "", userTeamMember, http.StatusOK, 2}, - {"/teams", methodGet, "", userViewer, http.StatusOK, 2}, - {"/teams", methodGet, "", userCommenter, http.StatusOK, 2}, - {"/teams", methodGet, "", userEditor, http.StatusOK, 2}, - {"/teams", methodGet, "", userAdmin, http.StatusOK, 2}, - {"/teams", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - for i := range ttCases { - ttCases[i].totalResults = 1 - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetTeam(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userGuest, http.StatusOK, 1}, - - {"/teams/empty-team", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userAdmin, http.StatusForbidden, 0}, - {"/teams/empty-team", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team", methodGet, "", userNoTeamMember, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team", methodGet, "", userGuest, http.StatusOK, 1}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsRegenerateSignupToken(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, - - {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusOK, 0}, - - {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusOK, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetTeamUsers(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/users", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/users", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/users", methodGet, "", userTeamMember, http.StatusOK, 6}, - {"/teams/test-team/users", methodGet, "", userViewer, http.StatusOK, 6}, - {"/teams/test-team/users", methodGet, "", userCommenter, http.StatusOK, 6}, - {"/teams/test-team/users", methodGet, "", userEditor, http.StatusOK, 6}, - {"/teams/test-team/users", methodGet, "", userAdmin, http.StatusOK, 6}, - {"/teams/test-team/users", methodGet, "", userGuest, http.StatusOK, 5}, - - {"/teams/empty-team/users", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team/users", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userAdmin, http.StatusForbidden, 0}, - {"/teams/empty-team/users", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/users", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/users", methodGet, "", userNoTeamMember, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userTeamMember, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userViewer, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userCommenter, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userEditor, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userAdmin, http.StatusOK, 7}, - {"/teams/test-team/users", methodGet, "", userGuest, http.StatusOK, 7}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsTeamArchiveExport(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/archive/export", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, - - {"/teams/empty-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team/archive/export", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, - } - - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - - {"/teams/empty-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/empty-team/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUploadFile(t *testing.T) { - ttCases := []TestCase{ - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userGuest, http.StatusForbidden, 0}, - - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions - {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetMe(t *testing.T) { - ttCases := []TestCase{ - {"/users/me", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/users/me", methodGet, "", userNoTeamMember, http.StatusOK, 1}, - {"/users/me", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/users/me", methodGet, "", userViewer, http.StatusOK, 1}, - {"/users/me", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/users/me", methodGet, "", userEditor, http.StatusOK, 1}, - {"/users/me", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/users/me", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetMyMemberships(t *testing.T) { - ttCases := []TestCase{ - {"/users/me/memberships", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/users/me/memberships", methodGet, "", userNoTeamMember, http.StatusOK, 0}, - {"/users/me/memberships", methodGet, "", userTeamMember, http.StatusOK, 0}, - {"/users/me/memberships", methodGet, "", userViewer, http.StatusOK, 4}, - {"/users/me/memberships", methodGet, "", userCommenter, http.StatusOK, 4}, - {"/users/me/memberships", methodGet, "", userEditor, http.StatusOK, 4}, - {"/users/me/memberships", methodGet, "", userAdmin, http.StatusOK, 4}, - {"/users/me/memberships", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetUser(t *testing.T) { - ttCases := []TestCase{ - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userNoTeamMember, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/users/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userGuest, http.StatusNotFound, 0}, - - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userNoTeamMember, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}", methodGet, "", userGuest, http.StatusNotFound, 0}, - - {"/users/{USER_VIEWER_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userNoTeamMember, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/users/{USER_VIEWER_ID}", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUserChangePassword(t *testing.T) { - postBody := toJSON(t, model.ChangePasswordRequest{ - OldPassword: password, - NewPassword: "newpa$$word123", - }) - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/users/{USER_ADMIN_ID}/changepassword", methodPost, postBody, userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_ADMIN_ID}/changepassword", methodPost, postBody, userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/users/{USER_ADMIN_ID}/changepassword", methodPost, postBody, userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_ADMIN_ID}/changepassword", methodPost, postBody, userAdmin, http.StatusOK, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUpdateUserConfig(t *testing.T) { - patch := toJSON(t, model.UserPreferencesPatch{UpdatedFields: map[string]string{"test": "test"}}) - - ttCases := []TestCase{ - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userAnon, http.StatusUnauthorized, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userNoTeamMember, http.StatusForbidden, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userTeamMember, http.StatusOK, 1}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userViewer, http.StatusForbidden, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userCommenter, http.StatusForbidden, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userEditor, http.StatusForbidden, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userAdmin, http.StatusForbidden, 0}, - {"/users/{USER_TEAM_MEMBER_ID}/config", methodPut, patch, userGuest, http.StatusForbidden, 0}, - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateBoardsAndBlocks(t *testing.T) { - bab := toJSON(t, model.BoardsAndBlocks{ - Boards: []*model.Board{{ID: "test", Title: "Test Board", TeamID: "test-team"}}, - Blocks: []*model.Block{ - {ID: "test-block", BoardID: "test", Type: "card", CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - }, - }) - - ttCases := []TestCase{ - {"/boards-and-blocks", methodPost, bab, userAnon, http.StatusUnauthorized, 0}, - {"/boards-and-blocks", methodPost, bab, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPost, bab, userTeamMember, http.StatusOK, 1}, - {"/boards-and-blocks", methodPost, bab, userViewer, http.StatusOK, 1}, - {"/boards-and-blocks", methodPost, bab, userCommenter, http.StatusOK, 1}, - {"/boards-and-blocks", methodPost, bab, userEditor, http.StatusOK, 1}, - {"/boards-and-blocks", methodPost, bab, userAdmin, http.StatusOK, 1}, - {"/boards-and-blocks", methodPost, bab, userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUpdateBoardsAndBlocks(t *testing.T) { - ttCasesF := func(t *testing.T, testData TestData) []TestCase { - newTitle := "New Block Title" - bab := toJSON(t, model.PatchBoardsAndBlocks{ - BoardIDs: []string{testData.publicBoard.ID}, - BoardPatches: []*model.BoardPatch{{Title: &newTitle}}, - BlockIDs: []string{"block-3"}, - BlockPatches: []*model.BlockPatch{{Title: &newTitle}}, - }) - - return []TestCase{ - {"/boards-and-blocks", methodPatch, bab, userAnon, http.StatusUnauthorized, 0}, - {"/boards-and-blocks", methodPatch, bab, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userViewer, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userCommenter, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userEditor, http.StatusOK, 1}, - {"/boards-and-blocks", methodPatch, bab, userAdmin, http.StatusOK, 1}, - {"/boards-and-blocks", methodPatch, bab, userGuest, http.StatusForbidden, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - runTestCases(t, ttCases, testData, clients) - }) - - // With type change - ttCasesF = func(t *testing.T, testData TestData) []TestCase { - newType := model.BoardTypePrivate - newTitle := "New Block Title" - bab := toJSON(t, model.PatchBoardsAndBlocks{ - BoardIDs: []string{testData.publicBoard.ID}, - BoardPatches: []*model.BoardPatch{{Type: &newType}}, - BlockIDs: []string{"block-3"}, - BlockPatches: []*model.BlockPatch{{Title: &newTitle}}, - }) - - return []TestCase{ - {"/boards-and-blocks", methodPatch, bab, userAnon, http.StatusUnauthorized, 0}, - {"/boards-and-blocks", methodPatch, bab, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userViewer, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userCommenter, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userEditor, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodPatch, bab, userAdmin, http.StatusOK, 1}, - {"/boards-and-blocks", methodPatch, bab, userGuest, http.StatusForbidden, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteBoardsAndBlocks(t *testing.T) { - ttCasesF := func(t *testing.T, testData TestData) []TestCase { - bab := toJSON(t, model.DeleteBoardsAndBlocks{ - Boards: []string{testData.publicBoard.ID}, - Blocks: []string{"block-3"}, - }) - return []TestCase{ - {"/boards-and-blocks", methodDelete, bab, userAnon, http.StatusUnauthorized, 0}, - {"/boards-and-blocks", methodDelete, bab, userNoTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userTeamMember, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userViewer, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userCommenter, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userEditor, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userGuest, http.StatusForbidden, 0}, - {"/boards-and-blocks", methodDelete, bab, userAdmin, http.StatusOK, 0}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - - _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: userGuestID, SchemeViewer: true}) - require.NoError(t, err) - - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, testData) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsLogin(t *testing.T) { - loginReq := func(username, password string) string { - return toJSON(t, model.LoginRequest{ - Type: "normal", - Username: username, - Password: password, - }) - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/login", methodPost, loginReq(userAnon, password), userAnon, http.StatusNotImplemented, 0}, - {"/login", methodPost, loginReq(userAdmin, password), userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/login", methodPost, loginReq(userAnon, password), userAnon, http.StatusUnauthorized, 0}, - {"/login", methodPost, loginReq(userAdmin, password), userAdmin, http.StatusOK, 1}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsLogout(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/logout", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/logout", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/logout", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/logout", methodPost, "", userAdmin, http.StatusOK, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsRegister(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/register", methodPost, "", userAnon, http.StatusNotImplemented, 0}, - {"/register", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - - team, resp := th.Client.GetTeam(model.GlobalTeamID) - th.CheckOK(resp) - require.NotNil(th.T, team) - require.NotNil(th.T, team.SignupToken) - - postData := toJSON(t, model.RegisterRequest{ - Username: "newuser", - Email: "newuser@test.com", - Password: password, - Token: team.SignupToken, - }) - - ttCases := []TestCase{ - {"/register", methodPost, postData, userAnon, http.StatusOK, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsClientConfig(t *testing.T) { - ttCases := []TestCase{ - {"/clientConfig", methodGet, "", userAnon, http.StatusOK, 1}, - {"/clientConfig", methodGet, "", userAdmin, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetCategories(t *testing.T) { - ttCases := []TestCase{ - {"/teams/test-team/categories", methodGet, "", userAnon, http.StatusUnauthorized, 1}, - {"/teams/test-team/categories", methodGet, "", userNoTeamMember, http.StatusForbidden, 1}, - {"/teams/test-team/categories", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/categories", methodGet, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/categories", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/categories", methodGet, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/categories", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team/categories", methodGet, "", userGuest, http.StatusOK, 1}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateCategory(t *testing.T) { - ttCasesF := func() []TestCase { - category := func(userID string) string { - return toJSON(t, model.Category{ - Name: "Test category", - TeamID: "test-team", - UserID: userID, - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }) - } - - return []TestCase{ - {"/teams/test-team/categories", methodPost, category(""), userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories", methodPost, category(userNoTeamMemberID), userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/categories", methodPost, category(userTeamMemberID), userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/categories", methodPost, category(userViewerID), userViewer, http.StatusOK, 1}, - {"/teams/test-team/categories", methodPost, category(userCommenterID), userCommenter, http.StatusOK, 1}, - {"/teams/test-team/categories", methodPost, category(userEditorID), userEditor, http.StatusOK, 1}, - {"/teams/test-team/categories", methodPost, category(userAdminID), userAdmin, http.StatusOK, 1}, - {"/teams/test-team/categories", methodPost, category(userGuestID), userGuest, http.StatusOK, 1}, - - {"/teams/test-team/categories", methodPost, category("other"), userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userNoTeamMember, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userTeamMember, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userViewer, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userCommenter, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userEditor, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userAdmin, http.StatusBadRequest, 0}, - {"/teams/test-team/categories", methodPost, category("other"), userGuest, http.StatusBadRequest, 0}, - - {"/teams/other-team/categories", methodPost, category(""), userAnon, http.StatusUnauthorized, 0}, - {"/teams/other-team/categories", methodPost, category(userNoTeamMemberID), userNoTeamMember, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userTeamMemberID), userTeamMember, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userViewerID), userViewer, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userCommenterID), userCommenter, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userEditorID), userEditor, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userAdminID), userAdmin, http.StatusBadRequest, 0}, - {"/teams/other-team/categories", methodPost, category(userGuestID), userGuest, http.StatusBadRequest, 0}, - } - } - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF() - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF() - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUpdateCategory(t *testing.T) { - ttCasesF := func(extraData map[string]string) []TestCase { - category := func(userID string, categoryID string) string { - return toJSON(t, model.Category{ - ID: categoryID, - Name: "Test category", - TeamID: "test-team", - UserID: userID, - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - Type: "custom", - }) - } - - return []TestCase{ - {"/teams/test-team/categories/any", methodPut, category("", "any"), userAnonID, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories/" + extraData["noTeamMember"], methodPut, category(userNoTeamMemberID, extraData["noTeamMember"]), userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/categories/" + extraData["teamMember"], methodPut, category(userTeamMemberID, extraData["teamMember"]), userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["viewer"], methodPut, category(userViewerID, extraData["viewer"]), userViewer, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["commenter"], methodPut, category(userCommenterID, extraData["commenter"]), userCommenter, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["editor"], methodPut, category(userEditorID, extraData["editor"]), userEditor, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["admin"], methodPut, category(userAdminID, extraData["admin"]), userAdmin, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["guest"], methodPut, category(userGuestID, extraData["guest"]), userGuest, http.StatusOK, 1}, - - {"/teams/test-team/categories/any", methodPut, category("other", "any"), userAnonID, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories/" + extraData["noTeamMember"], methodPut, category("other", extraData["noTeamMember"]), userNoTeamMember, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["teamMember"], methodPut, category("other", extraData["teamMember"]), userTeamMember, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["viewer"], methodPut, category("other", extraData["viewer"]), userViewer, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["commenter"], methodPut, category("other", extraData["commenter"]), userCommenter, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["editor"], methodPut, category("other", extraData["editor"]), userEditor, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["admin"], methodPut, category("other", extraData["admin"]), userAdmin, http.StatusBadRequest, 0}, - {"/teams/test-team/categories/" + extraData["guest"], methodPut, category("other", extraData["guest"]), userGuest, http.StatusBadRequest, 0}, - - {"/teams/other-team/categories/any", methodPut, category("", "any"), userAnonID, http.StatusUnauthorized, 0}, - {"/teams/other-team/categories/" + extraData["noTeamMember"], methodPut, category(userNoTeamMemberID, extraData["noTeamMember"]), userNoTeamMember, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["teamMember"], methodPut, category(userTeamMemberID, extraData["teamMember"]), userTeamMember, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["viewer"], methodPut, category(userViewerID, extraData["viewer"]), userViewer, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["commenter"], methodPut, category(userCommenterID, extraData["commenter"]), userCommenter, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["editor"], methodPut, category(userEditorID, extraData["editor"]), userEditor, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["admin"], methodPut, category(userAdminID, extraData["admin"]), userAdmin, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["guest"], methodPut, category(userGuestID, extraData["guest"]), userGuest, http.StatusBadRequest, 0}, - } - } - - extraSetup := func(t *testing.T, th *TestHelper) map[string]string { - categoryNoTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryViewer, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewerID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryCommenter, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenterID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryEditor, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditorID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryAdmin, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdminID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryGuest, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userGuestID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - return map[string]string{ - "noTeamMember": categoryNoTeamMember.ID, - "teamMember": categoryTeamMember.ID, - "viewer": categoryViewer.ID, - "commenter": categoryCommenter.ID, - "editor": categoryEditor.ID, - "admin": categoryAdmin.ID, - "guest": categoryGuest.ID, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(extraData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(extraData) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteCategory(t *testing.T) { - ttCasesF := func(extraData map[string]string) []TestCase { - return []TestCase{ - {"/teams/other-team/categories/any", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/other-team/categories/" + extraData["noTeamMember"], methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/other-team/categories/" + extraData["teamMember"], methodDelete, "", userTeamMember, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["viewer"], methodDelete, "", userViewer, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["commenter"], methodDelete, "", userCommenter, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["editor"], methodDelete, "", userEditor, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["admin"], methodDelete, "", userAdmin, http.StatusBadRequest, 0}, - {"/teams/other-team/categories/" + extraData["guest"], methodDelete, "", userGuest, http.StatusBadRequest, 0}, - - {"/teams/test-team/categories/any", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories/" + extraData["noTeamMember"], methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/categories/" + extraData["teamMember"], methodDelete, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["viewer"], methodDelete, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["commenter"], methodDelete, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["editor"], methodDelete, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["admin"], methodDelete, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team/categories/" + extraData["guest"], methodDelete, "", userGuest, http.StatusOK, 1}, - } - } - - extraSetup := func(t *testing.T, th *TestHelper) map[string]string { - categoryNoTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryViewer, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewerID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryCommenter, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenterID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryEditor, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditorID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryAdmin, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdminID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryGuest, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userGuestID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - return map[string]string{ - "noTeamMember": categoryNoTeamMember.ID, - "teamMember": categoryTeamMember.ID, - "viewer": categoryViewer.ID, - "commenter": categoryCommenter.ID, - "editor": categoryEditor.ID, - "admin": categoryAdmin.ID, - "guest": categoryGuest.ID, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(extraData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(extraData) - ttCases[1].expectedStatusCode = http.StatusBadRequest - ttCases[1].totalResults = 0 - ttCases[9].expectedStatusCode = http.StatusOK - ttCases[9].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsUpdateCategoryBoard(t *testing.T) { - ttCasesF := func(testData TestData, extraData map[string]string) []TestCase { - return []TestCase{ - {"/teams/test-team/categories/any/boards/any", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/categories/" + extraData["noTeamMember"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/categories/" + extraData["teamMember"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userTeamMember, http.StatusOK, 0}, - {"/teams/test-team/categories/" + extraData["viewer"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userViewer, http.StatusOK, 0}, - {"/teams/test-team/categories/" + extraData["commenter"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userCommenter, http.StatusOK, 0}, - {"/teams/test-team/categories/" + extraData["editor"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userEditor, http.StatusOK, 0}, - {"/teams/test-team/categories/" + extraData["admin"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userAdmin, http.StatusOK, 0}, - {"/teams/test-team/categories/" + extraData["guest"] + "/boards/" + testData.publicBoard.ID, methodPost, "", userGuest, http.StatusOK, 0}, - } - } - - extraSetup := func(t *testing.T, th *TestHelper) map[string]string { - categoryNoTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryTeamMember, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMemberID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryViewer, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewerID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryCommenter, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenterID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryEditor, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditorID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryAdmin, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdminID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - categoryGuest, err := th.Server.App().CreateCategory( - &model.Category{Name: "Test category", TeamID: "test-team", UserID: userGuestID, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, - ) - require.NoError(t, err) - return map[string]string{ - "noTeamMember": categoryNoTeamMember.ID, - "teamMember": categoryTeamMember.ID, - "viewer": categoryViewer.ID, - "commenter": categoryCommenter.ID, - "editor": categoryEditor.ID, - "admin": categoryAdmin.ID, - "guest": categoryGuest.ID, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(testData, extraData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraData := extraSetup(t, th) - ttCases := ttCasesF(testData, extraData) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 0 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetFile(t *testing.T) { - ttCasesF := func() []TestCase { - return []TestCase{ - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userViewer, http.StatusOK, 1}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userEditor, http.StatusOK, 1}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}", methodGet, "", userGuest, http.StatusOK, 1}, - - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/files/teams/test-team/{PRIVATE_BOARD_ID}/{NEW_FILE_ID}?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - - newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false) - require.NoError(t, err) - - ttCases := ttCasesF() - for i, tc := range ttCases { - ttCases[i].url = strings.Replace(tc.url, "{NEW_FILE_ID}", newFileID, 1) - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - - newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false) - require.NoError(t, err) - - ttCases := ttCasesF() - for i, tc := range ttCases { - ttCases[i].url = strings.Replace(tc.url, "{NEW_FILE_ID}", newFileID, 1) - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsCreateSubscription(t *testing.T) { - ttCases := func() []TestCase { - subscription := func(userID string) string { - return toJSON(t, model.Subscription{ - BlockType: "card", - BlockID: "block-3", - SubscriberType: "user", - SubscriberID: userID, - CreateAt: model.GetMillis(), - }) - } - return []TestCase{ - {"/subscriptions", methodPost, subscription(""), userAnon, http.StatusUnauthorized, 0}, - {"/subscriptions", methodPost, subscription(userNoTeamMemberID), userNoTeamMember, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userTeamMemberID), userTeamMember, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userViewerID), userViewer, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userCommenterID), userCommenter, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userEditorID), userEditor, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userAdminID), userAdmin, http.StatusOK, 1}, - {"/subscriptions", methodPost, subscription(userGuestID), userGuest, http.StatusOK, 1}, - } - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases(), testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases(), testData, clients) - }) -} - -func TestPermissionsGetSubscriptions(t *testing.T) { - ttCases := []TestCase{ - {"/subscriptions/{USER_ANON_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/subscriptions/{USER_NO_TEAM_MEMBER_ID}", methodGet, "", userNoTeamMember, http.StatusOK, 0}, - {"/subscriptions/{USER_TEAM_MEMBER_ID}", methodGet, "", userTeamMember, http.StatusOK, 0}, - {"/subscriptions/{USER_VIEWER_ID}", methodGet, "", userViewer, http.StatusOK, 0}, - {"/subscriptions/{USER_COMMENTER_ID}", methodGet, "", userCommenter, http.StatusOK, 0}, - {"/subscriptions/{USER_EDITOR_ID}", methodGet, "", userEditor, http.StatusOK, 0}, - {"/subscriptions/{USER_ADMIN_ID}", methodGet, "", userAdmin, http.StatusOK, 0}, - {"/subscriptions/{USER_GUEST_ID}", methodGet, "", userGuest, http.StatusOK, 0}, - - {"/subscriptions/other", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userAdmin, http.StatusForbidden, 0}, - {"/subscriptions/other", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsDeleteSubscription(t *testing.T) { - ttCases := []TestCase{ - {"/subscriptions/block-3/{USER_ANON_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, - {"/subscriptions/block-3/{USER_NO_TEAM_MEMBER_ID}", methodDelete, "", userNoTeamMember, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_TEAM_MEMBER_ID}", methodDelete, "", userTeamMember, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_VIEWER_ID}", methodDelete, "", userViewer, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_COMMENTER_ID}", methodDelete, "", userCommenter, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_EDITOR_ID}", methodDelete, "", userEditor, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_ADMIN_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, - {"/subscriptions/block-3/{USER_GUEST_ID}", methodDelete, "", userGuest, http.StatusOK, 0}, - - {"/subscriptions/block-3/other", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userViewer, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userCommenter, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userEditor, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userAdmin, http.StatusForbidden, 0}, - {"/subscriptions/block-3/other", methodDelete, "", userGuest, http.StatusForbidden, 0}, - } - - extraSetup := func(t *testing.T, th *TestHelper) { - _, err := th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userNoTeamMemberID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userTeamMemberID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userViewerID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userCommenterID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userEditorID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userAdminID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userGuestID, CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - _, err = th.Server.App().CreateSubscription( - &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: "other", CreateAt: model.GetMillis()}, - ) - require.NoError(t, err) - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - extraSetup(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - extraSetup(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsOnboard(t *testing.T) { - ttCases := []TestCase{ - {"/teams/test-team/onboard", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/onboard", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/onboard", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/onboard", methodPost, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/onboard", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/onboard", methodPost, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/onboard", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team/onboard", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - - err := th.Server.App().InitTemplates() - require.NoError(t, err, "InitTemplates should not fail") - - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - - err := th.Server.App().InitTemplates() - require.NoError(t, err, "InitTemplates should not fail") - - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsBoardArchiveExport(t *testing.T) { - ttCases := []TestCase{ - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userGuest, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userGuest, http.StatusForbidden, 0}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsBoardArchiveImport(t *testing.T) { - ttCases := []TestCase{ - {"/teams/test-team/archive/import", methodPost, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/archive/import", methodPost, "", userNoTeamMember, http.StatusForbidden, 1}, - {"/teams/test-team/archive/import", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userAdmin, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userGuest, http.StatusForbidden, 0}, - } - - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases[1].expectedStatusCode = http.StatusOK - ttCases[1].totalResults = 1 - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsMinimumRolesApplied(t *testing.T) { - ttCasesF := func(t *testing.T, th *TestHelper, minimumRole model.BoardRole, testData TestData) []TestCase { - counter := 0 - newBlockJSON := func(boardID string) string { - counter++ - return toJSON(t, []*model.Block{{ - ID: fmt.Sprintf("%d", counter), - Title: "Board To Create", - BoardID: boardID, - Type: "card", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }}) - } - _, err := th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.privateBoard.ID, userAdminID) - require.NoError(t, err) - _, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.publicBoard.ID, userAdminID) - require.NoError(t, err) - _, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.privateTemplate.ID, userAdminID) - require.NoError(t, err) - _, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.publicTemplate.ID, userAdminID) - require.NoError(t, err) - - if minimumRole == "viewer" || minimumRole == "commenter" { - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - } - } - return []TestCase{ - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, - - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, - {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, - } - } - - t.Run("plugin", func(t *testing.T) { - t.Run("minimum role viewer", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "viewer", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role commenter", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "commenter", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role editor", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "editor", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role admin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "admin", testData) - runTestCases(t, ttCases, testData, clients) - }) - }) - t.Run("local", func(t *testing.T) { - t.Run("minimum role viewer", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "viewer", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role commenter", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "commenter", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role editor", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "editor", testData) - runTestCases(t, ttCases, testData, clients) - }) - t.Run("minimum role admin", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := ttCasesF(t, th, "admin", testData) - runTestCases(t, ttCases, testData, clients) - }) - }) -} - -func TestPermissionsChannels(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/channels", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/channels", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/channels", methodGet, "", userTeamMember, http.StatusOK, 2}, - {"/teams/test-team/channels", methodGet, "", userViewer, http.StatusOK, 2}, - {"/teams/test-team/channels", methodGet, "", userCommenter, http.StatusOK, 2}, - {"/teams/test-team/channels", methodGet, "", userEditor, http.StatusOK, 2}, - {"/teams/test-team/channels", methodGet, "", userAdmin, http.StatusOK, 2}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/channels", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/channels", methodGet, "", userNoTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels", methodGet, "", userTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels", methodGet, "", userViewer, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels", methodGet, "", userCommenter, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels", methodGet, "", userEditor, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsChannel(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userAdmin, http.StatusOK, 1}, - - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/teams/test-team/channels/not-valid-channel-id", methodGet, "", userAdmin, http.StatusForbidden, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userNoTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userTeamMember, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userViewer, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userCommenter, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userEditor, http.StatusNotImplemented, 0}, - {"/teams/test-team/channels/valid-channel-id", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} - -func TestPermissionsGetStatistics(t *testing.T) { - t.Run("plugin", func(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - clients := setupClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/statistics", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/statistics", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, - {"/statistics", methodGet, "", userTeamMember, http.StatusForbidden, 0}, - {"/statistics", methodGet, "", userViewer, http.StatusForbidden, 0}, - {"/statistics", methodGet, "", userCommenter, http.StatusForbidden, 0}, - {"/statistics", methodGet, "", userEditor, http.StatusForbidden, 0}, - {"/statistics", methodGet, "", userAdmin, http.StatusOK, 1}, - {"/statistics", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) - t.Run("local", func(t *testing.T) { - th := SetupTestHelperLocalMode(t) - defer th.TearDown() - clients := setupLocalClients(th) - testData := setupData(t, th) - ttCases := []TestCase{ - {"/statistics", methodGet, "", userAnon, http.StatusUnauthorized, 0}, - {"/statistics", methodGet, "", userNoTeamMember, http.StatusNotImplemented, 0}, - {"/statistics", methodGet, "", userTeamMember, http.StatusNotImplemented, 0}, - {"/statistics", methodGet, "", userViewer, http.StatusNotImplemented, 0}, - {"/statistics", methodGet, "", userCommenter, http.StatusNotImplemented, 0}, - {"/statistics", methodGet, "", userEditor, http.StatusNotImplemented, 0}, - {"/statistics", methodGet, "", userAdmin, http.StatusNotImplemented, 1}, - {"/statistics", methodGet, "", userGuest, http.StatusForbidden, 0}, - } - runTestCases(t, ttCases, testData, clients) - }) -} diff --git a/server/boards/integrationtests/pluginteststore.go b/server/boards/integrationtests/pluginteststore.go deleted file mode 100644 index 8f25be52a0..0000000000 --- a/server/boards/integrationtests/pluginteststore.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "errors" - "os" - "strconv" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -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", - Username: "no-team-member", - Email: "no-team-member@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "team-member": { - ID: "team-member", - Username: "team-member", - Email: "team-member@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "viewer": { - ID: "viewer", - Username: "viewer", - Email: "viewer@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "commenter": { - ID: "commenter", - Username: "commenter", - Email: "commenter@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "editor": { - ID: "editor", - Username: "editor", - Email: "editor@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "admin": { - ID: "admin", - Username: "admin", - Email: "admin@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - }, - "guest": { - ID: "guest", - Username: "guest", - Email: "guest@sample.com", - CreateAt: model.GetMillis(), - UpdateAt: model.GetMillis(), - IsGuest: true, - }, - }, - 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", testTeamID: - 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 - case "guest": - return []*model.Team{s.testTeam}, 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) GetUserPreferences(userID string) (mm_model.Preferences, error) { - if userID == userTeamMember { - return mm_model.Preferences{{ - UserId: userTeamMember, - Category: "focalboard", - Name: "test", - Value: "test", - }}, nil - } - - return nil, errTestStore -} - -func (s *PluginTestStore) GetUsersByTeam(teamID string, asGuestID string, showEmail, showName bool) ([]*model.User, error) { - if asGuestID == "guest" { - return []*model.User{ - s.users["viewer"], - s.users["commenter"], - s.users["editor"], - s.users["admin"], - s.users["guest"], - }, nil - } - - switch { - case teamID == s.testTeam.ID: - return []*model.User{ - s.users["team-member"], - s.users["viewer"], - s.users["commenter"], - s.users["editor"], - s.users["admin"], - s.users["guest"], - }, 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, asGuestID string, excludeBots bool, showEmail, showName bool) ([]*model.User, error) { - users := []*model.User{} - teamUsers, err := s.GetUsersByTeam(teamID, asGuestID, showEmail, showName) - if err != nil { - return nil, err - } - - for _, user := range teamUsers { - if excludeBots && user.IsBot { - continue - } - if strings.Contains(user.Username, searchQuery) { - users = append(users, user) - } - } - return users, nil -} - -func (s *PluginTestStore) CanSeeUser(seerID string, seenID string) (bool, error) { - user, err := s.GetUserByID(seerID) - if err != nil { - return false, err - } - if !user.IsGuest { - return true, nil - } - seerMembers, err := s.GetMembersForUser(seerID) - if err != nil { - return false, err - } - seenMembers, err := s.GetMembersForUser(seenID) - if err != nil { - return false, err - } - for _, seerMember := range seerMembers { - for _, seenMember := range seenMembers { - if seerMember.BoardID == seenMember.BoardID { - return true, nil - } - } - } - return false, nil -} - -func (s *PluginTestStore) SearchUserChannels(teamID, userID, query string) ([]*mm_model.Channel, error) { - return []*mm_model.Channel{ - { - TeamId: teamID, - Id: "valid-channel-id", - DisplayName: "Valid Channel", - Name: "valid-channel", - }, - { - TeamId: teamID, - Id: "valid-channel-id-2", - DisplayName: "Valid Channel 2", - Name: "valid-channel-2", - }, - }, nil -} - -func (s *PluginTestStore) GetChannel(teamID, channel string) (*mm_model.Channel, error) { - if channel == "valid-channel-id" { - return &mm_model.Channel{ - TeamId: teamID, - Id: "valid-channel-id", - DisplayName: "Valid Channel", - Name: "valid-channel", - }, nil - } else if channel == "valid-channel-id-2" { - return &mm_model.Channel{ - TeamId: teamID, - Id: "valid-channel-id-2", - DisplayName: "Valid Channel 2", - Name: "valid-channel-2", - }, nil - } - return nil, errTestStore -} - -func (s *PluginTestStore) SearchBoardsForUser(term string, field model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) { - boards, err := s.Store.SearchBoardsForUser(term, field, userID, includePublicBoards) - if err != nil { - return nil, err - } - - teams, err := s.GetTeamsForUser(userID) - if err != nil { - return nil, err - } - - resultBoards := []*model.Board{} - for _, board := range boards { - for _, team := range teams { - if team.ID == board.TeamID { - resultBoards = append(resultBoards, board) - break - } - } - } - return resultBoards, nil -} - -func (s *PluginTestStore) GetLicense() *mm_model.License { - license := s.Store.GetLicense() - - if license == nil { - license = &mm_model.License{ - Id: mm_model.NewId(), - StartsAt: mm_model.GetMillis() - 2629746000, // 1 month - ExpiresAt: mm_model.GetMillis() + 2629746000, // - IssuedAt: mm_model.GetMillis() - 2629746000, - Features: &mm_model.Features{}, - } - license.Features.SetDefaults() - } - - complianceLicense := os.Getenv("FOCALBOARD_UNIT_TESTING_COMPLIANCE") - if complianceLicense != "" { - if val, err := strconv.ParseBool(complianceLicense); err == nil { - license.Features.Compliance = mm_model.NewBool(val) - } - } - - return license -} diff --git a/server/boards/integrationtests/sharing_test.go b/server/boards/integrationtests/sharing_test.go deleted file mode 100644 index b8eff2df13..0000000000 --- a/server/boards/integrationtests/sharing_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestSharing(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - var boardID string - token := utils.NewID(utils.IDTypeToken) - - t.Run("an unauthenticated client should not be able to get a sharing", func(t *testing.T) { - th.Logout(th.Client) - - sharing, resp := th.Client.GetSharing("board-id") - th.CheckUnauthorized(resp) - require.Nil(t, sharing) - }) - - t.Run("Check no initial sharing", func(t *testing.T) { - th.Login1() - - teamID := "0" - newBoard := &model.Board{ - TeamID: teamID, - Type: model.BoardTypeOpen, - } - - board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) - require.NoError(t, err) - require.NotNil(t, board) - boardID = board.ID - - s, err := th.Server.App().GetSharing(boardID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, s) - - sharing, resp := th.Client.GetSharing(boardID) - th.CheckNotFound(resp) - require.Nil(t, sharing) - }) - - t.Run("POST sharing, config = false", func(t *testing.T) { - sharing := model.Sharing{ - ID: boardID, - Token: token, - Enabled: true, - UpdateAt: 1, - } - - // it will fail with default config - success, resp := th.Client.PostSharing(&sharing) - require.False(t, success) - require.Error(t, resp.Error) - - t.Run("GET sharing", func(t *testing.T) { - sharing, resp := th.Client.GetSharing(boardID) - // Expect empty sharing object - th.CheckNotFound(resp) - require.Nil(t, sharing) - }) - }) - - t.Run("POST sharing, config = true", func(t *testing.T) { - th.Server.Config().EnablePublicSharedBoards = true - sharing := model.Sharing{ - ID: boardID, - Token: token, - Enabled: true, - UpdateAt: 1, - } - - // it will succeed with updated config - success, resp := th.Client.PostSharing(&sharing) - require.True(t, success) - require.NoError(t, resp.Error) - - t.Run("GET sharing", func(t *testing.T) { - sharing, resp := th.Client.GetSharing(boardID) - require.NoError(t, resp.Error) - require.NotNil(t, sharing) - require.Equal(t, sharing.ID, boardID) - require.True(t, sharing.Enabled) - require.Equal(t, sharing.Token, token) - }) - }) -} diff --git a/server/boards/integrationtests/sidebar_test.go b/server/boards/integrationtests/sidebar_test.go deleted file mode 100644 index c458ad8fdb..0000000000 --- a/server/boards/integrationtests/sidebar_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestSidebar(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - // we'll create a new board. - // The board should end up in a default "Boards" category - board := th.CreateBoard("team-id", "O") - - categoryBoards := th.GetUserCategoryBoards("team-id") - require.Equal(t, 1, len(categoryBoards)) - require.Equal(t, "Boards", categoryBoards[0].Name) - require.Equal(t, 1, len(categoryBoards[0].BoardMetadata)) - require.Equal(t, board.ID, categoryBoards[0].BoardMetadata[0].BoardID) - - // create a new category, a new board - // and move that board into the new category - board2 := th.CreateBoard("team-id", "O") - category := th.CreateCategory(model.Category{ - Name: "Category 2", - TeamID: "team-id", - UserID: "single-user", - }) - th.UpdateCategoryBoard("team-id", category.ID, board2.ID) - - categoryBoards = th.GetUserCategoryBoards("team-id") - // now there should be two categories - boards and the one - // we created just now - require.Equal(t, 2, len(categoryBoards)) - - // the newly created category should be the first one array - // as new categories end up on top in LHS - require.Equal(t, "Category 2", categoryBoards[0].Name) - require.Equal(t, 1, len(categoryBoards[0].BoardMetadata)) - require.Equal(t, board2.ID, categoryBoards[0].BoardMetadata[0].BoardID) - - // now we'll delete the custom category we created, "Category 2" - // and all it's boards should get moved to the Boards category - th.DeleteCategory("team-id", category.ID) - categoryBoards = th.GetUserCategoryBoards("team-id") - require.Equal(t, 1, len(categoryBoards)) - require.Equal(t, "Boards", categoryBoards[0].Name) - require.Equal(t, 2, len(categoryBoards[0].BoardMetadata)) - require.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: board.ID, Hidden: false}) - require.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: board2.ID, Hidden: false}) -} - -func TestHideUnhideBoard(t *testing.T) { - th := SetupTestHelperWithToken(t).Start() - defer th.TearDown() - - // we'll create a new board. - // The board should end up in a default "Boards" category - th.CreateBoard("team-id", "O") - - // the created board should not be hidden - categoryBoards := th.GetUserCategoryBoards("team-id") - require.Equal(t, 1, len(categoryBoards)) - require.Equal(t, "Boards", categoryBoards[0].Name) - require.Equal(t, 1, len(categoryBoards[0].BoardMetadata)) - require.False(t, categoryBoards[0].BoardMetadata[0].Hidden) - - // now we'll hide the board - response := th.Client.HideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID) - th.CheckOK(response) - - // verifying if the board has been marked as hidden - categoryBoards = th.GetUserCategoryBoards("team-id") - require.True(t, categoryBoards[0].BoardMetadata[0].Hidden) - - // trying to hide the already hidden board.This should have no effect - response = th.Client.HideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID) - th.CheckOK(response) - categoryBoards = th.GetUserCategoryBoards("team-id") - require.True(t, categoryBoards[0].BoardMetadata[0].Hidden) - - // now we'll unhide the board - response = th.Client.UnhideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID) - th.CheckOK(response) - - // verifying - categoryBoards = th.GetUserCategoryBoards("team-id") - require.False(t, categoryBoards[0].BoardMetadata[0].Hidden) - - // trying to unhide the already visible board.This should have no effect - response = th.Client.UnhideBoard("team-id", categoryBoards[0].ID, categoryBoards[0].BoardMetadata[0].BoardID) - th.CheckOK(response) - categoryBoards = th.GetUserCategoryBoards("team-id") - require.False(t, categoryBoards[0].BoardMetadata[0].Hidden) -} diff --git a/server/boards/integrationtests/statistics_test.go b/server/boards/integrationtests/statistics_test.go deleted file mode 100644 index 597a1af6bb..0000000000 --- a/server/boards/integrationtests/statistics_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/client" - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestStatisticsLocalMode(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - t.Run("an unauthenticated client should not be able to get statistics", func(t *testing.T) { - th.Logout(th.Client) - - stats, resp := th.Client.GetStatistics() - th.CheckUnauthorized(resp) - require.Nil(t, stats) - }) - - t.Run("Check authenticated user, not admin", func(t *testing.T) { - th.Login1() - - stats, resp := th.Client.GetStatistics() - th.CheckNotImplemented(resp) - require.Nil(t, stats) - }) -} - -func TestStatisticsPluginMode(t *testing.T) { - th := SetupTestHelperPluginMode(t) - defer th.TearDown() - - // Permissions are tested in permissions_test.go - // This tests the functionality. - t.Run("Check authenticated user, admin", func(t *testing.T) { - th.Client = client.NewClient(th.Server.Config().ServerRoot, "") - th.Client.HTTPHeader["Mattermost-User-Id"] = userAdmin - - stats, resp := th.Client.GetStatistics() - th.CheckOK(resp) - require.NotNil(t, stats) - - numberCards := 2 - th.CreateBoardAndCards("testTeam", model.BoardTypeOpen, numberCards) - - stats, resp = th.Client.GetStatistics() - th.CheckOK(resp) - require.NotNil(t, stats) - require.Equal(t, 1, stats.Boards) - require.Equal(t, numberCards, stats.Cards) - }) -} diff --git a/server/boards/integrationtests/subscriptions_test.go b/server/boards/integrationtests/subscriptions_test.go deleted file mode 100644 index 9261bcd2d3..0000000000 --- a/server/boards/integrationtests/subscriptions_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/client" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscription, string, error) { - newSubs := make([]*model.Subscription, 0, num) - - user, resp := client.GetMe() - if resp.Error != nil { - return nil, "", fmt.Errorf("cannot get current user: %w", resp.Error) - } - - board := &model.Board{ - TeamID: "0", - Type: model.BoardTypeOpen, - CreateAt: 1, - UpdateAt: 1, - } - board, resp = client.CreateBoard(board) - if resp.Error != nil { - return nil, "", fmt.Errorf("cannot insert test board block: %w", resp.Error) - } - - for n := 0; n < num; n++ { - newBlock := &model.Block{ - ID: utils.NewID(utils.IDTypeCard), - BoardID: board.ID, - CreateAt: 1, - UpdateAt: 1, - Type: model.TypeCard, - } - - newBlocks, resp := client.InsertBlocks(board.ID, []*model.Block{newBlock}, false) - if resp.Error != nil { - return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error) - } - newBlock = newBlocks[0] - - sub := &model.Subscription{ - BlockType: newBlock.Type, - BlockID: newBlock.ID, - SubscriberType: model.SubTypeUser, - SubscriberID: user.ID, - } - - subNew, resp := client.CreateSubscription(sub) - if resp.Error != nil { - return nil, "", resp.Error - } - newSubs = append(newSubs, subNew) - } - return newSubs, user.ID, nil -} - -func TestCreateSubscription(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - t.Run("Create valid subscription", func(t *testing.T) { - subs, userID, err := createTestSubscriptions(th.Client, 5) - require.NoError(t, err) - require.Len(t, subs, 5) - - // fetch the newly created subscriptions and compare - subsFound, resp := th.Client.GetSubscriptions(userID) - require.NoError(t, resp.Error) - require.Len(t, subsFound, 5) - assert.ElementsMatch(t, subs, subsFound) - }) - - t.Run("Create invalid subscription", func(t *testing.T) { - user, resp := th.Client.GetMe() - require.NoError(t, resp.Error) - - sub := &model.Subscription{ - SubscriberID: user.ID, - } - _, resp = th.Client.CreateSubscription(sub) - require.Error(t, resp.Error) - }) - - t.Run("Create subscription for another user", func(t *testing.T) { - sub := &model.Subscription{ - SubscriberID: utils.NewID(utils.IDTypeUser), - } - _, resp := th.Client.CreateSubscription(sub) - require.Error(t, resp.Error) - }) -} - -func TestGetSubscriptions(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - t.Run("Get subscriptions for user", func(t *testing.T) { - mySubs, user1ID, err := createTestSubscriptions(th.Client, 5) - require.NoError(t, err) - require.Len(t, mySubs, 5) - - // create more subscriptions with different user - otherSubs, _, err := createTestSubscriptions(th.Client2, 10) - require.NoError(t, err) - require.Len(t, otherSubs, 10) - - // fetch the newly created subscriptions for current user, making sure only - // the ones created for the current user are returned. - subsFound, resp := th.Client.GetSubscriptions(user1ID) - require.NoError(t, resp.Error) - require.Len(t, subsFound, 5) - assert.ElementsMatch(t, mySubs, subsFound) - }) -} - -func TestDeleteSubscription(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - t.Run("Delete valid subscription", func(t *testing.T) { - subs, userID, err := createTestSubscriptions(th.Client, 3) - require.NoError(t, err) - require.Len(t, subs, 3) - - resp := th.Client.DeleteSubscription(subs[1].BlockID, userID) - require.NoError(t, resp.Error) - - // fetch the subscriptions and ensure the list is correct - subsFound, resp := th.Client.GetSubscriptions(userID) - require.NoError(t, resp.Error) - require.Len(t, subsFound, 2) - - assert.Contains(t, subsFound, subs[0]) - assert.Contains(t, subsFound, subs[2]) - assert.NotContains(t, subsFound, subs[1]) - }) - - t.Run("Delete invalid subscription", func(t *testing.T) { - user, resp := th.Client.GetMe() - require.NoError(t, resp.Error) - - resp = th.Client.DeleteSubscription("bogus", user.ID) - require.Error(t, resp.Error) - }) -} diff --git a/server/boards/integrationtests/teststore.go b/server/boards/integrationtests/teststore.go deleted file mode 100644 index d78a462820..0000000000 --- a/server/boards/integrationtests/teststore.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "github.com/mattermost/mattermost/server/v8/boards/services/store" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -type TestStore struct { - store.Store - license *mm_model.License -} - -func NewTestEnterpriseStore(store store.Store) *TestStore { - usersValue := 10000 - trueValue := true - falseValue := false - license := &mm_model.License{ - Features: &mm_model.Features{ - Users: &usersValue, - LDAP: &trueValue, - LDAPGroups: &trueValue, - MFA: &trueValue, - GoogleOAuth: &trueValue, - Office365OAuth: &trueValue, - OpenId: &trueValue, - Compliance: &trueValue, - Cluster: &trueValue, - Metrics: &trueValue, - MHPNS: &trueValue, - SAML: &trueValue, - Elasticsearch: &trueValue, - Announcement: &trueValue, - ThemeManagement: &trueValue, - EmailNotificationContents: &trueValue, - DataRetention: &trueValue, - MessageExport: &trueValue, - CustomPermissionsSchemes: &trueValue, - CustomTermsOfService: &trueValue, - GuestAccounts: &trueValue, - GuestAccountsPermissions: &trueValue, - IDLoadedPushNotifications: &trueValue, - LockTeammateNameDisplay: &trueValue, - EnterprisePlugins: &trueValue, - AdvancedLogging: &trueValue, - Cloud: &falseValue, - SharedChannels: &trueValue, - RemoteClusterService: &trueValue, - FutureFeatures: &trueValue, - }, - } - - testStore := &TestStore{ - Store: store, - license: license, - } - - return testStore -} - -func NewTestProfessionalStore(store store.Store) *TestStore { - usersValue := 10000 - trueValue := true - falseValue := false - license := &mm_model.License{ - Features: &mm_model.Features{ - Users: &usersValue, - LDAP: &falseValue, - LDAPGroups: &falseValue, - MFA: &trueValue, - GoogleOAuth: &trueValue, - Office365OAuth: &trueValue, - OpenId: &trueValue, - Compliance: &falseValue, - Cluster: &falseValue, - Metrics: &trueValue, - MHPNS: &trueValue, - SAML: &trueValue, - Elasticsearch: &trueValue, - Announcement: &trueValue, - ThemeManagement: &trueValue, - EmailNotificationContents: &trueValue, - DataRetention: &trueValue, - MessageExport: &trueValue, - CustomPermissionsSchemes: &trueValue, - CustomTermsOfService: &trueValue, - GuestAccounts: &trueValue, - GuestAccountsPermissions: &trueValue, - IDLoadedPushNotifications: &trueValue, - LockTeammateNameDisplay: &trueValue, - EnterprisePlugins: &falseValue, - AdvancedLogging: &trueValue, - Cloud: &falseValue, - SharedChannels: &trueValue, - RemoteClusterService: &falseValue, - FutureFeatures: &trueValue, - }, - } - - testStore := &TestStore{ - Store: store, - license: license, - } - - return testStore -} - -func (s *TestStore) GetLicense() *mm_model.License { - return s.license -} diff --git a/server/boards/integrationtests/user_test.go b/server/boards/integrationtests/user_test.go deleted file mode 100644 index 2812a18b5d..0000000000 --- a/server/boards/integrationtests/user_test.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package integrationtests - -import ( - "bytes" - "crypto/rand" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -const ( - fakeUsername = "fakeUsername" - fakeEmail = "mock@test.com" -) - -func TestUserRegister(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - // register - registerRequest := &model.RegisterRequest{ - Username: fakeUsername, - Email: fakeEmail, - Password: utils.NewID(utils.IDTypeNone), - } - success, resp := th.Client.Register(registerRequest) - require.NoError(t, resp.Error) - require.True(t, success) - - // register again will fail - success, resp = th.Client.Register(registerRequest) - require.Error(t, resp.Error) - require.False(t, success) -} - -func TestUserLogin(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - t.Run("with nonexist user", func(t *testing.T) { - loginRequest := &model.LoginRequest{ - Type: "normal", - Username: "nonexistuser", - Email: "", - Password: utils.NewID(utils.IDTypeNone), - } - data, resp := th.Client.Login(loginRequest) - require.Error(t, resp.Error) - require.Nil(t, data) - }) - - t.Run("with registered user", func(t *testing.T) { - password := utils.NewID(utils.IDTypeNone) - // register - registerRequest := &model.RegisterRequest{ - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - success, resp := th.Client.Register(registerRequest) - require.NoError(t, resp.Error) - require.True(t, success) - - // login - loginRequest := &model.LoginRequest{ - Type: "normal", - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - data, resp := th.Client.Login(loginRequest) - require.NoError(t, resp.Error) - require.NotNil(t, data) - require.NotNil(t, data.Token) - }) -} - -func TestGetMe(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - t.Run("not login yet", func(t *testing.T) { - me, resp := th.Client.GetMe() - require.Error(t, resp.Error) - require.Nil(t, me) - }) - - t.Run("logged in", func(t *testing.T) { - // register - password := utils.NewID(utils.IDTypeNone) - registerRequest := &model.RegisterRequest{ - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - success, resp := th.Client.Register(registerRequest) - require.NoError(t, resp.Error) - require.True(t, success) - // login - loginRequest := &model.LoginRequest{ - Type: "normal", - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - data, resp := th.Client.Login(loginRequest) - require.NoError(t, resp.Error) - require.NotNil(t, data) - require.NotNil(t, data.Token) - - // get user me - me, resp := th.Client.GetMe() - require.NoError(t, resp.Error) - require.NotNil(t, me) - require.Equal(t, "", me.Email) - require.Equal(t, registerRequest.Username, me.Username) - }) -} - -func TestGetUser(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - // register - password := utils.NewID(utils.IDTypeNone) - registerRequest := &model.RegisterRequest{ - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - success, resp := th.Client.Register(registerRequest) - require.NoError(t, resp.Error) - require.True(t, success) - // login - loginRequest := &model.LoginRequest{ - Type: "normal", - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - data, resp := th.Client.Login(loginRequest) - require.NoError(t, resp.Error) - require.NotNil(t, data) - require.NotNil(t, data.Token) - - me, resp := th.Client.GetMe() - require.NoError(t, resp.Error) - require.NotNil(t, me) - - t.Run("me's id", func(t *testing.T) { - user, resp := th.Client.GetUser(me.ID) - require.NoError(t, resp.Error) - require.NotNil(t, user) - require.Equal(t, me.ID, user.ID) - require.Equal(t, me.Username, user.Username) - }) - - t.Run("nonexist user", func(t *testing.T) { - user, resp := th.Client.GetUser("nonexistid") - require.Error(t, resp.Error) - require.Nil(t, user) - }) -} - -func TestUserChangePassword(t *testing.T) { - th := SetupTestHelper(t).Start() - defer th.TearDown() - - // register - password := utils.NewID(utils.IDTypeNone) - registerRequest := &model.RegisterRequest{ - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - success, resp := th.Client.Register(registerRequest) - require.NoError(t, resp.Error) - require.True(t, success) - // login - loginRequest := &model.LoginRequest{ - Type: "normal", - Username: fakeUsername, - Email: fakeEmail, - Password: password, - } - data, resp := th.Client.Login(loginRequest) - require.NoError(t, resp.Error) - require.NotNil(t, data) - require.NotNil(t, data.Token) - - originalMe, resp := th.Client.GetMe() - require.NoError(t, resp.Error) - require.NotNil(t, originalMe) - - // change password - success, resp = th.Client.UserChangePassword(originalMe.ID, &model.ChangePasswordRequest{ - OldPassword: password, - NewPassword: utils.NewID(utils.IDTypeNone), - }) - require.NoError(t, resp.Error) - require.True(t, success) -} - -func randomBytes(t *testing.T, n int) []byte { - bb := make([]byte, n) - _, err := rand.Read(bb) - require.NoError(t, err) - return bb -} - -func TestTeamUploadFile(t *testing.T) { - t.Run("no permission", func(t *testing.T) { // native auth, but not login - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := "0" - boardID := utils.NewID(utils.IDTypeBoard) - data := randomBytes(t, 1024) - result, resp := th.Client.TeamUploadFile(teamID, boardID, bytes.NewReader(data)) - require.Error(t, resp.Error) - require.Nil(t, result) - }) - - t.Run("a board admin should be able to update a file", func(t *testing.T) { // single token auth - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := "0" - newBoard := &model.Board{ - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NotNil(t, board) - - data := randomBytes(t, 1024) - result, resp := th.Client.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) - th.CheckOK(resp) - require.NotNil(t, result) - require.NotEmpty(t, result.FileID) - // TODO get the uploaded file - }) - - t.Run("user that doesn't belong to the board should not be able to upload a file", func(t *testing.T) { - th := SetupTestHelper(t).InitBasic() - defer th.TearDown() - - teamID := "0" - newBoard := &model.Board{ - Type: model.BoardTypeOpen, - TeamID: teamID, - } - board, resp := th.Client.CreateBoard(newBoard) - th.CheckOK(resp) - require.NotNil(t, board) - - data := randomBytes(t, 1024) - - // a user that doesn't belong to the board tries to upload the file - result, resp := th.Client2.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) - th.CheckForbidden(resp) - require.Nil(t, result) - }) -} diff --git a/server/boards/model/auth.go b/server/boards/model/auth.go deleted file mode 100644 index dd7c626422..0000000000 --- a/server/boards/model/auth.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "fmt" - "io" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/services/auth" -) - -const ( - MinimumPasswordLength = 8 -) - -func NewErrAuthParam(msg string) *ErrAuthParam { - return &ErrAuthParam{ - msg: msg, - } -} - -type ErrAuthParam struct { - msg string -} - -func (pe *ErrAuthParam) Error() string { - return pe.msg -} - -// LoginRequest is a login request -// swagger:model -type LoginRequest struct { - // Type of login, currently must be set to "normal" - // required: true - Type string `json:"type"` - - // If specified, login using username - // required: false - Username string `json:"username"` - - // If specified, login using email - // required: false - Email string `json:"email"` - - // Password - // required: true - Password string `json:"password"` - - // MFA token - // required: false - // swagger:ignore - MfaToken string `json:"mfa_token"` -} - -// LoginResponse is a login response -// swagger:model -type LoginResponse struct { - // Session token - // required: true - Token string `json:"token"` -} - -func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) { - var resp LoginResponse - if err := json.NewDecoder(data).Decode(&resp); err != nil { - return nil, err - } - return &resp, nil -} - -// RegisterRequest is a user registration request -// swagger:model -type RegisterRequest struct { - // User name - // required: true - Username string `json:"username"` - - // User's email - // required: true - Email string `json:"email"` - - // Password - // required: true - Password string `json:"password"` - - // Registration authorization token - // required: true - Token string `json:"token"` -} - -func (rd *RegisterRequest) IsValid() error { - if strings.TrimSpace(rd.Username) == "" { - return NewErrAuthParam("username is required") - } - if strings.TrimSpace(rd.Email) == "" { - return NewErrAuthParam("email is required") - } - if !auth.IsEmailValid(rd.Email) { - return NewErrAuthParam("invalid email format") - } - if rd.Password == "" { - return NewErrAuthParam("password is required") - } - return isValidPassword(rd.Password) -} - -// ChangePasswordRequest is a user password change request -// swagger:model -type ChangePasswordRequest struct { - // Old password - // required: true - OldPassword string `json:"oldPassword"` - - // New password - // required: true - NewPassword string `json:"newPassword"` -} - -// IsValid validates a password change request. -func (rd *ChangePasswordRequest) IsValid() error { - if rd.OldPassword == "" { - return NewErrAuthParam("old password is required") - } - if rd.NewPassword == "" { - return NewErrAuthParam("new password is required") - } - return isValidPassword(rd.NewPassword) -} - -func isValidPassword(password string) error { - if len(password) < MinimumPasswordLength { - return NewErrAuthParam(fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength)) - } - return nil -} diff --git a/server/boards/model/block.go b/server/boards/model/block.go deleted file mode 100644 index bdf4aa8362..0000000000 --- a/server/boards/model/block.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" - "strconv" - - "github.com/mattermost/mattermost/server/v8/boards/services/audit" -) - -// Block is the basic data unit -// swagger:model -type Block struct { - // The id for this block - // required: true - ID string `json:"id"` - - // The id for this block's parent block. Empty for root blocks - // required: false - ParentID string `json:"parentId"` - - // The id for user who created this block - // required: true - CreatedBy string `json:"createdBy"` - - // The id for user who last modified this block - // required: true - ModifiedBy string `json:"modifiedBy"` - - // The schema version of this block - // required: true - Schema int64 `json:"schema"` - - // The block type - // required: true - Type BlockType `json:"type"` - - // The display title - // required: false - Title string `json:"title"` - - // The block fields - // required: false - Fields map[string]interface{} `json:"fields"` - - // The creation time in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"createAt"` - - // The last modified time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"updateAt"` - - // The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted - // required: false - DeleteAt int64 `json:"deleteAt"` - - // Deprecated. The workspace id that the block belongs to - // required: false - WorkspaceID string `json:"-"` - - // The board id that the block belongs to - // required: true - BoardID string `json:"boardId"` - - // Indicates if the card is limited - // required: false - Limited bool `json:"limited,omitempty"` -} - -// BlockPatch is a patch for modify blocks -// swagger:model -type BlockPatch struct { - // The id for this block's parent block. Empty for root blocks - // required: false - ParentID *string `json:"parentId"` - - // The schema version of this block - // required: false - Schema *int64 `json:"schema"` - - // The block type - // required: false - Type *BlockType `json:"type"` - - // The display title - // required: false - Title *string `json:"title"` - - // The block updated fields - // required: false - UpdatedFields map[string]interface{} `json:"updatedFields"` - - // The block removed fields - // required: false - DeletedFields []string `json:"deletedFields"` -} - -// BlockPatchBatch is a batch of IDs and patches for modify blocks -// swagger:model -type BlockPatchBatch struct { - // The id's for of the blocks to patch - BlockIDs []string `json:"block_ids"` - - // The BlockPatches to be applied - BlockPatches []BlockPatch `json:"block_patches"` -} - -// BoardModifier is a callback that can modify each board during an import. -// A cache of arbitrary data will be passed for each call and any changes -// to the cache will be preserved for the next call. -// Return true to import the block or false to skip import. -type BoardModifier func(board *Board, cache map[string]interface{}) bool - -// BlockModifier is a callback that can modify each block during an import. -// A cache of arbitrary data will be passed for each call and any changes -// to the cache will be preserved for the next call. -// Return true to import the block or false to skip import. -type BlockModifier func(block *Block, cache map[string]interface{}) bool - -func BlocksFromJSON(data io.Reader) []*Block { - var blocks []*Block - _ = json.NewDecoder(data).Decode(&blocks) - return blocks -} - -// LogClone implements the `mlog.LogCloner` interface to provide a subset of Block fields for logging. -func (b *Block) LogClone() interface{} { - return struct { - ID string - ParentID string - BoardID string - Type BlockType - }{ - ID: b.ID, - ParentID: b.ParentID, - BoardID: b.BoardID, - Type: b.Type, - } -} - -// Patch returns an update version of the block. -func (p *BlockPatch) Patch(block *Block) *Block { - if p.ParentID != nil { - block.ParentID = *p.ParentID - } - - if p.Schema != nil { - block.Schema = *p.Schema - } - - if p.Type != nil { - block.Type = *p.Type - } - - if p.Title != nil { - block.Title = *p.Title - } - - for key, field := range p.UpdatedFields { - block.Fields[key] = field - } - - for _, key := range p.DeletedFields { - delete(block.Fields, key) - } - - return block -} - -type QueryBlocksOptions struct { - BoardID string // if not empty then filter for blocks belonging to specified board - ParentID string // if not empty then filter for blocks belonging to specified parent - BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type - Page int // page number to select when paginating - PerPage int // number of blocks per page (default=0, meaning unlimited) -} - -// QuerySubtreeOptions are query options that can be passed to GetSubTree methods. -type QuerySubtreeOptions struct { - BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt - AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt - Limit uint64 // if non-zero then limit the number of returned records -} - -// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory. -type QueryBlockHistoryOptions struct { - BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt - AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt - Limit uint64 // if non-zero then limit the number of returned records - Descending bool // if true then the records are sorted by insert_at in descending order -} - -// QueryBoardHistoryOptions are query options that can be passed to GetBoardHistory. -type QueryBoardHistoryOptions struct { - BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt - AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt - Limit uint64 // if non-zero then limit the number of returned records - Descending bool // if true then the records are sorted by insert_at in descending order -} - -// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory. -type QueryBlockHistoryChildOptions struct { - BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt - AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt - Page int // page number to select when paginating - PerPage int // number of blocks per page (default=-1, meaning unlimited) -} - -func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) { - if userID == SingleUser { - userID = "" - } - - now := GetMillis() - for i := range blocks { - blocks[i].ModifiedBy = userID - blocks[i].UpdateAt = now - - if auditRec != nil { - auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i]) - } - } -} - -func (b *Block) ShouldBeLimited(cardLimitTimestamp int64) bool { - return b.Type == TypeCard && - b.UpdateAt < cardLimitTimestamp -} - -// Returns a limited version of the block that doesn't contain the -// contents of the block, only its IDs and type. -func (b *Block) GetLimited() *Block { - newBlock := &Block{ - Title: b.Title, - ID: b.ID, - ParentID: b.ParentID, - BoardID: b.BoardID, - Schema: b.Schema, - Type: b.Type, - CreateAt: b.CreateAt, - UpdateAt: b.UpdateAt, - DeleteAt: b.DeleteAt, - WorkspaceID: b.WorkspaceID, - Limited: true, - } - - if iconField, ok := b.Fields["icon"]; ok { - newBlock.Fields = map[string]interface{}{ - "icon": iconField, - } - } - - return newBlock -} diff --git a/server/boards/model/block_test.go b/server/boards/model/block_test.go deleted file mode 100644 index e9cb3918a9..0000000000 --- a/server/boards/model/block_test.go +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func TestGenerateBlockIDs(t *testing.T) { - t.Run("Should generate a new ID for a single block with no references", func(t *testing.T) { - blockID := utils.NewID(utils.IDTypeBlock) - blocks := []*Block{{ID: blockID}} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID, blocks[0].ID) - require.Zero(t, blocks[0].BoardID) - require.Zero(t, blocks[0].ParentID) - }) - - t.Run("Should generate a new ID for a single block with references", func(t *testing.T) { - blockID := utils.NewID(utils.IDTypeBlock) - boardID := utils.NewID(utils.IDTypeBlock) - parentID := utils.NewID(utils.IDTypeBlock) - blocks := []*Block{{ID: blockID, BoardID: boardID, ParentID: parentID}} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID, blocks[0].ID) - require.Equal(t, boardID, blocks[0].BoardID) - require.Equal(t, parentID, blocks[0].ParentID) - }) - - t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := utils.NewID(utils.IDTypeBlock) - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} - - blockID2 := utils.NewID(utils.IDTypeBlock) - boardID2 := blockID1 - parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} - - blocks := []*Block{block1, block2} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, boardID1, blocks[0].BoardID) - require.Equal(t, parentID1, blocks[0].ParentID) - - require.NotEqual(t, blockID2, blocks[1].ID) - require.NotEqual(t, boardID2, blocks[1].BoardID) - require.Equal(t, parentID2, blocks[1].ParentID) - - // blockID1 was referenced, so it should still be after the ID - // changes - require.Equal(t, blocks[0].ID, blocks[1].BoardID) - }) - - t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := "" - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} - - blockID2 := utils.NewID(utils.IDTypeBlock) - boardID2 := utils.NewID(utils.IDTypeBlock) - parentID2 := "" - block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} - - blocks := []*Block{block1, block2} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - // only the IDs should have changed - require.NotEqual(t, blockID1, blocks[0].ID) - require.Zero(t, blocks[0].BoardID) - require.Equal(t, parentID1, blocks[0].ParentID) - - require.NotEqual(t, blockID2, blocks[1].ID) - require.Equal(t, boardID2, blocks[1].BoardID) - require.Zero(t, blocks[1].ParentID) - }) - - t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := utils.NewID(utils.IDTypeBlock) - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} - - // linked to 1 - blockID2 := utils.NewID(utils.IDTypeBlock) - boardID2 := blockID1 - parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} - - // linked to 2 - blockID3 := utils.NewID(utils.IDTypeBlock) - boardID3 := blockID2 - parentID3 := utils.NewID(utils.IDTypeBlock) - block3 := &Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3} - - // linked to 1 - blockID4 := utils.NewID(utils.IDTypeBlock) - boardID4 := blockID1 - parentID4 := utils.NewID(utils.IDTypeBlock) - block4 := &Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4} - - // blocks are shuffled - blocks := []*Block{block4, block2, block1, block3} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - // block 1 - require.NotEqual(t, blockID1, blocks[2].ID) - require.Equal(t, boardID1, blocks[2].BoardID) - require.Equal(t, parentID1, blocks[2].ParentID) - - // block 2 - require.NotEqual(t, blockID2, blocks[1].ID) - require.NotEqual(t, boardID2, blocks[1].BoardID) - require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1 - require.Equal(t, parentID2, blocks[1].ParentID) - - // block 3 - require.NotEqual(t, blockID3, blocks[3].ID) - require.NotEqual(t, boardID3, blocks[3].BoardID) - require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2 - require.Equal(t, parentID3, blocks[3].ParentID) - - // block 4 - require.NotEqual(t, blockID4, blocks[0].ID) - require.NotEqual(t, boardID4, blocks[0].BoardID) - require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1 - require.Equal(t, parentID4, blocks[0].ParentID) - }) - - t.Run("Should update content order", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := utils.NewID(utils.IDTypeBlock) - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ - ID: blockID1, - BoardID: boardID1, - ParentID: parentID1, - } - - blockID2 := utils.NewID(utils.IDTypeBlock) - boardID2 := utils.NewID(utils.IDTypeBlock) - parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := &Block{ - ID: blockID2, - BoardID: boardID2, - ParentID: parentID2, - Fields: map[string]interface{}{ - "contentOrder": []interface{}{ - blockID1, - }, - }, - } - - blocks := []*Block{block1, block2} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, boardID1, blocks[0].BoardID) - require.Equal(t, parentID1, blocks[0].ParentID) - - require.NotEqual(t, blockID2, blocks[1].ID) - require.Equal(t, boardID2, blocks[1].BoardID) - require.Equal(t, parentID2, blocks[1].ParentID) - - // since block 1 was referenced in block 2, - // the ID should have been changed in content order - block2ContentOrder, ok := block2.Fields["contentOrder"].([]interface{}) - require.True(t, ok) - require.NotEqual(t, blockID1, block2ContentOrder[0].(string)) - require.Equal(t, blocks[0].ID, block2ContentOrder[0].(string)) - }) - - t.Run("Should update content order when it contain slices", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := utils.NewID(utils.IDTypeBlock) - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ - ID: blockID1, - BoardID: boardID1, - ParentID: parentID1, - } - - blockID2 := utils.NewID(utils.IDTypeBlock) - block2 := &Block{ - ID: blockID2, - BoardID: boardID1, - ParentID: parentID1, - } - - blockID3 := utils.NewID(utils.IDTypeBlock) - block3 := &Block{ - ID: blockID3, - BoardID: boardID1, - ParentID: parentID1, - } - - blockID4 := utils.NewID(utils.IDTypeBlock) - boardID2 := utils.NewID(utils.IDTypeBlock) - parentID2 := utils.NewID(utils.IDTypeBlock) - - block4 := &Block{ - ID: blockID4, - BoardID: boardID2, - ParentID: parentID2, - Fields: map[string]interface{}{ - "contentOrder": []interface{}{ - blockID1, - []interface{}{ - blockID2, - blockID3, - }, - }, - }, - } - - blocks := []*Block{block1, block2, block3, block4} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, boardID1, blocks[0].BoardID) - require.Equal(t, parentID1, blocks[0].ParentID) - - require.NotEqual(t, blockID4, blocks[3].ID) - require.Equal(t, boardID2, blocks[3].BoardID) - require.Equal(t, parentID2, blocks[3].ParentID) - - // since block 1 was referenced in block 2, - // the ID should have been changed in content order - block4ContentOrder, ok := block4.Fields["contentOrder"].([]interface{}) - require.True(t, ok) - require.NotEqual(t, blockID1, block4ContentOrder[0].(string)) - require.NotEqual(t, blockID2, block4ContentOrder[1].([]interface{})[0]) - require.NotEqual(t, blockID3, block4ContentOrder[1].([]interface{})[1]) - require.Equal(t, blocks[0].ID, block4ContentOrder[0].(string)) - require.Equal(t, blocks[1].ID, block4ContentOrder[1].([]interface{})[0]) - require.Equal(t, blocks[2].ID, block4ContentOrder[1].([]interface{})[1]) - }) - - t.Run("Should update Id of default template view", func(t *testing.T) { - blockID1 := utils.NewID(utils.IDTypeBlock) - boardID1 := utils.NewID(utils.IDTypeBlock) - parentID1 := utils.NewID(utils.IDTypeBlock) - block1 := &Block{ - ID: blockID1, - BoardID: boardID1, - ParentID: parentID1, - } - - blockID2 := utils.NewID(utils.IDTypeBlock) - boardID2 := utils.NewID(utils.IDTypeBlock) - parentID2 := utils.NewID(utils.IDTypeBlock) - block2 := &Block{ - ID: blockID2, - BoardID: boardID2, - ParentID: parentID2, - Fields: map[string]interface{}{ - "defaultTemplateId": blockID1, - }, - } - - blocks := []*Block{block1, block2} - - blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) - - require.NotEqual(t, blockID1, blocks[0].ID) - require.Equal(t, boardID1, blocks[0].BoardID) - require.Equal(t, parentID1, blocks[0].ParentID) - - require.NotEqual(t, blockID2, blocks[1].ID) - require.Equal(t, boardID2, blocks[1].BoardID) - require.Equal(t, parentID2, blocks[1].ParentID) - - block2DefaultTemplateID, ok := block2.Fields["defaultTemplateId"].(string) - require.True(t, ok) - require.NotEqual(t, blockID1, block2DefaultTemplateID) - require.Equal(t, blocks[0].ID, block2DefaultTemplateID) - }) -} - -func TestStampModificationMetadata(t *testing.T) { - t.Run("base case", func(t *testing.T) { - block := &Block{} - blocks := []*Block{block} - assert.Empty(t, block.ModifiedBy) - assert.Empty(t, block.UpdateAt) - - StampModificationMetadata("user_id_1", blocks, nil) - assert.Equal(t, "user_id_1", blocks[0].ModifiedBy) - assert.NotEmpty(t, blocks[0].UpdateAt) - }) -} diff --git a/server/boards/model/blockid.go b/server/boards/model/blockid.go deleted file mode 100644 index 7280680a4c..0000000000 --- a/server/boards/model/blockid.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// GenerateBlockIDs generates new IDs for all the blocks of the list, -// keeping consistent any references that other blocks would made to -// the original IDs, so a tree of blocks can get new IDs and maintain -// its shape. -func GenerateBlockIDs(blocks []*Block, logger mlog.LoggerIFace) []*Block { - blockIDs := map[string]BlockType{} - referenceIDs := map[string]bool{} - for _, block := range blocks { - if _, ok := blockIDs[block.ID]; !ok { - blockIDs[block.ID] = block.Type - } - - if _, ok := referenceIDs[block.BoardID]; !ok { - referenceIDs[block.BoardID] = true - } - if _, ok := referenceIDs[block.ParentID]; !ok { - referenceIDs[block.ParentID] = true - } - - if _, ok := block.Fields["contentOrder"]; ok { - contentOrder, typeOk := block.Fields["contentOrder"].([]interface{}) - if !typeOk { - logger.Warn( - "type assertion failed for content order when saving reference block IDs", - mlog.String("blockID", block.ID), - mlog.String("actionType", fmt.Sprintf("%T", block.Fields["contentOrder"])), - mlog.String("expectedType", "[]interface{}"), - mlog.String("contentOrder", fmt.Sprintf("%v", block.Fields["contentOrder"])), - ) - continue - } - - for _, blockID := range contentOrder { - switch v := blockID.(type) { - case []interface{}: - for _, columnBlockID := range v { - referenceIDs[columnBlockID.(string)] = true - } - case string: - referenceIDs[v] = true - default: - } - } - } - - if _, ok := block.Fields["defaultTemplateId"]; ok { - defaultTemplateID, typeOk := block.Fields["defaultTemplateId"].(string) - if !typeOk { - logger.Warn( - "type assertion failed for default template ID when saving reference block IDs", - mlog.String("blockID", block.ID), - mlog.String("actionType", fmt.Sprintf("%T", block.Fields["defaultTemplateId"])), - mlog.String("expectedType", "string"), - mlog.String("defaultTemplateId", fmt.Sprintf("%v", block.Fields["defaultTemplateId"])), - ) - continue - } - referenceIDs[defaultTemplateID] = true - } - } - - newIDs := map[string]string{} - for id, blockType := range blockIDs { - for referenceID := range referenceIDs { - if id == referenceID { - newIDs[id] = utils.NewID(BlockType2IDType(blockType)) - continue - } - } - } - - getExistingOrOldID := func(id string) string { - if existingID, ok := newIDs[id]; ok { - return existingID - } - return id - } - - getExistingOrNewID := func(id string) string { - if existingID, ok := newIDs[id]; ok { - return existingID - } - return utils.NewID(BlockType2IDType(blockIDs[id])) - } - - newBlocks := make([]*Block, len(blocks)) - for i, block := range blocks { - block.ID = getExistingOrNewID(block.ID) - block.BoardID = getExistingOrOldID(block.BoardID) - block.ParentID = getExistingOrOldID(block.ParentID) - - blockMod := block - if _, ok := blockMod.Fields["contentOrder"]; ok { - fixFieldIDs(blockMod, "contentOrder", getExistingOrOldID, logger) - } - - if _, ok := blockMod.Fields["cardOrder"]; ok { - fixFieldIDs(blockMod, "cardOrder", getExistingOrOldID, logger) - } - - if _, ok := blockMod.Fields["defaultTemplateId"]; ok { - defaultTemplateID, typeOk := blockMod.Fields["defaultTemplateId"].(string) - if !typeOk { - logger.Warn( - "type assertion failed for default template ID when saving reference block IDs", - mlog.String("blockID", blockMod.ID), - mlog.String("actionType", fmt.Sprintf("%T", blockMod.Fields["defaultTemplateId"])), - mlog.String("expectedType", "string"), - mlog.String("defaultTemplateId", fmt.Sprintf("%v", blockMod.Fields["defaultTemplateId"])), - ) - } else { - blockMod.Fields["defaultTemplateId"] = getExistingOrOldID(defaultTemplateID) - } - } - - newBlocks[i] = blockMod - } - - return newBlocks -} - -func fixFieldIDs(block *Block, fieldName string, getExistingOrOldID func(string) string, logger mlog.LoggerIFace) { - field, typeOk := block.Fields[fieldName].([]interface{}) - if !typeOk { - logger.Warn( - "type assertion failed for JSON field when setting new block IDs", - mlog.String("blockID", block.ID), - mlog.String("fieldName", fieldName), - mlog.String("actionType", fmt.Sprintf("%T", block.Fields[fieldName])), - mlog.String("expectedType", "[]interface{}"), - mlog.String("value", fmt.Sprintf("%v", block.Fields[fieldName])), - ) - } else { - for j := range field { - switch v := field[j].(type) { - case string: - field[j] = getExistingOrOldID(v) - case []interface{}: - subOrder := field[j].([]interface{}) - for k := range v { - subOrder[k] = getExistingOrOldID(v[k].(string)) - } - } - } - } -} diff --git a/server/boards/model/blocktype.go b/server/boards/model/blocktype.go deleted file mode 100644 index 4260aff2b6..0000000000 --- a/server/boards/model/blocktype.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "errors" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -// BlockType represents a block type. -type BlockType string - -const ( - TypeUnknown = "unknown" - TypeBoard = "board" - TypeCard = "card" - TypeView = "view" - TypeText = "text" - TypeCheckbox = "checkbox" - TypeComment = "comment" - TypeImage = "image" - TypeAttachment = "attachment" - TypeDivider = "divider" -) - -func (bt BlockType) String() string { - return string(bt) -} - -// BlockTypeFromString returns an appropriate BlockType for the specified string. -func BlockTypeFromString(s string) (BlockType, error) { - switch strings.ToLower(s) { - case "board": - return TypeBoard, nil - case "card": - return TypeCard, nil - case "view": - return TypeView, nil - case "text": - return TypeText, nil - case "checkbox": - return TypeCheckbox, nil - case "comment": - return TypeComment, nil - case "image": - return TypeImage, nil - case "attachment": - return TypeAttachment, nil - case "divider": - return TypeDivider, nil - } - return TypeUnknown, ErrInvalidBlockType{s} -} - -// BlockType2IDType returns an appropriate IDType for the specified BlockType. -func BlockType2IDType(blockType BlockType) utils.IDType { - switch blockType { - case TypeBoard: - return utils.IDTypeBoard - case TypeCard: - return utils.IDTypeCard - case TypeView: - return utils.IDTypeView - case TypeText, TypeCheckbox, TypeComment, TypeDivider: - return utils.IDTypeBlock - case TypeImage, TypeAttachment: - return utils.IDTypeAttachment - } - return utils.IDTypeNone -} - -// ErrInvalidBlockType is returned wherever an invalid block type was provided. -type ErrInvalidBlockType struct { - Type string -} - -func (e ErrInvalidBlockType) Error() string { - return e.Type + " is an invalid block type." -} - -// IsErrInvalidBlockType returns true if `err` is a IsErrInvalidBlockType or wraps one. -func IsErrInvalidBlockType(err error) bool { - var eibt *ErrInvalidBlockType - return errors.As(err, &eibt) -} diff --git a/server/boards/model/board.go b/server/boards/model/board.go deleted file mode 100644 index a7e8163b23..0000000000 --- a/server/boards/model/board.go +++ /dev/null @@ -1,429 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" - "time" -) - -type BoardType string -type BoardRole string -type BoardSearchField string - -const ( - BoardTypeOpen BoardType = "O" - BoardTypePrivate BoardType = "P" -) - -const ( - BoardRoleNone BoardRole = "" - BoardRoleViewer BoardRole = "viewer" - BoardRoleCommenter BoardRole = "commenter" - BoardRoleEditor BoardRole = "editor" - BoardRoleAdmin BoardRole = "admin" -) - -const ( - BoardSearchFieldNone BoardSearchField = "" - BoardSearchFieldTitle BoardSearchField = "title" - BoardSearchFieldPropertyName BoardSearchField = "property_name" -) - -// Board groups a set of blocks and its layout -// swagger:model -type Board struct { - // The ID for the board - // required: true - ID string `json:"id"` - - // The ID of the team that the board belongs to - // required: true - TeamID string `json:"teamId"` - - // The ID of the channel that the board was created from - // required: false - ChannelID string `json:"channelId"` - - // The ID of the user that created the board - // required: true - CreatedBy string `json:"createdBy"` - - // The ID of the last user that updated the board - // required: true - ModifiedBy string `json:"modifiedBy"` - - // The type of the board - // required: true - Type BoardType `json:"type"` - - // The minimum role applied when somebody joins the board - // required: true - MinimumRole BoardRole `json:"minimumRole"` - - // The title of the board - // required: false - Title string `json:"title"` - - // The description of the board - // required: false - Description string `json:"description"` - - // The icon of the board - // required: false - Icon string `json:"icon"` - - // Indicates if the board shows the description on the interface - // required: false - ShowDescription bool `json:"showDescription"` - - // Marks the template boards - // required: false - IsTemplate bool `json:"isTemplate"` - - // Marks the template boards - // required: false - TemplateVersion int `json:"templateVersion"` - - // The properties of the board - // required: false - Properties map[string]interface{} `json:"properties"` - - // The properties of the board cards - // required: false - CardProperties []map[string]interface{} `json:"cardProperties"` - - // The creation time in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"createAt"` - - // The last modified time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"updateAt"` - - // The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted - // required: false - DeleteAt int64 `json:"deleteAt"` -} - -// GetPropertyString returns the value of the specified property as a string, -// or error if the property does not exist or is not of type string. -func (b *Board) GetPropertyString(propName string) (string, error) { - val, ok := b.Properties[propName] - if !ok { - return "", NewErrNotFound(propName) - } - - s, ok := val.(string) - if !ok { - return "", ErrInvalidPropertyValueType - } - return s, nil -} - -// BoardPatch is a patch for modify boards -// swagger:model -type BoardPatch struct { - // The type of the board - // required: false - Type *BoardType `json:"type"` - - // The minimum role applied when somebody joins the board - // required: false - MinimumRole *BoardRole `json:"minimumRole"` - - // The title of the board - // required: false - Title *string `json:"title"` - - // The description of the board - // required: false - Description *string `json:"description"` - - // The icon of the board - // required: false - Icon *string `json:"icon"` - - // Indicates if the board shows the description on the interface - // required: false - ShowDescription *bool `json:"showDescription"` - - // Indicates if the board shows the description on the interface - // required: false - ChannelID *string `json:"channelId"` - - // The board updated properties - // required: false - UpdatedProperties map[string]interface{} `json:"updatedProperties"` - - // The board removed properties - // required: false - DeletedProperties []string `json:"deletedProperties"` - - // The board updated card properties - // required: false - UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"` - - // The board removed card properties - // required: false - DeletedCardProperties []string `json:"deletedCardProperties"` -} - -// BoardMember stores the information of the membership of a user on a board -// swagger:model -type BoardMember struct { - // The ID of the board - // required: true - BoardID string `json:"boardId"` - - // The ID of the user - // required: true - UserID string `json:"userId"` - - // The independent roles of the user on the board - // required: false - Roles string `json:"roles"` - - // Minimum role because the board configuration - // required: false - MinimumRole string `json:"minimumRole"` - - // Marks the user as an admin of the board - // required: true - SchemeAdmin bool `json:"schemeAdmin"` - - // Marks the user as an editor of the board - // required: true - SchemeEditor bool `json:"schemeEditor"` - - // Marks the user as an commenter of the board - // required: true - SchemeCommenter bool `json:"schemeCommenter"` - - // Marks the user as an viewer of the board - // required: true - SchemeViewer bool `json:"schemeViewer"` - - // Marks the membership as generated by an access group - // required: true - Synthetic bool `json:"synthetic"` -} - -// BoardMetadata contains metadata for a Board -// swagger:model -type BoardMetadata struct { - // The ID for the board - // required: true - BoardID string `json:"boardId"` - - // The most recent time a descendant of this board was added, modified, or deleted - // required: true - DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"` - - // The earliest time a descendant of this board was added, modified, or deleted - // required: true - DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"` - - // The ID of the user that created the board - // required: true - CreatedBy string `json:"createdBy"` - - // The ID of the user that last modified the most recently modified descendant - // required: true - LastModifiedBy string `json:"lastModifiedBy"` -} - -func BoardFromJSON(data io.Reader) *Board { - var board *Board - _ = json.NewDecoder(data).Decode(&board) - return board -} - -func BoardsFromJSON(data io.Reader) []*Board { - var boards []*Board - _ = json.NewDecoder(data).Decode(&boards) - return boards -} - -func BoardMemberFromJSON(data io.Reader) *BoardMember { - var boardMember *BoardMember - _ = json.NewDecoder(data).Decode(&boardMember) - return boardMember -} - -func BoardMembersFromJSON(data io.Reader) []*BoardMember { - var boardMembers []*BoardMember - _ = json.NewDecoder(data).Decode(&boardMembers) - return boardMembers -} - -func BoardMetadataFromJSON(data io.Reader) *BoardMetadata { - var boardMetadata *BoardMetadata - _ = json.NewDecoder(data).Decode(&boardMetadata) - return boardMetadata -} - -// Patch returns an updated version of the board. -func (p *BoardPatch) Patch(board *Board) *Board { - if p.Type != nil { - board.Type = *p.Type - } - - if p.Title != nil { - board.Title = *p.Title - } - - if p.MinimumRole != nil { - board.MinimumRole = *p.MinimumRole - } - - if p.Description != nil { - board.Description = *p.Description - } - - if p.Icon != nil { - board.Icon = *p.Icon - } - - if p.ShowDescription != nil { - board.ShowDescription = *p.ShowDescription - } - - if p.ChannelID != nil { - board.ChannelID = *p.ChannelID - } - - for key, property := range p.UpdatedProperties { - board.Properties[key] = property - } - - for _, key := range p.DeletedProperties { - delete(board.Properties, key) - } - - if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 { - // first we accumulate all properties indexed by, and maintain their order - keyOrder := []string{} - cardPropertyMap := map[string]map[string]interface{}{} - for _, prop := range board.CardProperties { - id, ok := prop["id"].(string) - if !ok { - // bad property, skipping - continue - } - - cardPropertyMap[id] = prop - keyOrder = append(keyOrder, id) - } - - // if there are properties marked for removal, we delete them - for _, propertyID := range p.DeletedCardProperties { - delete(cardPropertyMap, propertyID) - } - - // if there are properties marked for update, we replace the - // existing ones or add them - for _, newprop := range p.UpdatedCardProperties { - id, ok := newprop["id"].(string) - if !ok { - // bad new property, skipping - continue - } - - _, exists := cardPropertyMap[id] - if !exists { - keyOrder = append(keyOrder, id) - } - cardPropertyMap[id] = newprop - } - - // and finally we flatten and save the updated properties - newCardProperties := []map[string]interface{}{} - for _, key := range keyOrder { - p, exists := cardPropertyMap[key] - if exists { - newCardProperties = append(newCardProperties, p) - } - } - - board.CardProperties = newCardProperties - } - - return board -} - -func IsBoardTypeValid(t BoardType) bool { - return t == BoardTypeOpen || t == BoardTypePrivate -} - -func IsBoardMinimumRoleValid(r BoardRole) bool { - return r == BoardRoleNone || r == BoardRoleAdmin || r == BoardRoleEditor || r == BoardRoleCommenter || r == BoardRoleViewer -} - -func (p *BoardPatch) IsValid() error { - if p.Type != nil && !IsBoardTypeValid(*p.Type) { - return InvalidBoardErr{"invalid-board-type"} - } - - if p.MinimumRole != nil && !IsBoardMinimumRoleValid(*p.MinimumRole) { - return InvalidBoardErr{"invalid-board-minimum-role"} - } - - return nil -} - -type InvalidBoardErr struct { - msg string -} - -func (ibe InvalidBoardErr) Error() string { - return ibe.msg -} - -func (b *Board) IsValid() error { - if b.TeamID == "" { - return InvalidBoardErr{"empty-team-id"} - } - - if !IsBoardTypeValid(b.Type) { - return InvalidBoardErr{"invalid-board-type"} - } - - if !IsBoardMinimumRoleValid(b.MinimumRole) { - return InvalidBoardErr{"invalid-board-minimum-role"} - } - - return nil -} - -// BoardMemberHistoryEntry stores the information of the membership of a user on a board -// swagger:model -type BoardMemberHistoryEntry struct { - // The ID of the board - // required: true - BoardID string `json:"boardId"` - - // The ID of the user - // required: true - UserID string `json:"userId"` - - // The action that added this history entry (created or deleted) - // required: false - Action string `json:"action"` - - // The insertion time - // required: true - InsertAt time.Time `json:"insertAt"` -} - -func BoardSearchFieldFromString(field string) (BoardSearchField, error) { - switch field { - case string(BoardSearchFieldTitle): - return BoardSearchFieldTitle, nil - case string(BoardSearchFieldPropertyName): - return BoardSearchFieldPropertyName, nil - } - return BoardSearchFieldNone, ErrInvalidBoardSearchField -} diff --git a/server/boards/model/board_insights.go b/server/boards/model/board_insights.go deleted file mode 100644 index 4f5b46ea08..0000000000 --- a/server/boards/model/board_insights.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -// BoardInsightsList is a response type with pagination support. -type BoardInsightsList struct { - mm_model.InsightsListData - Items []*BoardInsight `json:"items"` -} - -// BoardInsight gives insight into activities in a Board -// swagger:model -type BoardInsight struct { - // ID of the board - // required: true - BoardID string `json:"boardID"` - - // icon of the board - // required: false - Icon string `json:"icon"` - - // Title of the board - // required: false - Title string `json:"title"` - - // Metric of how active the board is - // required: true - ActivityCount string `json:"activityCount"` - - // IDs of users active on the board - // required: true - ActiveUsers mm_model.StringArray `json:"activeUsers"` - - // ID of user who created the board - // required: true - CreatedBy string `json:"createdBy"` -} - -func BoardInsightsFromJSON(data io.Reader) []BoardInsight { - var boardInsights []BoardInsight - _ = json.NewDecoder(data).Decode(&boardInsights) - return boardInsights -} - -// GetTopBoardInsightsListWithPagination adds a rank to each item in the given list of BoardInsight and checks if there is -// another page that can be fetched based on the given limit and offset. The given list of BoardInsight is assumed to be -// sorted by ActivityCount(score). Returns a BoardInsightsList. -func GetTopBoardInsightsListWithPagination(boards []*BoardInsight, limit int) *BoardInsightsList { - // Add pagination support - var hasNext bool - if limit != 0 && len(boards) == limit+1 { - hasNext = true - boards = boards[:len(boards)-1] - } - - return &BoardInsightsList{InsightsListData: mm_model.InsightsListData{HasNext: hasNext}, Items: boards} -} diff --git a/server/boards/model/board_statistics.go b/server/boards/model/board_statistics.go deleted file mode 100644 index 4a06f843b3..0000000000 --- a/server/boards/model/board_statistics.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package model - -// BoardsStatistics is the representation of the statistics for the Boards server -// swagger:model -type BoardsStatistics struct { - // The maximum number of cards on the server - // required: true - Boards int `json:"board_count"` - - // The maximum number of cards on the server - // required: true - Cards int `json:"card_count"` -} diff --git a/server/boards/model/boards_and_blocks.go b/server/boards/model/boards_and_blocks.go deleted file mode 100644 index 710b0636da..0000000000 --- a/server/boards/model/boards_and_blocks.go +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "errors" - "fmt" - "io" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required") -var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required") -var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty") -var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match") -var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match") - -type BlockDoesntBelongToAnyBoardErr struct { - blockID string -} - -func (e BlockDoesntBelongToAnyBoardErr) Error() string { - return fmt.Sprintf("block %s doesn't belong to any board", e.blockID) -} - -// BoardsAndBlocks is used to operate over boards and blocks at the -// same time -// swagger:model -type BoardsAndBlocks struct { - // The boards - // required: false - Boards []*Board `json:"boards"` - - // The blocks - // required: false - Blocks []*Block `json:"blocks"` -} - -func (bab *BoardsAndBlocks) IsValid() error { - if len(bab.Boards) == 0 { - return ErrNoBoardsInBoardsAndBlocks - } - - if len(bab.Blocks) == 0 { - return ErrNoBlocksInBoardsAndBlocks - } - - boardsMap := map[string]bool{} - for _, board := range bab.Boards { - boardsMap[board.ID] = true - } - - for _, block := range bab.Blocks { - if _, ok := boardsMap[block.BoardID]; !ok { - return BlockDoesntBelongToAnyBoardErr{block.ID} - } - } - return nil -} - -// DeleteBoardsAndBlocks is used to list the boards and blocks to -// delete on a request -// swagger:model -type DeleteBoardsAndBlocks struct { - // The boards - // required: true - Boards []string `json:"boards"` - - // The blocks - // required: true - Blocks []string `json:"blocks"` -} - -func NewDeleteBoardsAndBlocksFromBabs(babs *BoardsAndBlocks) *DeleteBoardsAndBlocks { - boardIDs := make([]string, 0, len(babs.Boards)) - blockIDs := make([]string, 0, len(babs.Boards)) - - for _, board := range babs.Boards { - boardIDs = append(boardIDs, board.ID) - } - for _, block := range babs.Blocks { - blockIDs = append(blockIDs, block.ID) - } - return &DeleteBoardsAndBlocks{ - Boards: boardIDs, - Blocks: blockIDs, - } -} - -func (dbab *DeleteBoardsAndBlocks) IsValid() error { - if len(dbab.Boards) == 0 { - return ErrNoBoardsInBoardsAndBlocks - } - - return nil -} - -// PatchBoardsAndBlocks is used to patch multiple boards and blocks on -// a single request -// swagger:model -type PatchBoardsAndBlocks struct { - // The board IDs to patch - // required: true - BoardIDs []string `json:"boardIDs"` - - // The board patches - // required: true - BoardPatches []*BoardPatch `json:"boardPatches"` - - // The block IDs to patch - // required: true - BlockIDs []string `json:"blockIDs"` - - // The block patches - // required: true - BlockPatches []*BlockPatch `json:"blockPatches"` -} - -func (dbab *PatchBoardsAndBlocks) IsValid() error { - if len(dbab.BoardIDs) == 0 { - return ErrNoBoardsInBoardsAndBlocks - } - - if len(dbab.BoardIDs) != len(dbab.BoardPatches) { - return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks - } - - if len(dbab.BlockIDs) != len(dbab.BlockPatches) { - return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks - } - - return nil -} - -func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger mlog.LoggerIFace) (*BoardsAndBlocks, error) { - if err := bab.IsValid(); err != nil { - return nil, err - } - - blocksByBoard := map[string][]*Block{} - for _, block := range bab.Blocks { - blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block) - } - - boards := []*Board{} - blocks := []*Block{} - for _, board := range bab.Boards { - newID := utils.NewID(utils.IDTypeBoard) - for _, block := range blocksByBoard[board.ID] { - block.BoardID = newID - blocks = append(blocks, block) - } - - board.ID = newID - boards = append(boards, board) - } - - newBab := &BoardsAndBlocks{ - Boards: boards, - Blocks: GenerateBlockIDs(blocks, logger), - } - - return newBab, nil -} - -func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks { - var bab *BoardsAndBlocks - _ = json.NewDecoder(data).Decode(&bab) - return bab -} diff --git a/server/boards/model/boards_and_blocks_test.go b/server/boards/model/boards_and_blocks_test.go deleted file mode 100644 index 34659f1385..0000000000 --- a/server/boards/model/boards_and_blocks_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func TestIsValidBoardsAndBlocks(t *testing.T) { - t.Run("no boards", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Blocks: []*Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, - }, - } - - require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks) - }) - - t.Run("no blocks", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Boards: []*Board{ - {ID: "board-id-1", Type: BoardTypeOpen}, - {ID: "board-id-2", Type: BoardTypePrivate}, - }, - } - - require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks) - }) - - t.Run("block that doesn't belong to the boards", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Boards: []*Board{ - {ID: "board-id-1", Type: BoardTypeOpen}, - {ID: "board-id-2", Type: BoardTypePrivate}, - }, - Blocks: []*Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, - {ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, - }, - } - - require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"}) - }) - - t.Run("valid boards and blocks", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Boards: []*Board{ - {ID: "board-id-1", Type: BoardTypeOpen}, - {ID: "board-id-2", Type: BoardTypePrivate}, - }, - Blocks: []*Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, - {ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, - }, - } - - require.NoError(t, bab.IsValid()) - }) -} - -func TestGenerateBoardsAndBlocksIDs(t *testing.T) { - logger, err := mlog.NewLogger() - require.NoError(t, err) - - getBlockByType := func(blocks []*Block, blockType BlockType) *Block { - for _, b := range blocks { - if b.Type == blockType { - return b - } - } - return &Block{} - } - - getBoardByTitle := func(boards []*Board, title string) *Board { - for _, b := range boards { - if b.Title == title { - return b - } - } - return nil - } - - t.Run("invalid boards and blocks", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Blocks: []*Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, - }, - } - - rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) - require.Error(t, err) - require.Nil(t, rBab) - }) - - t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) { - bab := &BoardsAndBlocks{ - Boards: []*Board{ - {ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"}, - {ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"}, - {ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"}, - }, - Blocks: []*Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: TypeView}, - {ID: "block-id-3", BoardID: "board-id-2", Type: TypeText}, - }, - } - - rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) - require.NoError(t, err) - require.NotNil(t, rBab) - - // all boards and blocks should have refreshed their IDs, and - // blocks should be correctly linked to the new board IDs - board1 := getBoardByTitle(rBab.Boards, "board1") - require.NotNil(t, board1) - require.NotEmpty(t, board1.ID) - require.NotEqual(t, "board-id-1", board1.ID) - board2 := getBoardByTitle(rBab.Boards, "board2") - require.NotNil(t, board2) - require.NotEmpty(t, board2.ID) - require.NotEqual(t, "board-id-2", board2.ID) - board3 := getBoardByTitle(rBab.Boards, "board3") - require.NotNil(t, board3) - require.NotEmpty(t, board3.ID) - require.NotEqual(t, "board-id-3", board3.ID) - - block1 := getBlockByType(rBab.Blocks, TypeCard) - require.NotNil(t, block1) - require.NotEmpty(t, block1.ID) - require.NotEqual(t, "block-id-1", block1.ID) - require.Equal(t, board1.ID, block1.BoardID) - block2 := getBlockByType(rBab.Blocks, TypeView) - require.NotNil(t, block2) - require.NotEmpty(t, block2.ID) - require.NotEqual(t, "block-id-2", block2.ID) - require.Equal(t, board2.ID, block2.BoardID) - block3 := getBlockByType(rBab.Blocks, TypeText) - require.NotNil(t, block3) - require.NotEmpty(t, block3.ID) - require.NotEqual(t, "block-id-3", block3.ID) - require.Equal(t, board2.ID, block3.BoardID) - }) -} - -func TestIsValidPatchBoardsAndBlocks(t *testing.T) { - newTitle := "new title" - newDescription := "new description" - var schema int64 = 1 - - t.Run("no board ids", func(t *testing.T) { - pbab := &PatchBoardsAndBlocks{ - BoardIDs: []string{}, - BlockIDs: []string{"block-id-1"}, - BlockPatches: []*BlockPatch{ - {Title: &newTitle}, - {Schema: &schema}, - }, - } - - require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks) - }) - - t.Run("missmatch board IDs and patches", func(t *testing.T) { - pbab := &PatchBoardsAndBlocks{ - BoardIDs: []string{"board-id-1", "board-id-2"}, - BoardPatches: []*BoardPatch{ - {Title: &newTitle}, - }, - BlockIDs: []string{"block-id-1"}, - BlockPatches: []*BlockPatch{ - {Title: &newTitle}, - }, - } - - require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks) - }) - - t.Run("missmatch block IDs and patches", func(t *testing.T) { - pbab := &PatchBoardsAndBlocks{ - BoardIDs: []string{"board-id-1", "board-id-2"}, - BoardPatches: []*BoardPatch{ - {Title: &newTitle}, - {Description: &newDescription}, - }, - BlockIDs: []string{"block-id-1"}, - BlockPatches: []*BlockPatch{ - {Title: &newTitle}, - {Schema: &schema}, - }, - } - - require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks) - }) - - t.Run("valid", func(t *testing.T) { - pbab := &PatchBoardsAndBlocks{ - BoardIDs: []string{"board-id-1", "board-id-2"}, - BoardPatches: []*BoardPatch{ - {Title: &newTitle}, - {Description: &newDescription}, - }, - BlockIDs: []string{"block-id-1"}, - BlockPatches: []*BlockPatch{ - {Title: &newTitle}, - }, - } - - require.NoError(t, pbab.IsValid()) - }) -} - -func TestIsValidDeleteBoardsAndBlocks(t *testing.T) { - /* - TODO fix this - t.Run("no board ids", func(t *testing.T) { - dbab := &DeleteBoardsAndBlocks{ - TeamID: "team-id", - Blocks: []string{"block-id-1"}, - } - - require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr) - }) - - t.Run("no block ids", func(t *testing.T) { - dbab := &DeleteBoardsAndBlocks{ - TeamID: "team-id", - Boards: []string{"board-id-1", "board-id-2"}, - } - - require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr) - }) - - t.Run("valid", func(t *testing.T) { - dbab := &DeleteBoardsAndBlocks{ - TeamID: "team-id", - Boards: []string{"board-id-1", "board-id-2"}, - Blocks: []string{"block-id-1"}, - } - - require.NoError(t, dbab.IsValid()) - }) - */ -} diff --git a/server/boards/model/card.go b/server/boards/model/card.go deleted file mode 100644 index 938ac2d5bc..0000000000 --- a/server/boards/model/card.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "errors" - "fmt" - - "github.com/rivo/uniseg" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var ErrBoardIDMismatch = errors.New("Board IDs do not match") - -type ErrInvalidCard struct { - msg string -} - -func NewErrInvalidCard(msg string) ErrInvalidCard { - return ErrInvalidCard{ - msg: msg, - } -} - -func (e ErrInvalidCard) Error() string { - return fmt.Sprintf("invalid card, %s", e.msg) -} - -var ErrNotCardBlock = errors.New("not a card block") - -type ErrInvalidFieldType struct { - field string -} - -func (e ErrInvalidFieldType) Error() string { - return fmt.Sprintf("invalid type for field '%s'", e.field) -} - -// Card represents a group of content blocks and properties. -// swagger:model -type Card struct { - // The id for this card - // required: false - ID string `json:"id"` - - // The id for board this card belongs to. - // required: false - BoardID string `json:"boardId"` - - // The id for user who created this card - // required: false - CreatedBy string `json:"createdBy"` - - // The id for user who last modified this card - // required: false - ModifiedBy string `json:"modifiedBy"` - - // The display title - // required: false - Title string `json:"title"` - - // An array of content block ids specifying the ordering of content for this card. - // required: false - ContentOrder []string `json:"contentOrder"` - - // The icon of the card - // required: false - Icon string `json:"icon"` - - // True if this card belongs to a template - // required: false - IsTemplate bool `json:"isTemplate"` - - // A map of property ids to property values (option ids, strings, array of option ids) - // required: false - Properties map[string]any `json:"properties"` - - // The creation time in milliseconds since the current epoch - // required: false - CreateAt int64 `json:"createAt"` - - // The last modified time in milliseconds since the current epoch - // required: false - UpdateAt int64 `json:"updateAt"` - - // The deleted time in milliseconds since the current epoch. Set to indicate this card is deleted - // required: false - DeleteAt int64 `json:"deleteAt"` -} - -// Populate populates a Card with default values. -func (c *Card) Populate() { - if c.ID == "" { - c.ID = utils.NewID(utils.IDTypeCard) - } - if c.ContentOrder == nil { - c.ContentOrder = make([]string, 0) - } - if c.Properties == nil { - c.Properties = make(map[string]any) - } - now := utils.GetMillis() - if c.CreateAt == 0 { - c.CreateAt = now - } - if c.UpdateAt == 0 { - c.UpdateAt = now - } -} - -func (c *Card) PopulateWithBoardID(boardID string) { - c.BoardID = boardID - c.Populate() -} - -// CheckValid returns an error if the Card has invalid field values. -func (c *Card) CheckValid() error { - if c.ID == "" { - return ErrInvalidCard{"ID is missing"} - } - if c.BoardID == "" { - return ErrInvalidCard{"BoardID is missing"} - } - if c.ContentOrder == nil { - return ErrInvalidCard{"ContentOrder is missing"} - } - if uniseg.GraphemeClusterCount(c.Icon) > 1 { - return ErrInvalidCard{"Icon can have only one grapheme"} - } - if c.Properties == nil { - return ErrInvalidCard{"Properties"} - } - if c.CreateAt == 0 { - return ErrInvalidCard{"CreateAt"} - } - if c.UpdateAt == 0 { - return ErrInvalidCard{"UpdateAt"} - } - return nil -} - -// CardPatch is a patch for modifying cards -// swagger:model -type CardPatch struct { - // The display title - // required: false - Title *string `json:"title"` - - // An array of content block ids specifying the ordering of content for this card. - // required: false - ContentOrder *[]string `json:"contentOrder"` - - // The icon of the card - // required: false - Icon *string `json:"icon"` - - // A map of property ids to property option ids to be updated - // required: false - UpdatedProperties map[string]any `json:"updatedProperties"` -} - -// Patch returns an updated version of the card. -func (p *CardPatch) Patch(card *Card) *Card { - if p.Title != nil { - card.Title = *p.Title - } - - if p.ContentOrder != nil { - card.ContentOrder = *p.ContentOrder - } - - if p.Icon != nil { - card.Icon = *p.Icon - } - - if card.Properties == nil { - card.Properties = make(map[string]any) - } - - // if there are properties marked for update, we replace the - // existing ones or add them - for propID, propVal := range p.UpdatedProperties { - card.Properties[propID] = propVal - } - - return card -} - -// CheckValid returns an error if the CardPatch has invalid field values. -func (p *CardPatch) CheckValid() error { - if p.Icon != nil && uniseg.GraphemeClusterCount(*p.Icon) > 1 { - return ErrInvalidCard{"Icon can have only one grapheme"} - } - return nil -} - -// Card2Block converts a card to block using a shallow copy. Not needed once cards are first class entities. -func Card2Block(card *Card) *Block { - fields := make(map[string]interface{}) - - fields["contentOrder"] = card.ContentOrder - fields["icon"] = card.Icon - fields["isTemplate"] = card.IsTemplate - fields["properties"] = card.Properties - - return &Block{ - ID: card.ID, - ParentID: card.BoardID, - CreatedBy: card.CreatedBy, - ModifiedBy: card.ModifiedBy, - Schema: 1, - Type: TypeCard, - Title: card.Title, - Fields: fields, - CreateAt: card.CreateAt, - UpdateAt: card.UpdateAt, - DeleteAt: card.DeleteAt, - BoardID: card.BoardID, - } -} - -// Block2Card converts a block to a card. Not needed once cards are first class entities. -func Block2Card(block *Block) (*Card, error) { - if block.Type != TypeCard { - return nil, fmt.Errorf("cannot convert block to card: %w", ErrNotCardBlock) - } - - contentOrder := make([]string, 0) - icon := "" - isTemplate := false - properties := make(map[string]any) - - if co, ok := block.Fields["contentOrder"]; ok { - switch arr := co.(type) { - case []any: - for _, str := range arr { - if id, ok := str.(string); ok { - contentOrder = append(contentOrder, id) - } else { - return nil, ErrInvalidFieldType{"contentOrder item"} - } - } - case []string: - contentOrder = append(contentOrder, arr...) - default: - return nil, ErrInvalidFieldType{"contentOrder"} - } - } - - if iconAny, ok := block.Fields["icon"]; ok { - if id, ok := iconAny.(string); ok { - icon = id - } else { - return nil, ErrInvalidFieldType{"icon"} - } - } - - if isTemplateAny, ok := block.Fields["isTemplate"]; ok { - if b, ok := isTemplateAny.(bool); ok { - isTemplate = b - } else { - return nil, ErrInvalidFieldType{"isTemplate"} - } - } - - if props, ok := block.Fields["properties"]; ok { - if propMap, ok := props.(map[string]any); ok { - for k, v := range propMap { - properties[k] = v - } - } else { - return nil, ErrInvalidFieldType{"properties"} - } - } - - card := &Card{ - ID: block.ID, - BoardID: block.BoardID, - CreatedBy: block.CreatedBy, - ModifiedBy: block.ModifiedBy, - Title: block.Title, - ContentOrder: contentOrder, - Icon: icon, - IsTemplate: isTemplate, - Properties: properties, - CreateAt: block.CreateAt, - UpdateAt: block.UpdateAt, - DeleteAt: block.DeleteAt, - } - card.Populate() - return card, nil -} - -// CardPatch2BlockPatch converts a CardPatch to a BlockPatch. Not needed once cards are first class entities. -func CardPatch2BlockPatch(cardPatch *CardPatch) (*BlockPatch, error) { - if err := cardPatch.CheckValid(); err != nil { - return nil, err - } - - blockPatch := &BlockPatch{ - Title: cardPatch.Title, - } - - updatedFields := make(map[string]any, 0) - - if cardPatch.ContentOrder != nil { - updatedFields["contentOrder"] = cardPatch.ContentOrder - } - if cardPatch.Icon != nil { - updatedFields["icon"] = cardPatch.Icon - } - - properties := make(map[string]any) - for k, v := range cardPatch.UpdatedProperties { - properties[k] = v - } - - if len(properties) != 0 { - updatedFields["properties"] = cardPatch.UpdatedProperties - } - - blockPatch.UpdatedFields = updatedFields - - return blockPatch, nil -} diff --git a/server/boards/model/card_test.go b/server/boards/model/card_test.go deleted file mode 100644 index a7b4a3452a..0000000000 --- a/server/boards/model/card_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func TestBlock2Card(t *testing.T) { - blockID := utils.NewID(utils.IDTypeCard) - boardID := utils.NewID(utils.IDTypeBoard) - userID := utils.NewID(utils.IDTypeUser) - now := utils.GetMillis() - - var fields map[string]any - err := json.Unmarshal([]byte(sampleBlockFieldsJSON), &fields) - require.NoError(t, err) - - block := &Block{ - ID: blockID, - ParentID: boardID, - CreatedBy: userID, - ModifiedBy: userID, - Schema: 1, - Type: TypeCard, - Title: "My card title", - Fields: fields, - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - BoardID: boardID, - } - - t.Run("Good block", func(t *testing.T) { - card, err := Block2Card(block) - require.NoError(t, err) - - assert.Equal(t, block.ID, card.ID) - assert.Equal(t, []string{"acdxa8r8aht85pyoeuj1ed7tu8w", "73urm1huoupd4idzkdq5yaeuyay", "ay6sogs9owtd9xbyn49qt3395ko"}, card.ContentOrder) - assert.EqualValues(t, fields["icon"], card.Icon) - assert.EqualValues(t, fields["isTemplate"], card.IsTemplate) - assert.EqualValues(t, fields["properties"], card.Properties) - }) - - t.Run("Not a card", func(t *testing.T) { - blockNotCard := &Block{} - - card, err := Block2Card(blockNotCard) - require.Error(t, err) - require.Nil(t, card) - }) -} - -const sampleBlockFieldsJSON = ` -{ - "contentOrder":[ - "acdxa8r8aht85pyoeuj1ed7tu8w", - "73urm1huoupd4idzkdq5yaeuyay", - "ay6sogs9owtd9xbyn49qt3395ko" - ], - "icon":"🎨", - "isTemplate":false, - "properties":{ - "aa7swu9zz3ofdkcna3h867cum4y":"212-444-1234", - "af6fcbb8-ca56-4b73-83eb-37437b9a667d":"77c539af-309c-4db1-8329-d20ef7e9eacd", - "aiwt9ibi8jjrf9hzi1xzk8no8mo":"foo", - "aj65h4s6ghr6wgh3bnhqbzzmiaa":"77", - "ajy6xbebzopojaenbnmfpgtdwso":"{\"from\":1660046400000}", - "amc8wnk1xqj54rymkoqffhtw7ie":"zhqsoeqs1pg9i8gk81k9ryy83h", - "aooz77t119y7xtfmoyeiy4up75c":"someone@example.com", - "auskzaoaccsn55icuwarf4o3tfe":"https://www.google.com", - "aydsk41h6cs1z7nmghaw16jqcia":[ - "aw565znut6zphbxqhbwyawiuggy", - "aefd3pxciomrkur4rc6smg1usoc", - "a6c96kwrqaskbtochq9wunmzweh", - "atyexeuq993fwwb84bxoqixxqqr" - ], - "d6b1249b-bc18-45fc-889e-bec48fce80ef":"9a090e33-b110-4268-8909-132c5002c90e", - "d9725d14-d5a8-48e5-8de1-6f8c004a9680":"3245a32d-f688-463b-87f4-8e7142c1b397" - } -}` diff --git a/server/boards/model/category.go b/server/boards/model/category.go deleted file mode 100644 index 7829b38829..0000000000 --- a/server/boards/model/category.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "fmt" - "io" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -const ( - CategoryTypeSystem = "system" - CategoryTypeCustom = "custom" -) - -// Category is a board category -// swagger:model -type Category struct { - // The id for this category - // required: true - ID string `json:"id"` - - // The name for this category - // required: true - Name string `json:"name"` - - // The user's id for this category - // required: true - UserID string `json:"userID"` - - // The team id for this category - // required: true - TeamID string `json:"teamID"` - - // The creation time in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"createAt"` - - // The last modified time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"updateAt"` - - // The deleted time in miliseconds since the current epoch. Set to indicate this category is deleted - // required: false - DeleteAt int64 `json:"deleteAt"` - - // Category's state in client side - // required: true - Collapsed bool `json:"collapsed"` - - // Inter-category sort order per user - // required: true - SortOrder int `json:"sortOrder"` - - // The sorting method applied on this category - // required: true - Sorting string `json:"sorting"` - - // Category's type - // required: true - Type string `json:"type"` -} - -func (c *Category) Hydrate() { - if c.ID == "" { - c.ID = utils.NewID(utils.IDTypeNone) - } - - if c.CreateAt == 0 { - c.CreateAt = utils.GetMillis() - } - - if c.UpdateAt == 0 { - c.UpdateAt = c.CreateAt - } - - if c.SortOrder < 0 { - c.SortOrder = 0 - } - - if strings.TrimSpace(c.Type) == "" { - c.Type = CategoryTypeCustom - } -} - -func (c *Category) IsValid() error { - if strings.TrimSpace(c.ID) == "" { - return NewErrInvalidCategory("category ID cannot be empty") - } - - if strings.TrimSpace(c.Name) == "" { - return NewErrInvalidCategory("category name cannot be empty") - } - - if strings.TrimSpace(c.UserID) == "" { - return NewErrInvalidCategory("category user ID cannot be empty") - } - - if strings.TrimSpace(c.TeamID) == "" { - return NewErrInvalidCategory("category team id ID cannot be empty") - } - - if c.Type != CategoryTypeCustom && c.Type != CategoryTypeSystem { - return NewErrInvalidCategory(fmt.Sprintf("category type is invalid. Allowed types: %s and %s", CategoryTypeSystem, CategoryTypeCustom)) - } - - return nil -} - -func CategoryFromJSON(data io.Reader) *Category { - var category *Category - _ = json.NewDecoder(data).Decode(&category) - return category -} diff --git a/server/boards/model/category_boards.go b/server/boards/model/category_boards.go deleted file mode 100644 index a5b4c29cc5..0000000000 --- a/server/boards/model/category_boards.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -const CategoryBoardsSortOrderGap = 10 - -// CategoryBoards is a board category and associated boards -// swagger:model -type CategoryBoards struct { - Category - - // The IDs of boards in this category - // required: true - BoardMetadata []CategoryBoardMetadata `json:"boardMetadata"` - - // The relative sort order of this board in its category - // required: true - SortOrder int `json:"sortOrder"` -} - -type BoardCategoryWebsocketData struct { - BoardID string `json:"boardID"` - CategoryID string `json:"categoryID"` - Hidden bool `json:"hidden"` -} - -type CategoryBoardMetadata struct { - BoardID string `json:"boardID"` - Hidden bool `json:"hidden"` -} diff --git a/server/boards/model/clientConfig.go b/server/boards/model/clientConfig.go deleted file mode 100644 index d968d4862a..0000000000 --- a/server/boards/model/clientConfig.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -// ClientConfig is the client configuration -// swagger:model -type ClientConfig struct { - // Is telemetry enabled - // required: true - Telemetry bool `json:"telemetry"` - - // The telemetry ID - // required: true - TelemetryID string `json:"telemetryid"` - - // Is public shared boards enabled - // required: true - EnablePublicSharedBoards bool `json:"enablePublicSharedBoards"` - - // Is public shared boards enabled - // required: true - TeammateNameDisplay string `json:"teammateNameDisplay"` - - // The server feature flags - // required: true - FeatureFlags map[string]string `json:"featureFlags"` - - // Required for file upload to check the size of the file - // required: true - MaxFileSize int64 `json:"maxFileSize"` -} diff --git a/server/boards/model/cloud.go b/server/boards/model/cloud.go deleted file mode 100644 index e93f66c305..0000000000 --- a/server/boards/model/cloud.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -const LimitUnlimited = 0 - -// BoardsCloudLimits is the representation of the limits for the -// Boards server -// swagger:model -type BoardsCloudLimits struct { - // The maximum number of cards on the server - // required: true - Cards int `json:"cards"` - - // The current number of cards on the server - // required: true - UsedCards int `json:"used_cards"` - - // The updated_at timestamp of the limit card - // required: true - CardLimitTimestamp int64 `json:"card_limit_timestamp"` - - // The maximum number of views for each board - // required: true - Views int `json:"views"` -} diff --git a/server/boards/model/compliance.go b/server/boards/model/compliance.go deleted file mode 100644 index 7ecf3190a3..0000000000 --- a/server/boards/model/compliance.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package model - -// BaordsComplianceResponse is the response body to a request for boards. -// swagger:model -type BoardsComplianceResponse struct { - // True if there is a next page for pagination - // required: true - HasNext bool `json:"hasNext"` - - // The array of board records. - // required: true - Results []*Board `json:"results"` -} - -// BoardsComplianceHistoryResponse is the response body to a request for boards history. -// swagger:model -type BoardsComplianceHistoryResponse struct { - // True if there is a next page for pagination - // required: true - HasNext bool `json:"hasNext"` - - // The array of BoardHistory records. - // required: true - Results []*BoardHistory `json:"results"` -} - -// BlocksComplianceHistoryResponse is the response body to a request for blocks history. -// swagger:model -type BlocksComplianceHistoryResponse struct { - // True if there is a next page for pagination - // required: true - HasNext bool `json:"hasNext"` - - // The array of BlockHistory records. - // required: true - Results []*BlockHistory `json:"results"` -} - -// BoardHistory provides information about the history of a board. -// swagger:model -type BoardHistory struct { - ID string `json:"id"` - TeamID string `json:"teamId"` - IsDeleted bool `json:"isDeleted"` - DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"` - DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"` - CreatedBy string `json:"createdBy"` - LastModifiedBy string `json:"lastModifiedBy"` -} - -// BlockHistory provides information about the history of a block. -// swagger:model -type BlockHistory struct { - ID string `json:"id"` - TeamID string `json:"teamId"` - BoardID string `json:"boardId"` - Type string `json:"type"` - IsDeleted bool `json:"isDeleted"` - LastUpdateAt int64 `json:"lastUpdateAt"` - FirstUpdateAt int64 `json:"firstUpdateAt"` - CreatedBy string `json:"createdBy"` - LastModifiedBy string `json:"lastModifiedBy"` -} - -type QueryBoardsForComplianceOptions struct { - TeamID string // if not empty then filter for specific team, otherwise all teams are included - Page int // page number to select when paginating - PerPage int // number of blocks per page (default=60) -} - -type QueryBoardsComplianceHistoryOptions struct { - ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince - IncludeDeleted bool // if true then deleted blocks are included - TeamID string // if not empty then filter for specific team, otherwise all teams are included - Page int // page number to select when paginating - PerPage int // number of blocks per page (default=60) -} - -type QueryBlocksComplianceHistoryOptions struct { - ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince - IncludeDeleted bool // if true then deleted blocks are included - TeamID string // if not empty then filter for specific team, otherwise all teams are included - BoardID string // if not empty then filter for specific board, otherwise all boards are included - Page int // page number to select when paginating - PerPage int // number of blocks per page (default=60) -} diff --git a/server/boards/model/database.go b/server/boards/model/database.go deleted file mode 100644 index dfb3e59f94..0000000000 --- a/server/boards/model/database.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -const ( - PostgresDBType = "postgres" - MysqlDBType = "mysql" -) diff --git a/server/boards/model/error.go b/server/boards/model/error.go deleted file mode 100644 index 30077fddb5..0000000000 --- a/server/boards/model/error.go +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "database/sql" - "errors" - "fmt" - "net/http" - "strings" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -var ( - ErrViewsLimitReached = errors.New("views limit reached for board") - ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited") - - ErrInsufficientLicense = errors.New("appropriate license required") - - ErrCategoryPermissionDenied = errors.New("category doesn't belong to user") - ErrCategoryDeleted = errors.New("category is deleted") - - ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins") - - ErrRequestEntityTooLarge = errors.New("request entity too large") - - ErrInvalidBoardSearchField = errors.New("invalid board search field") -) - -// ErrNotFound is an error type that can be returned by store APIs -// when a query unexpectedly fetches no records. -type ErrNotFound struct { - entity string -} - -// NewErrNotFound creates a new ErrNotFound instance. -func NewErrNotFound(entity string) *ErrNotFound { - return &ErrNotFound{ - entity: entity, - } -} - -func (nf *ErrNotFound) Error() string { - return fmt.Sprintf("{%s} not found", nf.entity) -} - -// ErrNotAllFound is an error type that can be returned by store APIs -// when a query that should fetch a certain amount of records -// unexpectedly fetches less. -type ErrNotAllFound struct { - entity string - resources []string -} - -func NewErrNotAllFound(entity string, resources []string) *ErrNotAllFound { - return &ErrNotAllFound{ - entity: entity, - resources: resources, - } -} - -func (naf *ErrNotAllFound) Error() string { - return fmt.Sprintf("not all instances of {%s} in {%s} found", naf.entity, strings.Join(naf.resources, ", ")) -} - -// ErrBadRequest can be returned when the API handler receives a -// malformed request. -type ErrBadRequest struct { - reason string -} - -// NewErrNotFound creates a new ErrNotFound instance. -func NewErrBadRequest(reason string) *ErrBadRequest { - return &ErrBadRequest{ - reason: reason, - } -} - -func (br *ErrBadRequest) Error() string { - return br.reason -} - -// ErrUnauthorized can be returned when requester has provided an -// invalid authorization for a given resource or has not provided any. -type ErrUnauthorized struct { - reason string -} - -// NewErrUnauthorized creates a new ErrUnauthorized instance. -func NewErrUnauthorized(reason string) *ErrUnauthorized { - return &ErrUnauthorized{ - reason: reason, - } -} - -func (br *ErrUnauthorized) Error() string { - return br.reason -} - -// ErrPermission can be returned when requester lacks a permission for -// a given resource. -type ErrPermission struct { - reason string -} - -// NewErrPermission creates a new ErrPermission instance. -func NewErrPermission(reason string) *ErrPermission { - return &ErrPermission{ - reason: reason, - } -} - -func (br *ErrPermission) Error() string { - return br.reason -} - -// ErrForbidden can be returned when requester doesn't have access to -// a given resource. -type ErrForbidden struct { - reason string -} - -// NewErrForbidden creates a new ErrForbidden instance. -func NewErrForbidden(reason string) *ErrForbidden { - return &ErrForbidden{ - reason: reason, - } -} - -func (br *ErrForbidden) Error() string { - return br.reason -} - -type ErrInvalidCategory struct { - msg string -} - -func NewErrInvalidCategory(msg string) *ErrInvalidCategory { - return &ErrInvalidCategory{ - msg: msg, - } -} - -func (e *ErrInvalidCategory) Error() string { - return e.msg -} - -type ErrNotImplemented struct { - msg string -} - -func NewErrNotImplemented(msg string) *ErrNotImplemented { - return &ErrNotImplemented{ - msg: msg, - } -} - -func (ni *ErrNotImplemented) Error() string { - return ni.msg -} - -// IsErrBadRequest returns true if `err` is or wraps one of: -// - model.ErrBadRequest -// - model.ErrViewsLimitReached -// - model.ErrAuthParam -// - model.ErrInvalidCategory -// - model.ErrBoardMemberIsLastAdmin -// - model.ErrBoardIDMismatch. -func IsErrBadRequest(err error) bool { - if err == nil { - return false - } - - // check if this is a model.ErrBadRequest - var br *ErrBadRequest - if errors.As(err, &br) { - return true - } - - // check if this is a model.ErrAuthParam - var ap *ErrAuthParam - if errors.As(err, &ap) { - return true - } - - // check if this is a model.ErrViewsLimitReached - if errors.Is(err, ErrViewsLimitReached) { - return true - } - - // check if this is a model.ErrInvalidCategory - var ic *ErrInvalidCategory - if errors.As(err, &ic) { - return true - } - - // check if this is a model.ErrBoardIDMismatch - if errors.Is(err, ErrBoardMemberIsLastAdmin) { - return true - } - - // check if this is a model.ErrBoardMemberIsLastAdmin - return errors.Is(err, ErrBoardIDMismatch) -} - -// IsErrUnauthorized returns true if `err` is or wraps one of: -// - model.ErrUnauthorized. -func IsErrUnauthorized(err error) bool { - if err == nil { - return false - } - - // check if this is a model.ErrUnauthorized - var u *ErrUnauthorized - return errors.As(err, &u) -} - -// IsErrForbidden returns true if `err` is or wraps one of: -// - model.ErrForbidden -// - model.ErrPermission -// - model.ErrPatchUpdatesLimitedCards -// - model.ErrorCategoryPermissionDenied. -func IsErrForbidden(err error) bool { - if err == nil { - return false - } - - // check if this is a model.ErrForbidden - var f *ErrForbidden - if errors.As(err, &f) { - return true - } - - // check if this is a model.ErrPermission - var p *ErrPermission - if errors.As(err, &p) { - return true - } - - // check if this is a model.ErrPatchUpdatesLimitedCards - if errors.Is(err, ErrPatchUpdatesLimitedCards) { - return true - } - - // check if this is a model.ErrCategoryPermissionDenied - return errors.Is(err, ErrCategoryPermissionDenied) -} - -// IsErrNotFound returns true if `err` is or wraps one of: -// - model.ErrNotFound -// - model.ErrNotAllFound -// - sql.ErrNoRows -// - mattermost-plugin-api/ErrNotFound. -// - model.ErrCategoryDeleted. -func IsErrNotFound(err error) bool { - if err == nil { - return false - } - - // check if this is a model.ErrNotFound - var nf *ErrNotFound - if errors.As(err, &nf) { - return true - } - - // check if this is a model.ErrNotAllFound - var naf *ErrNotAllFound - if errors.As(err, &naf) { - return true - } - - // check if this is a sql.ErrNotFound - if errors.Is(err, sql.ErrNoRows) { - return true - } - - // check if this is a Mattermost AppError with a Not Found status - var appErr *mm_model.AppError - if errors.As(err, &appErr) { - if appErr.StatusCode == http.StatusNotFound { - return true - } - } - - // check if this is a model.ErrCategoryDeleted - return errors.Is(err, ErrCategoryDeleted) -} - -// IsErrRequestEntityTooLarge returns true if `err` is or wraps one of: -// - model.ErrRequestEntityTooLarge. -func IsErrRequestEntityTooLarge(err error) bool { - // check if this is a model.ErrRequestEntityTooLarge - return errors.Is(err, ErrRequestEntityTooLarge) -} - -// IsErrNotImplemented returns true if `err` is or wraps one of: -// - model.ErrNotImplemented -// - model.ErrInsufficientLicense. -func IsErrNotImplemented(err error) bool { - if err == nil { - return false - } - - // check if this is a model.ErrNotImplemented - var eni *ErrNotImplemented - if errors.As(err, &eni) { - return true - } - - // check if this is a model.ErrInsufficientLicense - return errors.Is(err, ErrInsufficientLicense) -} diff --git a/server/boards/model/errorResponse.go b/server/boards/model/errorResponse.go deleted file mode 100644 index 77aff106ab..0000000000 --- a/server/boards/model/errorResponse.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -// ErrorResponse is an error response -// swagger:model -type ErrorResponse struct { - // The error message - // required: false - Error string `json:"error"` - - // The error code - // required: false - ErrorCode int `json:"errorCode"` -} diff --git a/server/boards/model/file.go b/server/boards/model/file.go deleted file mode 100644 index 4ad8acb758..0000000000 --- a/server/boards/model/file.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package model - -import ( - "mime" - "path/filepath" - "strings" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func NewFileInfo(name string) *mm_model.FileInfo { - - extension := strings.ToLower(filepath.Ext(name)) - now := utils.GetMillis() - return &mm_model.FileInfo{ - CreatorId: "boards", - CreateAt: now, - UpdateAt: now, - Name: name, - Extension: extension, - MimeType: mime.TypeByExtension(extension), - } - -} diff --git a/server/boards/model/import_export.go b/server/boards/model/import_export.go deleted file mode 100644 index 92ae9f0042..0000000000 --- a/server/boards/model/import_export.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "errors" - "fmt" -) - -var ( - ErrInvalidImageBlock = errors.New("invalid image block") -) - -// Archive is an import / export archive. -// TODO: remove once default templates are converted to new archive format. -type Archive struct { - Version int64 `json:"version"` - Date int64 `json:"date"` - Blocks []Block `json:"blocks"` -} - -// ArchiveHeader is the content of the first file (`version.json`) within an archive. -type ArchiveHeader struct { - Version int `json:"version"` - Date int64 `json:"date"` -} - -// ArchiveLine is any line in an archive. -type ArchiveLine struct { - Type string `json:"type"` - Data json.RawMessage `json:"data"` -} - -// ExportArchiveOptions provides options when exporting one or more boards -// to an archive. -type ExportArchiveOptions struct { - TeamID string - - // BoardIDs is the list of boards to include in the archive. - // Empty slice means export all boards from workspace/team. - BoardIDs []string -} - -// ImportArchiveOptions provides options when importing an archive. -type ImportArchiveOptions struct { - TeamID string - ModifiedBy string - BoardModifier BoardModifier - BlockModifier BlockModifier -} - -// ErrUnsupportedArchiveVersion is an error returned when trying to import an -// archive with a version that this server does not support. -type ErrUnsupportedArchiveVersion struct { - got int - want int -} - -// NewErrUnsupportedArchiveVersion creates a ErrUnsupportedArchiveVersion error. -func NewErrUnsupportedArchiveVersion(got int, want int) ErrUnsupportedArchiveVersion { - return ErrUnsupportedArchiveVersion{ - got: got, - want: want, - } -} - -func (e ErrUnsupportedArchiveVersion) Error() string { - return fmt.Sprintf("unsupported archive version; got %d, want %d", e.got, e.want) -} - -// ErrUnsupportedArchiveLineType is an error returned when trying to import an -// archive containing an unsupported line type. -type ErrUnsupportedArchiveLineType struct { - line int - got string -} - -// NewErrUnsupportedArchiveLineType creates a ErrUnsupportedArchiveLineType error. -func NewErrUnsupportedArchiveLineType(line int, got string) ErrUnsupportedArchiveLineType { - return ErrUnsupportedArchiveLineType{ - line: line, - got: got, - } -} - -func (e ErrUnsupportedArchiveLineType) Error() string { - return fmt.Sprintf("unsupported archive line type; got %s, line %d", e.got, e.line) -} diff --git a/server/boards/model/mocks/mockservicesapi.go b/server/boards/model/mocks/mockservicesapi.go deleted file mode 100644 index b408f9fbcc..0000000000 --- a/server/boards/model/mocks/mockservicesapi.go +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/model (interfaces: ServicesAPI) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - sql "database/sql" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - mux "github.com/gorilla/mux" - model "github.com/mattermost/mattermost/server/public/model" - mlog "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// MockServicesAPI is a mock of ServicesAPI interface. -type MockServicesAPI struct { - ctrl *gomock.Controller - recorder *MockServicesAPIMockRecorder -} - -// MockServicesAPIMockRecorder is the mock recorder for MockServicesAPI. -type MockServicesAPIMockRecorder struct { - mock *MockServicesAPI -} - -// NewMockServicesAPI creates a new mock instance. -func NewMockServicesAPI(ctrl *gomock.Controller) *MockServicesAPI { - mock := &MockServicesAPI{ctrl: ctrl} - mock.recorder = &MockServicesAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockServicesAPI) EXPECT() *MockServicesAPIMockRecorder { - return m.recorder -} - -// CreateMember mocks base method. -func (m *MockServicesAPI) CreateMember(arg0, arg1 string) (*model.TeamMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateMember indicates an expected call of CreateMember. -func (mr *MockServicesAPIMockRecorder) CreateMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMember", reflect.TypeOf((*MockServicesAPI)(nil).CreateMember), arg0, arg1) -} - -// CreatePost mocks base method. -func (m *MockServicesAPI) CreatePost(arg0 *model.Post) (*model.Post, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreatePost indicates an expected call of CreatePost. -func (mr *MockServicesAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0) -} - -// DeletePreferencesForUser mocks base method. -func (m *MockServicesAPI) DeletePreferencesForUser(arg0 string, arg1 model.Preferences) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser. -func (mr *MockServicesAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).DeletePreferencesForUser), arg0, arg1) -} - -// EnsureBot mocks base method. -func (m *MockServicesAPI) EnsureBot(arg0 *model.Bot) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnsureBot", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// EnsureBot indicates an expected call of EnsureBot. -func (mr *MockServicesAPIMockRecorder) EnsureBot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBot", reflect.TypeOf((*MockServicesAPI)(nil).EnsureBot), arg0) -} - -// GetChannelByID mocks base method. -func (m *MockServicesAPI) GetChannelByID(arg0 string) (*model.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelByID", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChannelByID indicates an expected call of GetChannelByID. -func (mr *MockServicesAPIMockRecorder) GetChannelByID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByID", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelByID), arg0) -} - -// GetChannelMember mocks base method. -func (m *MockServicesAPI) GetChannelMember(arg0, arg1 string) (*model.ChannelMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChannelMember indicates an expected call of GetChannelMember. -func (mr *MockServicesAPIMockRecorder) GetChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMember", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelMember), arg0, arg1) -} - -// GetChannelsForTeamForUser mocks base method. -func (m *MockServicesAPI) GetChannelsForTeamForUser(arg0, arg1 string, arg2 bool) (model.ChannelList, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelsForTeamForUser", arg0, arg1, arg2) - ret0, _ := ret[0].(model.ChannelList) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChannelsForTeamForUser indicates an expected call of GetChannelsForTeamForUser. -func (mr *MockServicesAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2) -} - -// GetCloudLimits mocks base method. -func (m *MockServicesAPI) GetCloudLimits() (*model.ProductLimits, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCloudLimits") - ret0, _ := ret[0].(*model.ProductLimits) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCloudLimits indicates an expected call of GetCloudLimits. -func (mr *MockServicesAPIMockRecorder) GetCloudLimits() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockServicesAPI)(nil).GetCloudLimits)) -} - -// GetConfig mocks base method. -func (m *MockServicesAPI) GetConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetConfig indicates an expected call of GetConfig. -func (mr *MockServicesAPIMockRecorder) GetConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockServicesAPI)(nil).GetConfig)) -} - -// GetDiagnosticID mocks base method. -func (m *MockServicesAPI) GetDiagnosticID() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDiagnosticID") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetDiagnosticID indicates an expected call of GetDiagnosticID. -func (mr *MockServicesAPIMockRecorder) GetDiagnosticID() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDiagnosticID", reflect.TypeOf((*MockServicesAPI)(nil).GetDiagnosticID)) -} - -// GetDirectChannel mocks base method. -func (m *MockServicesAPI) GetDirectChannel(arg0, arg1 string) (*model.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDirectChannel", arg0, arg1) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDirectChannel indicates an expected call of GetDirectChannel. -func (mr *MockServicesAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannel), arg0, arg1) -} - -// GetDirectChannelOrCreate mocks base method. -func (m *MockServicesAPI) GetDirectChannelOrCreate(arg0, arg1 string) (*model.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDirectChannelOrCreate", arg0, arg1) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDirectChannelOrCreate indicates an expected call of GetDirectChannelOrCreate. -func (mr *MockServicesAPIMockRecorder) GetDirectChannelOrCreate(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannelOrCreate", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannelOrCreate), arg0, arg1) -} - -// GetFileInfo mocks base method. -func (m *MockServicesAPI) GetFileInfo(arg0 string) (*model.FileInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfo", arg0) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFileInfo indicates an expected call of GetFileInfo. -func (mr *MockServicesAPIMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockServicesAPI)(nil).GetFileInfo), arg0) -} - -// GetLicense mocks base method. -func (m *MockServicesAPI) GetLicense() *model.License { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLicense") - ret0, _ := ret[0].(*model.License) - return ret0 -} - -// GetLicense indicates an expected call of GetLicense. -func (mr *MockServicesAPIMockRecorder) GetLicense() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockServicesAPI)(nil).GetLicense)) -} - -// GetLogger mocks base method. -func (m *MockServicesAPI) GetLogger() mlog.LoggerIFace { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogger") - ret0, _ := ret[0].(mlog.LoggerIFace) - return ret0 -} - -// GetLogger indicates an expected call of GetLogger. -func (mr *MockServicesAPIMockRecorder) GetLogger() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockServicesAPI)(nil).GetLogger)) -} - -// GetMasterDB mocks base method. -func (m *MockServicesAPI) GetMasterDB() (*sql.DB, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMasterDB") - ret0, _ := ret[0].(*sql.DB) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMasterDB indicates an expected call of GetMasterDB. -func (mr *MockServicesAPIMockRecorder) GetMasterDB() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockServicesAPI)(nil).GetMasterDB)) -} - -// GetPreferencesForUser mocks base method. -func (m *MockServicesAPI) GetPreferencesForUser(arg0 string) (model.Preferences, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0) - ret0, _ := ret[0].(model.Preferences) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPreferencesForUser indicates an expected call of GetPreferencesForUser. -func (mr *MockServicesAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetPreferencesForUser), arg0) -} - -// GetTeamMember mocks base method. -func (m *MockServicesAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTeamMember indicates an expected call of GetTeamMember. -func (mr *MockServicesAPIMockRecorder) GetTeamMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMember", reflect.TypeOf((*MockServicesAPI)(nil).GetTeamMember), arg0, arg1) -} - -// GetUserByEmail mocks base method. -func (m *MockServicesAPI) GetUserByEmail(arg0 string) (*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByEmail", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByEmail indicates an expected call of GetUserByEmail. -func (mr *MockServicesAPIMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByEmail), arg0) -} - -// GetUserByID mocks base method. -func (m *MockServicesAPI) GetUserByID(arg0 string) (*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByID", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByID indicates an expected call of GetUserByID. -func (mr *MockServicesAPIMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByID), arg0) -} - -// GetUserByUsername mocks base method. -func (m *MockServicesAPI) GetUserByUsername(arg0 string) (*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByUsername", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByUsername indicates an expected call of GetUserByUsername. -func (mr *MockServicesAPIMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByUsername), arg0) -} - -// GetUsersFromProfiles mocks base method. -func (m *MockServicesAPI) GetUsersFromProfiles(arg0 *model.UserGetOptions) ([]*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersFromProfiles", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUsersFromProfiles indicates an expected call of GetUsersFromProfiles. -func (mr *MockServicesAPIMockRecorder) GetUsersFromProfiles(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersFromProfiles", reflect.TypeOf((*MockServicesAPI)(nil).GetUsersFromProfiles), arg0) -} - -// HasPermissionTo mocks base method. -func (m *MockServicesAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionTo indicates an expected call of HasPermissionTo. -func (mr *MockServicesAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionTo), arg0, arg1) -} - -// HasPermissionToChannel mocks base method. -func (m *MockServicesAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToChannel indicates an expected call of HasPermissionToChannel. -func (mr *MockServicesAPIMockRecorder) HasPermissionToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToChannel", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionToChannel), arg0, arg1, arg2) -} - -// HasPermissionToTeam mocks base method. -func (m *MockServicesAPI) HasPermissionToTeam(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToTeam", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToTeam indicates an expected call of HasPermissionToTeam. -func (mr *MockServicesAPIMockRecorder) HasPermissionToTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToTeam", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionToTeam), arg0, arg1, arg2) -} - -// KVSetWithOptions mocks base method. -func (m *MockServicesAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// KVSetWithOptions indicates an expected call of KVSetWithOptions. -func (mr *MockServicesAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockServicesAPI)(nil).KVSetWithOptions), arg0, arg1, arg2) -} - -// PublishPluginClusterEvent mocks base method. -func (m *MockServicesAPI) PublishPluginClusterEvent(arg0 model.PluginClusterEvent, arg1 model.PluginClusterEventSendOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishPluginClusterEvent", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// PublishPluginClusterEvent indicates an expected call of PublishPluginClusterEvent. -func (mr *MockServicesAPIMockRecorder) PublishPluginClusterEvent(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPluginClusterEvent", reflect.TypeOf((*MockServicesAPI)(nil).PublishPluginClusterEvent), arg0, arg1) -} - -// PublishWebSocketEvent mocks base method. -func (m *MockServicesAPI) PublishWebSocketEvent(arg0 string, arg1 map[string]interface{}, arg2 *model.WebsocketBroadcast) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebSocketEvent", arg0, arg1, arg2) -} - -// PublishWebSocketEvent indicates an expected call of PublishWebSocketEvent. -func (mr *MockServicesAPIMockRecorder) PublishWebSocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebSocketEvent", reflect.TypeOf((*MockServicesAPI)(nil).PublishWebSocketEvent), arg0, arg1, arg2) -} - -// RegisterRouter mocks base method. -func (m *MockServicesAPI) RegisterRouter(arg0 *mux.Router) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RegisterRouter", arg0) -} - -// RegisterRouter indicates an expected call of RegisterRouter. -func (mr *MockServicesAPIMockRecorder) RegisterRouter(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0) -} - -// UpdatePreferencesForUser mocks base method. -func (m *MockServicesAPI) UpdatePreferencesForUser(arg0 string, arg1 model.Preferences) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser. -func (mr *MockServicesAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdatePreferencesForUser), arg0, arg1) -} - -// UpdateUser mocks base method. -func (m *MockServicesAPI) UpdateUser(arg0 *model.User) (*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockServicesAPIMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdateUser), arg0) -} diff --git a/server/boards/model/mocks/propValueResolverMock.go b/server/boards/model/mocks/propValueResolverMock.go deleted file mode 100644 index 7e141c3bc9..0000000000 --- a/server/boards/model/mocks/propValueResolverMock.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/model (interfaces: PropValueResolver) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// MockPropValueResolver is a mock of PropValueResolver interface. -type MockPropValueResolver struct { - ctrl *gomock.Controller - recorder *MockPropValueResolverMockRecorder -} - -// MockPropValueResolverMockRecorder is the mock recorder for MockPropValueResolver. -type MockPropValueResolverMockRecorder struct { - mock *MockPropValueResolver -} - -// NewMockPropValueResolver creates a new mock instance. -func NewMockPropValueResolver(ctrl *gomock.Controller) *MockPropValueResolver { - mock := &MockPropValueResolver{ctrl: ctrl} - mock.recorder = &MockPropValueResolverMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPropValueResolver) EXPECT() *MockPropValueResolverMockRecorder { - return m.recorder -} - -// GetUserByID mocks base method. -func (m *MockPropValueResolver) GetUserByID(arg0 string) (*model.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByID", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByID indicates an expected call of GetUserByID. -func (mr *MockPropValueResolverMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockPropValueResolver)(nil).GetUserByID), arg0) -} diff --git a/server/boards/model/notification.go b/server/boards/model/notification.go deleted file mode 100644 index ca3ed086e2..0000000000 --- a/server/boards/model/notification.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "time" - - "github.com/mattermost/mattermost/server/v8/channels/utils" -) - -// NotificationHint provides a hint that a block has been modified and has subscribers that -// should be notified. -// swagger:model -type NotificationHint struct { - // BlockType is the block type of the entity (e.g. board, card) that was updated - // required: true - BlockType BlockType `json:"block_type"` - - // BlockID is id of the entity that was updated - // required: true - BlockID string `json:"block_id"` - - // ModifiedByID is the id of the user who made the block change - ModifiedByID string `json:"modified_by_id"` - - // CreatedAt is the timestamp this notification hint was created in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"create_at"` - - // NotifyAt is the timestamp this notification should be scheduled in miliseconds since the current epoch - // required: true - NotifyAt int64 `json:"notify_at"` -} - -func (s *NotificationHint) IsValid() error { - if s == nil { - return ErrInvalidNotificationHint{"cannot be nil"} - } - if s.BlockID == "" { - return ErrInvalidNotificationHint{"missing block id"} - } - if s.BlockType == "" { - return ErrInvalidNotificationHint{"missing block type"} - } - if s.ModifiedByID == "" { - return ErrInvalidNotificationHint{"missing modified_by id"} - } - return nil -} - -func (s *NotificationHint) Copy() *NotificationHint { - return &NotificationHint{ - BlockType: s.BlockType, - BlockID: s.BlockID, - ModifiedByID: s.ModifiedByID, - CreateAt: s.CreateAt, - NotifyAt: s.NotifyAt, - } -} - -func (s *NotificationHint) LogClone() interface{} { - return struct { - BlockType BlockType `json:"block_type"` - BlockID string `json:"block_id"` - ModifiedByID string `json:"modified_by_id"` - CreateAt string `json:"create_at"` - NotifyAt string `json:"notify_at"` - }{ - BlockType: s.BlockType, - BlockID: s.BlockID, - ModifiedByID: s.ModifiedByID, - CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli), - NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli), - } -} - -type ErrInvalidNotificationHint struct { - msg string -} - -func (e ErrInvalidNotificationHint) Error() string { - return e.msg -} diff --git a/server/boards/model/permission.go b/server/boards/model/permission.go deleted file mode 100644 index 1743a68ca2..0000000000 --- a/server/boards/model/permission.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -var ( - PermissionViewTeam = mm_model.PermissionViewTeam - PermissionManageTeam = mm_model.PermissionManageTeam - PermissionManageSystem = mm_model.PermissionManageSystem - PermissionReadChannel = mm_model.PermissionReadChannel - PermissionCreatePost = mm_model.PermissionCreatePost - PermissionViewMembers = mm_model.PermissionViewMembers - PermissionCreatePublicChannel = mm_model.PermissionCreatePublicChannel - PermissionCreatePrivateChannel = mm_model.PermissionCreatePrivateChannel - PermissionManageBoardType = &mm_model.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""} - PermissionDeleteBoard = &mm_model.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""} - PermissionViewBoard = &mm_model.Permission{Id: "view_board", Name: "", Description: "", Scope: ""} - PermissionManageBoardRoles = &mm_model.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""} - PermissionShareBoard = &mm_model.Permission{Id: "share_board", Name: "", Description: "", Scope: ""} - PermissionManageBoardCards = &mm_model.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""} - PermissionManageBoardProperties = &mm_model.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""} - PermissionCommentBoardCards = &mm_model.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""} - PermissionDeleteOthersComments = &mm_model.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""} -) diff --git a/server/boards/model/properties.go b/server/boards/model/properties.go deleted file mode 100644 index ae7b505fb5..0000000000 --- a/server/boards/model/properties.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/propValueResolverMock.go -package mocks . PropValueResolver - -package model - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -var ErrInvalidBoardBlock = errors.New("invalid board block") -var ErrInvalidPropSchema = errors.New("invalid property schema") -var ErrInvalidProperty = errors.New("invalid property") -var ErrInvalidPropertyValue = errors.New("invalid property value") -var ErrInvalidPropertyValueType = errors.New("invalid property value type") -var ErrInvalidDate = errors.New("invalid date property") - -// PropValueResolver allows PropDef.GetValue to further decode property values, such as -// looking up usernames from ids. -type PropValueResolver interface { - GetUserByID(userID string) (*User, error) -} - -// BlockProperties is a map of Prop's keyed by property id. -type BlockProperties map[string]BlockProp - -// BlockProp represent a property attached to a block (typically a card). -type BlockProp struct { - ID string `json:"id"` - Index int `json:"index"` - Name string `json:"name"` - Value string `json:"value"` -} - -// PropSchema is a map of PropDef's keyed by property id. -type PropSchema map[string]PropDef - -// PropDefOption represents an option within a property definition. -type PropDefOption struct { - ID string `json:"id"` - Index int `json:"index"` - Color string `json:"color"` - Value string `json:"value"` -} - -// PropDef represents a property definition as defined in a board's Fields member. -type PropDef struct { - ID string `json:"id"` - Index int `json:"index"` - Name string `json:"name"` - Type string `json:"type"` - Options map[string]PropDefOption `json:"options"` -} - -// GetValue resolves the value of a property if the passed value is an ID for an option, -// otherwise returns the original value. -func (pd PropDef) GetValue(v interface{}, resolver PropValueResolver) (string, error) { - switch pd.Type { - case "select": - // v is the id of an option - id, ok := v.(string) - if !ok { - return "", ErrInvalidPropertyValueType - } - opt, ok := pd.Options[id] - if !ok { - return "", ErrInvalidPropertyValue - } - return strings.ToUpper(opt.Value), nil - - case "date": - // v is a JSON string - date, ok := v.(string) - if !ok { - return "", ErrInvalidPropertyValueType - } - return pd.ParseDate(date) - - case "person": - // v is a userid - userID, ok := v.(string) - if !ok { - return "", ErrInvalidPropertyValueType - } - if resolver != nil { - user, err := resolver.GetUserByID(userID) - if err != nil { - return "", err - } - if user == nil { - return userID, nil - } - return user.Username, nil - } - return userID, nil - - case "multiPerson": - // v is a slice of user IDs - userIDs, ok := v.([]interface{}) - if !ok { - return "", fmt.Errorf("multiPerson property type: %w", ErrInvalidPropertyValueType) - } - if resolver != nil { - usernames := make([]string, len(userIDs)) - - for i, userIDInterface := range userIDs { - userID := userIDInterface.(string) - - user, err := resolver.GetUserByID(userID) - if err != nil { - return "", err - } - if user == nil { - usernames[i] = userID - } else { - usernames[i] = user.Username - } - } - - return strings.Join(usernames, ", "), nil - } - - case "multiSelect": - // v is a slice of strings containing option ids - ms, ok := v.([]interface{}) - if !ok { - return "", ErrInvalidPropertyValueType - } - var sb strings.Builder - prefix := "" - for _, optid := range ms { - id, ok := optid.(string) - if !ok { - return "", ErrInvalidPropertyValueType - } - opt, ok := pd.Options[id] - if !ok { - return "", ErrInvalidPropertyValue - } - sb.WriteString(prefix) - prefix = ", " - sb.WriteString(strings.ToUpper(opt.Value)) - } - return sb.String(), nil - } - return fmt.Sprintf("%v", v), nil -} - -func (pd PropDef) ParseDate(s string) (string, error) { - // s is a JSON snippet of the form: {"from":1642161600000, "to":1642161600000} in milliseconds UTC - // The UI does not yet support date ranges. - var m map[string]int64 - if err := json.Unmarshal([]byte(s), &m); err != nil { - return s, err - } - tsFrom, ok := m["from"] - if !ok { - return s, ErrInvalidDate - } - date := utils.GetTimeForMillis(tsFrom).Format("January 02, 2006") - tsTo, ok := m["to"] - if ok { - date += " -> " + utils.GetTimeForMillis(tsTo).Format("January 02, 2006") - } - return date, nil -} - -// ParsePropertySchema parses a board block's `Fields` to extract the properties -// schema for all cards within the board. -// The result is provided as a map for quick lookup, and the original order is -// preserved via the `Index` field. -func ParsePropertySchema(board *Board) (PropSchema, error) { - schema := make(map[string]PropDef) - - for i, prop := range board.CardProperties { - pd := PropDef{ - ID: getMapString("id", prop), - Index: i, - Name: getMapString("name", prop), - Type: getMapString("type", prop), - Options: make(map[string]PropDefOption), - } - optsIface, ok := prop["options"] - if ok { - opts, ok := optsIface.([]interface{}) - if !ok { - return nil, ErrInvalidPropSchema - } - for j, propOptIface := range opts { - propOpt, ok := propOptIface.(map[string]interface{}) - if !ok { - return nil, ErrInvalidPropSchema - } - po := PropDefOption{ - ID: getMapString("id", propOpt), - Index: j, - Value: getMapString("value", propOpt), - Color: getMapString("color", propOpt), - } - pd.Options[po.ID] = po - } - } - schema[pd.ID] = pd - } - return schema, nil -} - -func getMapString(key string, m map[string]interface{}) string { - iface, ok := m[key] - if !ok { - return "" - } - - s, ok := iface.(string) - if !ok { - return "" - } - return s -} - -// ParseProperties parses a block's `Fields` to extract the properties. Properties typically exist on -// card blocks. A resolver can optionally be provided to fetch usernames for `person` prop type. -func ParseProperties(block *Block, schema PropSchema, resolver PropValueResolver) (BlockProperties, error) { - props := make(map[string]BlockProp) - - if block == nil { - return props, nil - } - - // `properties` contains a map (untyped at this point). - propsIface, ok := block.Fields["properties"] - if !ok { - return props, nil // this is expected for blocks that don't have any properties. - } - - blockProps, ok := propsIface.(map[string]interface{}) - if !ok { - return props, fmt.Errorf("`properties` field wrong type: %w", ErrInvalidProperty) - } - - if len(blockProps) == 0 { - return props, nil - } - - for k, v := range blockProps { - s := fmt.Sprintf("%v", v) - - prop := BlockProp{ - ID: k, - Name: k, - Value: s, - } - - def, ok := schema[k] - if ok { - val, err := def.GetValue(v, resolver) - if err != nil { - return props, fmt.Errorf("could not parse property value (%s): %w", fmt.Sprintf("%v", v), err) - } - prop.Name = def.Name - prop.Value = val - prop.Index = def.Index - } - props[k] = prop - } - return props, nil -} diff --git a/server/boards/model/properties_test.go b/server/boards/model/properties_test.go deleted file mode 100644 index c2b749bb08..0000000000 --- a/server/boards/model/properties_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -type MockResolver struct{} - -func (r MockResolver) GetUserByID(userID string) (*User, error) { - if userID == "user_id_1" { - return &User{ - ID: "user_id_1", - Username: "username_1", - }, nil - } else if userID == "user_id_2" { - return &User{ - ID: "user_id_2", - Username: "username_2", - }, nil - } - - return nil, nil -} - -func Test_parsePropertySchema(t *testing.T) { - board := &Board{ - ID: utils.NewID(utils.IDTypeBoard), - Title: "Test Board", - TeamID: utils.NewID(utils.IDTypeTeam), - } - - err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties) - require.NoError(t, err) - - t.Run("parse schema", func(t *testing.T) { - schema, err := ParsePropertySchema(board) - require.NoError(t, err) - - assert.Len(t, schema, 6) - - prop, ok := schema["7c212e78-9345-4c60-81b5-0b0e37ce463f"] - require.True(t, ok) - - assert.Equal(t, "select", prop.Type) - assert.Equal(t, "Type", prop.Name) - assert.Len(t, prop.Options, 3) - - prop, ok = schema["a8spou7if43eo1rqzb9qeq488so"] - require.True(t, ok) - - assert.Equal(t, "date", prop.Type) - assert.Equal(t, "MyDate", prop.Name) - assert.Empty(t, prop.Options) - }) -} - -func Test_GetValue(t *testing.T) { - resolver := MockResolver{} - - propDef := PropDef{ - Type: "multiPerson", - } - - value, err := propDef.GetValue([]interface{}{"user_id_1", "user_id_2"}, resolver) - require.NoError(t, err) - require.Equal(t, "username_1, username_2", value) - - // trying with only user - value, err = propDef.GetValue([]interface{}{"user_id_1"}, resolver) - require.NoError(t, err) - require.Equal(t, "username_1", value) - - // trying with unknown user - value, err = propDef.GetValue([]interface{}{"user_id_1", "user_id_unknown"}, resolver) - require.NoError(t, err) - require.Equal(t, "username_1, user_id_unknown", value) - - // trying with multiple unknown users - value, err = propDef.GetValue([]interface{}{"michael_scott", "jim_halpert"}, resolver) - require.NoError(t, err) - require.Equal(t, "michael_scott, jim_halpert", value) -} - -const ( - cardPropertiesExample = `[ - { - "id":"7c212e78-9345-4c60-81b5-0b0e37ce463f", - "name":"Type", - "options":[ - { - "color":"propColorYellow", - "id":"31da50ca-f1a9-4d21-8636-17dc387c1a23", - "value":"Ad Hoc" - }, - { - "color":"propColorBlue", - "id":"def6317c-ec11-410d-8a6b-ea461320f392", - "value":"Standup" - }, - { - "color":"propColorPurple", - "id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7", - "value":"Weekly Sync" - } - ], - "type":"select" - }, - { - "id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4", - "name":"Summary", - "options":[], - "type":"text" - }, - { - "id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c", - "name":"Color", - "options":[ - { - "color":"propColorDefault", - "id":"efb0c783-f9ea-4938-8b86-9cf425296cd1", - "value":"RED" - }, - { - "color":"propColorDefault", - "id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311", - "value":"GREEN" - }, - { - "color":"propColorDefault", - "id":"a05bdc80-bd90-45b0-8805-a7e77a4884be", - "value":"BLUE" - } - ], - "type":"select" - }, - { - "id":"aawg1s8rxq8o1bbksxmsmpsdd3r", - "name":"MyTextProp", - "options":[], - "type":"text" - }, - { - "id":"awdwfigo4kse63bdfp56mzhip6w", - "name":"MyCheckBox", - "options":[], - "type":"checkbox" - }, - { - "id":"a8spou7if43eo1rqzb9qeq488so", - "name":"MyDate", - "options":[], - "type":"date" - } - ]` -) diff --git a/server/boards/model/services_api.go b/server/boards/model/services_api.go deleted file mode 100644 index b1c7d72e85..0000000000 --- a/server/boards/model/services_api.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen --build_flags= -copyright_file=../../copyright.txt -destination=mocks/mockservicesapi.go -package mocks . ServicesAPI - -package model - -import ( - "database/sql" - - "github.com/gorilla/mux" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - botUsername = "boards" - botDisplayname = "Boards" - botDescription = "Created by Boards plugin." -) - -func GetDefaultFocalboardBot() *mm_model.Bot { - return &mm_model.Bot{ - Username: botUsername, - DisplayName: botDisplayname, - Description: botDescription, - OwnerId: SystemUserID, - } -} - -type ServicesAPI interface { - // Channels service - GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) - GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) - GetChannelByID(channelID string) (*mm_model.Channel, error) - GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) - GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) - - // Post service - CreatePost(post *mm_model.Post) (*mm_model.Post, error) - - // User service - GetUserByID(userID string) (*mm_model.User, error) - GetUserByUsername(name string) (*mm_model.User, error) - GetUserByEmail(email string) (*mm_model.User, error) - UpdateUser(user *mm_model.User) (*mm_model.User, error) - GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) - - // Team service - GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) - CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) - - // Permissions service - HasPermissionTo(userID string, permission *mm_model.Permission) bool - HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool - HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool - - // Bot service - EnsureBot(bot *mm_model.Bot) (string, error) - - // License service - GetLicense() *mm_model.License - - // FileInfoStore service - GetFileInfo(fileID string) (*mm_model.FileInfo, error) - - // Cluster service - PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) - PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error - - // Cloud service - GetCloudLimits() (*mm_model.ProductLimits, error) - - // Config service - GetConfig() *mm_model.Config - - // Logger service - GetLogger() mlog.LoggerIFace - - // KVStore service - KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) - - // Store service - GetMasterDB() (*sql.DB, error) - - // System service - GetDiagnosticID() string - - // Router service - RegisterRouter(sub *mux.Router) - - // Preferences services - GetPreferencesForUser(userID string) (mm_model.Preferences, error) - UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error - DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error -} diff --git a/server/boards/model/sharing.go b/server/boards/model/sharing.go deleted file mode 100644 index 6269ba3b1b..0000000000 --- a/server/boards/model/sharing.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -// Sharing is sharing information for a root block -// swagger:model -type Sharing struct { - // ID of the root block - // required: true - ID string `json:"id"` - - // Is sharing enabled - // required: true - Enabled bool `json:"enabled"` - - // Access token - // required: true - Token string `json:"token"` - - // ID of the user who last modified this - // required: true - ModifiedBy string `json:"modifiedBy"` - - // Updated time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"update_at,omitempty"` -} - -func SharingFromJSON(data io.Reader) Sharing { - var sharing Sharing - _ = json.NewDecoder(data).Decode(&sharing) - return sharing -} diff --git a/server/boards/model/subscription.go b/server/boards/model/subscription.go deleted file mode 100644 index 71b202830a..0000000000 --- a/server/boards/model/subscription.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -const ( - SubTypeUser = "user" - SubTypeChannel = "channel" -) - -type SubscriberType string - -func (st SubscriberType) IsValid() bool { - switch st { - case SubTypeUser, SubTypeChannel: - return true - } - return false -} - -// Subscription is a subscription to a board, card, etc, for a user or channel. -// swagger:model -type Subscription struct { - // BlockType is the block type of the entity (e.g. board, card) subscribed to - // required: true - BlockType BlockType `json:"blockType"` - - // BlockID is id of the entity being subscribed to - // required: true - BlockID string `json:"blockId"` - - // SubscriberType is the type of the entity (e.g. user, channel) that is subscribing - // required: true - SubscriberType SubscriberType `json:"subscriberType"` - - // SubscriberID is the id of the entity that is subscribing - // required: true - SubscriberID string `json:"subscriberId"` - - // NotifiedAt is the timestamp of the last notification sent for this subscription - // required: true - NotifiedAt int64 `json:"notifiedAt,omitempty"` - - // CreatedAt is the timestamp this subscription was created in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"createAt"` - - // DeleteAt is the timestamp this subscription was deleted in miliseconds since the current epoch, or zero if not deleted - // required: true - DeleteAt int64 `json:"deleteAt"` -} - -func (s *Subscription) IsValid() error { - if s == nil { - return ErrInvalidSubscription{"cannot be nil"} - } - if s.BlockID == "" { - return ErrInvalidSubscription{"missing block id"} - } - if s.BlockType == "" { - return ErrInvalidSubscription{"missing block type"} - } - if s.SubscriberID == "" { - return ErrInvalidSubscription{"missing subscriber id"} - } - if !s.SubscriberType.IsValid() { - return ErrInvalidSubscription{"invalid subscriber type"} - } - return nil -} - -func SubscriptionFromJSON(data io.Reader) (*Subscription, error) { - var subscription Subscription - if err := json.NewDecoder(data).Decode(&subscription); err != nil { - return nil, err - } - return &subscription, nil -} - -type ErrInvalidSubscription struct { - msg string -} - -func (e ErrInvalidSubscription) Error() string { - return e.msg -} - -// Subscriber is an entity (e.g. user, channel) that can subscribe to events from boards, cards, etc -// swagger:model -type Subscriber struct { - // SubscriberType is the type of the entity (e.g. user, channel) that is subscribing - // required: true - SubscriberType SubscriberType `json:"subscriber_type"` - - // SubscriberID is the id of the entity that is subscribing - // required: true - SubscriberID string `json:"subscriber_id"` - - // NotifiedAt is the timestamp this subscriber was last notified - NotifiedAt int64 `json:"notified_at"` -} diff --git a/server/boards/model/team.go b/server/boards/model/team.go deleted file mode 100644 index f0feb99a0f..0000000000 --- a/server/boards/model/team.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -// Team is information global to a team -// swagger:model -type Team struct { - // ID of the team - // required: true - ID string `json:"id"` - - // Title of the team - // required: false - Title string `json:"title"` - - // Token required to register new users - // required: true - SignupToken string `json:"signupToken"` - - // Team settings - // required: false - Settings map[string]interface{} `json:"settings"` - - // ID of user who last modified this - // required: true - ModifiedBy string `json:"modifiedBy"` - - // Updated time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"updateAt"` -} - -func TeamFromJSON(data io.Reader) *Team { - var team *Team - _ = json.NewDecoder(data).Decode(&team) - return team -} - -func TeamsFromJSON(data io.Reader) []*Team { - var teams []*Team - _ = json.NewDecoder(data).Decode(&teams) - return teams -} diff --git a/server/boards/model/user.go b/server/boards/model/user.go deleted file mode 100644 index fb4cc5bbea..0000000000 --- a/server/boards/model/user.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -const ( - SingleUser = "single-user" - GlobalTeamID = "0" - SystemUserID = "system" - PreferencesCategoryFocalboard = "focalboard" -) - -// User is a user -// swagger:model -type User struct { - // The user ID - // required: true - ID string `json:"id"` - - // The user name - // required: true - Username string `json:"username"` - - // The user's email - // required: true - Email string `json:"-"` - - // The user's nickname - Nickname string `json:"nickname"` - // The user's first name - FirstName string `json:"firstname"` - // The user's last name - LastName string `json:"lastname"` - - // swagger:ignore - Password string `json:"-"` - - // swagger:ignore - MfaSecret string `json:"-"` - - // swagger:ignore - AuthService string `json:"-"` - - // swagger:ignore - AuthData string `json:"-"` - - // Created time in miliseconds since the current epoch - // required: true - CreateAt int64 `json:"create_at,omitempty"` - - // Updated time in miliseconds since the current epoch - // required: true - UpdateAt int64 `json:"update_at,omitempty"` - - // Deleted time in miliseconds since the current epoch, set to indicate user is deleted - // required: true - DeleteAt int64 `json:"delete_at"` - - // If the user is a bot or not - // required: true - IsBot bool `json:"is_bot"` - - // If the user is a guest or not - // required: true - IsGuest bool `json:"is_guest"` - - // Special Permissions the user may have - Permissions []string `json:"permissions,omitempty"` - - Roles string `json:"roles"` -} - -// UserPreferencesPatch is a user property patch -// swagger:model -type UserPreferencesPatch struct { - // The user preference updated fields - // required: false - UpdatedFields map[string]string `json:"updatedFields"` - - // The user preference removed fields - // required: false - DeletedFields []string `json:"deletedFields"` -} - -type Session struct { - ID string `json:"id"` - Token string `json:"token"` - UserID string `json:"user_id"` - AuthService string `json:"authService"` - Props map[string]interface{} `json:"props"` - CreateAt int64 `json:"create_at,omitempty"` - UpdateAt int64 `json:"update_at,omitempty"` -} - -func UserFromJSON(data io.Reader) (*User, error) { - var user User - if err := json.NewDecoder(data).Decode(&user); err != nil { - return nil, err - } - return &user, nil -} diff --git a/server/boards/model/util.go b/server/boards/model/util.go deleted file mode 100644 index 7cd1a9fc75..0000000000 --- a/server/boards/model/util.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "time" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -// GetMillis is a convenience method to get milliseconds since epoch. -func GetMillis() int64 { - return mm_model.GetMillis() -} - -// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. -func GetMillisForTime(thisTime time.Time) int64 { - return mm_model.GetMillisForTime(thisTime) -} - -// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch. -func GetTimeForMillis(millis int64) time.Time { - return mm_model.GetTimeForMillis(millis) -} diff --git a/server/boards/model/version.go b/server/boards/model/version.go deleted file mode 100644 index d5af6a17de..0000000000 --- a/server/boards/model/version.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import ( - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// This is a list of all the current versions including any patches. -// It should be maintained in chronological order with most current -// release at the front of the list. -var versions = []string{ - "7.9.0", - "7.8.0", - "7.7.0", - "7.6.0", - "7.5.0", - "7.4.0", - "7.3.0", - "7.2.0", - "7.0.0", - "0.16.0", - "0.15.0", - "0.14.0", - "0.12.0", - "0.11.0", - "0.10.0", - "0.9.4", - "0.9.3", - "0.9.2", - "0.9.1", - "0.9.0", - "0.8.2", - "0.8.1", - "0.8.0", - "0.7.3", - "0.7.2", - "0.7.1", - "0.7.0", - "0.6.7", - "0.6.6", - "0.6.5", - "0.6.2", - "0.6.1", - "0.6.0", - "0.5.0", -} - -var ( - CurrentVersion = versions[0] - BuildNumber string - BuildDate string - BuildHash string - Edition string -) - -// LogServerInfo logs information about the server instance. -func LogServerInfo(logger mlog.LoggerIFace) { - logger.Info("Focalboard server", - mlog.String("version", CurrentVersion), - mlog.String("edition", Edition), - mlog.String("build_number", BuildNumber), - mlog.String("build_date", BuildDate), - mlog.String("build_hash", BuildHash), - ) -} diff --git a/server/boards/product/api_adapter.go b/server/boards/product/api_adapter.go deleted file mode 100644 index 93c2bf3e9f..0000000000 --- a/server/boards/product/api_adapter.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package product - -import ( - "database/sql" - - "github.com/gorilla/mux" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/app/request" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// normalizeAppError returns a truly nil error if appErr is nil -// See https://golang.org/doc/faq#nil_error for more details. -func normalizeAppErr(appErr *mm_model.AppError) error { - if appErr == nil { - return nil - } - return appErr -} - -// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can -// be used as per the Plugin API. -// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app -// can be modified to use the services in modular fashion. -type serviceAPIAdapter struct { - api *boardsProduct - ctx *request.Context -} - -func newServiceAPIAdapter(api *boardsProduct) *serviceAPIAdapter { - return &serviceAPIAdapter{ - api: api, - ctx: request.EmptyContext(api.logger), - } -} - -// -// Channels service. -// - -func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetChannelByID(channelID) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) { - member, appErr := a.api.channelService.GetChannelMember(channelID, userID) - return member, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) { - opts := &mm_model.ChannelSearchOpts{ - IncludeDeleted: includeDeleted, - } - channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts) - return channels, normalizeAppErr(appErr) -} - -// -// Post service. -// - -func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) { - post, appErr := a.api.postService.CreatePost(a.ctx, post) - return post, normalizeAppErr(appErr) -} - -// -// User service. -// - -func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUser(userID) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUserByUsername(name) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUserByEmail(email) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) { - user, appErr := a.api.userService.UpdateUser(a.ctx, user, true) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) { - user, appErr := a.api.userService.GetUsersFromProfiles(options) - return user, normalizeAppErr(appErr) -} - -// -// Team service. -// - -func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) { - member, appErr := a.api.teamService.GetMember(teamID, userID) - return member, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) { - member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID) - return member, normalizeAppErr(appErr) -} - -// -// Permissions service. -// - -func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionTo(userID, permission) -} - -func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission) -} - -func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission) -} - -// -// Bot service. -// - -func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) { - return a.api.botService.EnsureBot(a.ctx, boardsProductID, bot) -} - -// -// License service. -// - -func (a *serviceAPIAdapter) GetLicense() *mm_model.License { - return a.api.licenseService.GetLicense() -} - -// -// FileInfoStore service. -// - -func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) { - fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID) - return fi, normalizeAppErr(appErr) -} - -// -// Cluster store. -// - -func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) { - a.api.clusterService.PublishWebSocketEvent(boardsProductName, event, payload, broadcast) -} - -func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error { - return a.api.clusterService.PublishPluginClusterEvent(boardsProductName, ev, opts) -} - -// -// Cloud service. -// - -func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { - return a.api.cloudService.GetCloudLimits() -} - -// -// Config service. -// - -func (a *serviceAPIAdapter) GetConfig() *mm_model.Config { - return a.api.configService.Config() -} - -// -// Logger service. -// - -func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace { - return a.api.logger -} - -// -// KVStore service. -// - -func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) { - b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductName, key, value, options) - return b, normalizeAppErr(appErr) -} - -// -// Store service. -// - -func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) { - return a.api.storeService.GetMasterDB(), nil -} - -// -// System service. -// - -func (a *serviceAPIAdapter) GetDiagnosticID() string { - return a.api.systemService.GetDiagnosticId() -} - -// -// Router service. -// - -func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) { - a.api.routerService.RegisterRouter(boardsProductName, sub) -} - -// -// Preferences service. -// - -func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) { - p, appErr := a.api.preferencesService.GetPreferencesForUser(userID) - return p, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error { - appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences) - return normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error { - appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences) - return normalizeAppErr(appErr) -} - -// Ensure the adapter implements ServicesAPI. -var _ model.ServicesAPI = &serviceAPIAdapter{} diff --git a/server/boards/product/boards_product.go b/server/boards/product/boards_product.go deleted file mode 100644 index 91917ee285..0000000000 --- a/server/boards/product/boards_product.go +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package product - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/server" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/product" -) - -const ( - boardsProductName = "boards" - boardsProductID = "com.mattermost.boards" -) - -//nolint:unused -var errServiceTypeAssert = errors.New("type assertion failed") - -/* -func init() { - product.RegisterProduct(boardsProductName, product.Manifest{ - Initializer: newBoardsProduct, - Dependencies: map[product.ServiceKey]struct{}{ - product.TeamKey: {}, - product.ChannelKey: {}, - product.UserKey: {}, - product.PostKey: {}, - product.PermissionsKey: {}, - product.BotKey: {}, - product.ClusterKey: {}, - product.ConfigKey: {}, - product.LogKey: {}, - product.LicenseKey: {}, - product.FilestoreKey: {}, - product.FileInfoStoreKey: {}, - product.RouterKey: {}, - product.CloudKey: {}, - product.KVStoreKey: {}, - product.StoreKey: {}, - product.SystemKey: {}, - product.PreferencesKey: {}, - product.HooksKey: {}, - }, - }) -} -*/ - -type boardsProduct struct { - teamService product.TeamService - channelService product.ChannelService - userService product.UserService - postService product.PostService - permissionsService product.PermissionService - botService product.BotService - clusterService product.ClusterService - configService product.ConfigService - logger mlog.LoggerIFace - licenseService product.LicenseService - filestoreService product.FilestoreService //nolint:unused - fileInfoStoreService product.FileInfoStoreService - routerService product.RouterService - cloudService product.CloudService - kvStoreService product.KVStoreService - storeService product.StoreService - systemService product.SystemService - preferencesService product.PreferencesService - hooksService product.HooksService - - boardsApp *server.BoardsService -} - -//nolint:unused -func newBoardsProduct(services map[product.ServiceKey]interface{}) (product.Product, error) { - boardsProd := &boardsProduct{} - - if err := populateServices(boardsProd, services); err != nil { - return nil, err - } - - boardsProd.logger.Info("Creating boards service") - - adapter := newServiceAPIAdapter(boardsProd) - boardsApp, err := server.NewBoardsService(adapter) - if err != nil { - return nil, fmt.Errorf("failed to create Boards service: %w", err) - } - - boardsProd.boardsApp = boardsApp - - // Add the Boards services API to the services map so other products can access Boards functionality. - boardsAPI := server.NewBoardsServiceAPI(boardsApp) - services[product.BoardsKey] = boardsAPI - - return boardsProd, nil -} - -// populateServices populates the boardProduct with all the services needed from the suite. -// -//nolint:unused -func populateServices(boardsProd *boardsProduct, services map[product.ServiceKey]interface{}) error { - for key, service := range services { - switch key { - case product.TeamKey: - teamService, ok := service.(product.TeamService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.teamService = teamService - case product.ChannelKey: - channelService, ok := service.(product.ChannelService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.channelService = channelService - case product.UserKey: - userService, ok := service.(product.UserService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.userService = userService - case product.PostKey: - postService, ok := service.(product.PostService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.postService = postService - case product.PermissionsKey: - permissionsService, ok := service.(product.PermissionService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.permissionsService = permissionsService - case product.BotKey: - botService, ok := service.(product.BotService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.botService = botService - case product.ClusterKey: - clusterService, ok := service.(product.ClusterService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.clusterService = clusterService - case product.ConfigKey: - configService, ok := service.(product.ConfigService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.configService = configService - case product.LogKey: - logger, ok := service.(mlog.LoggerIFace) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.logger = logger.With(mlog.String("product", boardsProductName)) - case product.LicenseKey: - licenseService, ok := service.(product.LicenseService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.licenseService = licenseService - case product.FilestoreKey: - filestoreService, ok := service.(product.FilestoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.filestoreService = filestoreService - case product.FileInfoStoreKey: - fileInfoStoreService, ok := service.(product.FileInfoStoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.fileInfoStoreService = fileInfoStoreService - case product.RouterKey: - routerService, ok := service.(product.RouterService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.routerService = routerService - case product.CloudKey: - cloudService, ok := service.(product.CloudService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.cloudService = cloudService - case product.KVStoreKey: - kvStoreService, ok := service.(product.KVStoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.kvStoreService = kvStoreService - case product.StoreKey: - storeService, ok := service.(product.StoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.storeService = storeService - case product.SystemKey: - systemService, ok := service.(product.SystemService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.systemService = systemService - case product.PreferencesKey: - preferencesService, ok := service.(product.PreferencesService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.preferencesService = preferencesService - case product.HooksKey: - hooksService, ok := service.(product.HooksService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - boardsProd.hooksService = hooksService - } - } - return nil -} - -func (bp *boardsProduct) Start() error { - bp.logger.Info("Starting boards service") - - adapter := newServiceAPIAdapter(bp) - boardsApp, err := server.NewBoardsService(adapter) - if err != nil { - return fmt.Errorf("failed to create Boards service: %w", err) - } - - model.LogServerInfo(bp.logger) - - if err := bp.hooksService.RegisterHooks(boardsProductName, bp); err != nil { - return fmt.Errorf("failed to register hooks: %w", err) - } - - bp.boardsApp = boardsApp - if err := bp.boardsApp.Start(); err != nil { - return fmt.Errorf("failed to start Boards service: %w", err) - } - - return nil -} - -func (bp *boardsProduct) Stop() error { - bp.logger.Info("Stopping boards service") - - if bp.boardsApp == nil { - return nil - } - - if err := bp.boardsApp.Stop(); err != nil { - return fmt.Errorf("error while stopping Boards service: %w", err) - } - - return nil -} - -// -// These callbacks are called by the suite automatically -// - -func (bp *boardsProduct) OnConfigurationChange() error { - if bp.boardsApp == nil { - return nil - } - return bp.boardsApp.OnConfigurationChange() -} - -func (bp *boardsProduct) OnWebSocketConnect(webConnID, userID string) { - if bp.boardsApp == nil { - return - } - bp.boardsApp.OnWebSocketConnect(webConnID, userID) -} - -func (bp *boardsProduct) OnWebSocketDisconnect(webConnID, userID string) { - if bp.boardsApp == nil { - return - } - bp.boardsApp.OnWebSocketDisconnect(webConnID, userID) -} - -func (bp *boardsProduct) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) { - if bp.boardsApp == nil { - return - } - bp.boardsApp.WebSocketMessageHasBeenPosted(webConnID, userID, req) -} - -func (bp *boardsProduct) OnPluginClusterEvent(ctx *plugin.Context, ev mm_model.PluginClusterEvent) { - if bp.boardsApp == nil { - return - } - bp.boardsApp.OnPluginClusterEvent(ctx, ev) -} - -func (bp *boardsProduct) MessageWillBePosted(ctx *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) { - if bp.boardsApp == nil { - return post, "" - } - return bp.boardsApp.MessageWillBePosted(ctx, post) -} - -func (bp *boardsProduct) MessageWillBeUpdated(ctx *plugin.Context, newPost, oldPost *mm_model.Post) (*mm_model.Post, string) { - if bp.boardsApp == nil { - return newPost, "" - } - return bp.boardsApp.MessageWillBeUpdated(ctx, newPost, oldPost) -} - -func (bp *boardsProduct) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) { - if bp.boardsApp == nil { - return - } - bp.boardsApp.OnCloudLimitsUpdated(limits) -} - -func (bp *boardsProduct) RunDataRetention(nowTime, batchSize int64) (int64, error) { - if bp.boardsApp == nil { - return 0, nil - } - return bp.boardsApp.RunDataRetention(nowTime, batchSize) -} diff --git a/server/boards/product/imports/boards_imports.go b/server/boards/product/imports/boards_imports.go deleted file mode 100644 index 92b215b2d2..0000000000 --- a/server/boards/product/imports/boards_imports.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package imports - -import ( - // Needed to ensure the init() method in the FocalBoard product is run. - // This file is copied to the mmserver imports package via makefile. - _ "github.com/mattermost/mattermost/server/v8/boards/product" -) diff --git a/server/boards/server/boards_service.go b/server/boards/server/boards_service.go deleted file mode 100644 index 2f578697a3..0000000000 --- a/server/boards/server/boards_service.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "fmt" - "net/http" - "sync" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mmpermissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/services/store/mattermostauthlayer" - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - boardsFeatureFlagName = "BoardsFeatureFlags" - PluginName = "focalboard" - SharedBoardsName = "enablepublicsharedboards" - - notifyFreqCardSecondsKey = "notify_freq_card_seconds" - notifyFreqBoardSecondsKey = "notify_freq_board_seconds" -) - -type BoardsEmbed struct { - OriginalPath string `json:"originalPath"` - TeamID string `json:"teamID"` - ViewID string `json:"viewID"` - BoardID string `json:"boardID"` - CardID string `json:"cardID"` - ReadToken string `json:"readToken,omitempty"` -} - -type BoardsService struct { - // configurationLock synchronizes access to the configuration. - configurationLock sync.RWMutex - - // configuration is the active plugin configuration. Consult getConfiguration and - // setConfiguration for usage. - configuration *configuration - - server *Server - wsPluginAdapter ws.PluginAdapterInterface - - servicesAPI model.ServicesAPI - logger mlog.LoggerIFace -} - -func NewBoardsServiceForTest(server *Server, wsPluginAdapter ws.PluginAdapterInterface, - api model.ServicesAPI, logger mlog.LoggerIFace) *BoardsService { - return &BoardsService{ - server: server, - wsPluginAdapter: wsPluginAdapter, - servicesAPI: api, - logger: logger, - } -} - -func NewBoardsService(api model.ServicesAPI) (*BoardsService, error) { - mmconfig := api.GetConfig() - logger := api.GetLogger() - - baseURL := "" - if mmconfig.ServiceSettings.SiteURL != nil { - baseURL = *mmconfig.ServiceSettings.SiteURL - } - serverID := api.GetDiagnosticID() - cfg := CreateBoardsConfig(*mmconfig, baseURL, serverID) - sqlDB, err := api.GetMasterDB() - if err != nil { - return nil, fmt.Errorf("cannot access database while initializing Boards: %w", err) - } - - storeParams := sqlstore.Params{ - DBType: cfg.DBType, - ConnectionString: cfg.DBConfigString, - TablePrefix: cfg.DBTablePrefix, - Logger: logger, - DB: sqlDB, - IsPlugin: true, - ServicesAPI: api, - ConfigFn: api.GetConfig, - } - - var db store.Store - db, err = sqlstore.New(storeParams) - if err != nil { - return nil, fmt.Errorf("error initializing the DB: %w", err) - } - if cfg.AuthMode == MattermostAuthMod { - layeredStore, err2 := mattermostauthlayer.New(cfg.DBType, sqlDB, db, logger, api, storeParams.TablePrefix) - if err2 != nil { - return nil, fmt.Errorf("error initializing the DB: %w", err2) - } - db = layeredStore - } - - permissionsService := mmpermissions.New(db, api, logger) - - wsPluginAdapter := ws.NewPluginAdapter(api, auth.New(cfg, db, permissionsService), db, logger) - - backendParams := notifyBackendParams{ - cfg: cfg, - servicesAPI: api, - appAPI: &appAPI{store: db}, - permissions: permissionsService, - serverRoot: baseURL + "/boards", - logger: logger, - } - - var notifyBackends []notify.Backend - - mentionsBackend, err := createMentionsNotifyBackend(backendParams) - if err != nil { - return nil, fmt.Errorf("error creating mention notifications backend: %w", err) - } - notifyBackends = append(notifyBackends, mentionsBackend) - - subscriptionsBackend, err2 := createSubscriptionsNotifyBackend(backendParams) - if err2 != nil { - return nil, fmt.Errorf("error creating subscription notifications backend: %w", err2) - } - notifyBackends = append(notifyBackends, subscriptionsBackend) - mentionsBackend.AddListener(subscriptionsBackend) - - params := Params{ - Cfg: cfg, - SingleUserToken: "", - DBStore: db, - Logger: logger, - ServerID: serverID, - WSAdapter: wsPluginAdapter, - NotifyBackends: notifyBackends, - PermissionsService: permissionsService, - IsPlugin: true, - } - - server, err := New(params) - if err != nil { - return nil, fmt.Errorf("error initializing the server: %w", err) - } - - backendParams.appAPI.init(db, server.App()) - - // ToDo: Cloud Limits have been disabled by design. We should - // revisit the decision and update the related code accordingly - /* - if utils.IsCloudLicense(api.GetLicense()) { - limits, err := api.GetCloudLimits() - if err != nil { - return nil, fmt.Errorf("error fetching cloud limits when starting Boards: %w", err) - } - - if err := server.App().SetCloudLimits(limits); err != nil { - return nil, fmt.Errorf("error setting cloud limits when starting Boards: %w", err) - } - } - */ - - return &BoardsService{ - server: server, - wsPluginAdapter: wsPluginAdapter, - servicesAPI: api, - logger: logger, - }, nil -} - -func (b *BoardsService) Start() error { - if err := b.server.Start(); err != nil { - return fmt.Errorf("error starting Boards server: %w", err) - } - - b.servicesAPI.RegisterRouter(b.server.GetRootRouter()) - - b.logger.Info("Boards product successfully started.") - - return nil -} - -func (b *BoardsService) Stop() error { - return b.server.Shutdown() -} - -// -// These callbacks are called automatically by the suite server. -// - -func (b *BoardsService) MessageWillBePosted(_ *plugin.Context, post *mm_model.Post) (*mm_model.Post, string) { - return postWithBoardsEmbed(post), "" -} - -func (b *BoardsService) MessageWillBeUpdated(_ *plugin.Context, newPost, _ *mm_model.Post) (*mm_model.Post, string) { - return postWithBoardsEmbed(newPost), "" -} - -func (b *BoardsService) OnWebSocketConnect(webConnID, userID string) { - b.wsPluginAdapter.OnWebSocketConnect(webConnID, userID) -} - -func (b *BoardsService) OnWebSocketDisconnect(webConnID, userID string) { - b.wsPluginAdapter.OnWebSocketDisconnect(webConnID, userID) -} - -func (b *BoardsService) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) { - b.wsPluginAdapter.WebSocketMessageHasBeenPosted(webConnID, userID, req) -} - -func (b *BoardsService) OnPluginClusterEvent(_ *plugin.Context, ev mm_model.PluginClusterEvent) { - b.wsPluginAdapter.HandleClusterEvent(ev) -} - -func (b *BoardsService) OnCloudLimitsUpdated(limits *mm_model.ProductLimits) { - if err := b.server.App().SetCloudLimits(limits); err != nil { - b.logger.Error("Error setting the cloud limits for Boards", mlog.Err(err)) - } -} - -func (b *BoardsService) Config() *config.Configuration { - return b.server.Config() -} - -func (b *BoardsService) ClientConfig() *model.ClientConfig { - return b.server.App().GetClientConfig() -} - -// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world. -func (b *BoardsService) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) { - router := b.server.GetRootRouter() - router.ServeHTTP(w, r) -} diff --git a/server/boards/server/boards_service_api.go b/server/boards/server/boards_service_api.go deleted file mode 100644 index 354c422c4a..0000000000 --- a/server/boards/server/boards_service_api.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "github.com/mattermost/mattermost/server/v8/boards/app" - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/product" -) - -// boardsServiceAPI provides a service API for other products such as Channels. -type boardsServiceAPI struct { - app *app.App -} - -func NewBoardsServiceAPI(app *BoardsService) *boardsServiceAPI { - return &boardsServiceAPI{ - app: app.server.App(), - } -} - -func (bs *boardsServiceAPI) GetTemplates(teamID string, userID string) ([]*model.Board, error) { - return bs.app.GetTemplateBoards(teamID, userID) -} - -func (bs *boardsServiceAPI) GetBoard(boardID string) (*model.Board, error) { - return bs.app.GetBoard(boardID) -} - -func (bs *boardsServiceAPI) CreateBoard(board *model.Board, userID string, addmember bool) (*model.Board, error) { - return bs.app.CreateBoard(board, userID, addmember) -} - -func (bs *boardsServiceAPI) PatchBoard(boardPatch *model.BoardPatch, boardID string, userID string) (*model.Board, error) { - return bs.app.PatchBoard(boardPatch, boardID, userID) -} - -func (bs *boardsServiceAPI) DeleteBoard(boardID string, userID string) error { - return bs.app.DeleteBoard(boardID, userID) -} - -func (bs *boardsServiceAPI) SearchBoards(searchTerm string, searchField model.BoardSearchField, - userID string, includePublicBoards bool) ([]*model.Board, error) { - return bs.app.SearchBoardsForUser(searchTerm, searchField, userID, includePublicBoards) -} - -func (bs *boardsServiceAPI) LinkBoardToChannel(boardID string, channelID string, userID string) (*model.Board, error) { - patch := &model.BoardPatch{ - ChannelID: &channelID, - } - return bs.app.PatchBoard(patch, boardID, userID) -} - -func (bs *boardsServiceAPI) GetCards(boardID string) ([]*model.Card, error) { - return bs.app.GetCardsForBoard(boardID, 0, 0) -} - -func (bs *boardsServiceAPI) GetCard(cardID string) (*model.Card, error) { - return bs.app.GetCardByID(cardID) -} - -func (bs *boardsServiceAPI) CreateCard(card *model.Card, boardID string, userID string) (*model.Card, error) { - return bs.app.CreateCard(card, boardID, userID, false) -} - -func (bs *boardsServiceAPI) PatchCard(cardPatch *model.CardPatch, cardID string, userID string) (*model.Card, error) { - return bs.app.PatchCard(cardPatch, cardID, userID, false) -} - -func (bs *boardsServiceAPI) DeleteCard(cardID string, userID string) error { - return bs.app.DeleteBlock(cardID, userID) -} - -func (bs *boardsServiceAPI) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { - return bs.app.HasPermissionToBoard(userID, boardID, permission) -} - -func (bs *boardsServiceAPI) DuplicateBoard(boardID string, userID string, - toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - return bs.app.DuplicateBoard(boardID, userID, toTeam, asTemplate) -} - -// Ensure boardsServiceAPI implements product.BoardsService interface. -var _ product.BoardsService = (*boardsServiceAPI)(nil) diff --git a/server/boards/server/boards_service_util.go b/server/boards/server/boards_service_util.go deleted file mode 100644 index e3c2377252..0000000000 --- a/server/boards/server/boards_service_util.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "math" - "path" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/services/config" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -const defaultS3Timeout = 60 * 1000 // 60 seconds - -func CreateBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID string) *config.Configuration { - filesS3Config := config.AmazonS3Config{} - if mmconfig.FileSettings.AmazonS3AccessKeyId != nil { - filesS3Config.AccessKeyID = *mmconfig.FileSettings.AmazonS3AccessKeyId - } - if mmconfig.FileSettings.AmazonS3SecretAccessKey != nil { - filesS3Config.SecretAccessKey = *mmconfig.FileSettings.AmazonS3SecretAccessKey - } - if mmconfig.FileSettings.AmazonS3Bucket != nil { - filesS3Config.Bucket = *mmconfig.FileSettings.AmazonS3Bucket - } - if mmconfig.FileSettings.AmazonS3PathPrefix != nil { - filesS3Config.PathPrefix = *mmconfig.FileSettings.AmazonS3PathPrefix - } - if mmconfig.FileSettings.AmazonS3Region != nil { - filesS3Config.Region = *mmconfig.FileSettings.AmazonS3Region - } - if mmconfig.FileSettings.AmazonS3Endpoint != nil { - filesS3Config.Endpoint = *mmconfig.FileSettings.AmazonS3Endpoint - } - if mmconfig.FileSettings.AmazonS3SSL != nil { - filesS3Config.SSL = *mmconfig.FileSettings.AmazonS3SSL - } - if mmconfig.FileSettings.AmazonS3SignV2 != nil { - filesS3Config.SignV2 = *mmconfig.FileSettings.AmazonS3SignV2 - } - if mmconfig.FileSettings.AmazonS3SSE != nil { - filesS3Config.SSE = *mmconfig.FileSettings.AmazonS3SSE - } - if mmconfig.FileSettings.AmazonS3Trace != nil { - filesS3Config.Trace = *mmconfig.FileSettings.AmazonS3Trace - } - if mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds != nil && *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds > 0 { - filesS3Config.Timeout = *mmconfig.FileSettings.AmazonS3RequestTimeoutMilliseconds - } else { - filesS3Config.Timeout = defaultS3Timeout - } - - enableTelemetry := false - if mmconfig.LogSettings.EnableDiagnostics != nil { - enableTelemetry = *mmconfig.LogSettings.EnableDiagnostics - } - - enableBoardsDeletion := false - if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil { - enableBoardsDeletion = true - } - - featureFlags := parseFeatureFlags(mmconfig.FeatureFlags.ToMap()) - - showEmailAddress := false - if mmconfig.PrivacySettings.ShowEmailAddress != nil { - showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress - } - - showFullName := false - if mmconfig.PrivacySettings.ShowFullName != nil { - showFullName = *mmconfig.PrivacySettings.ShowFullName - } - - return &config.Configuration{ - ServerRoot: baseURL + "/boards", - Port: -1, - DBType: *mmconfig.SqlSettings.DriverName, - DBConfigString: *mmconfig.SqlSettings.DataSource, - DBTablePrefix: "focalboard_", - UseSSL: false, - SecureCookie: true, - WebPath: path.Join(*mmconfig.PluginSettings.Directory, "focalboard", "pack"), - FilesDriver: *mmconfig.FileSettings.DriverName, - FilesPath: *mmconfig.FileSettings.Directory, - FilesS3Config: filesS3Config, - MaxFileSize: *mmconfig.FileSettings.MaxFileSize, - Telemetry: enableTelemetry, - TelemetryID: serverID, - WebhookUpdate: []string{}, - SessionExpireTime: 2592000, - SessionRefreshTime: 18000, - LocalOnly: false, - EnableLocalMode: false, - LocalModeSocketLocation: "", - AuthMode: "mattermost", - FeatureFlags: featureFlags, - NotifyFreqCardSeconds: getPluginSettingInt(mmconfig, notifyFreqCardSecondsKey, 120), - NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400), - EnableDataRetention: enableBoardsDeletion, - DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays, - TeammateNameDisplay: *mmconfig.TeamSettings.TeammateNameDisplay, - ShowEmailAddress: showEmailAddress, - ShowFullName: showFullName, - } -} - -func getPluginSetting(mmConfig mm_model.Config, key string) (interface{}, bool) { - plugin, ok := mmConfig.PluginSettings.Plugins[PluginName] - if !ok { - return nil, false - } - - val, ok := plugin[key] - if !ok { - return nil, false - } - return val, true -} - -func getPluginSettingInt(mmConfig mm_model.Config, key string, def int) int { - val, ok := getPluginSetting(mmConfig, key) - if !ok { - return def - } - valFloat, ok := val.(float64) - if !ok { - return def - } - return int(math.Round(valFloat)) -} - -func parseFeatureFlags(configFeatureFlags map[string]string) map[string]string { - featureFlags := make(map[string]string) - for key, value := range configFeatureFlags { - // Break out FeatureFlags and pass remaining - if key == boardsFeatureFlagName { - for _, flag := range strings.Split(value, "-") { - featureFlags[flag] = "true" - } - } else { - featureFlags[key] = value - } - } - return featureFlags -} diff --git a/server/boards/server/configuration.go b/server/boards/server/configuration.go deleted file mode 100644 index a63e3bb9c6..0000000000 --- a/server/boards/server/configuration.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "reflect" -) - -// configuration captures the plugin's external configuration as exposed in the Mattermost server -// configuration, as well as values computed from the configuration. Any public fields will be -// deserialized from the Mattermost server configuration in OnConfigurationChange. -// -// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin -// configuration can change at any time, access to the configuration must be synchronized. The -// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire -// struct whenever it changes. You may replace this with whatever strategy you choose. -// -// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep -// copy appropriate for your types. -type configuration struct { - EnablePublicSharedBoards bool -} - -// Clone shallow copies the configuration. Your implementation may require a deep copy if -// your configuration has reference types. -func (c *configuration) Clone() *configuration { - var clone = *c - return &clone -} - -// getConfiguration retrieves the active configuration under lock, making it safe to use -// concurrently. The active configuration may change underneath the client of this method, but -// the struct returned by this API call is considered immutable. -/* -func (b *BoardsService) getConfiguration() *configuration { - b.configurationLock.RLock() - defer b.configurationLock.RUnlock() - - if b.configuration == nil { - return &configuration{} - } - - return b.configuration -} -*/ - -// setConfiguration replaces the active configuration under lock. -// -// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not -// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a -// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur. -// -// This method panics if setConfiguration is called with the existing configuration. This almost -// certainly means that the configuration was modified without being cloned and may result in -// an unsafe access. -func (b *BoardsService) setConfiguration(configuration *configuration) { - b.configurationLock.Lock() - defer b.configurationLock.Unlock() - - if configuration != nil && b.configuration == configuration { - // Ignore assignment if the configuration struct is empty. Go will optimize the - // allocation for same to point at the same memory address, breaking the check - // above. - if reflect.ValueOf(*configuration).NumField() == 0 { - return - } - - panic("setConfiguration called with the existing configuration") - } - - b.configuration = configuration -} - -// OnConfigurationChange is invoked when configuration changes may have been made. -func (b *BoardsService) OnConfigurationChange() error { - // Have we been setup by OnActivate? - if b.server == nil { - return nil - } - mmconfig := b.servicesAPI.GetConfig() - - // handle plugin configuration settings - enableShareBoards := false - if mmconfig.PluginSettings.Plugins[PluginName][SharedBoardsName] == true { - enableShareBoards = true - } - configuration := &configuration{ - EnablePublicSharedBoards: enableShareBoards, - } - b.setConfiguration(configuration) - b.server.Config().EnablePublicSharedBoards = enableShareBoards - - // handle feature flags - b.server.Config().FeatureFlags = parseFeatureFlags(mmconfig.FeatureFlags.ToMap()) - - // handle Data Retention settings - enableBoardsDeletion := false - if mmconfig.DataRetentionSettings.EnableBoardsDeletion != nil { - enableBoardsDeletion = true - } - b.server.Config().EnableDataRetention = enableBoardsDeletion - b.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays - b.server.Config().TeammateNameDisplay = *mmconfig.TeamSettings.TeammateNameDisplay - showEmailAddress := false - if mmconfig.PrivacySettings.ShowEmailAddress != nil { - showEmailAddress = *mmconfig.PrivacySettings.ShowEmailAddress - } - b.server.Config().ShowEmailAddress = showEmailAddress - showFullName := false - if mmconfig.PrivacySettings.ShowFullName != nil { - showFullName = *mmconfig.PrivacySettings.ShowFullName - } - b.server.Config().ShowFullName = showFullName - maxFileSize := int64(0) - if mmconfig.FileSettings.MaxFileSize != nil { - maxFileSize = *mmconfig.FileSettings.MaxFileSize - } - b.server.Config().MaxFileSize = maxFileSize - - b.server.UpdateAppConfig() - b.wsPluginAdapter.BroadcastConfigChange(*b.server.App().GetClientConfig()) - return nil -} diff --git a/server/boards/server/data_retention.go b/server/boards/server/data_retention.go deleted file mode 100644 index 5ca2b6279f..0000000000 --- a/server/boards/server/data_retention.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package server - -import ( - "errors" - "time" -) - -var ErrInsufficientLicense = errors.New("appropriate license required") - -func (b *BoardsService) RunDataRetention(nowTime, batchSize int64) (int64, error) { - b.logger.Debug("Boards RunDataRetention") - license := b.server.Store().GetLicense() - if license == nil || !(*license.Features.DataRetention) { - return 0, ErrInsufficientLicense - } - - if b.server.Config().EnableDataRetention { - boardsRetentionDays := b.server.Config().DataRetentionDays - endTimeBoards := convertDaysToCutoff(boardsRetentionDays, time.Unix(nowTime/1000, 0)) - return b.server.Store().RunDataRetention(endTimeBoards, batchSize) - } - return 0, nil -} - -func convertDaysToCutoff(days int, now time.Time) int64 { - upToStartOfDay := now.AddDate(0, 0, -days) - cutoffDate := time.Date(upToStartOfDay.Year(), upToStartOfDay.Month(), upToStartOfDay.Day(), 0, 0, 0, 0, time.Local) - return cutoffDate.UnixNano() / int64(time.Millisecond) -} diff --git a/server/boards/server/data_retention_test.go b/server/boards/server/data_retention_test.go deleted file mode 100644 index 17e7a2ada3..0000000000 --- a/server/boards/server/data_retention_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "os" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions/localpermissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store/mockstore" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type TestHelperMockStore struct { - Server *Server - Store *mockstore.MockStore -} - -func SetupTestHelperMockStore(t *testing.T) (*TestHelperMockStore, func()) { - th := &TestHelperMockStore{} - - origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING") - os.Setenv("FOCALBOARD_UNIT_TESTING", "1") - - ctrl := gomock.NewController(t) - mockStore := mockstore.NewMockStore(ctrl) - - tearDown := func() { - defer ctrl.Finish() - os.Setenv("FOCALBOARD_UNIT_TESTING", origUnitTesting) - } - - th.Server = newTestServerMock(mockStore) - th.Store = mockStore - - return th, tearDown -} - -func newTestServerMock(mockStore *mockstore.MockStore) *Server { - config := &config.Configuration{ - EnableDataRetention: false, - DataRetentionDays: 10, - FilesDriver: "local", - FilesPath: "./files", - WebPath: "/", - } - - logger := mlog.CreateConsoleTestLogger(true, mlog.LvlDebug) - - mockStore.EXPECT().GetTeam(gomock.Any()).Return(nil, nil).AnyTimes() - mockStore.EXPECT().UpsertTeamSignupToken(gomock.Any()).AnyTimes() - mockStore.EXPECT().GetSystemSettings().AnyTimes() - mockStore.EXPECT().SetSystemSetting(gomock.Any(), gomock.Any()).AnyTimes() - - permissionsService := localpermissions.New(mockStore, logger) - - srv, err := New(Params{ - Cfg: config, - DBStore: mockStore, - Logger: logger, - PermissionsService: permissionsService, - }) - if err != nil { - panic(err) - } - - return srv -} - -func TestRunDataRetention(t *testing.T) { - th, tearDown := SetupTestHelperMockStore(t) - defer tearDown() - - b := &BoardsService{ - server: th.Server, - logger: mlog.CreateConsoleTestLogger(true, mlog.LvlError), - } - - now := time.Now().UnixNano() - - t.Run("test null license", func(t *testing.T) { - th.Store.EXPECT().GetLicense().Return(nil) - _, err := b.RunDataRetention(now, 10) - assert.Error(t, err) - assert.Equal(t, ErrInsufficientLicense, err) - }) - - t.Run("test invalid license", func(t *testing.T) { - falseValue := false - - th.Store.EXPECT().GetLicense().Return( - &model.License{ - Features: &model.Features{ - DataRetention: &falseValue, - }, - }, - ) - _, err := b.RunDataRetention(now, 10) - assert.Error(t, err) - assert.Equal(t, ErrInsufficientLicense, err) - }) - - t.Run("test valid license, invalid config", func(t *testing.T) { - trueValue := true - th.Store.EXPECT().GetLicense().Return( - &model.License{ - Features: &model.Features{ - DataRetention: &trueValue, - }, - }) - - count, err := b.RunDataRetention(now, 10) - assert.NoError(t, err) - assert.Equal(t, int64(0), count) - }) - - t.Run("test valid license, valid config", func(t *testing.T) { - trueValue := true - th.Store.EXPECT().GetLicense().Return( - &model.License{ - Features: &model.Features{ - DataRetention: &trueValue, - }, - }) - - th.Store.EXPECT().RunDataRetention(gomock.Any(), int64(10)).Return(int64(100), nil) - b.server.Config().EnableDataRetention = true - - count, err := b.RunDataRetention(now, 10) - - assert.NoError(t, err) - assert.Equal(t, int64(100), count) - }) -} diff --git a/server/boards/server/initHandlers.go b/server/boards/server/initHandlers.go deleted file mode 100644 index 0a26548f2e..0000000000 --- a/server/boards/server/initHandlers.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -func (s *Server) initHandlers() { - cfg := s.config - s.api.MattermostAuth = cfg.AuthMode == MattermostAuthMod -} diff --git a/server/boards/server/notifications.go b/server/boards/server/notifications.go deleted file mode 100644 index 151036e8e2..0000000000 --- a/server/boards/server/notifications.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "fmt" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/notify/notifymentions" - "github.com/mattermost/mattermost/server/v8/boards/services/notify/notifysubscriptions" - "github.com/mattermost/mattermost/server/v8/boards/services/notify/plugindelivery" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type notifyBackendParams struct { - cfg *config.Configuration - servicesAPI model.ServicesAPI - permissions permissions.PermissionsService - appAPI *appAPI - serverRoot string - logger mlog.LoggerIFace -} - -func createMentionsNotifyBackend(params notifyBackendParams) (*notifymentions.Backend, error) { - delivery, err := createDelivery(params.servicesAPI, params.serverRoot) - if err != nil { - return nil, err - } - - backendParams := notifymentions.BackendParams{ - AppAPI: params.appAPI, - Permissions: params.permissions, - Delivery: delivery, - Logger: params.logger, - } - - backend := notifymentions.New(backendParams) - - return backend, nil -} - -func createSubscriptionsNotifyBackend(params notifyBackendParams) (*notifysubscriptions.Backend, error) { - delivery, err := createDelivery(params.servicesAPI, params.serverRoot) - if err != nil { - return nil, err - } - - backendParams := notifysubscriptions.BackendParams{ - ServerRoot: params.serverRoot, - AppAPI: params.appAPI, - Permissions: params.permissions, - Delivery: delivery, - Logger: params.logger, - NotifyFreqCardSeconds: params.cfg.NotifyFreqCardSeconds, - NotifyFreqBoardSeconds: params.cfg.NotifyFreqBoardSeconds, - } - backend := notifysubscriptions.New(backendParams) - - return backend, nil -} - -func createDelivery(servicesAPI model.ServicesAPI, serverRoot string) (*plugindelivery.PluginDelivery, error) { - bot := model.GetDefaultFocalboardBot() - - botID, err := servicesAPI.EnsureBot(bot) - if err != nil { - return nil, fmt.Errorf("failed to ensure %s bot: %w", bot.DisplayName, err) - } - - return plugindelivery.New(botID, serverRoot, servicesAPI), nil -} - -type appIface interface { - CreateSubscription(sub *model.Subscription) (*model.Subscription, error) - AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) -} - -// appAPI provides app and store APIs for notification services. Where appropriate calls are made to the -// app layer to leverage the additional websocket notification logic present there, and other times the -// store APIs are called directly. -type appAPI struct { - store store.Store - app appIface -} - -func (a *appAPI) init(store store.Store, app appIface) { - a.store = store - a.app = app -} - -func (a *appAPI) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) { - return a.store.GetBlockHistory(blockID, opts) -} - -func (a *appAPI) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) { - return a.store.GetBlockHistoryNewestChildren(parentID, opts) -} - -func (a *appAPI) GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) { - return a.store.GetBoardAndCardByID(blockID) -} - -func (a *appAPI) GetUserByID(userID string) (*model.User, error) { - return a.store.GetUserByID(userID) -} - -func (a *appAPI) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { - return a.app.CreateSubscription(sub) -} - -func (a *appAPI) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) { - return a.store.GetSubscribersForBlock(blockID) -} - -func (a *appAPI) UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error { - return a.store.UpdateSubscribersNotifiedAt(blockID, notifyAt) -} - -func (a *appAPI) UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) { - return a.store.UpsertNotificationHint(hint, notificationFreq) -} - -func (a *appAPI) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) { - return a.store.GetNextNotificationHint(remove) -} - -func (a *appAPI) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) { - return a.store.GetMemberForBoard(boardID, userID) -} - -func (a *appAPI) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) { - return a.app.AddMemberToBoard(member) -} diff --git a/server/boards/server/params.go b/server/boards/server/params.go deleted file mode 100644 index 3ad3f70a3e..0000000000 --- a/server/boards/server/params.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type Params struct { - Cfg *config.Configuration - SingleUserToken string - DBStore store.Store - Logger mlog.LoggerIFace - ServerID string - WSAdapter ws.Adapter - NotifyBackends []notify.Backend - PermissionsService permissions.PermissionsService - ServicesAPI model.ServicesAPI - IsPlugin bool -} - -func (p Params) CheckValid() error { - if p.Cfg == nil { - return ErrServerParam{name: "Cfg", issue: "cannot be nil"} - } - - if p.DBStore == nil { - return ErrServerParam{name: "DbStore", issue: "cannot be nil"} - } - - if p.Logger == nil { - return ErrServerParam{name: "Logger", issue: "cannot be nil"} - } - - if p.PermissionsService == nil { - return ErrServerParam{name: "Permissions", issue: "cannot be nil"} - } - return nil -} - -type ErrServerParam struct { - name string - issue string -} - -func (e ErrServerParam) Error() string { - return fmt.Sprintf("invalid server params: %s %s", e.name, e.issue) -} diff --git a/server/boards/server/post.go b/server/boards/server/post.go deleted file mode 100644 index 0459fad986..0000000000 --- a/server/boards/server/post.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "encoding/json" - "fmt" - "net/url" - "strings" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/markdown" -) - -func postWithBoardsEmbed(post *mm_model.Post) *mm_model.Post { - if _, ok := post.GetProps()["boards"]; ok { - post.AddProp("boards", nil) - } - - firstLink, newPostMessage := getFirstLinkAndShortenAllBoardsLink(post.Message) - post.Message = newPostMessage - - if firstLink == "" { - return post - } - - u, err := url.Parse(firstLink) - - if err != nil { - return post - } - - // Trim away the first / because otherwise after we split the string, the first element in the array is a empty element - urlPath := u.Path - urlPath = strings.TrimPrefix(urlPath, "/") - urlPath = strings.TrimSuffix(urlPath, "/") - pathSplit := strings.Split(strings.ToLower(urlPath), "/") - queryParams := u.Query() - - if len(pathSplit) == 0 { - return post - } - - teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) - - if teamID != "" && boardID != "" && viewID != "" && cardID != "" { - b, _ := json.Marshal(BoardsEmbed{ - TeamID: teamID, - BoardID: boardID, - ViewID: viewID, - CardID: cardID, - ReadToken: queryParams.Get("r"), - OriginalPath: u.RequestURI(), - }) - - BoardsPostEmbed := &mm_model.PostEmbed{ - Type: mm_model.PostEmbedBoards, - Data: string(b), - } - - if post.Metadata == nil { - post.Metadata = &mm_model.PostMetadata{} - } - - post.Metadata.Embeds = []*mm_model.PostEmbed{BoardsPostEmbed} - post.AddProp("boards", string(b)) - } - - return post -} - -func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPostMessage string) { - newPostMessage = postMessage - seenLinks := make(map[string]bool) - markdown.Inspect(postMessage, func(blockOrInline interface{}) bool { - if autoLink, ok := blockOrInline.(*markdown.Autolink); ok { - link := autoLink.Destination() - - if firstLink == "" { - firstLink = link - } - - if seen := seenLinks[link]; !seen && isBoardsLink(link) { - // TODO: Make sure that is Internationalized and translated to the Users Language preference - markdownFormattedLink := fmt.Sprintf("[%s](%s)", "", link) - newPostMessage = strings.ReplaceAll(newPostMessage, link, markdownFormattedLink) - seenLinks[link] = true - } - } - if inlineLink, ok := blockOrInline.(*markdown.InlineLink); ok { - if link := inlineLink.Destination(); firstLink == "" { - firstLink = link - } - } - return true - }) - - return firstLink, newPostMessage -} - -func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) { - // The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths - index := -1 - for i := 0; i < len(pathArray); i++ { - if pathArray[i] == "boards" || pathArray[i] == "plugins" { - index = i - break - } - } - - if index == -1 { - return teamID, boardID, viewID, cardID - } - - // If at index, the parameter in the path is boards, - // then we've copied this directly as logged in user of that board - - // If at index, the parameter in the path is plugins, - // then we've copied this from a shared board - - // For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID - - // For card links copied on a shared board, the path looks like - // {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token - - // This is a non-shared board card link - if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" { - teamID = pathArray[index+2] - boardID = pathArray[index+3] - viewID = pathArray[index+4] - cardID = pathArray[index+5] - } else if len(pathArray)-index == 8 && pathArray[index] == "plugins" && - pathArray[index+1] == "focalboard" && - pathArray[index+2] == "team" && - pathArray[index+4] == "shared" { // This is a shared board card link - teamID = pathArray[index+3] - boardID = pathArray[index+5] - viewID = pathArray[index+6] - cardID = pathArray[index+7] - } - return teamID, boardID, viewID, cardID -} - -func isBoardsLink(link string) bool { - u, err := url.Parse(link) - - if err != nil { - return false - } - - urlPath := u.Path - urlPath = strings.TrimPrefix(urlPath, "/") - urlPath = strings.TrimSuffix(urlPath, "/") - pathSplit := strings.Split(strings.ToLower(urlPath), "/") - - if len(pathSplit) == 0 { - return false - } - - teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) - return teamID != "" && boardID != "" && viewID != "" && cardID != "" -} diff --git a/server/boards/server/server.go b/server/boards/server/server.go deleted file mode 100644 index aaa94193b2..0000000000 --- a/server/boards/server/server.go +++ /dev/null @@ -1,526 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "database/sql" - "fmt" - "net" - "net/http" - "os" - "runtime" - "sync" - "syscall" - "time" - - "github.com/gorilla/mux" - "github.com/pkg/errors" - - "github.com/oklog/run" - - "github.com/mattermost/mattermost/server/v8/boards/api" - "github.com/mattermost/mattermost/server/v8/boards/app" - "github.com/mattermost/mattermost/server/v8/boards/auth" - appModel "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/audit" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - "github.com/mattermost/mattermost/server/v8/boards/services/metrics" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/notify/notifylogger" - "github.com/mattermost/mattermost/server/v8/boards/services/scheduler" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mattermost/mattermost/server/v8/boards/services/telemetry" - "github.com/mattermost/mattermost/server/v8/boards/services/webhook" - "github.com/mattermost/mattermost/server/v8/boards/utils" - "github.com/mattermost/mattermost/server/v8/boards/web" - "github.com/mattermost/mattermost/server/v8/boards/ws" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" -) - -const ( - cleanupSessionTaskFrequency = 10 * time.Minute - updateMetricsTaskFrequency = 15 * time.Minute - - minSessionExpiryTime = int64(60 * 60 * 24 * 31) // 31 days - - MattermostAuthMod = "mattermost" -) - -type Server struct { - config *config.Configuration - wsAdapter ws.Adapter - webServer *web.Server - store store.Store - filesBackend filestore.FileBackend - telemetry *telemetry.Service - logger mlog.LoggerIFace - cleanUpSessionsTask *scheduler.ScheduledTask - metricsServer *metrics.Service - metricsService *metrics.Metrics - metricsUpdaterTask *scheduler.ScheduledTask - auditService *audit.Audit - notificationService *notify.Service - servicesStartStopMutex sync.Mutex - - localRouter *mux.Router - localModeServer *http.Server - api *api.API - app *app.App -} - -func New(params Params) (*Server, error) { - if err := params.CheckValid(); err != nil { - return nil, err - } - - authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService) - - // if no ws adapter is provided, we spin up a websocket server - wsAdapter := params.WSAdapter - if wsAdapter == nil { - wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore) - } - - filesBackendSettings := filestore.FileBackendSettings{} - filesBackendSettings.DriverName = params.Cfg.FilesDriver - filesBackendSettings.Directory = params.Cfg.FilesPath - filesBackendSettings.AmazonS3AccessKeyId = params.Cfg.FilesS3Config.AccessKeyID - filesBackendSettings.AmazonS3SecretAccessKey = params.Cfg.FilesS3Config.SecretAccessKey - filesBackendSettings.AmazonS3Bucket = params.Cfg.FilesS3Config.Bucket - filesBackendSettings.AmazonS3PathPrefix = params.Cfg.FilesS3Config.PathPrefix - filesBackendSettings.AmazonS3Region = params.Cfg.FilesS3Config.Region - filesBackendSettings.AmazonS3Endpoint = params.Cfg.FilesS3Config.Endpoint - filesBackendSettings.AmazonS3SSL = params.Cfg.FilesS3Config.SSL - filesBackendSettings.AmazonS3SignV2 = params.Cfg.FilesS3Config.SignV2 - filesBackendSettings.AmazonS3SSE = params.Cfg.FilesS3Config.SSE - filesBackendSettings.AmazonS3Trace = params.Cfg.FilesS3Config.Trace - filesBackendSettings.AmazonS3RequestTimeoutMilliseconds = params.Cfg.FilesS3Config.Timeout - - filesBackend, appErr := filestore.NewFileBackend(filesBackendSettings) - if appErr != nil { - params.Logger.Error("Unable to initialize the files storage", mlog.Err(appErr)) - - return nil, errors.New("unable to initialize the files storage") - } - - webhookClient := webhook.NewClient(params.Cfg, params.Logger) - - // Init metrics - instanceInfo := metrics.InstanceInfo{ - Version: appModel.CurrentVersion, - BuildNum: appModel.BuildNumber, - Edition: appModel.Edition, - InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"), - } - metricsService := metrics.NewMetrics(instanceInfo) - - // Init audit - auditService, errAudit := audit.NewAudit() - if errAudit != nil { - return nil, fmt.Errorf("unable to create the audit service: %w", errAudit) - } - if err := auditService.Configure(params.Cfg.AuditCfgFile, params.Cfg.AuditCfgJSON); err != nil { - return nil, fmt.Errorf("unable to initialize the audit service: %w", err) - } - - // Init notification services - notificationService, errNotify := initNotificationService(params.NotifyBackends, params.Logger) - if errNotify != nil { - return nil, fmt.Errorf("cannot initialize notification service(s): %w", errNotify) - } - - appServices := app.Services{ - Auth: authenticator, - Store: params.DBStore, - FilesBackend: filesBackend, - Webhook: webhookClient, - Metrics: metricsService, - Notifications: notificationService, - Logger: params.Logger, - Permissions: params.PermissionsService, - ServicesAPI: params.ServicesAPI, - SkipTemplateInit: utils.IsRunningUnitTests(), - } - app := app.New(params.Cfg, wsAdapter, appServices) - - focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService, params.IsPlugin) - - // Local router for admin APIs - localRouter := mux.NewRouter() - focalboardAPI.RegisterAdminRoutes(localRouter) - - // Init team - if _, err := app.GetRootTeam(); err != nil { - params.Logger.Error("Unable to get root team", mlog.Err(err)) - return nil, err - } - - webServer := web.NewServer(params.Cfg.WebPath, params.Cfg.ServerRoot, params.Cfg.Port, - params.Cfg.UseSSL, params.Cfg.LocalOnly, params.Logger) - // if the adapter is a routed service, register it before the API - if routedService, ok := wsAdapter.(web.RoutedService); ok { - webServer.AddRoutes(routedService) - } - webServer.AddRoutes(focalboardAPI) - - settings, err := params.DBStore.GetSystemSettings() - if err != nil { - return nil, err - } - - // Init telemetry - telemetryID := settings["TelemetryID"] - if telemetryID == "" { - telemetryID = utils.NewID(utils.IDTypeNone) - if err = params.DBStore.SetSystemSetting("TelemetryID", telemetryID); err != nil { - return nil, err - } - } - telemetryOpts := telemetryOptions{ - app: app, - cfg: params.Cfg, - telemetryID: telemetryID, - serverID: params.ServerID, - logger: params.Logger, - singleUser: params.SingleUserToken != "", - } - telemetryService := initTelemetry(telemetryOpts) - - server := Server{ - config: params.Cfg, - wsAdapter: wsAdapter, - webServer: webServer, - store: params.DBStore, - filesBackend: filesBackend, - telemetry: telemetryService, - metricsServer: metrics.NewMetricsServer(params.Cfg.PrometheusAddress, metricsService, params.Logger), - metricsService: metricsService, - auditService: auditService, - notificationService: notificationService, - logger: params.Logger, - localRouter: localRouter, - api: focalboardAPI, - app: app, - } - - server.initHandlers() - - return &server, nil -} - -func NewStore(config *config.Configuration, isSingleUser bool, logger mlog.LoggerIFace) (store.Store, error) { - sqlDB, err := sql.Open(config.DBType, config.DBConfigString) - if err != nil { - logger.Error("connectDatabase failed", mlog.Err(err)) - return nil, err - } - - err = sqlDB.Ping() - if err != nil { - logger.Error(`Database Ping failed`, mlog.Err(err)) - return nil, err - } - - storeParams := sqlstore.Params{ - DBType: config.DBType, - ConnectionString: config.DBConfigString, - TablePrefix: config.DBTablePrefix, - Logger: logger, - DB: sqlDB, - IsPlugin: false, - IsSingleUser: isSingleUser, - } - - var db store.Store - db, err = sqlstore.New(storeParams) - if err != nil { - return nil, err - } - return db, nil -} - -func (s *Server) Start() error { - s.logger.Info("Server.Start") - - s.webServer.Start() - - s.servicesStartStopMutex.Lock() - defer s.servicesStartStopMutex.Unlock() - - if s.config.EnableLocalMode { - if err := s.startLocalModeServer(); err != nil { - return err - } - } - - if s.config.AuthMode != MattermostAuthMod { - s.cleanUpSessionsTask = scheduler.CreateRecurringTask("cleanUpSessions", func() { - secondsAgo := minSessionExpiryTime - if secondsAgo < s.config.SessionExpireTime { - secondsAgo = s.config.SessionExpireTime - } - - if err := s.store.CleanUpSessions(secondsAgo); err != nil { - s.logger.Error("Unable to clean up the sessions", mlog.Err(err)) - } - }, cleanupSessionTaskFrequency) - } - - metricsUpdater := func() { - blockCounts, err := s.store.GetBlockCountsByType() - if err != nil { - s.logger.Error("Error updating metrics", mlog.String("group", "blocks"), mlog.Err(err)) - return - } - s.logger.Log(mlog.LvlFBMetrics, "Block metrics collected", mlog.Map("block_counts", blockCounts)) - for blockType, count := range blockCounts { - s.metricsService.ObserveBlockCount(blockType, count) - } - boardCount, err := s.store.GetBoardCount() - if err != nil { - s.logger.Error("Error updating metrics", mlog.String("group", "boards"), mlog.Err(err)) - return - } - s.logger.Log(mlog.LvlFBMetrics, "Board metrics collected", mlog.Int64("board_count", boardCount)) - s.metricsService.ObserveBoardCount(boardCount) - teamCount, err := s.store.GetTeamCount() - if err != nil { - s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err)) - return - } - s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount)) - s.metricsService.ObserveTeamCount(teamCount) - } - // metricsUpdater() Calling this immediately causes integration unit tests to fail. - s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency) - - if s.config.Telemetry { - firstRun := utils.GetMillis() - s.telemetry.RunTelemetryJob(firstRun) - } - - var group run.Group - if s.config.PrometheusAddress != "" { - group.Add(func() error { - if err := s.metricsServer.Run(); err != nil { - return errors.Wrap(err, "PromServer Run") - } - return nil - }, func(error) { - _ = s.metricsServer.Shutdown() - }) - - if err := group.Run(); err != nil { - return err - } - } - return nil -} - -func (s *Server) Shutdown() error { - if err := s.webServer.Shutdown(); err != nil { - return err - } - - s.stopLocalModeServer() - - s.servicesStartStopMutex.Lock() - defer s.servicesStartStopMutex.Unlock() - - if s.cleanUpSessionsTask != nil { - s.cleanUpSessionsTask.Cancel() - } - - if s.metricsUpdaterTask != nil { - s.metricsUpdaterTask.Cancel() - } - - if err := s.telemetry.Shutdown(); err != nil { - s.logger.Warn("Error occurred when shutting down telemetry", mlog.Err(err)) - } - - if err := s.auditService.Shutdown(); err != nil { - s.logger.Warn("Error occurred when shutting down audit service", mlog.Err(err)) - } - - if err := s.notificationService.Shutdown(); err != nil { - s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err)) - } - - s.app.Shutdown() - - defer s.logger.Info("Server.Shutdown") - - return nil -} - -func (s *Server) Config() *config.Configuration { - return s.config -} - -func (s *Server) Logger() mlog.LoggerIFace { - return s.logger -} - -func (s *Server) App() *app.App { - return s.app -} - -func (s *Server) Store() store.Store { - return s.store -} - -func (s *Server) UpdateAppConfig() { - s.app.SetConfig(s.config) -} - -// Local server - -func (s *Server) startLocalModeServer() error { - s.localModeServer = &http.Server{ //nolint:gosec - Handler: s.localRouter, - ConnContext: api.SetContextConn, - } - - // TODO: Close and delete socket file on shutdown - // Delete existing socket if it exists - if _, err := os.Stat(s.config.LocalModeSocketLocation); err == nil { - if err := syscall.Unlink(s.config.LocalModeSocketLocation); err != nil { - s.logger.Error("Unable to unlink socket.", mlog.Err(err)) - } - } - - socket := s.config.LocalModeSocketLocation - unixListener, err := net.Listen("unix", socket) - if err != nil { - return err - } - if err = os.Chmod(socket, 0600); err != nil { - return err - } - - go func() { - s.logger.Info("Starting unix socket server") - err = s.localModeServer.Serve(unixListener) - if err != nil && !errors.Is(err, http.ErrServerClosed) { - s.logger.Error("Error starting unix socket server", mlog.Err(err)) - } - }() - - return nil -} - -func (s *Server) stopLocalModeServer() { - if s.localModeServer != nil { - _ = s.localModeServer.Close() - s.localModeServer = nil - } -} - -func (s *Server) GetRootRouter() *mux.Router { - return s.webServer.Router() -} - -type telemetryOptions struct { - app *app.App - cfg *config.Configuration - telemetryID string - serverID string - logger mlog.LoggerIFace - singleUser bool -} - -func initTelemetry(opts telemetryOptions) *telemetry.Service { - telemetryService := telemetry.New(opts.telemetryID, opts.logger) - - telemetryService.RegisterTracker("server", func() (telemetry.Tracker, error) { - return map[string]interface{}{ - "version": appModel.CurrentVersion, - "build_number": appModel.BuildNumber, - "build_hash": appModel.BuildHash, - "edition": appModel.Edition, - "operating_system": runtime.GOOS, - "server_id": opts.serverID, - }, nil - }) - telemetryService.RegisterTracker("config", func() (telemetry.Tracker, error) { - return map[string]interface{}{ - "serverRoot": opts.cfg.ServerRoot == config.DefaultServerRoot, - "port": opts.cfg.Port == config.DefaultPort, - "useSSL": opts.cfg.UseSSL, - "dbType": opts.cfg.DBType, - "single_user": opts.singleUser, - "allow_public_shared_boards": opts.cfg.EnablePublicSharedBoards, - }, nil - }) - telemetryService.RegisterTracker("activity", func() (telemetry.Tracker, error) { - m := make(map[string]interface{}) - var count int - var err error - if count, err = opts.app.GetRegisteredUserCount(); err != nil { - return nil, err - } - m["registered_users"] = count - - if count, err = opts.app.GetDailyActiveUsers(); err != nil { - return nil, err - } - m["daily_active_users"] = count - - if count, err = opts.app.GetWeeklyActiveUsers(); err != nil { - return nil, err - } - m["weekly_active_users"] = count - - if count, err = opts.app.GetMonthlyActiveUsers(); err != nil { - return nil, err - } - m["monthly_active_users"] = count - return m, nil - }) - telemetryService.RegisterTracker("blocks", func() (telemetry.Tracker, error) { - blockCounts, err := opts.app.GetBlockCountsByType() - if err != nil { - return nil, err - } - m := make(map[string]interface{}) - for k, v := range blockCounts { - m[k] = v - } - return m, nil - }) - telemetryService.RegisterTracker("boards", func() (telemetry.Tracker, error) { - boardCount, err := opts.app.GetBoardCount() - if err != nil { - return nil, err - } - m := map[string]interface{}{ - "boards": boardCount, - } - return m, nil - }) - telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) { - count, err := opts.app.GetTeamCount() - if err != nil { - return nil, err - } - m := map[string]interface{}{ - "teams": count, - } - return m, nil - }) - return telemetryService -} - -func initNotificationService(backends []notify.Backend, logger mlog.LoggerIFace) (*notify.Service, error) { - loggerBackend := notifylogger.New(logger, mlog.LvlDebug) - - backends = append(backends, loggerBackend) - - service, err := notify.New(logger, backends...) - return service, err -} diff --git a/server/boards/services/audit/audit.go b/server/boards/services/audit/audit.go deleted file mode 100644 index b32881ddff..0000000000 --- a/server/boards/services/audit/audit.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package audit - -import ( - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - DefMaxQueueSize = 1000 - - KeyAPIPath = "api_path" - KeyEvent = "event" - KeyStatus = "status" - KeyUserID = "user_id" - KeySessionID = "session_id" - KeyClient = "client" - KeyIPAddress = "ip_address" - KeyClusterID = "cluster_id" - KeyTeamID = "team_id" - - Success = "success" - Attempt = "attempt" - Fail = "fail" -) - -var ( - LevelAuth = mlog.Level{ID: 1000, Name: "auth"} - LevelModify = mlog.Level{ID: 1001, Name: "mod"} - LevelRead = mlog.Level{ID: 1002, Name: "read"} -) - -// Audit provides auditing service. -type Audit struct { - auditLogger *mlog.Logger -} - -// NewAudit creates a new Audit instance which can be configured via `(*Audit).Configure`. -func NewAudit(options ...mlog.Option) (*Audit, error) { - logger, err := mlog.NewLogger(options...) - if err != nil { - return nil, err - } - return &Audit{ - auditLogger: logger, - }, nil -} - -// Configure provides a new configuration for this audit service. -// Zero or more sources of config can be provided: -// -// cfgFile - path to file containing JSON -// cfgEscaped - JSON string probably from ENV var -// -// For each case JSON containing log targets is provided. Target name collisions are resolved -// using the following precedence: -// -// cfgFile > cfgEscaped -func (a *Audit) Configure(cfgFile string, cfgEscaped string) error { - return a.auditLogger.Configure(cfgFile, cfgEscaped, nil) -} - -// Shutdown shuts down the audit service after making best efforts to flush any -// remaining records. -func (a *Audit) Shutdown() error { - return a.auditLogger.Shutdown() -} - -// LogRecord emits an audit record with complete info. -func (a *Audit) LogRecord(level mlog.Level, rec *Record) { - fields := make([]mlog.Field, 0, 7+len(rec.Meta)) - - fields = append(fields, mlog.String(KeyAPIPath, rec.APIPath)) - fields = append(fields, mlog.String(KeyEvent, rec.Event)) - fields = append(fields, mlog.String(KeyStatus, rec.Status)) - fields = append(fields, mlog.String(KeyUserID, rec.UserID)) - fields = append(fields, mlog.String(KeySessionID, rec.SessionID)) - fields = append(fields, mlog.String(KeyClient, rec.Client)) - fields = append(fields, mlog.String(KeyIPAddress, rec.IPAddress)) - - for _, meta := range rec.Meta { - fields = append(fields, mlog.Any(meta.K, meta.V)) - } - - a.auditLogger.Log(level, "audit "+rec.Event, fields...) -} diff --git a/server/boards/services/audit/record.go b/server/boards/services/audit/record.go deleted file mode 100644 index ce835b80ef..0000000000 --- a/server/boards/services/audit/record.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package audit - -import "github.com/mattermost/mattermost/server/public/shared/mlog" - -// Meta represents metadata that can be added to a audit record as name/value pairs. -type Meta struct { - K string - V interface{} -} - -// FuncMetaTypeConv defines a function that can convert meta data types into something -// that serializes well for audit records. -type FuncMetaTypeConv func(val interface{}) (newVal interface{}, converted bool) - -// Record provides a consistent set of fields used for all audit logging. -type Record struct { - APIPath string - Event string - Status string - UserID string - SessionID string - Client string - IPAddress string - Meta []Meta - metaConv []FuncMetaTypeConv -} - -// Success marks the audit record status as successful. -func (rec *Record) Success() { - rec.Status = Success -} - -// Success marks the audit record status as failed. -func (rec *Record) Fail() { - rec.Status = Fail -} - -// AddMeta adds a single name/value pair to this audit record's metadata. -func (rec *Record) AddMeta(name string, val interface{}) { - if rec.Meta == nil { - rec.Meta = []Meta{} - } - - // possibly convert val to something better suited for serializing - // via zero or more conversion functions. - for _, conv := range rec.metaConv { - converted, wasConverted := conv(val) - if wasConverted { - val = converted - break - } - } - - lc, ok := val.(mlog.LogCloner) - if ok { - val = lc.LogClone() - } - - rec.Meta = append(rec.Meta, Meta{K: name, V: val}) -} - -// AddMetaTypeConverter adds a function capable of converting meta field types -// into something more suitable for serialization. -func (rec *Record) AddMetaTypeConverter(f FuncMetaTypeConv) { - rec.metaConv = append(rec.metaConv, f) -} diff --git a/server/boards/services/audit/record_test.go b/server/boards/services/audit/record_test.go deleted file mode 100644 index 1ec715556d..0000000000 --- a/server/boards/services/audit/record_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package audit - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -type bloated struct { - fld1 string - fld2 string - fld3 string - fld4 string -} - -type wilted struct { - wilt1 string -} - -func conv(val interface{}) (interface{}, bool) { - if b, ok := val.(*bloated); ok { - return &wilted{wilt1: b.fld1}, true - } - return val, false -} - -func TestRecord_AddMeta(t *testing.T) { - type fields struct { - metaConv []FuncMetaTypeConv - } - type args struct { - name string - val interface{} - } - tests := []struct { - name string - fields fields - args args - wantWilt bool - wantVal string - }{ - {name: "no converter", wantWilt: false, wantVal: "ok", fields: fields{}, args: args{name: "prop", val: "ok"}}, - {name: "don't convert", wantWilt: false, wantVal: "ok", fields: fields{metaConv: []FuncMetaTypeConv{conv}}, args: args{name: "prop", val: "ok"}}, - {name: "convert", wantWilt: true, wantVal: "1", fields: fields{metaConv: []FuncMetaTypeConv{conv}}, args: args{name: "prop", val: &bloated{ - fld1: "1", fld2: "2", fld3: "3", fld4: "4"}}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rec := &Record{ - metaConv: tt.fields.metaConv, - } - rec.AddMeta(tt.args.name, tt.args.val) - - // fetch the prop store in auditRecord meta data - var ok bool - var got interface{} - for _, meta := range rec.Meta { - if meta.K == "prop" { - ok = true - got = meta.V - break - } - } - require.True(t, ok) - - // check if conversion was expected - val, ok := got.(*wilted) - require.Equal(t, tt.wantWilt, ok) - - if ok { - // if converted to wilt then make sure field was copied - require.Equal(t, tt.wantVal, val.wilt1) - } else { - // if not converted, make sure val is unchanged - require.Equal(t, tt.wantVal, got) - } - }) - } -} diff --git a/server/boards/services/auth/email.go b/server/boards/services/auth/email.go deleted file mode 100644 index ba996f22ca..0000000000 --- a/server/boards/services/auth/email.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import "regexp" - -var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - -// IsEmailValid checks if the email provided passes the required structure and length. -func IsEmailValid(e string) bool { - if len(e) < 3 || len(e) > 254 { - return false - } - return emailRegex.MatchString(e) -} diff --git a/server/boards/services/auth/password.go b/server/boards/services/auth/password.go deleted file mode 100644 index 4b6c6876da..0000000000 --- a/server/boards/services/auth/password.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import ( - "fmt" - "strings" - - "golang.org/x/crypto/bcrypt" -) - -const ( - PasswordMaximumLength = 64 - PasswordSpecialChars = "!\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" //nolint:gosec - PasswordNumbers = "0123456789" - PasswordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - PasswordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz" - PasswordAllChars = PasswordSpecialChars + PasswordNumbers + PasswordUpperCaseLetters + PasswordLowerCaseLetters - - InvalidLowercasePassword = "lowercase" - InvalidMinLengthPassword = "min-length" - InvalidMaxLengthPassword = "max-length" - InvalidNumberPassword = "number" - InvalidUppercasePassword = "uppercase" - InvalidSymbolPassword = "symbol" -) - -var PasswordHashStrength = 10 - -// HashPassword generates a hash using the bcrypt.GenerateFromPassword. -func HashPassword(password string) string { - hash, err := bcrypt.GenerateFromPassword([]byte(password), PasswordHashStrength) - if err != nil { - panic(err) - } - - return string(hash) -} - -// ComparePassword compares the hash. -func ComparePassword(hash, password string) bool { - if password == "" || hash == "" { - return false - } - - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -type InvalidPasswordError struct { - FailingCriterias []string -} - -func (ipe *InvalidPasswordError) Error() string { - return fmt.Sprintf("invalid password, failing criteria: %s", strings.Join(ipe.FailingCriterias, ", ")) -} - -type PasswordSettings struct { - MinimumLength int - Lowercase bool - Number bool - Uppercase bool - Symbol bool -} - -func IsPasswordValid(password string, settings PasswordSettings) error { - err := &InvalidPasswordError{ - FailingCriterias: []string{}, - } - - if len(password) < settings.MinimumLength { - err.FailingCriterias = append(err.FailingCriterias, InvalidMinLengthPassword) - } - - if len(password) > PasswordMaximumLength { - err.FailingCriterias = append(err.FailingCriterias, InvalidMaxLengthPassword) - } - - if settings.Lowercase { - if !strings.ContainsAny(password, PasswordLowerCaseLetters) { - err.FailingCriterias = append(err.FailingCriterias, InvalidLowercasePassword) - } - } - - if settings.Uppercase { - if !strings.ContainsAny(password, PasswordUpperCaseLetters) { - err.FailingCriterias = append(err.FailingCriterias, InvalidUppercasePassword) - } - } - - if settings.Number { - if !strings.ContainsAny(password, PasswordNumbers) { - err.FailingCriterias = append(err.FailingCriterias, InvalidNumberPassword) - } - } - - if settings.Symbol { - if !strings.ContainsAny(password, PasswordSpecialChars) { - err.FailingCriterias = append(err.FailingCriterias, InvalidSymbolPassword) - } - } - - if len(err.FailingCriterias) > 0 { - return err - } - - return nil -} diff --git a/server/boards/services/auth/password_test.go b/server/boards/services/auth/password_test.go deleted file mode 100644 index fdd6f7988d..0000000000 --- a/server/boards/services/auth/password_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPasswordHash(t *testing.T) { - hash := HashPassword("Test") - - assert.True(t, ComparePassword(hash, "Test"), "Passwords don't match") - assert.False(t, ComparePassword(hash, "Test2"), "Passwords should not have matched") -} - -func TestIsPasswordValidWithSettings(t *testing.T) { - for name, tc := range map[string]struct { - Password string - Settings PasswordSettings - ExpectedFailingCriterias []string - }{ - "Short": { - Password: strings.Repeat("x", 3), - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: false, - Uppercase: false, - Number: false, - Symbol: false, - }, - }, - "Long": { - Password: strings.Repeat("x", PasswordMaximumLength), - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: false, - Uppercase: false, - Number: false, - Symbol: false, - }, - }, - "TooShort": { - Password: strings.Repeat("x", 2), - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: false, - Uppercase: false, - Number: false, - Symbol: false, - }, - ExpectedFailingCriterias: []string{"min-length"}, - }, - "TooLong": { - Password: strings.Repeat("x", PasswordMaximumLength+1), - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: false, - Uppercase: false, - Number: false, - Symbol: false, - }, - ExpectedFailingCriterias: []string{"max-length"}, - }, - "MissingLower": { - Password: "AAAAAAAAAAASD123!@#", - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: true, - Uppercase: false, - Number: false, - Symbol: false, - }, - ExpectedFailingCriterias: []string{"lowercase"}, - }, - "MissingUpper": { - Password: "aaaaaaaaaaaaasd123!@#", - Settings: PasswordSettings{ - MinimumLength: 3, - Uppercase: true, - Lowercase: false, - Number: false, - Symbol: false, - }, - ExpectedFailingCriterias: []string{"uppercase"}, - }, - "MissingNumber": { - Password: "asasdasdsadASD!@#", - Settings: PasswordSettings{ - MinimumLength: 3, - Number: true, - Lowercase: false, - Uppercase: false, - Symbol: false, - }, - ExpectedFailingCriterias: []string{"number"}, - }, - "MissingSymbol": { - Password: "asdasdasdasdasdASD123", - Settings: PasswordSettings{ - MinimumLength: 3, - Symbol: true, - Lowercase: false, - Uppercase: false, - Number: false, - }, - ExpectedFailingCriterias: []string{"symbol"}, - }, - "MissingMultiple": { - Password: "asdasdasdasdasdasd", - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: true, - Uppercase: true, - Number: true, - Symbol: true, - }, - ExpectedFailingCriterias: []string{"uppercase", "number", "symbol"}, - }, - "Everything": { - Password: "asdASD!@#123", - Settings: PasswordSettings{ - MinimumLength: 3, - Lowercase: true, - Uppercase: true, - Number: true, - Symbol: true, - }, - }, - } { - t.Run(name, func(t *testing.T) { - err := IsPasswordValid(tc.Password, tc.Settings) - if len(tc.ExpectedFailingCriterias) == 0 { - assert.NoError(t, err) - } else { - require.Error(t, err) - var errFC *InvalidPasswordError - if assert.ErrorAs(t, err, &errFC) { - assert.Equal(t, tc.ExpectedFailingCriterias, errFC.FailingCriterias) - } - } - }) - } -} diff --git a/server/boards/services/auth/request_parser.go b/server/boards/services/auth/request_parser.go deleted file mode 100644 index d6bb80b5f0..0000000000 --- a/server/boards/services/auth/request_parser.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import ( - "net/http" - "strings" -) - -const ( - HeaderToken = "token" - HeaderAuth = "Authorization" - HeaderBearer = "BEARER" - SessionCookieToken = "FOCALBOARDAUTHTOKEN" -) - -type TokenLocation int - -const ( - TokenLocationNotFound TokenLocation = iota - TokenLocationHeader - TokenLocationCookie - TokenLocationQueryString -) - -func (tl TokenLocation) String() string { - switch tl { - case TokenLocationNotFound: - return "Not Found" - case TokenLocationHeader: - return "Header" - case TokenLocationCookie: - return "Cookie" - case TokenLocationQueryString: - return "QueryString" - default: - return "Unknown" - } -} - -func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) { - authHeader := r.Header.Get(HeaderAuth) - - // Attempt to parse the token from the cookie - if cookie, err := r.Cookie(SessionCookieToken); err == nil { - return cookie.Value, TokenLocationCookie - } - - // Parse the token from the header - if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == HeaderBearer { - // Default session token - return authHeader[7:], TokenLocationHeader - } - - if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == HeaderToken { - // OAuth token - return authHeader[6:], TokenLocationHeader - } - - // Attempt to parse token out of the query string - if token := r.URL.Query().Get("access_token"); token != "" { - return token, TokenLocationQueryString - } - - return "", TokenLocationNotFound -} diff --git a/server/boards/services/auth/request_parser_test.go b/server/boards/services/auth/request_parser_test.go deleted file mode 100644 index efcaf8be23..0000000000 --- a/server/boards/services/auth/request_parser_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package auth - -import ( - "net/http" - "net/http/httptest" - "strconv" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParseAuthTokenFromRequest(t *testing.T) { - cases := []struct { - header string - cookie string - query string - expectedToken string - expectedLocation TokenLocation - }{ - {"", "", "", "", TokenLocationNotFound}, - {"token mytoken", "", "", "mytoken", TokenLocationHeader}, - {"BEARER mytoken", "", "", "mytoken", TokenLocationHeader}, - {"", "mytoken", "", "mytoken", TokenLocationCookie}, - {"", "", "mytoken", "mytoken", TokenLocationQueryString}, - } - - for testnum, tc := range cases { - pathname := "/test/here" - if tc.query != "" { - pathname += "?access_token=" + tc.query - } - req := httptest.NewRequest("GET", pathname, nil) - if tc.header != "" { - req.Header.Add(HeaderAuth, tc.header) - } - if tc.cookie != "" { - req.AddCookie(&http.Cookie{ - Name: "FOCALBOARDAUTHTOKEN", - Value: tc.cookie, - }) - } - - token, location := ParseAuthTokenFromRequest(req) - - require.Equal(t, tc.expectedToken, token, "Wrong token on test "+strconv.Itoa(testnum)) - require.Equal(t, tc.expectedLocation, location, "Wrong location on test "+strconv.Itoa(testnum)) - } -} diff --git a/server/boards/services/config/config.go b/server/boards/services/config/config.go deleted file mode 100644 index f51eda61f6..0000000000 --- a/server/boards/services/config/config.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -import ( - "log" - - "github.com/spf13/viper" -) - -const ( - DefaultServerRoot = "http://localhost:8000" - DefaultPort = 8000 -) - -type AmazonS3Config struct { - AccessKeyID string - SecretAccessKey string - Bucket string - PathPrefix string - Region string - Endpoint string - SSL bool - SignV2 bool - SSE bool - Trace bool - Timeout int64 -} - -// Configuration is the app configuration stored in a json file. -type Configuration struct { - ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"` - Port int `json:"port" mapstructure:"port"` - DBType string `json:"dbtype" mapstructure:"dbtype"` - DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"` - DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"` - UseSSL bool `json:"useSSL" mapstructure:"useSSL"` - SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"` - WebPath string `json:"webpath" mapstructure:"webpath"` - FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"` - FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"` - FilesPath string `json:"filespath" mapstructure:"filespath"` - MaxFileSize int64 `json:"maxfilesize" mapstructure:"maxfilesize"` - Telemetry bool `json:"telemetry" mapstructure:"telemetry"` - TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"` - PrometheusAddress string `json:"prometheusaddress" mapstructure:"prometheusaddress"` - WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"` - Secret string `json:"secret" mapstructure:"secret"` - SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"` - SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"` - LocalOnly bool `json:"localonly" mapstructure:"localonly"` - EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"` - LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"` - EnablePublicSharedBoards bool `json:"enablePublicSharedBoards" mapstructure:"enablePublicSharedBoards"` - FeatureFlags map[string]string `json:"featureFlags" mapstructure:"featureFlags"` - EnableDataRetention bool `json:"enable_data_retention" mapstructure:"enable_data_retention"` - DataRetentionDays int `json:"data_retention_days" mapstructure:"data_retention_days"` - TeammateNameDisplay string `json:"teammate_name_display" mapstructure:"teammateNameDisplay"` - ShowEmailAddress bool `json:"show_email_address" mapstructure:"showEmailAddress"` - ShowFullName bool `json:"show_full_name" mapstructure:"showFullName"` - - AuthMode string `json:"authMode" mapstructure:"authMode"` - - LoggingCfgFile string `json:"logging_cfg_file" mapstructure:"logging_cfg_file"` - LoggingCfgJSON string `json:"logging_cfg_json" mapstructure:"logging_cfg_json"` - - AuditCfgFile string `json:"audit_cfg_file" mapstructure:"audit_cfg_file"` - AuditCfgJSON string `json:"audit_cfg_json" mapstructure:"audit_cfg_json"` - - NotifyFreqCardSeconds int `json:"notify_freq_card_seconds" mapstructure:"notify_freq_card_seconds"` - NotifyFreqBoardSeconds int `json:"notify_freq_board_seconds" mapstructure:"notify_freq_board_seconds"` -} - -// ReadConfigFile read the configuration from the filesystem. -func ReadConfigFile(configFilePath string) (*Configuration, error) { - if configFilePath == "" { - viper.SetConfigFile("./config.json") - } else { - viper.SetConfigFile(configFilePath) - } - - viper.SetEnvPrefix("focalboard") - viper.AutomaticEnv() // read config values from env like FOCALBOARD_SERVERROOT=... - viper.SetDefault("ServerRoot", DefaultServerRoot) - viper.SetDefault("Port", DefaultPort) - viper.SetDefault("DBType", "postgres") - viper.SetDefault("DBConfigString", "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes") - viper.SetDefault("DBTablePrefix", "") - viper.SetDefault("SecureCookie", false) - viper.SetDefault("WebPath", "./pack") - viper.SetDefault("FilesPath", "./files") - viper.SetDefault("FilesDriver", "local") - viper.SetDefault("Telemetry", true) - viper.SetDefault("TelemetryID", "") - viper.SetDefault("WebhookUpdate", nil) - viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime - viper.SetDefault("SessionRefreshTime", 60*60*5) // 5 minutes session refresh - viper.SetDefault("LocalOnly", false) - viper.SetDefault("EnableLocalMode", false) - viper.SetDefault("LocalModeSocketLocation", "/var/tmp/focalboard_local.socket") - viper.SetDefault("EnablePublicSharedBoards", false) - viper.SetDefault("FeatureFlags", map[string]string{}) - viper.SetDefault("AuthMode", "native") - viper.SetDefault("NotifyFreqCardSeconds", 120) // 2 minutes after last card edit - viper.SetDefault("NotifyFreqBoardSeconds", 86400) // 1 day after last card edit - viper.SetDefault("EnableDataRetention", false) - viper.SetDefault("DataRetentionDays", 365) // 1 year is default - viper.SetDefault("PrometheusAddress", "") - viper.SetDefault("TeammateNameDisplay", "username") - viper.SetDefault("ShowEmailAddress", false) - viper.SetDefault("ShowFullName", false) - - err := viper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - return nil, err - } - - configuration := Configuration{} - - err = viper.Unmarshal(&configuration) - if err != nil { - return nil, err - } - - log.Println("readConfigFile") - log.Printf("%+v", removeSecurityData(configuration)) - - return &configuration, nil -} - -func removeSecurityData(config Configuration) Configuration { - clean := config - return clean -} diff --git a/server/boards/services/metrics/metrics.go b/server/boards/services/metrics/metrics.go deleted file mode 100644 index e536a3383d..0000000000 --- a/server/boards/services/metrics/metrics.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package metrics - -import ( - "os" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" -) - -const ( - MetricsNamespace = "focalboard" - MetricsSubsystemBlocks = "blocks" - MetricsSubsystemBoards = "boards" - MetricsSubsystemTeams = "teams" - MetricsSubsystemSystem = "system" - - MetricsCloudInstallationLabel = "installationId" -) - -type InstanceInfo struct { - Version string - BuildNum string - Edition string - InstallationID string -} - -// Metrics used to instrumentate metrics in prometheus. -type Metrics struct { - registry *prometheus.Registry - - instance *prometheus.GaugeVec - startTime prometheus.Gauge - - loginCount prometheus.Counter - logoutCount prometheus.Counter - loginFailCount prometheus.Counter - - blocksInsertedCount prometheus.Counter - blocksPatchedCount prometheus.Counter - blocksDeletedCount prometheus.Counter - - blockCount *prometheus.GaugeVec - boardCount prometheus.Gauge - teamCount prometheus.Gauge - - blockLastActivity prometheus.Gauge -} - -// NewMetrics Factory method to create a new metrics collector. -func NewMetrics(info InstanceInfo) *Metrics { - m := &Metrics{} - - m.registry = prometheus.NewRegistry() - options := collectors.ProcessCollectorOpts{ - Namespace: MetricsNamespace, - } - m.registry.MustRegister(collectors.NewProcessCollector(options)) - m.registry.MustRegister(collectors.NewGoCollector()) - - additionalLabels := map[string]string{} - if info.InstallationID != "" { - additionalLabels[MetricsCloudInstallationLabel] = os.Getenv("MM_CLOUD_INSTALLATION_ID") - } - - m.loginCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "login_total", - Help: "Total number of logins.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.loginCount) - - m.logoutCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "logout_total", - Help: "Total number of logouts.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.logoutCount) - - m.loginFailCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "login_fail_total", - Help: "Total number of failed logins.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.loginFailCount) - - m.instance = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "focalboard_instance_info", - Help: "Instance information for Focalboard.", - ConstLabels: additionalLabels, - }, []string{"Version", "BuildNum", "Edition"}) - m.registry.MustRegister(m.instance) - m.instance.WithLabelValues(info.Version, info.BuildNum, info.Edition).Set(1) - - m.startTime = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "server_start_time", - Help: "The time the server started.", - ConstLabels: additionalLabels, - }) - m.startTime.SetToCurrentTime() - m.registry.MustRegister(m.startTime) - - m.blocksInsertedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBlocks, - Name: "blocks_inserted_total", - Help: "Total number of blocks inserted.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.blocksInsertedCount) - - m.blocksPatchedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBlocks, - Name: "blocks_patched_total", - Help: "Total number of blocks patched.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.blocksPatchedCount) - - m.blocksDeletedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBlocks, - Name: "blocks_deleted_total", - Help: "Total number of blocks deleted.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.blocksDeletedCount) - - m.blockCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBlocks, - Name: "blocks_total", - Help: "Total number of blocks.", - ConstLabels: additionalLabels, - }, []string{"BlockType"}) - m.registry.MustRegister(m.blockCount) - - m.boardCount = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBoards, - Name: "boards_total", - Help: "Total number of boards.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.boardCount) - - m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemTeams, - Name: "teams_total", - Help: "Total number of teams.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.teamCount) - - m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemBlocks, - Name: "blocks_last_activity", - Help: "Time of last block insert, update, delete.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.blockLastActivity) - - return m -} - -func (m *Metrics) IncrementLoginCount(num int) { - if m != nil { - m.loginCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementLogoutCount(num int) { - if m != nil { - m.logoutCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementLoginFailCount(num int) { - if m != nil { - m.loginFailCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementBlocksInserted(num int) { - if m != nil { - m.blocksInsertedCount.Add(float64(num)) - m.blockLastActivity.SetToCurrentTime() - } -} - -func (m *Metrics) IncrementBlocksPatched(num int) { - if m != nil { - m.blocksPatchedCount.Add(float64(num)) - m.blockLastActivity.SetToCurrentTime() - } -} - -func (m *Metrics) IncrementBlocksDeleted(num int) { - if m != nil { - m.blocksDeletedCount.Add(float64(num)) - m.blockLastActivity.SetToCurrentTime() - } -} - -func (m *Metrics) ObserveBlockCount(blockType string, count int64) { - if m != nil { - m.blockCount.WithLabelValues(blockType).Set(float64(count)) - } -} - -func (m *Metrics) ObserveBoardCount(count int64) { - if m != nil { - m.boardCount.Set(float64(count)) - } -} - -func (m *Metrics) ObserveTeamCount(count int64) { - if m != nil { - m.teamCount.Set(float64(count)) - } -} diff --git a/server/boards/services/metrics/service.go b/server/boards/services/metrics/service.go deleted file mode 100644 index 7ec21b8c96..0000000000 --- a/server/boards/services/metrics/service.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package metrics - -import ( - "net/http" - - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// Service prometheus to run the server. -type Service struct { - *http.Server -} - -// NewMetricsServer factory method to create a new prometheus server. -func NewMetricsServer(address string, metricsService *Metrics, logger mlog.LoggerIFace) *Service { - return &Service{ - &http.Server{ //nolint:gosec - Addr: address, - Handler: promhttp.HandlerFor(metricsService.registry, promhttp.HandlerOpts{ - ErrorLog: logger.StdLogger(mlog.LvlError), - }), - }, - } -} - -// Run will start the prometheus server. -func (h *Service) Run() error { - return errors.Wrap(h.Server.ListenAndServe(), "prometheus ListenAndServe") -} - -// Shutdown will shutdown the prometheus server. -func (h *Service) Shutdown() error { - return errors.Wrap(h.Server.Close(), "prometheus Close") -} diff --git a/server/boards/services/notify/notifylogger/logger_backend.go b/server/boards/services/notify/notifylogger/logger_backend.go deleted file mode 100644 index d5657bf688..0000000000 --- a/server/boards/services/notify/notifylogger/logger_backend.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifylogger - -import ( - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - backendName = "notifyLogger" -) - -type Backend struct { - logger mlog.LoggerIFace - level mlog.Level -} - -func New(logger mlog.LoggerIFace, level mlog.Level) *Backend { - return &Backend{ - logger: logger, - level: level, - } -} - -func (b *Backend) Start() error { - return nil -} - -func (b *Backend) ShutDown() error { - _ = b.logger.Flush() - return nil -} - -func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { - var board string - var card string - - if evt.Board != nil { - board = evt.Board.Title - } - if evt.Card != nil { - card = evt.Card.Title - } - - b.logger.Log(b.level, "Block change event", - mlog.String("action", string(evt.Action)), - mlog.String("board", board), - mlog.String("card", card), - mlog.String("block_id", evt.BlockChanged.ID), - ) - return nil -} - -func (b *Backend) Name() string { - return backendName -} diff --git a/server/boards/services/notify/notifymentions/app_api.go b/server/boards/services/notify/notifymentions/app_api.go deleted file mode 100644 index 85f704921b..0000000000 --- a/server/boards/services/notify/notifymentions/app_api.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package notifymentions - -import "github.com/mattermost/mattermost/server/v8/boards/model" - -type AppAPI interface { - GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) - AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) -} diff --git a/server/boards/services/notify/notifymentions/delivery.go b/server/boards/services/notify/notifymentions/delivery.go deleted file mode 100644 index a23e5119d0..0000000000 --- a/server/boards/services/notify/notifymentions/delivery.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import ( - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -// MentionDelivery provides an interface for delivering @mention notifications to other systems, such as -// channels server via plugin API. -// On success the user id of the user mentioned is returned. -type MentionDelivery interface { - MentionDeliver(mentionedUser *mm_model.User, extract string, evt notify.BlockChangeEvent) (string, error) - UserByUsername(mentionUsername string) (*mm_model.User, error) -} diff --git a/server/boards/services/notify/notifymentions/extract.go b/server/boards/services/notify/notifymentions/extract.go deleted file mode 100644 index dc1e45bdbd..0000000000 --- a/server/boards/services/notify/notifymentions/extract.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import "strings" - -const ( - defPrefixLines = 2 - defPrefixMaxChars = 100 - defSuffixLines = 2 - defSuffixMaxChars = 100 -) - -type limits struct { - prefixLines int - prefixMaxChars int - suffixLines int - suffixMaxChars int -} - -func newLimits() limits { - return limits{ - prefixLines: defPrefixLines, - prefixMaxChars: defPrefixMaxChars, - suffixLines: defSuffixLines, - suffixMaxChars: defSuffixMaxChars, - } -} - -// extractText returns all or a subset of the input string, such that -// no more than `prefixLines` lines preceding the mention and `suffixLines` -// lines after the mention are returned, and no more than approx -// prefixMaxChars+suffixMaxChars are returned. -func extractText(s string, mention string, limits limits) string { - if !strings.HasPrefix(mention, "@") { - mention = "@" + mention - } - lines := strings.Split(s, "\n") - - // find first line with mention - found := -1 - for i, l := range lines { - if strings.Contains(l, mention) { - found = i - break - } - } - if found == -1 { - return "" - } - - prefix := safeConcat(lines, found-limits.prefixLines, found) - suffix := safeConcat(lines, found+1, found+limits.suffixLines+1) - combined := strings.TrimSpace(strings.Join([]string{prefix, lines[found], suffix}, "\n")) - - // find mention position within - pos := strings.Index(combined, mention) - pos = max(pos, 0) - - return safeSubstr(combined, pos-limits.prefixMaxChars, pos+limits.suffixMaxChars) -} - -func safeConcat(lines []string, start int, end int) string { - count := len(lines) - start = min(max(start, 0), count) - end = min(max(end, start), count) - - var sb strings.Builder - for i := start; i < end; i++ { - if lines[i] != "" { - sb.WriteString(lines[i]) - sb.WriteByte('\n') - } - } - return strings.TrimSpace(sb.String()) -} - -func safeSubstr(s string, start int, end int) string { - count := len(s) - start = min(max(start, 0), count) - end = min(max(end, start), count) - return s[start:end] -} - -func min(a int, b int) int { - if a < b { - return a - } - return b -} - -func max(a int, b int) int { - if a > b { - return a - } - return b -} diff --git a/server/boards/services/notify/notifymentions/extract_test.go b/server/boards/services/notify/notifymentions/extract_test.go deleted file mode 100644 index a068b9b66e..0000000000 --- a/server/boards/services/notify/notifymentions/extract_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import ( - "strings" - "testing" -) - -const ( - s0 = "Zero is in the mind @billy." - s1 = "This is line 1." - s2 = "Line two is right here." - s3 = "Three is the line I am." - s4 = "'Four score and seven years...', said @lincoln." - s5 = "Fast Five was arguably the best F&F film." - s6 = "Big Hero 6 may have an inflated sense of self." - s7 = "The seventh sign, @sarah, will be a failed unit test." -) - -var ( - all = []string{s0, s1, s2, s3, s4, s5, s6, s7} - allConcat = strings.Join(all, "\n") - - extractLimits = limits{ - prefixLines: 2, - prefixMaxChars: 100, - suffixLines: 2, - suffixMaxChars: 100, - } -) - -func join(s ...string) string { - return strings.Join(s, "\n") -} - -func Test_extractText(t *testing.T) { - type args struct { - s string - mention string - limits limits - } - tests := []struct { - name string - args args - want string - }{ - {name: "good", want: join(s2, s3, s4, s5, s6), args: args{mention: "@lincoln", limits: extractLimits, s: allConcat}}, - {name: "not found", want: "", args: args{mention: "@bogus", limits: extractLimits, s: allConcat}}, - {name: "one line", want: join(s4), args: args{mention: "@lincoln", limits: extractLimits, s: s4}}, - {name: "two lines", want: join(s4, s5), args: args{mention: "@lincoln", limits: extractLimits, s: join(s4, s5)}}, - {name: "zero lines", want: "", args: args{mention: "@lincoln", limits: extractLimits, s: ""}}, - {name: "first line mention", want: join(s0, s1, s2), args: args{mention: "@billy", limits: extractLimits, s: allConcat}}, - {name: "last line mention", want: join(s5[7:], s6, s7), args: args{mention: "@sarah", limits: extractLimits, s: allConcat}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := extractText(tt.args.s, tt.args.mention, tt.args.limits); got != tt.want { - t.Errorf("extractText()\ngot:\n%v\nwant:\n%v\n", got, tt.want) - } - }) - } -} - -func Test_safeConcat(t *testing.T) { - type args struct { - lines []string - start int - end int - } - tests := []struct { - name string - args args - want string - }{ - {name: "out of range", want: join(s0, s1, s2, s3, s4, s5, s6, s7), args: args{start: -22, end: 99, lines: all}}, - {name: "2,3", want: join(s2, s3), args: args{start: 2, end: 4, lines: all}}, - {name: "mismatch", want: "", args: args{start: 4, end: 2, lines: all}}, - {name: "empty", want: "", args: args{start: 2, end: 4, lines: []string{}}}, - {name: "nil", want: "", args: args{start: 2, end: 4, lines: nil}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := safeConcat(tt.args.lines, tt.args.start, tt.args.end); got != tt.want { - t.Errorf("safeConcat() = [%v], want [%v]", got, tt.want) - } - }) - } -} - -func Test_safeSubstr(t *testing.T) { - type args struct { - s string - start int - end int - } - tests := []struct { - name string - args args - want string - }{ - {name: "good", want: "is line", args: args{start: 33, end: 40, s: join(s0, s1, s2)}}, - {name: "out of range", want: allConcat, args: args{start: -10, end: 1000, s: allConcat}}, - {name: "mismatch", want: "", args: args{start: 33, end: 26, s: allConcat}}, - {name: "empty", want: "", args: args{start: 2, end: 4, s: ""}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := safeSubstr(tt.args.s, tt.args.start, tt.args.end); got != tt.want { - t.Errorf("safeSubstr()\ngot:\n[%v]\nwant:\n[%v]\n", got, tt.want) - } - }) - } -} diff --git a/server/boards/services/notify/notifymentions/mentions.go b/server/boards/services/notify/notifymentions/mentions.go deleted file mode 100644 index d169a33294..0000000000 --- a/server/boards/services/notify/notifymentions/mentions.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import ( - "regexp" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`) - -// extractMentions extracts any mentions in the specified block and returns -// a slice of usernames. -func extractMentions(block *model.Block) map[string]struct{} { - mentions := make(map[string]struct{}) - if block == nil || !strings.Contains(block.Title, "@") { - return mentions - } - - str := block.Title - - for _, match := range atMentionRegexp.FindAllString(str, -1) { - name := mm_model.NormalizeUsername(match[1:]) - if mm_model.IsValidUsernameAllowRemote(name) { - mentions[name] = struct{}{} - } - } - return mentions -} diff --git a/server/boards/services/notify/notifymentions/mentions_backend.go b/server/boards/services/notify/notifymentions/mentions_backend.go deleted file mode 100644 index f0f654bc7b..0000000000 --- a/server/boards/services/notify/notifymentions/mentions_backend.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import ( - "errors" - "fmt" - "sync" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - backendName = "notifyMentions" -) - -var ( - ErrMentionPermission = errors.New("mention not permitted") -) - -type MentionListener interface { - OnMention(userID string, evt notify.BlockChangeEvent) -} - -type BackendParams struct { - AppAPI AppAPI - Permissions permissions.PermissionsService - Delivery MentionDelivery - Logger mlog.LoggerIFace -} - -// Backend provides the notification backend for @mentions. -type Backend struct { - appAPI AppAPI - permissions permissions.PermissionsService - delivery MentionDelivery - logger mlog.LoggerIFace - - mux sync.RWMutex - listeners []MentionListener -} - -func New(params BackendParams) *Backend { - return &Backend{ - appAPI: params.AppAPI, - permissions: params.Permissions, - delivery: params.Delivery, - logger: params.Logger, - } -} - -func (b *Backend) Start() error { - return nil -} - -func (b *Backend) ShutDown() error { - _ = b.logger.Flush() - return nil -} - -func (b *Backend) Name() string { - return backendName -} - -func (b *Backend) AddListener(l MentionListener) { - b.mux.Lock() - defer b.mux.Unlock() - b.listeners = append(b.listeners, l) - b.logger.Debug("Mention listener added.", mlog.Int("listener_count", len(b.listeners))) -} - -func (b *Backend) RemoveListener(l MentionListener) { - b.mux.Lock() - defer b.mux.Unlock() - list := make([]MentionListener, 0, len(b.listeners)) - for _, listener := range b.listeners { - if listener != l { - list = append(list, listener) - } - } - b.listeners = list - b.logger.Debug("Mention listener removed.", mlog.Int("listener_count", len(b.listeners))) -} - -func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { - if evt.Board == nil || evt.Card == nil { - return nil - } - - if evt.Action == notify.Delete { - return nil - } - - switch evt.BlockChanged.Type { - case model.TypeText, model.TypeComment, model.TypeImage: - default: - return nil - } - - mentions := extractMentions(evt.BlockChanged) - if len(mentions) == 0 { - return nil - } - - oldMentions := extractMentions(evt.BlockOld) - merr := merror.New() - - b.mux.RLock() - listeners := make([]MentionListener, len(b.listeners)) - copy(listeners, b.listeners) - b.mux.RUnlock() - - for username := range mentions { - if _, exists := oldMentions[username]; exists { - // the mention already existed; no need to notify again - continue - } - - extract := extractText(evt.BlockChanged.Title, username, newLimits()) - - userID, err := b.deliverMentionNotification(username, extract, evt) - if err != nil { - if errors.Is(err, ErrMentionPermission) { - b.logger.Debug("Cannot deliver notification", mlog.String("user", username), mlog.Err(err)) - } else { - merr.Append(fmt.Errorf("cannot deliver notification for @%s: %w", username, err)) - } - } - - if userID == "" { - // was a `@` followed by something other than a username. - continue - } - - b.logger.Debug("Mention notification delivered", - mlog.String("user", username), - mlog.Int("listener_count", len(listeners)), - ) - - for _, listener := range listeners { - safeCallListener(listener, userID, evt, b.logger) - } - } - return merr.ErrorOrNil() -} - -func safeCallListener(listener MentionListener, userID string, evt notify.BlockChangeEvent, logger mlog.LoggerIFace) { - // don't let panicky listeners stop notifications - defer func() { - if r := recover(); r != nil { - logger.Error("panic calling @mention notification listener", mlog.Any("err", r)) - } - }() - listener.OnMention(userID, evt) -} - -func (b *Backend) deliverMentionNotification(username string, extract string, evt notify.BlockChangeEvent) (string, error) { - mentionedUser, err := b.delivery.UserByUsername(username) - if err != nil { - if model.IsErrNotFound(err) { - // not really an error; could just be someone typed "@sometext" - return "", nil - } - return "", fmt.Errorf("cannot lookup mentioned user: %w", err) - } - - if evt.ModifiedBy == nil { - return "", fmt.Errorf("invalid user cannot mention: %w", ErrMentionPermission) - } - - if evt.Board.Type == model.BoardTypeOpen { - // public board rules: - // - admin, editor, commenter: can mention anyone on team (mentioned users are automatically added to board) - // - guest: can mention board members - switch { - case evt.ModifiedBy.SchemeAdmin, evt.ModifiedBy.SchemeEditor, evt.ModifiedBy.SchemeCommenter: - if !b.permissions.HasPermissionToTeam(mentionedUser.Id, evt.TeamID, model.PermissionViewTeam) { - return "", fmt.Errorf("%s cannot mention non-team member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission) - } - // add mentioned user to board (if not already a member) - member, err := b.appAPI.GetMemberForBoard(evt.Board.ID, mentionedUser.Id) - if member == nil || model.IsErrNotFound(err) { - // create memberships based on minimum board role - newBoardMember := &model.BoardMember{ - UserID: mentionedUser.Id, - BoardID: evt.Board.ID, - SchemeViewer: evt.Board.MinimumRole == model.BoardRoleViewer || - evt.Board.MinimumRole == model.BoardRoleCommenter || - evt.Board.MinimumRole == model.BoardRoleEditor, - SchemeCommenter: evt.Board.MinimumRole == model.BoardRoleCommenter || - evt.Board.MinimumRole == model.BoardRoleEditor, - SchemeEditor: evt.Board.MinimumRole == model.BoardRoleEditor, - } - if _, err = b.appAPI.AddMemberToBoard(newBoardMember); err != nil { - return "", fmt.Errorf("cannot add mentioned user %s to board %s: %w", mentionedUser.Id, evt.Board.ID, err) - } - b.logger.Debug("auto-added mentioned user to board", - mlog.String("user_id", mentionedUser.Id), - mlog.String("board_id", evt.Board.ID), - mlog.String("board_type", string(evt.Board.Type)), - ) - } else { - b.logger.Debug("skipping auto-add mentioned user to board; already a member", - mlog.String("user_id", mentionedUser.Id), - mlog.String("board_id", evt.Board.ID), - mlog.String("board_type", string(evt.Board.Type)), - ) - } - case evt.ModifiedBy.SchemeViewer: - // viewer should not have gotten this far since they cannot add text to a card - return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission) - default: - // this is a guest - if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) { - return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission) - } - } - } else { - // private board rules: - // - admin, editor, commenter, guest: can mention board members - switch { - case evt.ModifiedBy.SchemeViewer: - // viewer should not have gotten this far since they cannot add text to a card - return "", fmt.Errorf("%s (viewer) cannot mention user %s: %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission) - default: - // everyone else can mention board members - if !b.permissions.HasPermissionToBoard(mentionedUser.Id, evt.Board.ID, model.PermissionViewBoard) { - return "", fmt.Errorf("%s cannot mention non-board member %s : %w", evt.ModifiedBy.UserID, mentionedUser.Id, ErrMentionPermission) - } - } - } - - return b.delivery.MentionDeliver(mentionedUser, extract, evt) -} diff --git a/server/boards/services/notify/notifymentions/mentions_test.go b/server/boards/services/notify/notifymentions/mentions_test.go deleted file mode 100644 index 8f479cc149..0000000000 --- a/server/boards/services/notify/notifymentions/mentions_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifymentions - -import ( - "reflect" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -func Test_extractMentions(t *testing.T) { - tests := []struct { - name string - block *model.Block - want map[string]struct{} - }{ - {name: "empty", block: makeBlock(""), want: makeMap()}, - {name: "zero mentions", block: makeBlock("This is some text."), want: makeMap()}, - {name: "one mention", block: makeBlock("Hello @user1"), want: makeMap("user1")}, - {name: "multiple mentions", block: makeBlock("Hello @user1, @user2 and @user3"), want: makeMap("user1", "user2", "user3")}, - {name: "include period", block: makeBlock("Hello @user1."), want: makeMap("user1.")}, - {name: "include underscore", block: makeBlock("Hello @user1_"), want: makeMap("user1_")}, - {name: "don't include comma", block: makeBlock("Hello @user1,"), want: makeMap("user1")}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := extractMentions(tt.block); !reflect.DeepEqual(got, tt.want) { - t.Errorf("extractMentions() = %v, want %v", got, tt.want) - } - }) - } -} - -func makeBlock(text string) *model.Block { - return &model.Block{ - ID: mm_model.NewId(), - Type: model.TypeComment, - Title: text, - } -} - -func makeMap(mentions ...string) map[string]struct{} { - m := make(map[string]struct{}) - for _, mention := range mentions { - m[mention] = struct{}{} - } - return m -} diff --git a/server/boards/services/notify/notifysubscriptions/app_api.go b/server/boards/services/notify/notifysubscriptions/app_api.go deleted file mode 100644 index f15c0690e5..0000000000 --- a/server/boards/services/notify/notifysubscriptions/app_api.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type AppAPI interface { - GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) - GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) - GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) - - GetUserByID(userID string) (*model.User, error) - - CreateSubscription(sub *model.Subscription) (*model.Subscription, error) - GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) - UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error - - UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) - GetNextNotificationHint(remove bool) (*model.NotificationHint, error) -} diff --git a/server/boards/services/notify/notifysubscriptions/delivery.go b/server/boards/services/notify/notifysubscriptions/delivery.go deleted file mode 100644 index 06df5aa26a..0000000000 --- a/server/boards/services/notify/notifysubscriptions/delivery.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -// SubscriptionDelivery provides an interface for delivering subscription notifications to other systems, such as -// channels server via plugin API. -type SubscriptionDelivery interface { - SubscriptionDeliverSlackAttachments(teamID string, subscriberID string, subscriberType model.SubscriberType, - attachments []*mm_model.SlackAttachment) error -} diff --git a/server/boards/services/notify/notifysubscriptions/diff.go b/server/boards/services/notify/notifysubscriptions/diff.go deleted file mode 100644 index 527a4411ad..0000000000 --- a/server/boards/services/notify/notifysubscriptions/diff.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "fmt" - "sort" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// Diff represents a difference between two versions of a block. -type Diff struct { - Board *model.Board - Card *model.Block - Authors StringMap - - BlockType model.BlockType - OldBlock *model.Block - NewBlock *model.Block - - UpdateAt int64 // the UpdateAt of the latest version of the block - - schemaDiffs []SchemaDiff - PropDiffs []PropDiff - - Diffs []*Diff // Diffs for child blocks -} - -type PropDiff struct { - ID string // property id - Index int - Name string - OldValue string - NewValue string -} - -type SchemaDiff struct { - Board *model.Board - - OldPropDef *model.PropDef - NewPropDef *model.PropDef -} - -type diffGenerator struct { - board *model.Board - card *model.Block - - store AppAPI - hint *model.NotificationHint - lastNotifyAt int64 - logger mlog.LoggerIFace -} - -func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { - // use block_history to fetch blocks in case they were deleted and no longer exist in blocks table. - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: true, - } - blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts) - if err != nil { - return nil, fmt.Errorf("could not get block for notification: %w", err) - } - if len(blocks) == 0 { - return nil, fmt.Errorf("block not found for notification: %w", err) - } - block := blocks[0] - - if dg.board == nil || dg.card == nil { - return nil, fmt.Errorf("cannot generate diff for block %s; must have a valid board and card: %w", dg.hint.BlockID, err) - } - - // parse board's property schema here so it only happens once. - schema, err := model.ParsePropertySchema(dg.board) - if err != nil { - return nil, fmt.Errorf("could not parse property schema for board %s: %w", dg.board.ID, err) - } - - switch block.Type { - case model.TypeBoard: - dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID)) - // TODO: Fix this - // return dg.generateDiffsForBoard(block, schema) - return nil, nil - case model.TypeCard: - diff, err := dg.generateDiffsForCard(block, schema) - if err != nil || diff == nil { - return nil, err - } - return []*Diff{diff}, nil - default: - diff, err := dg.generateDiffForBlock(block, schema) - if err != nil || diff == nil { - return nil, err - } - return []*Diff{diff}, nil - } -} - -// TODO: fix this -/* -func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) { - opts := model.QuerySubtreeOptions{ - AfterUpdateAt: dg.lastNotifyAt, - } - - find all child blocks of the board that updated since last notify. - blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts) - if err != nil { - return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err) - } - - var diffs []*Diff - - generate diff for board title change or description - boardDiff, err := dg.generateDiffForBlock(board, schema) - if err != nil { - return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err) - } - - if boardDiff != nil { - TODO: phase 2 feature (generate schema diffs and add to board diff) goes here. - diffs = append(diffs, boardDiff) - } - - for _, b := range blocks { - block := b - if block.Type == model.TypeCard { - cardDiffs, err := dg.generateDiffsForCard(&block, schema) - if err != nil { - return nil, err - } - diffs = append(diffs, cardDiffs) - } - } - return diffs, nil -} -*/ - -func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) { - // generate diff for card title change and properties. - cardDiff, err := dg.generateDiffForBlock(card, schema) - if err != nil { - return nil, fmt.Errorf("could not generate diff for card %s: %w", card.ID, err) - } - - // fetch all card content blocks that were updated after last notify - opts := model.QueryBlockHistoryChildOptions{ - AfterUpdateAt: dg.lastNotifyAt, - } - blocks, _, err := dg.store.GetBlockHistoryNewestChildren(card.ID, opts) - if err != nil { - return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err) - } - - authors := make(StringMap) - - // walk child blocks - var childDiffs []*Diff - for i := range blocks { - if blocks[i].ID == card.ID { - continue - } - - blockDiff, err := dg.generateDiffForBlock(blocks[i], schema) - if err != nil { - return nil, fmt.Errorf("could not generate diff for block %s: %w", blocks[i].ID, err) - } - if blockDiff != nil { - childDiffs = append(childDiffs, blockDiff) - authors.Append(blockDiff.Authors) - } - } - - dg.logger.Debug("generateDiffsForCard", - mlog.Bool("has_top_changes", cardDiff != nil), - mlog.Int("subtree", len(blocks)), - mlog.Array("author_names", authors.Values()), - mlog.Int("child_diffs", len(childDiffs)), - ) - - if len(childDiffs) != 0 { - if cardDiff == nil { // will be nil if the card has no other changes besides child diffs - cardDiff = &Diff{ - Board: dg.board, - Card: card, - Authors: make(StringMap), - BlockType: card.Type, - OldBlock: card, - NewBlock: card, - UpdateAt: card.UpdateAt, - PropDiffs: nil, - schemaDiffs: nil, - } - } - cardDiff.Diffs = childDiffs - } - cardDiff.Authors.Append(authors) - - return cardDiff, nil -} - -func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema model.PropSchema) (*Diff, error) { - dg.logger.Debug("generateDiffForBlock - new block", - mlog.String("block_id", newBlock.ID), - mlog.String("block_type", string(newBlock.Type)), - mlog.String("modified_by", newBlock.ModifiedBy), - mlog.Int64("update_at", newBlock.UpdateAt), - ) - - // find the version of the block as it was at the time of last notify. - opts := model.QueryBlockHistoryOptions{ - BeforeUpdateAt: dg.lastNotifyAt + 1, - Limit: 1, - Descending: true, - } - history, err := dg.store.GetBlockHistory(newBlock.ID, opts) - if err != nil { - return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err) - } - - var oldBlock *model.Block - if len(history) != 0 { - oldBlock = history[0] - - dg.logger.Debug("generateDiffForBlock - old block", - mlog.String("block_id", oldBlock.ID), - mlog.String("block_type", string(oldBlock.Type)), - mlog.Int64("before_update_at", dg.lastNotifyAt), - mlog.String("modified_by", oldBlock.ModifiedBy), - mlog.Int64("update_at", oldBlock.UpdateAt), - ) - } - - // find all the versions of the blocks that changed so we can gather all the author usernames. - opts = model.QueryBlockHistoryOptions{ - AfterUpdateAt: dg.lastNotifyAt, - Descending: true, - } - chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts) - if err != nil { - return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err) - } - authors := make(StringMap) - - dg.logger.Debug("generateDiffForBlock - authors", - mlog.Int64("after_update_at", dg.lastNotifyAt), - mlog.Int("history_count", len(chgBlocks)), - ) - - // have to loop through history slice because GetBlockHistory does not return pointers. - for _, b := range chgBlocks { - user, err := dg.store.GetUserByID(b.ModifiedBy) - if err != nil || user == nil { - dg.logger.Error("could not fetch username for block", - mlog.String("modified_by", b.ModifiedBy), - mlog.Err(err), - ) - authors.Add(b.ModifiedBy, "unknown_user") // todo: localize this when server has i18n - } else { - authors.Add(user.ID, user.Username) - } - } - - propDiffs := dg.generatePropDiffs(oldBlock, newBlock, schema) - - dg.logger.Debug("generateDiffForBlock - results", - mlog.String("block_id", newBlock.ID), - mlog.String("block_type", string(newBlock.Type)), - mlog.Array("author_names", authors.Values()), - mlog.Int("history_count", len(history)), - mlog.Int("prop_diff_count", len(propDiffs)), - ) - - diff := &Diff{ - Board: dg.board, - Card: dg.card, - Authors: authors, - BlockType: newBlock.Type, - OldBlock: oldBlock, - NewBlock: newBlock, - UpdateAt: newBlock.UpdateAt, - PropDiffs: propDiffs, - schemaDiffs: nil, - } - return diff, nil -} - -func (dg *diffGenerator) generatePropDiffs(oldBlock, newBlock *model.Block, schema model.PropSchema) []PropDiff { - var propDiffs []PropDiff - - oldProps, err := model.ParseProperties(oldBlock, schema, dg.store) - if err != nil { - dg.logger.Error("Cannot parse properties for old block", - mlog.String("block_id", oldBlock.ID), - mlog.Err(err), - ) - } - - newProps, err := model.ParseProperties(newBlock, schema, dg.store) - if err != nil { - dg.logger.Error("Cannot parse properties for new block", - mlog.String("block_id", oldBlock.ID), - mlog.Err(err), - ) - } - - // look for new or changed properties. - for k, prop := range newProps { - oldP, ok := oldProps[k] - if ok { - // prop changed - if prop.Value != oldP.Value { - propDiffs = append(propDiffs, PropDiff{ - ID: prop.ID, - Index: prop.Index, - Name: prop.Name, - NewValue: prop.Value, - OldValue: oldP.Value, - }) - } - } else { - // prop added - propDiffs = append(propDiffs, PropDiff{ - ID: prop.ID, - Index: prop.Index, - Name: prop.Name, - NewValue: prop.Value, - OldValue: "", - }) - } - } - - // look for deleted properties - for k, prop := range oldProps { - _, ok := newProps[k] - if !ok { - // prop deleted - propDiffs = append(propDiffs, PropDiff{ - ID: prop.ID, - Index: prop.Index, - Name: prop.Name, - NewValue: "", - OldValue: prop.Value, - }) - } - } - return sortPropDiffs(propDiffs) -} - -func sortPropDiffs(propDiffs []PropDiff) []PropDiff { - if len(propDiffs) == 0 { - return propDiffs - } - - sort.Slice(propDiffs, func(i, j int) bool { - return propDiffs[i].Index < propDiffs[j].Index - }) - return propDiffs -} diff --git a/server/boards/services/notify/notifysubscriptions/diff2markdown.go b/server/boards/services/notify/notifysubscriptions/diff2markdown.go deleted file mode 100644 index 8fc1edb899..0000000000 --- a/server/boards/services/notify/notifysubscriptions/diff2markdown.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "strings" - - "github.com/sergi/go-diff/diffmatchpatch" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func generateMarkdownDiff(oldText string, newText string, logger mlog.LoggerIFace) string { - oldTxtNorm := normalizeText(oldText) - newTxtNorm := normalizeText(newText) - - dmp := diffmatchpatch.New() - - diffs := dmp.DiffMain(oldTxtNorm, newTxtNorm, false) - - diffs = dmp.DiffCleanupSemantic(diffs) - diffs = dmp.DiffCleanupEfficiency(diffs) - - // check there is at least one insert or delete - var editFound bool - for _, d := range diffs { - if (d.Type == diffmatchpatch.DiffInsert || d.Type == diffmatchpatch.DiffDelete) && strings.TrimSpace(d.Text) != "" { - editFound = true - break - } - } - - if !editFound { - logger.Debug("skipping notification for superficial diff") - return "" - } - - cfg := markDownCfg{ - insertOpen: "`", - insertClose: "`", - deleteOpen: "~~`", - deleteClose: "`~~", - } - markdown := generateMarkdown(diffs, cfg) - markdown = strings.ReplaceAll(markdown, "¶", "\n") - - return markdown -} - -const ( - truncLenEquals = 60 - truncLenInserts = 120 - truncLenDeletes = 80 -) - -type markDownCfg struct { - insertOpen string - insertClose string - deleteOpen string - deleteClose string -} - -func generateMarkdown(diffs []diffmatchpatch.Diff, cfg markDownCfg) string { - sb := &strings.Builder{} - - var first, last bool - - for i, diff := range diffs { - first = i == 0 - last = i == len(diffs)-1 - - switch diff.Type { - case diffmatchpatch.DiffInsert: - sb.WriteString(cfg.insertOpen) - sb.WriteString(truncate(diff.Text, truncLenInserts, first, last)) - sb.WriteString(cfg.insertClose) - - case diffmatchpatch.DiffDelete: - sb.WriteString(cfg.deleteOpen) - sb.WriteString(truncate(diff.Text, truncLenDeletes, first, last)) - sb.WriteString(cfg.deleteClose) - - case diffmatchpatch.DiffEqual: - sb.WriteString(truncate(diff.Text, truncLenEquals, first, last)) - } - } - return sb.String() -} - -func truncate(s string, maxLen int, first bool, last bool) string { - if len(s) < maxLen { - return s - } - - var result string - - switch { - case first: - // truncate left - result = " ... " + rightWords(s, maxLen) - case last: - // truncate right - result = leftWords(s, maxLen) + " ... " - default: - // truncate in the middle - half := len(s) / 2 - - left := leftWords(s[:half], maxLen/2) - right := rightWords(s[half:], maxLen/2) - - result = left + " ... " + right - } - - return strings.ReplaceAll(result, "¶", "↩") -} - -func normalizeText(s string) string { - s = strings.ReplaceAll(s, "\t", " ") - s = strings.ReplaceAll(s, " ", " ") - s = strings.ReplaceAll(s, "\n\n", "\n") - s = strings.ReplaceAll(s, "\n", "¶") - return s -} - -// leftWords returns approximately maxLen characters from the left part of the source string by truncating on the right, -// with best effort to include whole words. -func leftWords(s string, maxLen int) string { - if len(s) < maxLen { - return s - } - fields := strings.Fields(s) - fields = words(fields, maxLen) - - return strings.Join(fields, " ") -} - -// rightWords returns approximately maxLen from the right part of the source string by truncating from the left, -// with best effort to include whole words. -func rightWords(s string, maxLen int) string { - if len(s) < maxLen { - return s - } - fields := strings.Fields(s) - - // reverse the fields so that the right-most words end up at the beginning. - reverse(fields) - - fields = words(fields, maxLen) - - // reverse the fields again so that the original order is restored. - reverse(fields) - - return strings.Join(fields, " ") -} - -func reverse(ss []string) { - ssLen := len(ss) - for i := 0; i < ssLen/2; i++ { - ss[i], ss[ssLen-i-1] = ss[ssLen-i-1], ss[i] - } -} - -// words returns a subslice containing approximately maxChars of characters. The last item may be truncated. -func words(words []string, maxChars int) []string { - var count int - result := make([]string, 0, len(words)) - - for i, w := range words { - wordLen := len(w) - if wordLen+count > maxChars { - switch { - case i == 0: - result = append(result, w[:maxChars]) - case wordLen < 8: - result = append(result, w) - } - return result - } - count += wordLen - result = append(result, w) - } - return result -} diff --git a/server/boards/services/notify/notifysubscriptions/diff2markdown_test.go b/server/boards/services/notify/notifysubscriptions/diff2markdown_test.go deleted file mode 100644 index 28c3148c97..0000000000 --- a/server/boards/services/notify/notifysubscriptions/diff2markdown_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_reverse(t *testing.T) { - tests := []struct { - name string - ss []string - want []string - }{ - {name: "even", ss: []string{"one", "two", "three", "four"}, want: []string{"four", "three", "two", "one"}}, - {name: "odd", ss: []string{"one", "two", "three"}, want: []string{"three", "two", "one"}}, - {name: "one", ss: []string{"one"}, want: []string{"one"}}, - {name: "empty", ss: []string{}, want: []string{}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reverse(tt.ss) - assert.Equal(t, tt.want, tt.ss) - }) - } -} diff --git a/server/boards/services/notify/notifysubscriptions/diff2slackattachments.go b/server/boards/services/notify/notifysubscriptions/diff2slackattachments.go deleted file mode 100644 index 38738f788a..0000000000 --- a/server/boards/services/notify/notifysubscriptions/diff2slackattachments.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "bytes" - "fmt" - "io" - "strings" - "sync" - "text/template" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - // card change notifications. - defAddCardNotify = "{{.Authors | printAuthors \"unknown_user\" }} has added the card {{. | makeLink}}\n" - defModifyCardNotify = "###### {{.Authors | printAuthors \"unknown_user\" }} has modified the card {{. | makeLink}} on the board {{. | makeBoardLink}}\n" - defDeleteCardNotify = "{{.Authors | printAuthors \"unknown_user\" }} has deleted the card {{. | makeLink}}\n" -) - -var ( - // templateCache is a map of text templateCache keyed by languange code. - templateCache = make(map[string]*template.Template) - templateCacheMux sync.Mutex -) - -// DiffConvOpts provides options when converting diffs to slack attachments. -type DiffConvOpts struct { - Language string - MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string - MakeBoardLink func(board *model.Board) string - Logger mlog.LoggerIFace -} - -// getTemplate returns a new or cached named template based on the language specified. -func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template, error) { - templateCacheMux.Lock() - defer templateCacheMux.Unlock() - - key := name + "&" + opts.Language - t, ok := templateCache[key] - if !ok { - t = template.New(key) - - if opts.MakeCardLink == nil { - opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string { - return fmt.Sprintf("`%s`", block.Title) - } - } - - if opts.MakeBoardLink == nil { - opts.MakeBoardLink = func(board *model.Board) string { - return fmt.Sprintf("`%s`", board.Title) - } - } - myFuncs := template.FuncMap{ - "getBoardDescription": getBoardDescription, - "makeLink": func(diff *Diff) string { - return opts.MakeCardLink(diff.NewBlock, diff.Board, diff.Card) - }, - "makeBoardLink": func(diff *Diff) string { - return opts.MakeBoardLink(diff.Board) - }, - "stripNewlines": func(s string) string { - return strings.TrimSpace(strings.ReplaceAll(s, "\n", "¶ ")) - }, - "printAuthors": func(empty string, authors StringMap) string { - return makeAuthorsList(authors, empty) - }, - } - t.Funcs(myFuncs) - - s := def // TODO: lookup i18n string when supported on server - t2, err := t.Parse(s) - if err != nil { - return nil, fmt.Errorf("cannot parse markdown template '%s' for notifications: %w", key, err) - } - templateCache[key] = t2 - } - return t, nil -} - -func makeAuthorsList(authors StringMap, empty string) string { - if len(authors) == 0 { - return empty - } - prefix := "" - sb := &strings.Builder{} - for _, name := range authors.Values() { - sb.WriteString(prefix) - sb.WriteString("@") - sb.WriteString(strings.TrimSpace(name)) - prefix = ", " - } - return sb.String() -} - -// execTemplate executes the named template corresponding to the template name and language specified. -func execTemplate(w io.Writer, name string, opts DiffConvOpts, def string, data interface{}) error { - t, err := getTemplate(name, opts, def) - if err != nil { - return err - } - return t.Execute(w, data) -} - -// Diffs2SlackAttachments converts a slice of `Diff` to slack attachments to be used in a post. -func Diffs2SlackAttachments(diffs []*Diff, opts DiffConvOpts) ([]*mm_model.SlackAttachment, error) { - var attachments []*mm_model.SlackAttachment - merr := merror.New() - - for _, d := range diffs { - // only handle cards for now. - if d.BlockType == model.TypeCard { - a, err := cardDiff2SlackAttachment(d, opts) - if err != nil { - merr.Append(err) - continue - } - if a == nil { - continue - } - attachments = append(attachments, a) - } - } - return attachments, merr.ErrorOrNil() -} - -func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.SlackAttachment, error) { - // sanity check - if cardDiff.NewBlock == nil && cardDiff.OldBlock == nil { - return nil, nil - } - - attachment := &mm_model.SlackAttachment{} - buf := &bytes.Buffer{} - - // card added - if cardDiff.NewBlock != nil && cardDiff.OldBlock == nil { - if err := execTemplate(buf, "AddCardNotify", opts, defAddCardNotify, cardDiff); err != nil { - return nil, err - } - attachment.Pretext = buf.String() - attachment.Fallback = attachment.Pretext - return attachment, nil - } - - // card deleted - if (cardDiff.NewBlock == nil || cardDiff.NewBlock.DeleteAt != 0) && cardDiff.OldBlock != nil { - buf.Reset() - if err := execTemplate(buf, "DeleteCardNotify", opts, defDeleteCardNotify, cardDiff); err != nil { - return nil, err - } - attachment.Pretext = buf.String() - attachment.Fallback = attachment.Pretext - return attachment, nil - } - - // at this point new and old block are non-nil - - opts.Logger.Debug("cardDiff2SlackAttachment", - mlog.String("board_id", cardDiff.Board.ID), - mlog.String("card_id", cardDiff.Card.ID), - mlog.String("new_block_id", cardDiff.NewBlock.ID), - mlog.String("old_block_id", cardDiff.OldBlock.ID), - mlog.Int("childDiffs", len(cardDiff.Diffs)), - ) - - buf.Reset() - if err := execTemplate(buf, "ModifyCardNotify", opts, defModifyCardNotify, cardDiff); err != nil { - return nil, fmt.Errorf("cannot write notification for card %s: %w", cardDiff.NewBlock.ID, err) - } - attachment.Pretext = buf.String() - attachment.Fallback = attachment.Pretext - - // title changes - attachment.Fields = appendTitleChanges(attachment.Fields, cardDiff) - - // property changes - attachment.Fields = appendPropertyChanges(attachment.Fields, cardDiff) - - // comment add/delete - attachment.Fields = appendCommentChanges(attachment.Fields, cardDiff) - - // File Attachment add/delete - attachment.Fields = appendAttachmentChanges(attachment.Fields, cardDiff) - - // content/description changes - attachment.Fields = appendContentChanges(attachment.Fields, cardDiff, opts.Logger) - - if len(attachment.Fields) == 0 { - return nil, nil - } - return attachment, nil -} - -func appendTitleChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField { - if cardDiff.NewBlock.Title != cardDiff.OldBlock.Title { - fields = append(fields, &mm_model.SlackAttachmentField{ - Short: false, - Title: "Title", - Value: fmt.Sprintf("%s ~~`%s`~~", stripNewlines(cardDiff.NewBlock.Title), stripNewlines(cardDiff.OldBlock.Title)), - }) - } - return fields -} - -func appendPropertyChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField { - if len(cardDiff.PropDiffs) == 0 { - return fields - } - - for _, propDiff := range cardDiff.PropDiffs { - if propDiff.NewValue == propDiff.OldValue { - continue - } - - var val string - if propDiff.OldValue != "" { - val = fmt.Sprintf("%s ~~`%s`~~", stripNewlines(propDiff.NewValue), stripNewlines(propDiff.OldValue)) - } else { - val = propDiff.NewValue - } - - fields = append(fields, &mm_model.SlackAttachmentField{ - Short: false, - Title: propDiff.Name, - Value: val, - }) - } - return fields -} - -func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField { - for _, child := range cardDiff.Diffs { - if child.BlockType == model.TypeComment { - var format string - var msg string - if child.NewBlock != nil && child.OldBlock == nil { - // added comment - format = "%s" - msg = child.NewBlock.Title - } - - if (child.NewBlock == nil || child.NewBlock.DeleteAt != 0) && child.OldBlock != nil { - // deleted comment - format = "~~`%s`~~" - msg = stripNewlines(child.OldBlock.Title) - } - - if format != "" { - fields = append(fields, &mm_model.SlackAttachmentField{ - Short: false, - Title: "Comment by " + makeAuthorsList(child.Authors, "unknown_user"), // todo: localize this when server has i18n - Value: fmt.Sprintf(format, msg), - }) - } - } - } - return fields -} - -func appendAttachmentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField { - for _, child := range cardDiff.Diffs { - if child.BlockType == model.TypeAttachment { - var format string - var msg string - if child.NewBlock != nil && child.OldBlock == nil { - format = "Added an attachment: **`%s`**" - msg = child.NewBlock.Title - } else { - format = "Removed ~~`%s`~~ attachment" - msg = stripNewlines(child.OldBlock.Title) - } - - if format != "" { - fields = append(fields, &mm_model.SlackAttachmentField{ - Short: false, - Title: "Changed by " + makeAuthorsList(child.Authors, "unknown_user"), // TODO: localize this when server has i18n - Value: fmt.Sprintf(format, msg), - }) - } - } - } - return fields -} - -func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField { - for _, child := range cardDiff.Diffs { - var opAdd, opDelete bool - var opString string - - switch { - case child.OldBlock == nil && child.NewBlock != nil: - opAdd = true - opString = "added" // TODO: localize when i18n added to server - case child.NewBlock == nil || child.NewBlock.DeleteAt != 0: - opDelete = true - opString = "deleted" - default: - opString = "modified" - } - - var newTitle, oldTitle string - if child.OldBlock != nil { - oldTitle = child.OldBlock.Title - } - if child.NewBlock != nil { - newTitle = child.NewBlock.Title - } - - switch child.BlockType { - case model.TypeDivider, model.TypeComment: - // do nothing - continue - case model.TypeImage: - if newTitle == "" { - newTitle = "An image was " + opString + "." // TODO: localize when i18n added to server - } - oldTitle = "" - case model.TypeAttachment: - if newTitle == "" { - newTitle = "A file attachment was " + opString + "." // TODO: localize when i18n added to server - } - oldTitle = "" - default: - if !opAdd { - if opDelete { - newTitle = "" - } - // only strip newlines when modifying or deleting - oldTitle = stripNewlines(oldTitle) - newTitle = stripNewlines(newTitle) - } - if newTitle == oldTitle { - continue - } - } - - logger.Trace("appendContentChanges", - mlog.String("type", string(child.BlockType)), - mlog.String("opString", opString), - mlog.String("oldTitle", oldTitle), - mlog.String("newTitle", newTitle), - ) - - markdown := generateMarkdownDiff(oldTitle, newTitle, logger) - if markdown == "" { - continue - } - - fields = append(fields, &mm_model.SlackAttachmentField{ - Short: false, - Title: "Description", - Value: markdown, - }) - } - return fields -} diff --git a/server/boards/services/notify/notifysubscriptions/notifier.go b/server/boards/services/notify/notifysubscriptions/notifier.go deleted file mode 100644 index 6e4acf6e23..0000000000 --- a/server/boards/services/notify/notifysubscriptions/notifier.go +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - defBlockNotificationFreq = time.Minute * 2 - enqueueNotifyHintTimeout = time.Second * 10 - hintQueueSize = 20 -) - -var ( - errEnqueueNotifyHintTimeout = errors.New("enqueue notify hint timed out") -) - -// notifier provides block change notifications for subscribers. Block change events are batched -// via notifications hints written to the database so that fewer notifications are sent for active -// blocks. -type notifier struct { - serverRoot string - store AppAPI - permissions permissions.PermissionsService - delivery SubscriptionDelivery - logger mlog.LoggerIFace - - hints chan *model.NotificationHint - - mux sync.Mutex - done chan struct{} -} - -func newNotifier(params BackendParams) *notifier { - return ¬ifier{ - serverRoot: params.ServerRoot, - store: params.AppAPI, - permissions: params.Permissions, - delivery: params.Delivery, - logger: params.Logger, - done: nil, - hints: make(chan *model.NotificationHint, hintQueueSize), - } -} - -func (n *notifier) start() { - n.mux.Lock() - defer n.mux.Unlock() - - if n.done == nil { - n.done = make(chan struct{}) - go n.loop() - } -} - -func (n *notifier) stop() { - n.mux.Lock() - defer n.mux.Unlock() - - if n.done != nil { - close(n.done) - n.done = nil - } -} - -func (n *notifier) loop() { - done := n.done - var nextNotify time.Time - - for { - hint, err := n.store.GetNextNotificationHint(false) - switch { - case model.IsErrNotFound(err): - // no hints in table; wait up to an hour or when `onNotifyHint` is called again - nextNotify = time.Now().Add(time.Hour * 1) - n.logger.Debug("notify loop - no hints in queue", mlog.Time("next_check", nextNotify)) - case err != nil: - // try again in a minute - nextNotify = time.Now().Add(time.Minute * 1) - n.logger.Error("notify loop - error fetching next notification", mlog.Err(err)) - case hint.NotifyAt > utils.GetMillis(): - // next hint is not ready yet; sleep until hint.NotifyAt - nextNotify = utils.GetTimeForMillis(hint.NotifyAt) - default: - // it's time to notify - n.notify() - continue - } - - n.logger.Debug("subscription notifier loop", - mlog.Time("next_notify", nextNotify), - ) - - select { - case <-n.hints: - // A new hint was added. Wake up and check if next hint is ready to go. - case <-time.After(time.Until(nextNotify)): - // Next scheduled hint should be ready now. - case <-done: - return - } - } -} - -func (n *notifier) onNotifyHint(hint *model.NotificationHint) error { - n.logger.Debug("onNotifyHint - enqueing hint", mlog.Any("hint", hint)) - - select { - case n.hints <- hint: - case <-time.After(enqueueNotifyHintTimeout): - return errEnqueueNotifyHintTimeout - } - return nil -} - -func (n *notifier) notify() { - var hint *model.NotificationHint - var err error - - hint, err = n.store.GetNextNotificationHint(true) - if err != nil { - if model.IsErrNotFound(err) { - // Expected when multiple nodes in a cluster try to process the same hint at the same time. - // This simply means the other node won. Returning here will simply try fetching another hint. - return - } - n.logger.Error("notify - error fetching next notification", mlog.Err(err)) - return - } - - if err = n.notifySubscribers(hint); err != nil { - n.logger.Error("Error notifying subscribers", mlog.Err(err)) - } -} - -func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { - // get the subscriber list - subs, err := n.store.GetSubscribersForBlock(hint.BlockID) - if err != nil { - return err - } - if len(subs) == 0 { - n.logger.Debug("notifySubscribers - no subscribers", mlog.Any("hint", hint)) - return nil - } - - // subs slice is sorted by `NotifiedAt`, therefore subs[0] contains the oldest NotifiedAt needed - oldestNotifiedAt := subs[0].NotifiedAt - - // need the block's board and card. - board, card, err := n.store.GetBoardAndCardByID(hint.BlockID) - if err != nil || board == nil || card == nil { - return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err) - } - - n.logger.Debug("notifySubscribers - subscribers", - mlog.Any("hint", hint), - mlog.String("board_id", board.ID), - mlog.String("card_id", card.ID), - mlog.Int("sub_count", len(subs)), - ) - - dg := &diffGenerator{ - board: board, - card: card, - store: n.store, - hint: hint, - lastNotifyAt: oldestNotifiedAt, - logger: n.logger, - } - diffs, err := dg.generateDiffs() - if err != nil { - return err - } - - n.logger.Debug("notifySubscribers - diffs", - mlog.Any("hint", hint), - mlog.Int("diff_count", len(diffs)), - ) - - if len(diffs) == 0 { - return nil - } - - diffAuthors := make(StringMap) - for _, d := range diffs { - diffAuthors.Append(d.Authors) - } - - opts := DiffConvOpts{ - Language: "en", // TODO: use correct language when i18n is available on server. - MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string { - return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID)) - }, - MakeBoardLink: func(board *model.Board) string { - return fmt.Sprintf("[%s](%s)", board.Title, utils.MakeBoardLink(n.serverRoot, board.TeamID, board.ID)) - }, - Logger: n.logger, - } - - attachments, err := Diffs2SlackAttachments(diffs, opts) - if err != nil { - return err - } - - merr := merror.New() - if len(attachments) > 0 { - for _, sub := range subs { - // don't notify the author of their own changes. - authorName, isAuthor := diffAuthors[sub.SubscriberID] - if isAuthor && len(diffAuthors) == 1 { - n.logger.Debug("notifySubscribers - skipping author", - mlog.Any("hint", hint), - mlog.String("author_id", sub.SubscriberID), - mlog.String("author_username", authorName), - ) - continue - } - - // make sure the subscriber still has permissions for the board. - if !n.permissions.HasPermissionToBoard(sub.SubscriberID, board.ID, model.PermissionViewBoard) { - n.logger.Debug("notifySubscribers - skipping non-board member", - mlog.Any("hint", hint), - mlog.String("subscriber_id", sub.SubscriberID), - mlog.String("board_id", board.ID), - ) - continue - } - - n.logger.Debug("notifySubscribers - deliver", - mlog.Any("hint", hint), - mlog.String("modified_by_id", hint.ModifiedByID), - mlog.String("subscriber_id", sub.SubscriberID), - mlog.String("subscriber_type", string(sub.SubscriberType)), - ) - - if err = n.delivery.SubscriptionDeliverSlackAttachments(board.TeamID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil { - merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w", - sub.SubscriberID, sub.SubscriberType, err)) - } - } - } else { - n.logger.Debug("notifySubscribers - skip delivery; no chg", - mlog.Any("hint", hint), - mlog.String("modified_by_id", hint.ModifiedByID), - ) - } - - // find the new NotifiedAt based on the newest diff. - var notifiedAt int64 - for _, d := range diffs { - if d.UpdateAt > notifiedAt { - notifiedAt = d.UpdateAt - } - for _, c := range d.Diffs { - if c.UpdateAt > notifiedAt { - notifiedAt = c.UpdateAt - } - } - } - - // update the last notified_at for all subscribers since we at least attempted to notify all of them. - err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt) - if err != nil { - merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err)) - } - - return merr.ErrorOrNil() -} diff --git a/server/boards/services/notify/notifysubscriptions/subscriptions_backend.go b/server/boards/services/notify/notifysubscriptions/subscriptions_backend.go deleted file mode 100644 index 6108618aa1..0000000000 --- a/server/boards/services/notify/notifysubscriptions/subscriptions_backend.go +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "fmt" - "os" - "strconv" - "time" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - backendName = "notifySubscriptions" -) - -type BackendParams struct { - ServerRoot string - AppAPI AppAPI - Permissions permissions.PermissionsService - Delivery SubscriptionDelivery - Logger mlog.LoggerIFace - NotifyFreqCardSeconds int - NotifyFreqBoardSeconds int -} - -// Backend provides the notification backend for subscriptions. -type Backend struct { - appAPI AppAPI - permissions permissions.PermissionsService - delivery SubscriptionDelivery - notifier *notifier - logger mlog.LoggerIFace - notifyFreqCardSeconds int - notifyFreqBoardSeconds int -} - -func New(params BackendParams) *Backend { - return &Backend{ - appAPI: params.AppAPI, - delivery: params.Delivery, - permissions: params.Permissions, - notifier: newNotifier(params), - logger: params.Logger, - notifyFreqCardSeconds: params.NotifyFreqCardSeconds, - notifyFreqBoardSeconds: params.NotifyFreqBoardSeconds, - } -} - -func (b *Backend) Start() error { - b.logger.Debug("Starting subscriptions backend", - mlog.Int("freq_card", b.notifyFreqCardSeconds), - mlog.Int("freq_board", b.notifyFreqBoardSeconds), - ) - b.notifier.start() - return nil -} - -func (b *Backend) ShutDown() error { - b.logger.Debug("Stopping subscriptions backend") - b.notifier.stop() - _ = b.logger.Flush() - return nil -} - -func (b *Backend) Name() string { - return backendName -} - -func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration { - // check for env variable override - sFreq := os.Getenv("MM_BOARDS_NOTIFY_FREQ_SECONDS") - if sFreq != "" && sFreq != "0" { - if freq, err := strconv.ParseInt(sFreq, 10, 64); err != nil { - b.logger.Error("Environment variable MM_BOARDS_NOTIFY_FREQ_SECONDS invalid (ignoring)", mlog.Err(err)) - } else { - return time.Second * time.Duration(freq) - } - } - - switch blockType { - case model.TypeCard: - return time.Second * time.Duration(b.notifyFreqCardSeconds) - default: - return defBlockNotificationFreq - } -} - -func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { - if evt.Board == nil { - b.logger.Warn("No board found for block, skipping notify", - mlog.String("block_id", evt.BlockChanged.ID), - ) - return nil - } - - merr := merror.New() - var err error - - // if new card added, automatically subscribe the author. - if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard { - sub := &model.Subscription{ - BlockType: model.TypeCard, - BlockID: evt.BlockChanged.ID, - SubscriberType: model.SubTypeUser, - SubscriberID: evt.ModifiedBy.UserID, - } - - if _, err = b.appAPI.CreateSubscription(sub); err != nil { - b.logger.Warn("Cannot subscribe card author to card", - mlog.String("card_id", evt.BlockChanged.ID), - mlog.Err(err), - ) - } - } - - // notify board subscribers - subs, err := b.appAPI.GetSubscribersForBlock(evt.Board.ID) - if err != nil { - merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err)) - } - if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedBy.UserID); err != nil { - merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err)) - } - - if evt.Card == nil { - return merr.ErrorOrNil() - } - - // notify card subscribers - subs, err = b.appAPI.GetSubscribersForBlock(evt.Card.ID) - if err != nil { - merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err)) - } - if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedBy.UserID); err != nil { - merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err)) - } - - // notify block subscribers (if/when other types can be subscribed to) - if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID { - subs, err := b.appAPI.GetSubscribersForBlock(evt.BlockChanged.ID) - if err != nil { - merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err)) - } - if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedBy.UserID); err != nil { - merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err)) - } - } - return merr.ErrorOrNil() -} - -// notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database. -func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error { - if len(subs) == 0 { - return nil - } - - hint := &model.NotificationHint{ - BlockType: idType, - BlockID: blockID, - ModifiedByID: modifiedByID, - } - - hint, err := b.appAPI.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType)) - if err != nil { - return fmt.Errorf("cannot upsert notification hint: %w", err) - } - if err := b.notifier.onNotifyHint(hint); err != nil { - return err - } - - return nil -} - -// OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification -// is sent. Here we create a subscription for the mentioned user to the card. -func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) { - if evt.Card == nil { - b.logger.Debug("Cannot subscribe mentioned user to nil card", - mlog.String("user_id", userID), - mlog.String("block_id", evt.BlockChanged.ID), - ) - return - } - - // user mentioned must be a board member to subscribe to card. - if !b.permissions.HasPermissionToBoard(userID, evt.Board.ID, model.PermissionViewBoard) { - b.logger.Debug("Not subscribing mentioned non-board member to card", - mlog.String("user_id", userID), - mlog.String("block_id", evt.BlockChanged.ID), - ) - return - } - - sub := &model.Subscription{ - BlockType: model.TypeCard, - BlockID: evt.Card.ID, - SubscriberType: model.SubTypeUser, - SubscriberID: userID, - } - - var err error - if _, err = b.appAPI.CreateSubscription(sub); err != nil { - b.logger.Warn("Cannot subscribe mentioned user to card", - mlog.String("user_id", userID), - mlog.String("card_id", evt.Card.ID), - mlog.Err(err), - ) - return - } - - b.logger.Debug("Subscribed mentioned user to card", - mlog.String("user_id", userID), - mlog.String("card_id", evt.Card.ID), - ) -} diff --git a/server/boards/services/notify/notifysubscriptions/util.go b/server/boards/services/notify/notifysubscriptions/util.go deleted file mode 100644 index e808009c9f..0000000000 --- a/server/boards/services/notify/notifysubscriptions/util.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notifysubscriptions - -import ( - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func getBoardDescription(board *model.Block) string { - if board == nil { - return "" - } - - descr, ok := board.Fields["description"] - if !ok { - return "" - } - - description, ok := descr.(string) - if !ok { - return "" - } - - return description -} - -func stripNewlines(s string) string { - return strings.TrimSpace(strings.ReplaceAll(s, "\n", "¶ ")) -} - -type StringMap map[string]string - -func (sm StringMap) Add(k string, v string) { - sm[k] = v -} - -func (sm StringMap) Append(m StringMap) { - for k, v := range m { - sm[k] = v - } -} - -func (sm StringMap) Keys() []string { - keys := make([]string, 0, len(sm)) - for k := range sm { - keys = append(keys, k) - } - return keys -} - -func (sm StringMap) Values() []string { - values := make([]string, 0, len(sm)) - for _, v := range sm { - values = append(values, v) - } - return values -} diff --git a/server/boards/services/notify/plugindelivery/mention_deliver.go b/server/boards/services/notify/plugindelivery/mention_deliver.go deleted file mode 100644 index 44f073cf39..0000000000 --- a/server/boards/services/notify/plugindelivery/mention_deliver.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/services/notify" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -// MentionDeliver notifies a user they have been mentioned in a blockv ia the plugin API. -func (pd *PluginDelivery) MentionDeliver(mentionedUser *mm_model.User, extract string, evt notify.BlockChangeEvent) (string, error) { - author, err := pd.api.GetUserByID(evt.ModifiedBy.UserID) - if err != nil { - return "", fmt.Errorf("cannot find user: %w", err) - } - - channel, err := pd.getDirectChannel(evt.TeamID, mentionedUser.Id, pd.botID) - if err != nil { - return "", fmt.Errorf("cannot get direct channel: %w", err) - } - link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID) - boardLink := utils.MakeBoardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID) - - post := &mm_model.Post{ - UserId: pd.botID, - ChannelId: channel.Id, - Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged, boardLink, evt.Board.Title), - } - - if _, err := pd.api.CreatePost(post); err != nil { - return "", err - } - - return mentionedUser.Id, nil -} diff --git a/server/boards/services/notify/plugindelivery/message.go b/server/boards/services/notify/plugindelivery/message.go deleted file mode 100644 index ab96233db0..0000000000 --- a/server/boards/services/notify/plugindelivery/message.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -const ( - // TODO: localize these when i18n is available. - defCommentTemplate = "@%s mentioned you in a comment on the card [%s](%s) in board [%s](%s)\n> %s" - defDescriptionTemplate = "@%s mentioned you in the card [%s](%s) in board [%s](%s)\n> %s" -) - -func formatMessage(author string, extract string, card string, link string, block *model.Block, boardLink string, board string) string { - template := defDescriptionTemplate - if block.Type == model.TypeComment { - template = defCommentTemplate - } - return fmt.Sprintf(template, author, card, link, board, boardLink, extract) -} diff --git a/server/boards/services/notify/plugindelivery/plugin_delivery.go b/server/boards/services/notify/plugindelivery/plugin_delivery.go deleted file mode 100644 index b3538b83b9..0000000000 --- a/server/boards/services/notify/plugindelivery/plugin_delivery.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -type servicesAPI interface { - // GetDirectChannelOrCreate gets a direct message channel, - // or creates one if it does not already exist - GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) - - // CreatePost creates a post. - CreatePost(post *mm_model.Post) (*mm_model.Post, error) - - // GetUserByID gets a user by their ID. - GetUserByID(userID string) (*mm_model.User, error) - - // GetUserByUsername gets a user by their username. - GetUserByUsername(name string) (*mm_model.User, error) - - // GetTeamMember gets a team member by their user id. - GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) - - // GetChannelByID gets a Channel by its ID. - GetChannelByID(channelID string) (*mm_model.Channel, error) - - // GetChannelMember gets a channel member by userID. - GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) - - // CreateMember adds a user to the specified team. Safe to call if the user is - // already a member of the team. - CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) -} - -// PluginDelivery provides ability to send notifications to direct message channels via Mattermost plugin API. -type PluginDelivery struct { - botID string - serverRoot string - api servicesAPI -} - -// New creates a PluginDelivery instance. -func New(botID string, serverRoot string, api servicesAPI) *PluginDelivery { - return &PluginDelivery{ - botID: botID, - serverRoot: serverRoot, - api: api, - } -} diff --git a/server/boards/services/notify/plugindelivery/subscription_deliver.go b/server/boards/services/notify/plugindelivery/subscription_deliver.go deleted file mode 100644 index bc7df9e4d1..0000000000 --- a/server/boards/services/notify/plugindelivery/subscription_deliver.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -var ( - ErrUnsupportedSubscriberType = errors.New("invalid subscriber type") -) - -// SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to. -func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(teamID string, subscriberID string, subscriptionType model.SubscriberType, - attachments []*mm_model.SlackAttachment) error { - // check subscriber is member of channel - _, err := pd.api.GetUserByID(subscriberID) - if err != nil { - if model.IsErrNotFound(err) { - // subscriber is not a member of the channel; fail silently. - return nil - } - return fmt.Errorf("cannot fetch channel member for user %s: %w", subscriberID, err) - } - - channelID, err := pd.getDirectChannelID(teamID, subscriberID, subscriptionType, pd.botID) - if err != nil { - return err - } - - post := &mm_model.Post{ - UserId: pd.botID, - ChannelId: channelID, - } - - mm_model.ParseSlackAttachment(post, attachments) - - _, err = pd.api.CreatePost(post) - return err -} - -func (pd *PluginDelivery) getDirectChannelID(teamID string, subscriberID string, subscriberType model.SubscriberType, botID string) (string, error) { - switch subscriberType { - case model.SubTypeUser: - user, err := pd.api.GetUserByID(subscriberID) - if err != nil { - return "", fmt.Errorf("cannot find user: %w", err) - } - channel, err := pd.getDirectChannel(teamID, user.Id, botID) - if err != nil || channel == nil { - return "", fmt.Errorf("cannot get direct channel: %w", err) - } - return channel.Id, nil - case model.SubTypeChannel: - return subscriberID, nil - default: - return "", ErrUnsupportedSubscriberType - } -} - -func (pd *PluginDelivery) getDirectChannel(teamID string, userID string, botID string) (*mm_model.Channel, error) { - // first ensure the bot is a member of the team. - _, err := pd.api.CreateMember(teamID, botID) - if err != nil { - return nil, fmt.Errorf("cannot add bot to team %s: %w", teamID, err) - } - return pd.api.GetDirectChannelOrCreate(userID, botID) -} diff --git a/server/boards/services/notify/plugindelivery/user.go b/server/boards/services/notify/plugindelivery/user.go deleted file mode 100644 index 0052dde595..0000000000 --- a/server/boards/services/notify/plugindelivery/user.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -const ( - usernameSpecialChars = ".-_ " -) - -func (pd *PluginDelivery) UserByUsername(username string) (*mm_model.User, error) { - // check for usernames that might have trailing punctuation - var user *mm_model.User - var err error - ok := true - trimmed := username - for ok { - user, err = pd.api.GetUserByUsername(trimmed) - if err != nil && !model.IsErrNotFound(err) { - return nil, err - } - - if err == nil { - break - } - - trimmed, ok = trimUsernameSpecialChar(trimmed) - } - - if user == nil { - return nil, err - } - - return user, nil -} - -// trimUsernameSpecialChar tries to remove the last character from word if it -// is a special character for usernames (dot, dash or underscore). If not, it -// returns the same string. -func trimUsernameSpecialChar(word string) (string, bool) { - l := len(word) - - if l > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (l-1) { - return word[:l-1], true - } - - return word, false -} diff --git a/server/boards/services/notify/plugindelivery/user_test.go b/server/boards/services/notify/plugindelivery/user_test.go deleted file mode 100644 index c5b19f12c4..0000000000 --- a/server/boards/services/notify/plugindelivery/user_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugindelivery - -import ( - "reflect" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -var ( - defTeamID = mm_model.NewId() - - user1 = &mm_model.User{ - Id: mm_model.NewId(), - Username: "dlauder", - } - user2 = &mm_model.User{ - Id: mm_model.NewId(), - Username: "steve.mqueen", - } - user3 = &mm_model.User{ - Id: mm_model.NewId(), - Username: "bart_", - } - user4 = &mm_model.User{ - Id: mm_model.NewId(), - Username: "missing_", - } - user5 = &mm_model.User{ - Id: mm_model.NewId(), - Username: "wrong_team", - } - - mockUsers = map[string]*mm_model.User{ - "dlauder": user1, - "steve.mqueen": user2, - "bart_": user3, - "wrong_team": user5, - } -) - -func Test_userByUsername(t *testing.T) { - servicesAPI := newServicesAPIMock(mockUsers) - delivery := New("bot_id", "server_root", servicesAPI) - - tests := []struct { - name string - uname string - teamID string - want *mm_model.User - wantErr bool - }{ - {name: "user1", uname: user1.Username, want: user1, wantErr: false}, - {name: "user1 with period", uname: user1.Username + ".", want: user1, wantErr: false}, - {name: "user1 with period plus more", uname: user1.Username + ". ", want: user1, wantErr: false}, - {name: "user2 with periods", uname: user2.Username + "...", want: user2, wantErr: false}, - {name: "user2 with underscore", uname: user2.Username + "_", want: user2, wantErr: false}, - {name: "user2 with hyphen plus more", uname: user2.Username + "- ", want: user2, wantErr: false}, - {name: "user2 with hyphen plus all", uname: user2.Username + ".-_ ", want: user2, wantErr: false}, - {name: "user3 with underscore", uname: user3.Username + "_", want: user3, wantErr: false}, - {name: "user4 missing", uname: user4.Username, want: nil, wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := delivery.UserByUsername(tt.uname) - if (err != nil) != tt.wantErr { - t.Errorf("userByUsername() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("userByUsername()\ngot:\n%v\nwant:\n%v\n", got, tt.want) - } - }) - } -} - -type servicesAPIMock struct { - users map[string]*mm_model.User -} - -func newServicesAPIMock(users map[string]*mm_model.User) servicesAPIMock { - return servicesAPIMock{ - users: users, - } -} - -func (m servicesAPIMock) GetUserByUsername(name string) (*mm_model.User, error) { - user, ok := m.users[name] - if !ok { - return nil, model.NewErrNotFound(name) - } - return user, nil -} - -func (m servicesAPIMock) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) { - return nil, nil -} - -func (m servicesAPIMock) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) { - return nil, nil -} - -func (m servicesAPIMock) CreatePost(post *mm_model.Post) (*mm_model.Post, error) { - return post, nil -} - -func (m servicesAPIMock) GetUserByID(userID string) (*mm_model.User, error) { - for _, user := range m.users { - if user.Id == userID { - return user, nil - } - } - return nil, model.NewErrNotFound(userID) -} - -func (m servicesAPIMock) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) { - user, err := m.GetUserByID(userID) - if err != nil { - return nil, err - } - - if teamID != defTeamID { - return nil, model.NewErrNotFound(teamID) - } - - member := &mm_model.TeamMember{ - UserId: user.Id, - TeamId: teamID, - } - return member, nil -} - -func (m servicesAPIMock) GetChannelByID(channelID string) (*mm_model.Channel, error) { - return nil, model.NewErrNotFound(channelID) -} - -func (m servicesAPIMock) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) { - return nil, model.NewErrNotFound(userID) -} - -func (m servicesAPIMock) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) { - member := &mm_model.TeamMember{ - UserId: userID, - TeamId: teamID, - } - return member, nil -} diff --git a/server/boards/services/notify/service.go b/server/boards/services/notify/service.go deleted file mode 100644 index 5f9533ee71..0000000000 --- a/server/boards/services/notify/service.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package notify - -import ( - "sync" - - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type Action string - -const ( - Add Action = "add" - Update Action = "update" - Delete Action = "delete" -) - -type BlockChangeEvent struct { - Action Action - TeamID string - Board *model.Board - Card *model.Block - BlockChanged *model.Block - BlockOld *model.Block - ModifiedBy *model.BoardMember -} - -// Backend provides an interface for sending notifications. -type Backend interface { - Start() error - ShutDown() error - BlockChanged(evt BlockChangeEvent) error - Name() string -} - -// Service is a service that sends notifications based on block activity using one or more backends. -type Service struct { - mux sync.RWMutex - backends []Backend - logger mlog.LoggerIFace -} - -// New creates a notification service with one or more Backends capable of sending notifications. -func New(logger mlog.LoggerIFace, backends ...Backend) (*Service, error) { - notify := &Service{ - backends: make([]Backend, 0, len(backends)), - logger: logger, - } - - merr := merror.New() - for _, backend := range backends { - if err := notify.AddBackend(backend); err != nil { - merr.Append(err) - } else { - logger.Info("Initialized notification backend", mlog.String("name", backend.Name())) - } - } - return notify, merr.ErrorOrNil() -} - -// AddBackend adds a backend to the list that will be informed of any block changes. -func (s *Service) AddBackend(backend Backend) error { - if err := backend.Start(); err != nil { - return err - } - s.mux.Lock() - defer s.mux.Unlock() - s.backends = append(s.backends, backend) - return nil -} - -// Shutdown calls shutdown for all backends. -func (s *Service) Shutdown() error { - s.mux.Lock() - defer s.mux.Unlock() - - merr := merror.New() - for _, backend := range s.backends { - if err := backend.ShutDown(); err != nil { - merr.Append(err) - } - } - s.backends = nil - return merr.ErrorOrNil() -} - -// BlockChanged should be called whenever a block is added/updated/deleted. -// All backends are informed of the event. -func (s *Service) BlockChanged(evt BlockChangeEvent) { - s.mux.RLock() - defer s.mux.RUnlock() - - for _, backend := range s.backends { - if err := backend.BlockChanged(evt); err != nil { - s.logger.Error("Error delivering notification", - mlog.String("backend", backend.Name()), - mlog.String("action", string(evt.Action)), - mlog.String("block_id", evt.BlockChanged.ID), - mlog.Err(err), - ) - } - } -} diff --git a/server/boards/services/permissions/localpermissions/helpers_test.go b/server/boards/services/permissions/localpermissions/helpers_test.go deleted file mode 100644 index 4ac1fe9e48..0000000000 --- a/server/boards/services/permissions/localpermissions/helpers_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package localpermissions - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - permissionsMocks "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mocks" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -type TestHelper struct { - t *testing.T - ctrl *gomock.Controller - store *permissionsMocks.MockStore - permissions *Service -} - -func SetupTestHelper(t *testing.T) *TestHelper { - ctrl := gomock.NewController(t) - mockStore := permissionsMocks.NewMockStore(ctrl) - return &TestHelper{ - t: t, - ctrl: ctrl, - store: mockStore, - permissions: New(mockStore, mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)), - } -} - -func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, hasPermissionTo, hasNotPermissionTo []*mm_model.Permission) { - for _, p := range hasPermissionTo { - th.t.Run(roleName+" "+p.Id, func(t *testing.T) { - th.store.EXPECT(). - GetMemberForBoard(member.BoardID, member.UserID). - Return(member, nil). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) - assert.True(t, hasPermission) - }) - } - - for _, p := range hasNotPermissionTo { - th.t.Run(roleName+" "+p.Id, func(t *testing.T) { - th.store.EXPECT(). - GetMemberForBoard(member.BoardID, member.UserID). - Return(member, nil). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) - assert.False(t, hasPermission) - }) - } -} diff --git a/server/boards/services/permissions/localpermissions/localpermissions.go b/server/boards/services/permissions/localpermissions/localpermissions.go deleted file mode 100644 index ab0d35e98a..0000000000 --- a/server/boards/services/permissions/localpermissions/localpermissions.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package localpermissions - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type Service struct { - store permissions.Store - logger mlog.LoggerIFace -} - -func New(store permissions.Store, logger mlog.LoggerIFace) *Service { - return &Service{ - store: store, - logger: logger, - } -} - -func (s *Service) HasPermissionTo(userID string, permission *mm_model.Permission) bool { - return false -} - -func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool { - if userID == "" || teamID == "" || permission == nil { - return false - } - if permission.Id == model.PermissionManageTeam.Id { - return false - } - return true -} - -func (s *Service) HasPermissionToChannel(userID, channelID string, permission *mm_model.Permission) bool { - if userID == "" || channelID == "" || permission == nil { - return false - } - return true -} - -func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { - if userID == "" || boardID == "" || permission == nil { - return false - } - - member, err := s.store.GetMemberForBoard(boardID, userID) - if model.IsErrNotFound(err) { - return false - } - if err != nil { - s.logger.Error("error getting member for board", - mlog.String("boardID", boardID), - mlog.String("userID", userID), - mlog.Err(err), - ) - return false - } - - switch member.MinimumRole { - case "admin": - member.SchemeAdmin = true - case "editor": - member.SchemeEditor = true - case "commenter": - member.SchemeCommenter = true - case "viewer": - member.SchemeViewer = true - } - - switch permission { - case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments: - return member.SchemeAdmin - case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: - return member.SchemeAdmin || member.SchemeEditor - case model.PermissionCommentBoardCards: - return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter - case model.PermissionViewBoard: - return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer - default: - return false - } -} diff --git a/server/boards/services/permissions/localpermissions/localpermissions_test.go b/server/boards/services/permissions/localpermissions/localpermissions_test.go deleted file mode 100644 index 82b5549c5a..0000000000 --- a/server/boards/services/permissions/localpermissions/localpermissions_test.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package localpermissions - -import ( - "database/sql" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/stretchr/testify/assert" -) - -func TestHasPermissionToTeam(t *testing.T) { - th := SetupTestHelper(t) - - t.Run("empty input should always unauthorize", func(t *testing.T) { - assert.False(t, th.permissions.HasPermissionToTeam("", "team-id", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToTeam("user-id", "", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToTeam("user-id", "team-id", nil)) - }) - - t.Run("all users have all permissions on teams", func(t *testing.T) { - hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards) - assert.True(t, hasPermission) - }) - - t.Run("no users have PermissionManageTeam on teams", func(t *testing.T) { - hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageTeam) - assert.False(t, hasPermission) - }) -} - -func TestHasPermissionToBoard(t *testing.T) { - th := SetupTestHelper(t) - - t.Run("empty input should always unauthorize", func(t *testing.T) { - assert.False(t, th.permissions.HasPermissionToBoard("", "board-id", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToBoard("user-id", "", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToBoard("user-id", "board-id", nil)) - }) - - t.Run("nonexistent user", func(t *testing.T) { - userID := "user-id" - boardID := "board-id" - - th.store.EXPECT(). - GetMemberForBoard(boardID, userID). - Return(nil, sql.ErrNoRows). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) - assert.False(t, hasPermission) - }) - - t.Run("board admin", func(t *testing.T) { - member := &model.BoardMember{ - UserID: "user-id", - BoardID: "board-id", - SchemeAdmin: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionViewBoard, - model.PermissionManageBoardProperties, - } - - hasNotPermissionTo := []*mm_model.Permission{} - - th.checkBoardPermissions("admin", member, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board editor", func(t *testing.T) { - member := &model.BoardMember{ - UserID: "user-id", - BoardID: "board-id", - SchemeEditor: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardCards, - model.PermissionViewBoard, - model.PermissionManageBoardProperties, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - } - - th.checkBoardPermissions("editor", member, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board commenter", func(t *testing.T) { - member := &model.BoardMember{ - UserID: "user-id", - BoardID: "board-id", - SchemeCommenter: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionViewBoard, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionManageBoardProperties, - } - - th.checkBoardPermissions("commenter", member, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board viewer", func(t *testing.T) { - member := &model.BoardMember{ - UserID: "user-id", - BoardID: "board-id", - SchemeViewer: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionViewBoard, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionManageBoardProperties, - } - - th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("Manage Team Permission ", func(t *testing.T) { - member := &model.BoardMember{ - UserID: "user-id", - BoardID: "board-id", - SchemeViewer: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionViewBoard, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionManageBoardProperties, - } - - th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) - }) -} diff --git a/server/boards/services/permissions/mmpermissions/helpers_test.go b/server/boards/services/permissions/mmpermissions/helpers_test.go deleted file mode 100644 index c378334993..0000000000 --- a/server/boards/services/permissions/mmpermissions/helpers_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package mmpermissions - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - mmpermissionsMocks "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mmpermissions/mocks" - permissionsMocks "github.com/mattermost/mattermost/server/v8/boards/services/permissions/mocks" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -type TestHelper struct { - t *testing.T - ctrl *gomock.Controller - store *permissionsMocks.MockStore - api *mmpermissionsMocks.MockAPI - permissions *Service -} - -func SetupTestHelper(t *testing.T) *TestHelper { - ctrl := gomock.NewController(t) - mockStore := permissionsMocks.NewMockStore(ctrl) - mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) - - return &TestHelper{ - t: t, - ctrl: ctrl, - store: mockStore, - api: mockAPI, - permissions: New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError)), - } -} - -func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, teamID string, - hasPermissionTo, hasNotPermissionTo []*mm_model.Permission) { - for _, p := range hasPermissionTo { - th.t.Run(roleName+" "+p.Id, func(t *testing.T) { - th.store.EXPECT(). - GetBoard(member.BoardID). - Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). - Times(1) - - th.api.EXPECT(). - HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). - Return(true). - Times(1) - - th.store.EXPECT(). - GetMemberForBoard(member.BoardID, member.UserID). - Return(member, nil). - Times(1) - - if !member.SchemeAdmin { - th.api.EXPECT(). - HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). - Return(roleName == "elevated-admin"). - Times(1) - } - - hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) - assert.True(t, hasPermission) - }) - } - - for _, p := range hasNotPermissionTo { - th.t.Run(roleName+" "+p.Id, func(t *testing.T) { - th.store.EXPECT(). - GetBoard(member.BoardID). - Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). - Times(1) - - th.api.EXPECT(). - HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). - Return(true). - Times(1) - - th.store.EXPECT(). - GetMemberForBoard(member.BoardID, member.UserID). - Return(member, nil). - Times(1) - - if !member.SchemeAdmin { - th.api.EXPECT(). - HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam). - Return(roleName == "elevated-admin"). - Times(1) - } - - hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) - assert.False(t, hasPermission) - }) - } -} diff --git a/server/boards/services/permissions/mmpermissions/mmpermissions.go b/server/boards/services/permissions/mmpermissions/mmpermissions.go deleted file mode 100644 index 324e884bbb..0000000000 --- a/server/boards/services/permissions/mmpermissions/mmpermissions.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package mmpermissions - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/permissions" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type APIInterface interface { - HasPermissionTo(userID string, permission *mm_model.Permission) bool - HasPermissionToTeam(userID string, teamID string, permission *mm_model.Permission) bool - HasPermissionToChannel(userID string, channelID string, permission *mm_model.Permission) bool -} - -type Service struct { - store permissions.Store - api APIInterface - logger mlog.LoggerIFace -} - -func New(store permissions.Store, api APIInterface, logger mlog.LoggerIFace) *Service { - return &Service{ - store: store, - api: api, - logger: logger, - } -} - -func (s *Service) HasPermissionTo(userID string, permission *mm_model.Permission) bool { - if userID == "" || permission == nil { - return false - } - return s.api.HasPermissionTo(userID, permission) -} - -func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool { - if userID == "" || teamID == "" || permission == nil { - return false - } - return s.api.HasPermissionToTeam(userID, teamID, permission) -} - -func (s *Service) HasPermissionToChannel(userID, channelID string, permission *mm_model.Permission) bool { - if userID == "" || channelID == "" || permission == nil { - return false - } - return s.api.HasPermissionToChannel(userID, channelID, permission) -} - -func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool { - if userID == "" || boardID == "" || permission == nil { - return false - } - - board, err := s.store.GetBoard(boardID) - if model.IsErrNotFound(err) { - var boards []*model.Board - boards, err = s.store.GetBoardHistory(boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true}) - if err != nil { - return false - } - if len(boards) == 0 { - return false - } - board = boards[0] - } else if err != nil { - s.logger.Error("error getting board", - mlog.String("boardID", boardID), - mlog.String("userID", userID), - mlog.Err(err), - ) - return false - } - - // we need to check that the user has permission to see the team - // regardless of its local permissions to the board - if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) { - return false - } - member, err := s.store.GetMemberForBoard(boardID, userID) - if model.IsErrNotFound(err) { - return false - } - if err != nil { - s.logger.Error("error getting member for board", - mlog.String("boardID", boardID), - mlog.String("userID", userID), - mlog.Err(err), - ) - return false - } - - switch member.MinimumRole { - case "admin": - member.SchemeAdmin = true - case "editor": - member.SchemeEditor = true - case "commenter": - member.SchemeCommenter = true - case "viewer": - member.SchemeViewer = true - } - - // Admins become member of boards, but get minimal role - // if they are a System/Team Admin (model.PermissionManageTeam) - // elevate their permissions - if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) { - return true - } - - switch permission { - case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments: - return member.SchemeAdmin - case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: - return member.SchemeAdmin || member.SchemeEditor - case model.PermissionCommentBoardCards: - return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter - case model.PermissionViewBoard: - return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer - default: - return false - } -} diff --git a/server/boards/services/permissions/mmpermissions/mmpermissions_test.go b/server/boards/services/permissions/mmpermissions/mmpermissions_test.go deleted file mode 100644 index 1cb68b08c5..0000000000 --- a/server/boards/services/permissions/mmpermissions/mmpermissions_test.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../../../copyright.txt -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost/server/public/plugin API -package mmpermissions - -import ( - "database/sql" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/stretchr/testify/assert" -) - -const ( - testTeamID = "team-id" - testBoardID = "board-id" - testUserID = "user-id" -) - -func TestHasPermissionsToTeam(t *testing.T) { - th := SetupTestHelper(t) - - t.Run("empty input should always unauthorize", func(t *testing.T) { - assert.False(t, th.permissions.HasPermissionToTeam("", testTeamID, model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToTeam(testUserID, "", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToTeam(testUserID, testTeamID, nil)) - }) - - t.Run("should authorize if the plugin API does", func(t *testing.T) { - userID := testUserID - teamID := testTeamID - - th.api.EXPECT(). - HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). - Return(true). - Times(1) - - hasPermission := th.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) - assert.True(t, hasPermission) - }) - - t.Run("should not authorize if the plugin API doesn't", func(t *testing.T) { - userID := testUserID - teamID := testTeamID - - th.api.EXPECT(). - HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). - Return(false). - Times(1) - - hasPermission := th.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) - assert.False(t, hasPermission) - }) -} - -// test case for user removed. -func TestHasPermissionToBoard(t *testing.T) { - th := SetupTestHelper(t) - - t.Run("empty input should always unauthorize", func(t *testing.T) { - assert.False(t, th.permissions.HasPermissionToBoard("", testBoardID, model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToBoard(testUserID, "", model.PermissionManageBoardCards)) - assert.False(t, th.permissions.HasPermissionToBoard(testUserID, testBoardID, nil)) - }) - - userID := testUserID - boardID := testBoardID - teamID := testTeamID - - t.Run("nonexistent member", func(t *testing.T) { - th.store.EXPECT(). - GetBoard(boardID). - Return(&model.Board{ID: boardID, TeamID: teamID}, nil). - Times(1) - - th.api.EXPECT(). - HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). - Return(true). - Times(1) - - th.store.EXPECT(). - GetMemberForBoard(boardID, userID). - Return(nil, sql.ErrNoRows). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) - assert.False(t, hasPermission) - }) - - t.Run("nonexistent board", func(t *testing.T) { - th.store.EXPECT(). - GetBoard(boardID). - Return(nil, sql.ErrNoRows). - Times(1) - - th.store.EXPECT(). - GetBoardHistory(boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true}). - Return(nil, sql.ErrNoRows). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) - assert.False(t, hasPermission) - }) - - t.Run("user that has been removed from the team", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: true, - } - - th.store.EXPECT(). - GetBoard(boardID). - Return(&model.Board{ID: boardID, TeamID: teamID}, nil). - Times(1) - - th.api.EXPECT(). - HasPermissionToTeam(userID, teamID, model.PermissionViewTeam). - Return(true). - Times(1) - - th.store.EXPECT(). - GetMemberForBoard(member.BoardID, member.UserID). - Return(member, nil). - Times(1) - - hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, model.PermissionViewBoard) - assert.True(t, hasPermission) - }) - - t.Run("board admin", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionViewBoard, - model.PermissionManageBoardProperties, - } - - hasNotPermissionTo := []*mm_model.Permission{} - - th.checkBoardPermissions("admin", member, teamID, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board editor", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeEditor: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardCards, - model.PermissionViewBoard, - model.PermissionManageBoardProperties, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - } - - th.checkBoardPermissions("editor", member, teamID, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board commenter", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeCommenter: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionViewBoard, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionManageBoardProperties, - } - - th.checkBoardPermissions("commenter", member, teamID, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("board viewer", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeViewer: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionViewBoard, - } - - hasNotPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionManageBoardProperties, - } - - th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo) - }) - - t.Run("elevate board viewer permissions", func(t *testing.T) { - member := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeViewer: true, - } - - hasPermissionTo := []*mm_model.Permission{ - model.PermissionManageBoardType, - model.PermissionDeleteBoard, - model.PermissionManageBoardRoles, - model.PermissionShareBoard, - model.PermissionManageBoardCards, - model.PermissionViewBoard, - model.PermissionManageBoardProperties, - } - - hasNotPermissionTo := []*mm_model.Permission{} - th.checkBoardPermissions("elevated-admin", member, teamID, hasPermissionTo, hasNotPermissionTo) - }) -} diff --git a/server/boards/services/permissions/mmpermissions/mocks/mockpluginapi.go b/server/boards/services/permissions/mmpermissions/mocks/mockpluginapi.go deleted file mode 100644 index f3cfc95df8..0000000000 --- a/server/boards/services/permissions/mmpermissions/mocks/mockpluginapi.go +++ /dev/null @@ -1,2659 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/public/plugin (interfaces: API) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - io "io" - http "net/http" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/public/model" -) - -// MockAPI is a mock of API interface. -type MockAPI struct { - ctrl *gomock.Controller - recorder *MockAPIMockRecorder -} - -// MockAPIMockRecorder is the mock recorder for MockAPI. -type MockAPIMockRecorder struct { - mock *MockAPI -} - -// NewMockAPI creates a new mock instance. -func NewMockAPI(ctrl *gomock.Controller) *MockAPI { - mock := &MockAPI{ctrl: ctrl} - mock.recorder = &MockAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAPI) EXPECT() *MockAPIMockRecorder { - return m.recorder -} - -// AddChannelMember mocks base method. -func (m *MockAPI) AddChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddChannelMember indicates an expected call of AddChannelMember. -func (mr *MockAPIMockRecorder) AddChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddChannelMember", reflect.TypeOf((*MockAPI)(nil).AddChannelMember), arg0, arg1) -} - -// AddReaction mocks base method. -func (m *MockAPI) AddReaction(arg0 *model.Reaction) (*model.Reaction, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddReaction", arg0) - ret0, _ := ret[0].(*model.Reaction) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddReaction indicates an expected call of AddReaction. -func (mr *MockAPIMockRecorder) AddReaction(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddReaction", reflect.TypeOf((*MockAPI)(nil).AddReaction), arg0) -} - -// AddUserToChannel mocks base method. -func (m *MockAPI) AddUserToChannel(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddUserToChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddUserToChannel indicates an expected call of AddUserToChannel. -func (mr *MockAPIMockRecorder) AddUserToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUserToChannel", reflect.TypeOf((*MockAPI)(nil).AddUserToChannel), arg0, arg1, arg2) -} - -// CopyFileInfos mocks base method. -func (m *MockAPI) CopyFileInfos(arg0 string, arg1 []string) ([]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CopyFileInfos", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CopyFileInfos indicates an expected call of CopyFileInfos. -func (mr *MockAPIMockRecorder) CopyFileInfos(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFileInfos", reflect.TypeOf((*MockAPI)(nil).CopyFileInfos), arg0, arg1) -} - -// CreateBot mocks base method. -func (m *MockAPI) CreateBot(arg0 *model.Bot) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBot", arg0) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateBot indicates an expected call of CreateBot. -func (mr *MockAPIMockRecorder) CreateBot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBot", reflect.TypeOf((*MockAPI)(nil).CreateBot), arg0) -} - -// CreateChannel mocks base method. -func (m *MockAPI) CreateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateChannel indicates an expected call of CreateChannel. -func (mr *MockAPIMockRecorder) CreateChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannel", reflect.TypeOf((*MockAPI)(nil).CreateChannel), arg0) -} - -// CreateChannelSidebarCategory mocks base method. -func (m *MockAPI) CreateChannelSidebarCategory(arg0, arg1 string, arg2 *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateChannelSidebarCategory", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.SidebarCategoryWithChannels) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateChannelSidebarCategory indicates an expected call of CreateChannelSidebarCategory. -func (mr *MockAPIMockRecorder) CreateChannelSidebarCategory(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannelSidebarCategory", reflect.TypeOf((*MockAPI)(nil).CreateChannelSidebarCategory), arg0, arg1, arg2) -} - -// CreateCommand mocks base method. -func (m *MockAPI) CreateCommand(arg0 *model.Command) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCommand", arg0) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateCommand indicates an expected call of CreateCommand. -func (mr *MockAPIMockRecorder) CreateCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCommand", reflect.TypeOf((*MockAPI)(nil).CreateCommand), arg0) -} - -// CreateOAuthApp mocks base method. -func (m *MockAPI) CreateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateOAuthApp indicates an expected call of CreateOAuthApp. -func (mr *MockAPIMockRecorder) CreateOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOAuthApp", reflect.TypeOf((*MockAPI)(nil).CreateOAuthApp), arg0) -} - -// CreatePost mocks base method. -func (m *MockAPI) CreatePost(arg0 *model.Post) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreatePost indicates an expected call of CreatePost. -func (mr *MockAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockAPI)(nil).CreatePost), arg0) -} - -// CreateSession mocks base method. -func (m *MockAPI) CreateSession(arg0 *model.Session) (*model.Session, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSession", arg0) - ret0, _ := ret[0].(*model.Session) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateSession indicates an expected call of CreateSession. -func (mr *MockAPIMockRecorder) CreateSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockAPI)(nil).CreateSession), arg0) -} - -// CreateTeam mocks base method. -func (m *MockAPI) CreateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeam indicates an expected call of CreateTeam. -func (mr *MockAPIMockRecorder) CreateTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeam", reflect.TypeOf((*MockAPI)(nil).CreateTeam), arg0) -} - -// CreateTeamMember mocks base method. -func (m *MockAPI) CreateTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMember indicates an expected call of CreateTeamMember. -func (mr *MockAPIMockRecorder) CreateTeamMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMember", reflect.TypeOf((*MockAPI)(nil).CreateTeamMember), arg0, arg1) -} - -// CreateTeamMembers mocks base method. -func (m *MockAPI) CreateTeamMembers(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMembers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMembers indicates an expected call of CreateTeamMembers. -func (mr *MockAPIMockRecorder) CreateTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembers", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembers), arg0, arg1, arg2) -} - -// CreateTeamMembersGracefully mocks base method. -func (m *MockAPI) CreateTeamMembersGracefully(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMemberWithError, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMembersGracefully", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMemberWithError) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMembersGracefully indicates an expected call of CreateTeamMembersGracefully. -func (mr *MockAPIMockRecorder) CreateTeamMembersGracefully(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembersGracefully", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembersGracefully), arg0, arg1, arg2) -} - -// CreateUploadSession mocks base method. -func (m *MockAPI) CreateUploadSession(arg0 *model.UploadSession) (*model.UploadSession, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUploadSession", arg0) - ret0, _ := ret[0].(*model.UploadSession) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateUploadSession indicates an expected call of CreateUploadSession. -func (mr *MockAPIMockRecorder) CreateUploadSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUploadSession", reflect.TypeOf((*MockAPI)(nil).CreateUploadSession), arg0) -} - -// CreateUser mocks base method. -func (m *MockAPI) CreateUser(arg0 *model.User) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateUser indicates an expected call of CreateUser. -func (mr *MockAPIMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAPI)(nil).CreateUser), arg0) -} - -// CreateUserAccessToken mocks base method. -func (m *MockAPI) CreateUserAccessToken(arg0 *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUserAccessToken", arg0) - ret0, _ := ret[0].(*model.UserAccessToken) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateUserAccessToken indicates an expected call of CreateUserAccessToken. -func (mr *MockAPIMockRecorder) CreateUserAccessToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserAccessToken", reflect.TypeOf((*MockAPI)(nil).CreateUserAccessToken), arg0) -} - -// DeleteChannel mocks base method. -func (m *MockAPI) DeleteChannel(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChannel", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteChannel indicates an expected call of DeleteChannel. -func (mr *MockAPIMockRecorder) DeleteChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannel", reflect.TypeOf((*MockAPI)(nil).DeleteChannel), arg0) -} - -// DeleteChannelMember mocks base method. -func (m *MockAPI) DeleteChannelMember(arg0, arg1 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteChannelMember indicates an expected call of DeleteChannelMember. -func (mr *MockAPIMockRecorder) DeleteChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannelMember", reflect.TypeOf((*MockAPI)(nil).DeleteChannelMember), arg0, arg1) -} - -// DeleteCommand mocks base method. -func (m *MockAPI) DeleteCommand(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCommand", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteCommand indicates an expected call of DeleteCommand. -func (mr *MockAPIMockRecorder) DeleteCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCommand", reflect.TypeOf((*MockAPI)(nil).DeleteCommand), arg0) -} - -// DeleteEphemeralPost mocks base method. -func (m *MockAPI) DeleteEphemeralPost(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteEphemeralPost", arg0, arg1) -} - -// DeleteEphemeralPost indicates an expected call of DeleteEphemeralPost. -func (mr *MockAPIMockRecorder) DeleteEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEphemeralPost", reflect.TypeOf((*MockAPI)(nil).DeleteEphemeralPost), arg0, arg1) -} - -// DeleteOAuthApp mocks base method. -func (m *MockAPI) DeleteOAuthApp(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOAuthApp", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteOAuthApp indicates an expected call of DeleteOAuthApp. -func (mr *MockAPIMockRecorder) DeleteOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuthApp", reflect.TypeOf((*MockAPI)(nil).DeleteOAuthApp), arg0) -} - -// DeletePost mocks base method. -func (m *MockAPI) DeletePost(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePost", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeletePost indicates an expected call of DeletePost. -func (mr *MockAPIMockRecorder) DeletePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockAPI)(nil).DeletePost), arg0) -} - -// DeletePreferencesForUser mocks base method. -func (m *MockAPI) DeletePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser. -func (mr *MockAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).DeletePreferencesForUser), arg0, arg1) -} - -// DeleteTeam mocks base method. -func (m *MockAPI) DeleteTeam(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTeam", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteTeam indicates an expected call of DeleteTeam. -func (mr *MockAPIMockRecorder) DeleteTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeam", reflect.TypeOf((*MockAPI)(nil).DeleteTeam), arg0) -} - -// DeleteTeamMember mocks base method. -func (m *MockAPI) DeleteTeamMember(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTeamMember", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteTeamMember indicates an expected call of DeleteTeamMember. -func (mr *MockAPIMockRecorder) DeleteTeamMember(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeamMember", reflect.TypeOf((*MockAPI)(nil).DeleteTeamMember), arg0, arg1, arg2) -} - -// DeleteUser mocks base method. -func (m *MockAPI) DeleteUser(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteUser", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteUser indicates an expected call of DeleteUser. -func (mr *MockAPIMockRecorder) DeleteUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockAPI)(nil).DeleteUser), arg0) -} - -// DisablePlugin mocks base method. -func (m *MockAPI) DisablePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisablePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DisablePlugin indicates an expected call of DisablePlugin. -func (mr *MockAPIMockRecorder) DisablePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisablePlugin", reflect.TypeOf((*MockAPI)(nil).DisablePlugin), arg0) -} - -// EnablePlugin mocks base method. -func (m *MockAPI) EnablePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnablePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// EnablePlugin indicates an expected call of EnablePlugin. -func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnablePlugin", reflect.TypeOf((*MockAPI)(nil).EnablePlugin), arg0) -} - -// EnsureBotUser mocks base method. -func (m *MockAPI) EnsureBotUser(arg0 *model.Bot) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnsureBotUser", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// EnsureBotUser indicates an expected call of EnsureBotUser. -func (mr *MockAPIMockRecorder) EnsureBotUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBotUser", reflect.TypeOf((*MockAPI)(nil).EnsureBotUser), arg0) -} - -// ExecuteSlashCommand mocks base method. -func (m *MockAPI) ExecuteSlashCommand(arg0 *model.CommandArgs) (*model.CommandResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExecuteSlashCommand", arg0) - ret0, _ := ret[0].(*model.CommandResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ExecuteSlashCommand indicates an expected call of ExecuteSlashCommand. -func (mr *MockAPIMockRecorder) ExecuteSlashCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSlashCommand", reflect.TypeOf((*MockAPI)(nil).ExecuteSlashCommand), arg0) -} - -// ExtendSessionExpiry mocks base method. -func (m *MockAPI) ExtendSessionExpiry(arg0 string, arg1 int64) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExtendSessionExpiry", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// ExtendSessionExpiry indicates an expected call of ExtendSessionExpiry. -func (mr *MockAPIMockRecorder) ExtendSessionExpiry(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendSessionExpiry", reflect.TypeOf((*MockAPI)(nil).ExtendSessionExpiry), arg0, arg1) -} - -// GetBot mocks base method. -func (m *MockAPI) GetBot(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBot", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetBot indicates an expected call of GetBot. -func (mr *MockAPIMockRecorder) GetBot(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBot", reflect.TypeOf((*MockAPI)(nil).GetBot), arg0, arg1) -} - -// GetBots mocks base method. -func (m *MockAPI) GetBots(arg0 *model.BotGetOptions) ([]*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBots", arg0) - ret0, _ := ret[0].([]*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetBots indicates an expected call of GetBots. -func (mr *MockAPIMockRecorder) GetBots(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBots", reflect.TypeOf((*MockAPI)(nil).GetBots), arg0) -} - -// GetBundlePath mocks base method. -func (m *MockAPI) GetBundlePath() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBundlePath") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBundlePath indicates an expected call of GetBundlePath. -func (mr *MockAPIMockRecorder) GetBundlePath() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBundlePath", reflect.TypeOf((*MockAPI)(nil).GetBundlePath)) -} - -// GetChannel mocks base method. -func (m *MockAPI) GetChannel(arg0 string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannel indicates an expected call of GetChannel. -func (mr *MockAPIMockRecorder) GetChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannel", reflect.TypeOf((*MockAPI)(nil).GetChannel), arg0) -} - -// GetChannelByName mocks base method. -func (m *MockAPI) GetChannelByName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelByName", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelByName indicates an expected call of GetChannelByName. -func (mr *MockAPIMockRecorder) GetChannelByName(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByName", reflect.TypeOf((*MockAPI)(nil).GetChannelByName), arg0, arg1, arg2) -} - -// GetChannelByNameForTeamName mocks base method. -func (m *MockAPI) GetChannelByNameForTeamName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelByNameForTeamName", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelByNameForTeamName indicates an expected call of GetChannelByNameForTeamName. -func (mr *MockAPIMockRecorder) GetChannelByNameForTeamName(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByNameForTeamName", reflect.TypeOf((*MockAPI)(nil).GetChannelByNameForTeamName), arg0, arg1, arg2) -} - -// GetChannelMember mocks base method. -func (m *MockAPI) GetChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMember indicates an expected call of GetChannelMember. -func (mr *MockAPIMockRecorder) GetChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMember", reflect.TypeOf((*MockAPI)(nil).GetChannelMember), arg0, arg1) -} - -// GetChannelMembers mocks base method. -func (m *MockAPI) GetChannelMembers(arg0 string, arg1, arg2 int) (model.ChannelMembers, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembers", arg0, arg1, arg2) - ret0, _ := ret[0].(model.ChannelMembers) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembers indicates an expected call of GetChannelMembers. -func (mr *MockAPIMockRecorder) GetChannelMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembers", reflect.TypeOf((*MockAPI)(nil).GetChannelMembers), arg0, arg1, arg2) -} - -// GetChannelMembersByIds mocks base method. -func (m *MockAPI) GetChannelMembersByIds(arg0 string, arg1 []string) (model.ChannelMembers, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembersByIds", arg0, arg1) - ret0, _ := ret[0].(model.ChannelMembers) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembersByIds indicates an expected call of GetChannelMembersByIds. -func (mr *MockAPIMockRecorder) GetChannelMembersByIds(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersByIds", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersByIds), arg0, arg1) -} - -// GetChannelMembersForUser mocks base method. -func (m *MockAPI) GetChannelMembersForUser(arg0, arg1 string, arg2, arg3 int) ([]*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembersForUser", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembersForUser indicates an expected call of GetChannelMembersForUser. -func (mr *MockAPIMockRecorder) GetChannelMembersForUser(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersForUser), arg0, arg1, arg2, arg3) -} - -// GetChannelSidebarCategories mocks base method. -func (m *MockAPI) GetChannelSidebarCategories(arg0, arg1 string) (*model.OrderedSidebarCategories, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelSidebarCategories", arg0, arg1) - ret0, _ := ret[0].(*model.OrderedSidebarCategories) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelSidebarCategories indicates an expected call of GetChannelSidebarCategories. -func (mr *MockAPIMockRecorder) GetChannelSidebarCategories(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).GetChannelSidebarCategories), arg0, arg1) -} - -// GetChannelStats mocks base method. -func (m *MockAPI) GetChannelStats(arg0 string) (*model.ChannelStats, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelStats", arg0) - ret0, _ := ret[0].(*model.ChannelStats) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelStats indicates an expected call of GetChannelStats. -func (mr *MockAPIMockRecorder) GetChannelStats(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelStats", reflect.TypeOf((*MockAPI)(nil).GetChannelStats), arg0) -} - -// GetChannelsForTeamForUser mocks base method. -func (m *MockAPI) GetChannelsForTeamForUser(arg0, arg1 string, arg2 bool) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelsForTeamForUser", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelsForTeamForUser indicates an expected call of GetChannelsForTeamForUser. -func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2) -} - -// GetCloudLimits mocks base method. -func (m *MockAPI) GetCloudLimits() (*model.ProductLimits, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCloudLimits") - ret0, _ := ret[0].(*model.ProductLimits) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCloudLimits indicates an expected call of GetCloudLimits. -func (mr *MockAPIMockRecorder) GetCloudLimits() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockAPI)(nil).GetCloudLimits)) -} - -// GetCommand mocks base method. -func (m *MockAPI) GetCommand(arg0 string) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCommand", arg0) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCommand indicates an expected call of GetCommand. -func (mr *MockAPIMockRecorder) GetCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommand", reflect.TypeOf((*MockAPI)(nil).GetCommand), arg0) -} - -// GetConfig mocks base method. -func (m *MockAPI) GetConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetConfig indicates an expected call of GetConfig. -func (mr *MockAPIMockRecorder) GetConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockAPI)(nil).GetConfig)) -} - -// GetDiagnosticId mocks base method. -func (m *MockAPI) GetDiagnosticId() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDiagnosticId") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetDiagnosticId indicates an expected call of GetDiagnosticId. -func (mr *MockAPIMockRecorder) GetDiagnosticId() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDiagnosticId", reflect.TypeOf((*MockAPI)(nil).GetDiagnosticId)) -} - -// GetDirectChannel mocks base method. -func (m *MockAPI) GetDirectChannel(arg0, arg1 string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDirectChannel", arg0, arg1) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetDirectChannel indicates an expected call of GetDirectChannel. -func (mr *MockAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockAPI)(nil).GetDirectChannel), arg0, arg1) -} - -// GetEmoji mocks base method. -func (m *MockAPI) GetEmoji(arg0 string) (*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmoji", arg0) - ret0, _ := ret[0].(*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmoji indicates an expected call of GetEmoji. -func (mr *MockAPIMockRecorder) GetEmoji(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmoji", reflect.TypeOf((*MockAPI)(nil).GetEmoji), arg0) -} - -// GetEmojiByName mocks base method. -func (m *MockAPI) GetEmojiByName(arg0 string) (*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiByName", arg0) - ret0, _ := ret[0].(*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmojiByName indicates an expected call of GetEmojiByName. -func (mr *MockAPIMockRecorder) GetEmojiByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiByName", reflect.TypeOf((*MockAPI)(nil).GetEmojiByName), arg0) -} - -// GetEmojiImage mocks base method. -func (m *MockAPI) GetEmojiImage(arg0 string) ([]byte, string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiImage", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(*model.AppError) - return ret0, ret1, ret2 -} - -// GetEmojiImage indicates an expected call of GetEmojiImage. -func (mr *MockAPIMockRecorder) GetEmojiImage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiImage", reflect.TypeOf((*MockAPI)(nil).GetEmojiImage), arg0) -} - -// GetEmojiList mocks base method. -func (m *MockAPI) GetEmojiList(arg0 string, arg1, arg2 int) ([]*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiList", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmojiList indicates an expected call of GetEmojiList. -func (mr *MockAPIMockRecorder) GetEmojiList(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiList", reflect.TypeOf((*MockAPI)(nil).GetEmojiList), arg0, arg1, arg2) -} - -// GetFile mocks base method. -func (m *MockAPI) GetFile(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFile", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFile indicates an expected call of GetFile. -func (mr *MockAPIMockRecorder) GetFile(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFile", reflect.TypeOf((*MockAPI)(nil).GetFile), arg0) -} - -// GetFileInfo mocks base method. -func (m *MockAPI) GetFileInfo(arg0 string) (*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfo", arg0) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileInfo indicates an expected call of GetFileInfo. -func (mr *MockAPIMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockAPI)(nil).GetFileInfo), arg0) -} - -// GetFileInfos mocks base method. -func (m *MockAPI) GetFileInfos(arg0, arg1 int, arg2 *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfos", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileInfos indicates an expected call of GetFileInfos. -func (mr *MockAPIMockRecorder) GetFileInfos(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfos", reflect.TypeOf((*MockAPI)(nil).GetFileInfos), arg0, arg1, arg2) -} - -// GetFileLink mocks base method. -func (m *MockAPI) GetFileLink(arg0 string) (string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileLink", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileLink indicates an expected call of GetFileLink. -func (mr *MockAPIMockRecorder) GetFileLink(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileLink", reflect.TypeOf((*MockAPI)(nil).GetFileLink), arg0) -} - -// GetGroup mocks base method. -func (m *MockAPI) GetGroup(arg0 string) (*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroup", arg0) - ret0, _ := ret[0].(*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroup indicates an expected call of GetGroup. -func (mr *MockAPIMockRecorder) GetGroup(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroup", reflect.TypeOf((*MockAPI)(nil).GetGroup), arg0) -} - -// GetGroupByName mocks base method. -func (m *MockAPI) GetGroupByName(arg0 string) (*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupByName", arg0) - ret0, _ := ret[0].(*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupByName indicates an expected call of GetGroupByName. -func (mr *MockAPIMockRecorder) GetGroupByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockAPI)(nil).GetGroupByName), arg0) -} - -// GetGroupChannel mocks base method. -func (m *MockAPI) GetGroupChannel(arg0 []string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupChannel indicates an expected call of GetGroupChannel. -func (mr *MockAPIMockRecorder) GetGroupChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupChannel", reflect.TypeOf((*MockAPI)(nil).GetGroupChannel), arg0) -} - -// GetGroupMemberUsers mocks base method. -func (m *MockAPI) GetGroupMemberUsers(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupMemberUsers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupMemberUsers indicates an expected call of GetGroupMemberUsers. -func (mr *MockAPIMockRecorder) GetGroupMemberUsers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMemberUsers", reflect.TypeOf((*MockAPI)(nil).GetGroupMemberUsers), arg0, arg1, arg2) -} - -// GetGroupsBySource mocks base method. -func (m *MockAPI) GetGroupsBySource(arg0 model.GroupSource) ([]*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupsBySource", arg0) - ret0, _ := ret[0].([]*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupsBySource indicates an expected call of GetGroupsBySource. -func (mr *MockAPIMockRecorder) GetGroupsBySource(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsBySource", reflect.TypeOf((*MockAPI)(nil).GetGroupsBySource), arg0) -} - -// GetGroupsForUser mocks base method. -func (m *MockAPI) GetGroupsForUser(arg0 string) ([]*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupsForUser", arg0) - ret0, _ := ret[0].([]*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupsForUser indicates an expected call of GetGroupsForUser. -func (mr *MockAPIMockRecorder) GetGroupsForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsForUser", reflect.TypeOf((*MockAPI)(nil).GetGroupsForUser), arg0) -} - -// GetLDAPUserAttributes mocks base method. -func (m *MockAPI) GetLDAPUserAttributes(arg0 string, arg1 []string) (map[string]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLDAPUserAttributes", arg0, arg1) - ret0, _ := ret[0].(map[string]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetLDAPUserAttributes indicates an expected call of GetLDAPUserAttributes. -func (mr *MockAPIMockRecorder) GetLDAPUserAttributes(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLDAPUserAttributes", reflect.TypeOf((*MockAPI)(nil).GetLDAPUserAttributes), arg0, arg1) -} - -// GetLicense mocks base method. -func (m *MockAPI) GetLicense() *model.License { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLicense") - ret0, _ := ret[0].(*model.License) - return ret0 -} - -// GetLicense indicates an expected call of GetLicense. -func (mr *MockAPIMockRecorder) GetLicense() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockAPI)(nil).GetLicense)) -} - -// GetOAuthApp mocks base method. -func (m *MockAPI) GetOAuthApp(arg0 string) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetOAuthApp indicates an expected call of GetOAuthApp. -func (mr *MockAPIMockRecorder) GetOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuthApp", reflect.TypeOf((*MockAPI)(nil).GetOAuthApp), arg0) -} - -// GetPluginConfig mocks base method. -func (m *MockAPI) GetPluginConfig() map[string]interface{} { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPluginConfig") - ret0, _ := ret[0].(map[string]interface{}) - return ret0 -} - -// GetPluginConfig indicates an expected call of GetPluginConfig. -func (mr *MockAPIMockRecorder) GetPluginConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginConfig", reflect.TypeOf((*MockAPI)(nil).GetPluginConfig)) -} - -// GetPluginStatus mocks base method. -func (m *MockAPI) GetPluginStatus(arg0 string) (*model.PluginStatus, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPluginStatus", arg0) - ret0, _ := ret[0].(*model.PluginStatus) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPluginStatus indicates an expected call of GetPluginStatus. -func (mr *MockAPIMockRecorder) GetPluginStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginStatus", reflect.TypeOf((*MockAPI)(nil).GetPluginStatus), arg0) -} - -// GetPlugins mocks base method. -func (m *MockAPI) GetPlugins() ([]*model.Manifest, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPlugins") - ret0, _ := ret[0].([]*model.Manifest) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPlugins indicates an expected call of GetPlugins. -func (mr *MockAPIMockRecorder) GetPlugins() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlugins", reflect.TypeOf((*MockAPI)(nil).GetPlugins)) -} - -// GetPost mocks base method. -func (m *MockAPI) GetPost(arg0 string) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPost indicates an expected call of GetPost. -func (mr *MockAPIMockRecorder) GetPost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPost", reflect.TypeOf((*MockAPI)(nil).GetPost), arg0) -} - -// GetPostThread mocks base method. -func (m *MockAPI) GetPostThread(arg0 string) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostThread", arg0) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostThread indicates an expected call of GetPostThread. -func (mr *MockAPIMockRecorder) GetPostThread(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostThread", reflect.TypeOf((*MockAPI)(nil).GetPostThread), arg0) -} - -// GetPostsAfter mocks base method. -func (m *MockAPI) GetPostsAfter(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsAfter", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsAfter indicates an expected call of GetPostsAfter. -func (mr *MockAPIMockRecorder) GetPostsAfter(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsAfter", reflect.TypeOf((*MockAPI)(nil).GetPostsAfter), arg0, arg1, arg2, arg3) -} - -// GetPostsBefore mocks base method. -func (m *MockAPI) GetPostsBefore(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsBefore", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsBefore indicates an expected call of GetPostsBefore. -func (mr *MockAPIMockRecorder) GetPostsBefore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsBefore", reflect.TypeOf((*MockAPI)(nil).GetPostsBefore), arg0, arg1, arg2, arg3) -} - -// GetPostsForChannel mocks base method. -func (m *MockAPI) GetPostsForChannel(arg0 string, arg1, arg2 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsForChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsForChannel indicates an expected call of GetPostsForChannel. -func (mr *MockAPIMockRecorder) GetPostsForChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsForChannel", reflect.TypeOf((*MockAPI)(nil).GetPostsForChannel), arg0, arg1, arg2) -} - -// GetPostsSince mocks base method. -func (m *MockAPI) GetPostsSince(arg0 string, arg1 int64) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsSince", arg0, arg1) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsSince indicates an expected call of GetPostsSince. -func (mr *MockAPIMockRecorder) GetPostsSince(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsSince", reflect.TypeOf((*MockAPI)(nil).GetPostsSince), arg0, arg1) -} - -// GetPreferencesForUser mocks base method. -func (m *MockAPI) GetPreferencesForUser(arg0 string) ([]model.Preference, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0) - ret0, _ := ret[0].([]model.Preference) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPreferencesForUser indicates an expected call of GetPreferencesForUser. -func (mr *MockAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockAPI)(nil).GetPreferencesForUser), arg0) -} - -// GetProfileImage mocks base method. -func (m *MockAPI) GetProfileImage(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProfileImage", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetProfileImage indicates an expected call of GetProfileImage. -func (mr *MockAPIMockRecorder) GetProfileImage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileImage", reflect.TypeOf((*MockAPI)(nil).GetProfileImage), arg0) -} - -// GetPublicChannelsForTeam mocks base method. -func (m *MockAPI) GetPublicChannelsForTeam(arg0 string, arg1, arg2 int) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicChannelsForTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPublicChannelsForTeam indicates an expected call of GetPublicChannelsForTeam. -func (mr *MockAPIMockRecorder) GetPublicChannelsForTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicChannelsForTeam", reflect.TypeOf((*MockAPI)(nil).GetPublicChannelsForTeam), arg0, arg1, arg2) -} - -// GetReactions mocks base method. -func (m *MockAPI) GetReactions(arg0 string) ([]*model.Reaction, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetReactions", arg0) - ret0, _ := ret[0].([]*model.Reaction) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetReactions indicates an expected call of GetReactions. -func (mr *MockAPIMockRecorder) GetReactions(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReactions", reflect.TypeOf((*MockAPI)(nil).GetReactions), arg0) -} - -// GetServerVersion mocks base method. -func (m *MockAPI) GetServerVersion() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServerVersion") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetServerVersion indicates an expected call of GetServerVersion. -func (mr *MockAPIMockRecorder) GetServerVersion() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerVersion", reflect.TypeOf((*MockAPI)(nil).GetServerVersion)) -} - -// GetSession mocks base method. -func (m *MockAPI) GetSession(arg0 string) (*model.Session, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSession", arg0) - ret0, _ := ret[0].(*model.Session) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetSession indicates an expected call of GetSession. -func (mr *MockAPIMockRecorder) GetSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockAPI)(nil).GetSession), arg0) -} - -// GetSystemInstallDate mocks base method. -func (m *MockAPI) GetSystemInstallDate() (int64, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSystemInstallDate") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetSystemInstallDate indicates an expected call of GetSystemInstallDate. -func (mr *MockAPIMockRecorder) GetSystemInstallDate() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemInstallDate", reflect.TypeOf((*MockAPI)(nil).GetSystemInstallDate)) -} - -// GetTeam mocks base method. -func (m *MockAPI) GetTeam(arg0 string) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeam indicates an expected call of GetTeam. -func (mr *MockAPIMockRecorder) GetTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeam", reflect.TypeOf((*MockAPI)(nil).GetTeam), arg0) -} - -// GetTeamByName mocks base method. -func (m *MockAPI) GetTeamByName(arg0 string) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamByName", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamByName indicates an expected call of GetTeamByName. -func (mr *MockAPIMockRecorder) GetTeamByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamByName", reflect.TypeOf((*MockAPI)(nil).GetTeamByName), arg0) -} - -// GetTeamIcon mocks base method. -func (m *MockAPI) GetTeamIcon(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamIcon", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamIcon indicates an expected call of GetTeamIcon. -func (mr *MockAPIMockRecorder) GetTeamIcon(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamIcon", reflect.TypeOf((*MockAPI)(nil).GetTeamIcon), arg0) -} - -// GetTeamMember mocks base method. -func (m *MockAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMember indicates an expected call of GetTeamMember. -func (mr *MockAPIMockRecorder) GetTeamMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMember", reflect.TypeOf((*MockAPI)(nil).GetTeamMember), arg0, arg1) -} - -// GetTeamMembers mocks base method. -func (m *MockAPI) GetTeamMembers(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMembers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMembers indicates an expected call of GetTeamMembers. -func (mr *MockAPIMockRecorder) GetTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembers", reflect.TypeOf((*MockAPI)(nil).GetTeamMembers), arg0, arg1, arg2) -} - -// GetTeamMembersForUser mocks base method. -func (m *MockAPI) GetTeamMembersForUser(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMembersForUser", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMembersForUser indicates an expected call of GetTeamMembersForUser. -func (mr *MockAPIMockRecorder) GetTeamMembersForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamMembersForUser), arg0, arg1, arg2) -} - -// GetTeamStats mocks base method. -func (m *MockAPI) GetTeamStats(arg0 string) (*model.TeamStats, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamStats", arg0) - ret0, _ := ret[0].(*model.TeamStats) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamStats indicates an expected call of GetTeamStats. -func (mr *MockAPIMockRecorder) GetTeamStats(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamStats", reflect.TypeOf((*MockAPI)(nil).GetTeamStats), arg0) -} - -// GetTeams mocks base method. -func (m *MockAPI) GetTeams() ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeams") - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeams indicates an expected call of GetTeams. -func (mr *MockAPIMockRecorder) GetTeams() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeams", reflect.TypeOf((*MockAPI)(nil).GetTeams)) -} - -// GetTeamsForUser mocks base method. -func (m *MockAPI) GetTeamsForUser(arg0 string) ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamsForUser", arg0) - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamsForUser indicates an expected call of GetTeamsForUser. -func (mr *MockAPIMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsForUser), arg0) -} - -// GetTeamsUnreadForUser mocks base method. -func (m *MockAPI) GetTeamsUnreadForUser(arg0 string) ([]*model.TeamUnread, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamsUnreadForUser", arg0) - ret0, _ := ret[0].([]*model.TeamUnread) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamsUnreadForUser indicates an expected call of GetTeamsUnreadForUser. -func (mr *MockAPIMockRecorder) GetTeamsUnreadForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsUnreadForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsUnreadForUser), arg0) -} - -// GetTelemetryId mocks base method. -func (m *MockAPI) GetTelemetryId() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTelemetryId") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetTelemetryId indicates an expected call of GetTelemetryId. -func (mr *MockAPIMockRecorder) GetTelemetryId() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryId", reflect.TypeOf((*MockAPI)(nil).GetTelemetryId)) -} - -// GetUnsanitizedConfig mocks base method. -func (m *MockAPI) GetUnsanitizedConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUnsanitizedConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetUnsanitizedConfig indicates an expected call of GetUnsanitizedConfig. -func (mr *MockAPIMockRecorder) GetUnsanitizedConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnsanitizedConfig", reflect.TypeOf((*MockAPI)(nil).GetUnsanitizedConfig)) -} - -// GetUploadSession mocks base method. -func (m *MockAPI) GetUploadSession(arg0 string) (*model.UploadSession, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUploadSession", arg0) - ret0, _ := ret[0].(*model.UploadSession) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUploadSession indicates an expected call of GetUploadSession. -func (mr *MockAPIMockRecorder) GetUploadSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUploadSession", reflect.TypeOf((*MockAPI)(nil).GetUploadSession), arg0) -} - -// GetUser mocks base method. -func (m *MockAPI) GetUser(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUser indicates an expected call of GetUser. -func (mr *MockAPIMockRecorder) GetUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockAPI)(nil).GetUser), arg0) -} - -// GetUserByEmail mocks base method. -func (m *MockAPI) GetUserByEmail(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByEmail", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserByEmail indicates an expected call of GetUserByEmail. -func (mr *MockAPIMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockAPI)(nil).GetUserByEmail), arg0) -} - -// GetUserByUsername mocks base method. -func (m *MockAPI) GetUserByUsername(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByUsername", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserByUsername indicates an expected call of GetUserByUsername. -func (mr *MockAPIMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockAPI)(nil).GetUserByUsername), arg0) -} - -// GetUserStatus mocks base method. -func (m *MockAPI) GetUserStatus(arg0 string) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserStatus", arg0) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserStatus indicates an expected call of GetUserStatus. -func (mr *MockAPIMockRecorder) GetUserStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatus", reflect.TypeOf((*MockAPI)(nil).GetUserStatus), arg0) -} - -// GetUserStatusesByIds mocks base method. -func (m *MockAPI) GetUserStatusesByIds(arg0 []string) ([]*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserStatusesByIds", arg0) - ret0, _ := ret[0].([]*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserStatusesByIds indicates an expected call of GetUserStatusesByIds. -func (mr *MockAPIMockRecorder) GetUserStatusesByIds(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusesByIds", reflect.TypeOf((*MockAPI)(nil).GetUserStatusesByIds), arg0) -} - -// GetUsers mocks base method. -func (m *MockAPI) GetUsers(arg0 *model.UserGetOptions) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsers", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsers indicates an expected call of GetUsers. -func (mr *MockAPIMockRecorder) GetUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockAPI)(nil).GetUsers), arg0) -} - -// GetUsersByUsernames mocks base method. -func (m *MockAPI) GetUsersByUsernames(arg0 []string) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersByUsernames", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersByUsernames indicates an expected call of GetUsersByUsernames. -func (mr *MockAPIMockRecorder) GetUsersByUsernames(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByUsernames", reflect.TypeOf((*MockAPI)(nil).GetUsersByUsernames), arg0) -} - -// GetUsersInChannel mocks base method. -func (m *MockAPI) GetUsersInChannel(arg0, arg1 string, arg2, arg3 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersInChannel", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersInChannel indicates an expected call of GetUsersInChannel. -func (mr *MockAPIMockRecorder) GetUsersInChannel(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInChannel", reflect.TypeOf((*MockAPI)(nil).GetUsersInChannel), arg0, arg1, arg2, arg3) -} - -// GetUsersInTeam mocks base method. -func (m *MockAPI) GetUsersInTeam(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersInTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersInTeam indicates an expected call of GetUsersInTeam. -func (mr *MockAPIMockRecorder) GetUsersInTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInTeam", reflect.TypeOf((*MockAPI)(nil).GetUsersInTeam), arg0, arg1, arg2) -} - -// HasPermissionTo mocks base method. -func (m *MockAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionTo indicates an expected call of HasPermissionTo. -func (mr *MockAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockAPI)(nil).HasPermissionTo), arg0, arg1) -} - -// HasPermissionToChannel mocks base method. -func (m *MockAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToChannel indicates an expected call of HasPermissionToChannel. -func (mr *MockAPIMockRecorder) HasPermissionToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToChannel", reflect.TypeOf((*MockAPI)(nil).HasPermissionToChannel), arg0, arg1, arg2) -} - -// HasPermissionToTeam mocks base method. -func (m *MockAPI) HasPermissionToTeam(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToTeam", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToTeam indicates an expected call of HasPermissionToTeam. -func (mr *MockAPIMockRecorder) HasPermissionToTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToTeam", reflect.TypeOf((*MockAPI)(nil).HasPermissionToTeam), arg0, arg1, arg2) -} - -// InstallPlugin mocks base method. -func (m *MockAPI) InstallPlugin(arg0 io.Reader, arg1 bool) (*model.Manifest, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstallPlugin", arg0, arg1) - ret0, _ := ret[0].(*model.Manifest) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// InstallPlugin indicates an expected call of InstallPlugin. -func (mr *MockAPIMockRecorder) InstallPlugin(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockAPI)(nil).InstallPlugin), arg0, arg1) -} - -// IsEnterpriseReady mocks base method. -func (m *MockAPI) IsEnterpriseReady() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsEnterpriseReady") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsEnterpriseReady indicates an expected call of IsEnterpriseReady. -func (mr *MockAPIMockRecorder) IsEnterpriseReady() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnterpriseReady", reflect.TypeOf((*MockAPI)(nil).IsEnterpriseReady)) -} - -// KVCompareAndDelete mocks base method. -func (m *MockAPI) KVCompareAndDelete(arg0 string, arg1 []byte) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVCompareAndDelete", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVCompareAndDelete indicates an expected call of KVCompareAndDelete. -func (mr *MockAPIMockRecorder) KVCompareAndDelete(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndDelete", reflect.TypeOf((*MockAPI)(nil).KVCompareAndDelete), arg0, arg1) -} - -// KVCompareAndSet mocks base method. -func (m *MockAPI) KVCompareAndSet(arg0 string, arg1, arg2 []byte) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVCompareAndSet", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVCompareAndSet indicates an expected call of KVCompareAndSet. -func (mr *MockAPIMockRecorder) KVCompareAndSet(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndSet", reflect.TypeOf((*MockAPI)(nil).KVCompareAndSet), arg0, arg1, arg2) -} - -// KVDelete mocks base method. -func (m *MockAPI) KVDelete(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVDelete", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVDelete indicates an expected call of KVDelete. -func (mr *MockAPIMockRecorder) KVDelete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDelete", reflect.TypeOf((*MockAPI)(nil).KVDelete), arg0) -} - -// KVDeleteAll mocks base method. -func (m *MockAPI) KVDeleteAll() *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVDeleteAll") - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVDeleteAll indicates an expected call of KVDeleteAll. -func (mr *MockAPIMockRecorder) KVDeleteAll() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDeleteAll", reflect.TypeOf((*MockAPI)(nil).KVDeleteAll)) -} - -// KVGet mocks base method. -func (m *MockAPI) KVGet(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVGet", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVGet indicates an expected call of KVGet. -func (mr *MockAPIMockRecorder) KVGet(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVGet", reflect.TypeOf((*MockAPI)(nil).KVGet), arg0) -} - -// KVList mocks base method. -func (m *MockAPI) KVList(arg0, arg1 int) ([]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVList", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVList indicates an expected call of KVList. -func (mr *MockAPIMockRecorder) KVList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVList", reflect.TypeOf((*MockAPI)(nil).KVList), arg0, arg1) -} - -// KVSet mocks base method. -func (m *MockAPI) KVSet(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSet", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVSet indicates an expected call of KVSet. -func (mr *MockAPIMockRecorder) KVSet(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSet", reflect.TypeOf((*MockAPI)(nil).KVSet), arg0, arg1) -} - -// KVSetWithExpiry mocks base method. -func (m *MockAPI) KVSetWithExpiry(arg0 string, arg1 []byte, arg2 int64) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSetWithExpiry", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVSetWithExpiry indicates an expected call of KVSetWithExpiry. -func (mr *MockAPIMockRecorder) KVSetWithExpiry(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithExpiry", reflect.TypeOf((*MockAPI)(nil).KVSetWithExpiry), arg0, arg1, arg2) -} - -// KVSetWithOptions mocks base method. -func (m *MockAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVSetWithOptions indicates an expected call of KVSetWithOptions. -func (mr *MockAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockAPI)(nil).KVSetWithOptions), arg0, arg1, arg2) -} - -// ListBuiltInCommands mocks base method. -func (m *MockAPI) ListBuiltInCommands() ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListBuiltInCommands") - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListBuiltInCommands indicates an expected call of ListBuiltInCommands. -func (mr *MockAPIMockRecorder) ListBuiltInCommands() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuiltInCommands", reflect.TypeOf((*MockAPI)(nil).ListBuiltInCommands)) -} - -// ListCommands mocks base method. -func (m *MockAPI) ListCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListCommands indicates an expected call of ListCommands. -func (mr *MockAPIMockRecorder) ListCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommands", reflect.TypeOf((*MockAPI)(nil).ListCommands), arg0) -} - -// ListCustomCommands mocks base method. -func (m *MockAPI) ListCustomCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListCustomCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListCustomCommands indicates an expected call of ListCustomCommands. -func (mr *MockAPIMockRecorder) ListCustomCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCustomCommands", reflect.TypeOf((*MockAPI)(nil).ListCustomCommands), arg0) -} - -// ListPluginCommands mocks base method. -func (m *MockAPI) ListPluginCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPluginCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPluginCommands indicates an expected call of ListPluginCommands. -func (mr *MockAPIMockRecorder) ListPluginCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPluginCommands", reflect.TypeOf((*MockAPI)(nil).ListPluginCommands), arg0) -} - -// LoadPluginConfiguration mocks base method. -func (m *MockAPI) LoadPluginConfiguration(arg0 interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadPluginConfiguration", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// LoadPluginConfiguration indicates an expected call of LoadPluginConfiguration. -func (mr *MockAPIMockRecorder) LoadPluginConfiguration(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPluginConfiguration", reflect.TypeOf((*MockAPI)(nil).LoadPluginConfiguration), arg0) -} - -// LogDebug mocks base method. -func (m *MockAPI) LogDebug(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogDebug", varargs...) -} - -// LogDebug indicates an expected call of LogDebug. -func (mr *MockAPIMockRecorder) LogDebug(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogDebug", reflect.TypeOf((*MockAPI)(nil).LogDebug), varargs...) -} - -// LogError mocks base method. -func (m *MockAPI) LogError(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogError", varargs...) -} - -// LogError indicates an expected call of LogError. -func (mr *MockAPIMockRecorder) LogError(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogError", reflect.TypeOf((*MockAPI)(nil).LogError), varargs...) -} - -// LogInfo mocks base method. -func (m *MockAPI) LogInfo(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogInfo", varargs...) -} - -// LogInfo indicates an expected call of LogInfo. -func (mr *MockAPIMockRecorder) LogInfo(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogInfo", reflect.TypeOf((*MockAPI)(nil).LogInfo), varargs...) -} - -// LogWarn mocks base method. -func (m *MockAPI) LogWarn(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogWarn", varargs...) -} - -// LogWarn indicates an expected call of LogWarn. -func (mr *MockAPIMockRecorder) LogWarn(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogWarn", reflect.TypeOf((*MockAPI)(nil).LogWarn), varargs...) -} - -// OpenInteractiveDialog mocks base method. -func (m *MockAPI) OpenInteractiveDialog(arg0 model.OpenDialogRequest) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OpenInteractiveDialog", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// OpenInteractiveDialog indicates an expected call of OpenInteractiveDialog. -func (mr *MockAPIMockRecorder) OpenInteractiveDialog(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenInteractiveDialog", reflect.TypeOf((*MockAPI)(nil).OpenInteractiveDialog), arg0) -} - -// PatchBot mocks base method. -func (m *MockAPI) PatchBot(arg0 string, arg1 *model.BotPatch) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBot", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// PatchBot indicates an expected call of PatchBot. -func (mr *MockAPIMockRecorder) PatchBot(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBot", reflect.TypeOf((*MockAPI)(nil).PatchBot), arg0, arg1) -} - -// PermanentDeleteBot mocks base method. -func (m *MockAPI) PermanentDeleteBot(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PermanentDeleteBot", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// PermanentDeleteBot indicates an expected call of PermanentDeleteBot. -func (mr *MockAPIMockRecorder) PermanentDeleteBot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentDeleteBot", reflect.TypeOf((*MockAPI)(nil).PermanentDeleteBot), arg0) -} - -// PluginHTTP mocks base method. -func (m *MockAPI) PluginHTTP(arg0 *http.Request) *http.Response { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PluginHTTP", arg0) - ret0, _ := ret[0].(*http.Response) - return ret0 -} - -// PluginHTTP indicates an expected call of PluginHTTP. -func (mr *MockAPIMockRecorder) PluginHTTP(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginHTTP", reflect.TypeOf((*MockAPI)(nil).PluginHTTP), arg0) -} - -// PublishPluginClusterEvent mocks base method. -func (m *MockAPI) PublishPluginClusterEvent(arg0 model.PluginClusterEvent, arg1 model.PluginClusterEventSendOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishPluginClusterEvent", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// PublishPluginClusterEvent indicates an expected call of PublishPluginClusterEvent. -func (mr *MockAPIMockRecorder) PublishPluginClusterEvent(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPluginClusterEvent", reflect.TypeOf((*MockAPI)(nil).PublishPluginClusterEvent), arg0, arg1) -} - -// PublishUserTyping mocks base method. -func (m *MockAPI) PublishUserTyping(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishUserTyping", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// PublishUserTyping indicates an expected call of PublishUserTyping. -func (mr *MockAPIMockRecorder) PublishUserTyping(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishUserTyping", reflect.TypeOf((*MockAPI)(nil).PublishUserTyping), arg0, arg1, arg2) -} - -// PublishWebSocketEvent mocks base method. -func (m *MockAPI) PublishWebSocketEvent(arg0 string, arg1 map[string]interface{}, arg2 *model.WebsocketBroadcast) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebSocketEvent", arg0, arg1, arg2) -} - -// PublishWebSocketEvent indicates an expected call of PublishWebSocketEvent. -func (mr *MockAPIMockRecorder) PublishWebSocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebSocketEvent", reflect.TypeOf((*MockAPI)(nil).PublishWebSocketEvent), arg0, arg1, arg2) -} - -// ReadFile mocks base method. -func (m *MockAPI) ReadFile(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadFile", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// ReadFile indicates an expected call of ReadFile. -func (mr *MockAPIMockRecorder) ReadFile(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockAPI)(nil).ReadFile), arg0) -} - -// RegisterCollectionAndTopic mocks base method. -func (m *MockAPI) RegisterCollectionAndTopic(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RegisterCollectionAndTopic", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// RegisterCollectionAndTopic indicates an expected call of RegisterCollectionAndTopic. -func (mr *MockAPIMockRecorder) RegisterCollectionAndTopic(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCollectionAndTopic", reflect.TypeOf((*MockAPI)(nil).RegisterCollectionAndTopic), arg0, arg1) -} - -// RegisterCommand mocks base method. -func (m *MockAPI) RegisterCommand(arg0 *model.Command) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RegisterCommand", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RegisterCommand indicates an expected call of RegisterCommand. -func (mr *MockAPIMockRecorder) RegisterCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCommand", reflect.TypeOf((*MockAPI)(nil).RegisterCommand), arg0) -} - -// RemovePlugin mocks base method. -func (m *MockAPI) RemovePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemovePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemovePlugin indicates an expected call of RemovePlugin. -func (mr *MockAPIMockRecorder) RemovePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePlugin", reflect.TypeOf((*MockAPI)(nil).RemovePlugin), arg0) -} - -// RemoveReaction mocks base method. -func (m *MockAPI) RemoveReaction(arg0 *model.Reaction) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveReaction", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveReaction indicates an expected call of RemoveReaction. -func (mr *MockAPIMockRecorder) RemoveReaction(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveReaction", reflect.TypeOf((*MockAPI)(nil).RemoveReaction), arg0) -} - -// RemoveTeamIcon mocks base method. -func (m *MockAPI) RemoveTeamIcon(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveTeamIcon", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveTeamIcon indicates an expected call of RemoveTeamIcon. -func (mr *MockAPIMockRecorder) RemoveTeamIcon(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTeamIcon", reflect.TypeOf((*MockAPI)(nil).RemoveTeamIcon), arg0) -} - -// RemoveUserCustomStatus mocks base method. -func (m *MockAPI) RemoveUserCustomStatus(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveUserCustomStatus", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveUserCustomStatus indicates an expected call of RemoveUserCustomStatus. -func (mr *MockAPIMockRecorder) RemoveUserCustomStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).RemoveUserCustomStatus), arg0) -} - -// RequestTrialLicense mocks base method. -func (m *MockAPI) RequestTrialLicense(arg0 string, arg1 int, arg2, arg3 bool) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestTrialLicense", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RequestTrialLicense indicates an expected call of RequestTrialLicense. -func (mr *MockAPIMockRecorder) RequestTrialLicense(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestTrialLicense", reflect.TypeOf((*MockAPI)(nil).RequestTrialLicense), arg0, arg1, arg2, arg3) -} - -// RevokeSession mocks base method. -func (m *MockAPI) RevokeSession(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeSession", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RevokeSession indicates an expected call of RevokeSession. -func (mr *MockAPIMockRecorder) RevokeSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSession", reflect.TypeOf((*MockAPI)(nil).RevokeSession), arg0) -} - -// RevokeUserAccessToken mocks base method. -func (m *MockAPI) RevokeUserAccessToken(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeUserAccessToken", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RevokeUserAccessToken indicates an expected call of RevokeUserAccessToken. -func (mr *MockAPIMockRecorder) RevokeUserAccessToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockAPI)(nil).RevokeUserAccessToken), arg0) -} - -// RolesGrantPermission mocks base method. -func (m *MockAPI) RolesGrantPermission(arg0 []string, arg1 string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RolesGrantPermission", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// RolesGrantPermission indicates an expected call of RolesGrantPermission. -func (mr *MockAPIMockRecorder) RolesGrantPermission(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RolesGrantPermission", reflect.TypeOf((*MockAPI)(nil).RolesGrantPermission), arg0, arg1) -} - -// SaveConfig mocks base method. -func (m *MockAPI) SaveConfig(arg0 *model.Config) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveConfig", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SaveConfig indicates an expected call of SaveConfig. -func (mr *MockAPIMockRecorder) SaveConfig(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveConfig", reflect.TypeOf((*MockAPI)(nil).SaveConfig), arg0) -} - -// SavePluginConfig mocks base method. -func (m *MockAPI) SavePluginConfig(arg0 map[string]interface{}) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePluginConfig", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SavePluginConfig indicates an expected call of SavePluginConfig. -func (mr *MockAPIMockRecorder) SavePluginConfig(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePluginConfig", reflect.TypeOf((*MockAPI)(nil).SavePluginConfig), arg0) -} - -// SearchChannels mocks base method. -func (m *MockAPI) SearchChannels(arg0, arg1 string) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchChannels", arg0, arg1) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchChannels indicates an expected call of SearchChannels. -func (mr *MockAPIMockRecorder) SearchChannels(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchChannels", reflect.TypeOf((*MockAPI)(nil).SearchChannels), arg0, arg1) -} - -// SearchPostsInTeam mocks base method. -func (m *MockAPI) SearchPostsInTeam(arg0 string, arg1 []*model.SearchParams) ([]*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchPostsInTeam", arg0, arg1) - ret0, _ := ret[0].([]*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchPostsInTeam indicates an expected call of SearchPostsInTeam. -func (mr *MockAPIMockRecorder) SearchPostsInTeam(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeam", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeam), arg0, arg1) -} - -// SearchPostsInTeamForUser mocks base method. -func (m *MockAPI) SearchPostsInTeamForUser(arg0, arg1 string, arg2 model.SearchParameter) (*model.PostSearchResults, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchPostsInTeamForUser", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.PostSearchResults) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchPostsInTeamForUser indicates an expected call of SearchPostsInTeamForUser. -func (mr *MockAPIMockRecorder) SearchPostsInTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeamForUser", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeamForUser), arg0, arg1, arg2) -} - -// SearchTeams mocks base method. -func (m *MockAPI) SearchTeams(arg0 string) ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchTeams", arg0) - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchTeams indicates an expected call of SearchTeams. -func (mr *MockAPIMockRecorder) SearchTeams(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchTeams", reflect.TypeOf((*MockAPI)(nil).SearchTeams), arg0) -} - -// SearchUsers mocks base method. -func (m *MockAPI) SearchUsers(arg0 *model.UserSearch) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchUsers", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchUsers indicates an expected call of SearchUsers. -func (mr *MockAPIMockRecorder) SearchUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockAPI)(nil).SearchUsers), arg0) -} - -// SendEphemeralPost mocks base method. -func (m *MockAPI) SendEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendEphemeralPost", arg0, arg1) - ret0, _ := ret[0].(*model.Post) - return ret0 -} - -// SendEphemeralPost indicates an expected call of SendEphemeralPost. -func (mr *MockAPIMockRecorder) SendEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEphemeralPost", reflect.TypeOf((*MockAPI)(nil).SendEphemeralPost), arg0, arg1) -} - -// SendMail mocks base method. -func (m *MockAPI) SendMail(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMail", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SendMail indicates an expected call of SendMail. -func (mr *MockAPIMockRecorder) SendMail(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMail", reflect.TypeOf((*MockAPI)(nil).SendMail), arg0, arg1, arg2) -} - -// SetProfileImage mocks base method. -func (m *MockAPI) SetProfileImage(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProfileImage", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SetProfileImage indicates an expected call of SetProfileImage. -func (mr *MockAPIMockRecorder) SetProfileImage(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileImage", reflect.TypeOf((*MockAPI)(nil).SetProfileImage), arg0, arg1) -} - -// SetTeamIcon mocks base method. -func (m *MockAPI) SetTeamIcon(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetTeamIcon", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SetTeamIcon indicates an expected call of SetTeamIcon. -func (mr *MockAPIMockRecorder) SetTeamIcon(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTeamIcon", reflect.TypeOf((*MockAPI)(nil).SetTeamIcon), arg0, arg1) -} - -// SetUserStatusTimedDND mocks base method. -func (m *MockAPI) SetUserStatusTimedDND(arg0 string, arg1 int64) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserStatusTimedDND", arg0, arg1) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SetUserStatusTimedDND indicates an expected call of SetUserStatusTimedDND. -func (mr *MockAPIMockRecorder) SetUserStatusTimedDND(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserStatusTimedDND", reflect.TypeOf((*MockAPI)(nil).SetUserStatusTimedDND), arg0, arg1) -} - -// UnregisterCommand mocks base method. -func (m *MockAPI) UnregisterCommand(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnregisterCommand", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnregisterCommand indicates an expected call of UnregisterCommand. -func (mr *MockAPIMockRecorder) UnregisterCommand(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnregisterCommand", reflect.TypeOf((*MockAPI)(nil).UnregisterCommand), arg0, arg1) -} - -// UpdateBotActive mocks base method. -func (m *MockAPI) UpdateBotActive(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateBotActive", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateBotActive indicates an expected call of UpdateBotActive. -func (mr *MockAPIMockRecorder) UpdateBotActive(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBotActive", reflect.TypeOf((*MockAPI)(nil).UpdateBotActive), arg0, arg1) -} - -// UpdateChannel mocks base method. -func (m *MockAPI) UpdateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannel indicates an expected call of UpdateChannel. -func (mr *MockAPIMockRecorder) UpdateChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannel", reflect.TypeOf((*MockAPI)(nil).UpdateChannel), arg0) -} - -// UpdateChannelMemberNotifications mocks base method. -func (m *MockAPI) UpdateChannelMemberNotifications(arg0, arg1 string, arg2 map[string]string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelMemberNotifications", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelMemberNotifications indicates an expected call of UpdateChannelMemberNotifications. -func (mr *MockAPIMockRecorder) UpdateChannelMemberNotifications(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberNotifications", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberNotifications), arg0, arg1, arg2) -} - -// UpdateChannelMemberRoles mocks base method. -func (m *MockAPI) UpdateChannelMemberRoles(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelMemberRoles", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelMemberRoles indicates an expected call of UpdateChannelMemberRoles. -func (mr *MockAPIMockRecorder) UpdateChannelMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberRoles), arg0, arg1, arg2) -} - -// UpdateChannelSidebarCategories mocks base method. -func (m *MockAPI) UpdateChannelSidebarCategories(arg0, arg1 string, arg2 []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelSidebarCategories", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.SidebarCategoryWithChannels) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelSidebarCategories indicates an expected call of UpdateChannelSidebarCategories. -func (mr *MockAPIMockRecorder) UpdateChannelSidebarCategories(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).UpdateChannelSidebarCategories), arg0, arg1, arg2) -} - -// UpdateCommand mocks base method. -func (m *MockAPI) UpdateCommand(arg0 string, arg1 *model.Command) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateCommand", arg0, arg1) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateCommand indicates an expected call of UpdateCommand. -func (mr *MockAPIMockRecorder) UpdateCommand(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCommand", reflect.TypeOf((*MockAPI)(nil).UpdateCommand), arg0, arg1) -} - -// UpdateEphemeralPost mocks base method. -func (m *MockAPI) UpdateEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateEphemeralPost", arg0, arg1) - ret0, _ := ret[0].(*model.Post) - return ret0 -} - -// UpdateEphemeralPost indicates an expected call of UpdateEphemeralPost. -func (mr *MockAPIMockRecorder) UpdateEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEphemeralPost", reflect.TypeOf((*MockAPI)(nil).UpdateEphemeralPost), arg0, arg1) -} - -// UpdateOAuthApp mocks base method. -func (m *MockAPI) UpdateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateOAuthApp indicates an expected call of UpdateOAuthApp. -func (mr *MockAPIMockRecorder) UpdateOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuthApp", reflect.TypeOf((*MockAPI)(nil).UpdateOAuthApp), arg0) -} - -// UpdatePost mocks base method. -func (m *MockAPI) UpdatePost(arg0 *model.Post) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdatePost indicates an expected call of UpdatePost. -func (mr *MockAPIMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockAPI)(nil).UpdatePost), arg0) -} - -// UpdatePreferencesForUser mocks base method. -func (m *MockAPI) UpdatePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser. -func (mr *MockAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).UpdatePreferencesForUser), arg0, arg1) -} - -// UpdateTeam mocks base method. -func (m *MockAPI) UpdateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateTeam indicates an expected call of UpdateTeam. -func (mr *MockAPIMockRecorder) UpdateTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeam", reflect.TypeOf((*MockAPI)(nil).UpdateTeam), arg0) -} - -// UpdateTeamMemberRoles mocks base method. -func (m *MockAPI) UpdateTeamMemberRoles(arg0, arg1, arg2 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTeamMemberRoles", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateTeamMemberRoles indicates an expected call of UpdateTeamMemberRoles. -func (mr *MockAPIMockRecorder) UpdateTeamMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeamMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateTeamMemberRoles), arg0, arg1, arg2) -} - -// UpdateUser mocks base method. -func (m *MockAPI) UpdateUser(arg0 *model.User) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockAPIMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockAPI)(nil).UpdateUser), arg0) -} - -// UpdateUserActive mocks base method. -func (m *MockAPI) UpdateUserActive(arg0 string, arg1 bool) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserActive", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdateUserActive indicates an expected call of UpdateUserActive. -func (mr *MockAPIMockRecorder) UpdateUserActive(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserActive", reflect.TypeOf((*MockAPI)(nil).UpdateUserActive), arg0, arg1) -} - -// UpdateUserCustomStatus mocks base method. -func (m *MockAPI) UpdateUserCustomStatus(arg0 string, arg1 *model.CustomStatus) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserCustomStatus", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdateUserCustomStatus indicates an expected call of UpdateUserCustomStatus. -func (mr *MockAPIMockRecorder) UpdateUserCustomStatus(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserCustomStatus), arg0, arg1) -} - -// UpdateUserStatus mocks base method. -func (m *MockAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserStatus", arg0, arg1) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateUserStatus indicates an expected call of UpdateUserStatus. -func (mr *MockAPIMockRecorder) UpdateUserStatus(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserStatus), arg0, arg1) -} - -// UploadData mocks base method. -func (m *MockAPI) UploadData(arg0 *model.UploadSession, arg1 io.Reader) (*model.FileInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UploadData", arg0, arg1) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UploadData indicates an expected call of UploadData. -func (mr *MockAPIMockRecorder) UploadData(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadData", reflect.TypeOf((*MockAPI)(nil).UploadData), arg0, arg1) -} - -// UploadFile mocks base method. -func (m *MockAPI) UploadFile(arg0 []byte, arg1, arg2 string) (*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UploadFile", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UploadFile indicates an expected call of UploadFile. -func (mr *MockAPIMockRecorder) UploadFile(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadFile", reflect.TypeOf((*MockAPI)(nil).UploadFile), arg0, arg1, arg2) -} diff --git a/server/boards/services/permissions/mocks/mockstore.go b/server/boards/services/permissions/mocks/mockstore.go deleted file mode 100644 index d70a57290a..0000000000 --- a/server/boards/services/permissions/mocks/mockstore.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/services/permissions (interfaces: Store) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// MockStore is a mock of Store interface. -type MockStore struct { - ctrl *gomock.Controller - recorder *MockStoreMockRecorder -} - -// MockStoreMockRecorder is the mock recorder for MockStore. -type MockStoreMockRecorder struct { - mock *MockStore -} - -// NewMockStore creates a new mock instance. -func NewMockStore(ctrl *gomock.Controller) *MockStore { - mock := &MockStore{ctrl: ctrl} - mock.recorder = &MockStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStore) EXPECT() *MockStoreMockRecorder { - return m.recorder -} - -// GetBoard mocks base method. -func (m *MockStore) GetBoard(arg0 string) (*model.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoard", arg0) - ret0, _ := ret[0].(*model.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoard indicates an expected call of GetBoard. -func (mr *MockStoreMockRecorder) GetBoard(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoard", reflect.TypeOf((*MockStore)(nil).GetBoard), arg0) -} - -// GetBoardHistory mocks base method. -func (m *MockStore) GetBoardHistory(arg0 string, arg1 model.QueryBoardHistoryOptions) ([]*model.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardHistory", arg0, arg1) - ret0, _ := ret[0].([]*model.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardHistory indicates an expected call of GetBoardHistory. -func (mr *MockStoreMockRecorder) GetBoardHistory(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardHistory", reflect.TypeOf((*MockStore)(nil).GetBoardHistory), arg0, arg1) -} - -// GetMemberForBoard mocks base method. -func (m *MockStore) GetMemberForBoard(arg0, arg1 string) (*model.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMemberForBoard", arg0, arg1) - ret0, _ := ret[0].(*model.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMemberForBoard indicates an expected call of GetMemberForBoard. -func (mr *MockStoreMockRecorder) GetMemberForBoard(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberForBoard", reflect.TypeOf((*MockStore)(nil).GetMemberForBoard), arg0, arg1) -} diff --git a/server/boards/services/permissions/permissions.go b/server/boards/services/permissions/permissions.go deleted file mode 100644 index 5a50a17596..0000000000 --- a/server/boards/services/permissions/permissions.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../../copyright.txt -destination=mocks/mockstore.go -package mocks . Store - -package permissions - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -type PermissionsService interface { - HasPermissionTo(userID string, permission *mm_model.Permission) bool - HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool - HasPermissionToChannel(userID, channelID string, permission *mm_model.Permission) bool - HasPermissionToBoard(userID, boardID string, permission *mm_model.Permission) bool -} - -type Store interface { - GetBoard(boardID string) (*model.Board, error) - GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) - GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) -} diff --git a/server/boards/services/scheduler/scheduler.go b/server/boards/services/scheduler/scheduler.go deleted file mode 100644 index 52731fb179..0000000000 --- a/server/boards/services/scheduler/scheduler.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package scheduler - -import ( - "fmt" - "time" -) - -type TaskFunc func() - -type ScheduledTask struct { - Name string `json:"name"` - Interval time.Duration `json:"interval"` - Recurring bool `json:"recurring"` - function func() - cancel chan struct{} - cancelled chan struct{} -} - -func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { - return createTask(name, function, timeToExecution, false) -} - -func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { - return createTask(name, function, interval, true) -} - -func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask { - task := &ScheduledTask{ - Name: name, - Interval: interval, - Recurring: recurring, - function: function, - cancel: make(chan struct{}), - cancelled: make(chan struct{}), - } - - go func() { - defer close(task.cancelled) - - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - function() - case <-task.cancel: - return - } - - if !task.Recurring { - break - } - } - }() - - return task -} - -func (task *ScheduledTask) Cancel() { - close(task.cancel) - <-task.cancelled -} - -func (task *ScheduledTask) String() string { - return fmt.Sprintf( - "%s\nInterval: %s\nRecurring: %t\n", - task.Name, - task.Interval.String(), - task.Recurring, - ) -} diff --git a/server/boards/services/scheduler/scheduler_test.go b/server/boards/services/scheduler/scheduler_test.go deleted file mode 100644 index f6d5948ec4..0000000000 --- a/server/boards/services/scheduler/scheduler_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package scheduler - -import ( - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestCreateTask(t *testing.T) { - taskName := "Test Task" - taskTime := time.Millisecond * 200 - taskWait := time.Millisecond * 100 - - executionCount := new(int32) - testFunc := func() { - atomic.AddInt32(executionCount, 1) - } - - task := CreateTask(taskName, testFunc, taskTime) - - assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) - - time.Sleep(taskTime + taskWait) - - assert.EqualValues(t, 1, atomic.LoadInt32(executionCount)) - assert.Equal(t, taskName, task.Name) - assert.Equal(t, taskTime, task.Interval) - assert.False(t, task.Recurring) -} - -func TestCreateRecurringTask(t *testing.T) { - taskName := "Test Recurring Task" - taskTime := time.Millisecond * 500 - taskWait := time.Millisecond * 200 - - executionCount := new(int32) - testFunc := func() { - atomic.AddInt32(executionCount, 1) - } - - task := CreateRecurringTask(taskName, testFunc, taskTime) - - assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) - - time.Sleep(taskTime + taskWait) - - assert.EqualValues(t, 1, atomic.LoadInt32(executionCount)) - - time.Sleep(taskTime) - - assert.EqualValues(t, 2, atomic.LoadInt32(executionCount)) - assert.Equal(t, taskName, task.Name) - assert.Equal(t, taskTime, task.Interval) - assert.True(t, task.Recurring) - - task.Cancel() -} - -func TestCancelTask(t *testing.T) { - taskName := "Test Task" - taskTime := time.Millisecond * 100 - taskWait := time.Millisecond * 100 - - executionCount := new(int32) - testFunc := func() { - atomic.AddInt32(executionCount, 1) - } - - task := CreateTask(taskName, testFunc, taskTime) - - assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) - task.Cancel() - - time.Sleep(taskTime + taskWait) - assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) -} diff --git a/server/boards/services/store/generators/main.go b/server/boards/services/store/generators/main.go deleted file mode 100644 index 02fde8335c..0000000000 --- a/server/boards/services/store/generators/main.go +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package main - -import ( - "bytes" - "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "io" - "log" - "os" - "path" - "strings" - "text/template" -) - -const ( - WithTransactionComment = "@withTransaction" - ErrorType = "error" - StringType = "string" - IntType = "int" - Int32Type = "int32" - Int64Type = "int64" - BoolType = "bool" -) - -func isError(typeName string) bool { - return strings.Contains(typeName, ErrorType) -} - -func isString(typeName string) bool { - return typeName == StringType -} - -func isInt(typeName string) bool { - return typeName == IntType || typeName == Int32Type || typeName == Int64Type -} - -func isBool(typeName string) bool { - return typeName == BoolType -} - -func main() { - if err := buildTransactionalStore(); err != nil { - log.Fatal(err) - } -} - -func buildTransactionalStore() error { - code, err := generateLayer("TransactionalStore", "transactional_store.go.tmpl") - if err != nil { - return err - } - formatedCode, err := format.Source(code) - if err != nil { - return err - } - - return os.WriteFile(path.Join("sqlstore/public_methods.go"), formatedCode, 0644) //nolint:gosec -} - -type methodParam struct { - Name string - Type string -} - -type methodData struct { - Params []methodParam - Results []string - WithTransaction bool -} - -type storeMetadata struct { - Name string - Methods map[string]methodData -} - -var blacklistedStoreMethodNames = map[string]bool{ - "Shutdown": true, - "DBType": true, - "DBVersion": true, -} - -func extractMethodMetadata(method *ast.Field, src []byte) methodData { - params := []methodParam{} - results := []string{} - withTransaction := false - ast.Inspect(method.Type, func(expr ast.Node) bool { - //nolint:gocritic - switch e := expr.(type) { - case *ast.FuncType: - if method.Doc != nil { - for _, comment := range method.Doc.List { - if strings.Contains(comment.Text, WithTransactionComment) { - withTransaction = true - break - } - } - } - if e.Params != nil { - for _, param := range e.Params.List { - for _, paramName := range param.Names { - params = append(params, methodParam{Name: paramName.Name, Type: string(src[param.Type.Pos()-1 : param.Type.End()-1])}) - } - } - } - if e.Results != nil { - for _, result := range e.Results.List { - results = append(results, string(src[result.Type.Pos()-1:result.Type.End()-1])) - } - } - } - return true - }) - return methodData{Params: params, Results: results, WithTransaction: withTransaction} -} - -func extractStoreMetadata() (*storeMetadata, error) { - // Create the AST by parsing src. - fset := token.NewFileSet() // positions are relative to fset - - file, err := os.Open("store.go") - if err != nil { - return nil, fmt.Errorf("unable to open store/store.go file: %w", err) - } - src, err := io.ReadAll(file) - if err != nil { - return nil, err - } - file.Close() - f, err := parser.ParseFile(fset, "", src, parser.AllErrors|parser.ParseComments) - if err != nil { - return nil, err - } - - metadata := storeMetadata{Methods: map[string]methodData{}} - - ast.Inspect(f, func(n ast.Node) bool { - //nolint:gocritic - switch x := n.(type) { - case *ast.TypeSpec: - if x.Name.Name == "Store" { - for _, method := range x.Type.(*ast.InterfaceType).Methods.List { - methodName := method.Names[0].Name - if _, ok := blacklistedStoreMethodNames[methodName]; ok { - continue - } - - metadata.Methods[methodName] = extractMethodMetadata(method, src) - } - } - } - return true - }) - - return &metadata, nil -} - -func generateLayer(name, templateFile string) ([]byte, error) { - out := bytes.NewBufferString("") - metadata, err := extractStoreMetadata() - if err != nil { - return nil, err - } - metadata.Name = name - - myFuncs := template.FuncMap{ - "joinResultsForSignature": func(results []string) string { - if len(results) == 0 { - return "" - } - if len(results) == 1 { - return strings.Join(results, ", ") - } - return fmt.Sprintf("(%s)", strings.Join(results, ", ")) - }, - "genResultsVars": func(results []string, withNilError bool) string { - vars := []string{} - for i, typeName := range results { - switch { - case isError(typeName): - if withNilError { - vars = append(vars, "nil") - } else { - vars = append(vars, "err") - } - case i == 0: - vars = append(vars, "result") - default: - vars = append(vars, fmt.Sprintf("resultVar%d", i)) - } - } - return strings.Join(vars, ", ") - }, - "errorPresent": func(results []string) bool { - for _, typeName := range results { - if isError(typeName) { - return true - } - } - return false - }, - "errorVar": func(results []string) string { - for _, typeName := range results { - if isError(typeName) { - return "err" - } - } - return "" - }, - "joinParams": func(params []methodParam) string { - paramsNames := make([]string, 0, len(params)) - for _, param := range params { - tParams := "" - if strings.HasPrefix(param.Type, "...") { - tParams = "..." - } - paramsNames = append(paramsNames, param.Name+tParams) - } - return strings.Join(paramsNames, ", ") - }, - "joinParamsWithType": func(params []methodParam) string { - paramsWithType := []string{} - for _, param := range params { - switch param.Type { - case "Container": - paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type)) - default: - paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type)) - } - } - return strings.Join(paramsWithType, ", ") - }, - "renameStoreMethod": func(methodName string) string { - return strings.ToLower(methodName[0:1]) + methodName[1:] - }, - "genErrorResultsVars": func(results []string, errName string) string { - vars := []string{} - for _, typeName := range results { - switch { - case isError(typeName): - vars = append(vars, errName) - case isString(typeName): - vars = append(vars, "\"\"") - case isInt(typeName): - vars = append(vars, "0") - case isBool(typeName): - vars = append(vars, "false") - default: - vars = append(vars, "nil") - } - } - return strings.Join(vars, ", ") - }, - } - - t := template.Must(template.New(templateFile).Funcs(myFuncs).ParseFiles("generators/" + templateFile)) - if err = t.Execute(out, metadata); err != nil { - return nil, err - } - return out.Bytes(), nil -} diff --git a/server/boards/services/store/generators/transactional_store.go.tmpl b/server/boards/services/store/generators/transactional_store.go.tmpl deleted file mode 100644 index 55be08f151..0000000000 --- a/server/boards/services/store/generators/transactional_store.go.tmpl +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by "make generate" from the Store interface -// DO NOT EDIT - -// To add a public method, create an entry in the Store interface, -// prefix it with a @withTransaction comment if you need it to be -// transactional and then add a private method in the store itself -// with db sq.BaseRunner as the first parameter before running `make -// generate` - -package sqlstore - -import ( - "context" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -{{range $index, $element := .Methods}} -func (s *SQLStore) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} { - {{- if $element.WithTransaction}} - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return {{ genErrorResultsVars $element.Results "txErr"}} - } - - {{- if $element.Results | len | eq 0}} - s.{{$index | renameStoreMethod}}(tx, {{$element.Params | joinParams}}) - - if err := tx.Commit(); err != nil { - return {{ genErrorResultsVars $element.Results "err"}} - } - {{else}} - {{genResultsVars $element.Results false }} := s.{{$index | renameStoreMethod}}(tx, {{$element.Params | joinParams}}) - {{- if $element.Results | errorPresent }} - if {{$element.Results | errorVar}} != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "{{$index}}")) - } - return {{ genErrorResultsVars $element.Results "err"}} - } - {{end}} - if err := tx.Commit(); err != nil { - return {{ genErrorResultsVars $element.Results "err"}} - } - - return {{ genResultsVars $element.Results true -}} - {{end}} - {{else}} - return s.{{$index | renameStoreMethod}}(s.db, {{$element.Params | joinParams}}) - {{end}} -} -{{end}} diff --git a/server/boards/services/store/mattermostauthlayer/mattermostauthlayer.go b/server/boards/services/store/mattermostauthlayer/mattermostauthlayer.go deleted file mode 100644 index a743942451..0000000000 --- a/server/boards/services/store/mattermostauthlayer/mattermostauthlayer.go +++ /dev/null @@ -1,1353 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package mattermostauthlayer - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var boardsBotID string - -// servicesAPI is the interface required my the MattermostAuthLayer to interact with -// the mattermost-server. You can use plugin-api or product-api adapter implementations. -type servicesAPI interface { - GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) - GetChannelByID(channelID string) (*mm_model.Channel, error) - GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) - GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) - GetUserByID(userID string) (*mm_model.User, error) - UpdateUser(user *mm_model.User) (*mm_model.User, error) - GetUserByEmail(email string) (*mm_model.User, error) - GetUserByUsername(username string) (*mm_model.User, error) - GetLicense() *mm_model.License - GetFileInfo(fileID string) (*mm_model.FileInfo, error) - GetCloudLimits() (*mm_model.ProductLimits, error) - EnsureBot(bot *mm_model.Bot) (string, error) - CreatePost(post *mm_model.Post) (*mm_model.Post, error) - GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) - GetPreferencesForUser(userID string) (mm_model.Preferences, error) - DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error - UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error -} - -// Store represents the abstraction of the data storage. -type MattermostAuthLayer struct { - store.Store - dbType string - mmDB *sql.DB - logger mlog.LoggerIFace - servicesAPI servicesAPI - tablePrefix string -} - -// New creates a new SQL implementation of the store. -func New(dbType string, db *sql.DB, store store.Store, logger mlog.LoggerIFace, api servicesAPI, tablePrefix string) (*MattermostAuthLayer, error) { - layer := &MattermostAuthLayer{ - Store: store, - dbType: dbType, - mmDB: db, - logger: logger, - servicesAPI: api, - tablePrefix: tablePrefix, - } - - return layer, nil -} - -// For MattermostAuthLayer we don't close the database connection -// because it's directly managed by the platform -func (s *MattermostAuthLayer) Shutdown() error { - return nil -} - -func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) { - query := s.getQueryBuilder(). - Select("count(*)"). - From("Users"). - Where(sq.Eq{"deleteAt": 0}) - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - return 0, err - } - - return count, nil -} - -func (s *MattermostAuthLayer) GetUserByID(userID string) (*model.User, error) { - mmuser, err := s.servicesAPI.GetUserByID(userID) - if err != nil { - return nil, err - } - user := mmUserToFbUser(mmuser) - return &user, nil -} - -func (s *MattermostAuthLayer) GetUserByEmail(email string) (*model.User, error) { - mmuser, err := s.servicesAPI.GetUserByEmail(email) - if err != nil { - return nil, err - } - user := mmUserToFbUser(mmuser) - return &user, nil -} - -func (s *MattermostAuthLayer) GetUserByUsername(username string) (*model.User, error) { - mmuser, err := s.servicesAPI.GetUserByUsername(username) - if err != nil { - return nil, err - } - user := mmUserToFbUser(mmuser) - return &user, nil -} - -func (s *MattermostAuthLayer) CreateUser(user *model.User) (*model.User, error) { - return nil, store.NewNotSupportedError("no user creation allowed from focalboard, create it using mattermost") -} - -func (s *MattermostAuthLayer) UpdateUser(user *model.User) (*model.User, error) { - return nil, store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) UpdateUserPassword(username, password string) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) { - preferences, err := s.GetUserPreferences(userID) - if err != nil { - return nil, err - } - - if len(patch.UpdatedFields) > 0 { - updatedPreferences := mm_model.Preferences{} - for key, value := range patch.UpdatedFields { - preference := mm_model.Preference{ - UserId: userID, - Category: model.PreferencesCategoryFocalboard, - Name: key, - Value: value, - } - - updatedPreferences = append(updatedPreferences, preference) - } - - if err := s.servicesAPI.UpdatePreferencesForUser(userID, updatedPreferences); err != nil { - s.logger.Error("failed to update user preferences", mlog.String("user_id", userID), mlog.Err(err)) - return nil, err - } - - // we update the preferences list replacing or adding those - // that were updated - newPreferences := mm_model.Preferences{} - for _, existingPreference := range preferences { - hasBeenUpdated := false - for _, updatedPreference := range updatedPreferences { - if updatedPreference.Name == existingPreference.Name { - hasBeenUpdated = true - break - } - } - - if !hasBeenUpdated { - newPreferences = append(newPreferences, existingPreference) - } - } - newPreferences = append(newPreferences, updatedPreferences...) - preferences = newPreferences - } - - if len(patch.DeletedFields) > 0 { - deletedPreferences := mm_model.Preferences{} - for _, key := range patch.DeletedFields { - preference := mm_model.Preference{ - UserId: userID, - Category: model.PreferencesCategoryFocalboard, - Name: key, - } - - deletedPreferences = append(deletedPreferences, preference) - } - - if err := s.servicesAPI.DeletePreferencesForUser(userID, deletedPreferences); err != nil { - s.logger.Error("failed to delete user preferences", mlog.String("user_id", userID), mlog.Err(err)) - return nil, err - } - - // we update the preferences removing those that have been - // deleted - newPreferences := mm_model.Preferences{} - for _, existingPreference := range preferences { - hasBeenDeleted := false - for _, deletedPreference := range deletedPreferences { - if deletedPreference.Name == existingPreference.Name { - hasBeenDeleted = true - break - } - } - - if !hasBeenDeleted { - newPreferences = append(newPreferences, existingPreference) - } - } - preferences = newPreferences - } - - return preferences, nil -} - -func (s *MattermostAuthLayer) GetUserPreferences(userID string) (mm_model.Preferences, error) { - return s.servicesAPI.GetPreferencesForUser(userID) -} - -// GetActiveUserCount returns the number of users with active sessions within N seconds ago. -func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) { - query := s.getQueryBuilder(). - Select("count(distinct userId)"). - From("Sessions"). - Where(sq.Gt{"LastActivityAt": utils.GetMillis() - utils.SecondsToMillis(updatedSecondsAgo)}) - - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - return 0, err - } - - return count, nil -} - -func (s *MattermostAuthLayer) GetSession(token string, expireTime int64) (*model.Session, error) { - return nil, store.NewNotSupportedError("sessions not used when using mattermost") -} - -func (s *MattermostAuthLayer) CreateSession(session *model.Session) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) RefreshSession(session *model.Session) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) UpdateSession(session *model.Session) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) DeleteSession(sessionID string) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) CleanUpSessions(expireTime int64) error { - return store.NewNotSupportedError("no update allowed from focalboard, update it using mattermost") -} - -func (s *MattermostAuthLayer) GetTeam(id string) (*model.Team, error) { - if id == "0" { - team := model.Team{ - ID: id, - Title: "", - } - - return &team, nil - } - - query := s.getQueryBuilder(). - Select("DisplayName"). - From("Teams"). - Where(sq.Eq{"ID": id}) - - row := query.QueryRow() - var displayName string - err := row.Scan(&displayName) - if err != nil && !model.IsErrNotFound(err) { - s.logger.Error("GetTeam scan error", - mlog.String("team_id", id), - mlog.Err(err), - ) - return nil, err - } - - return &model.Team{ID: id, Title: displayName}, nil -} - -// GetTeamsForUser retrieves all the teams that the user is a member of. -func (s *MattermostAuthLayer) GetTeamsForUser(userID string) ([]*model.Team, error) { - query := s.getQueryBuilder(). - Select("t.Id", "t.DisplayName"). - From("Teams as t"). - Join("TeamMembers as tm on t.Id=tm.TeamId"). - Where(sq.Eq{"tm.UserId": userID}). - Where(sq.Eq{"tm.DeleteAt": 0}) - - rows, err := query.Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - teams := []*model.Team{} - for rows.Next() { - var team model.Team - - err := rows.Scan( - &team.ID, - &team.Title, - ) - if err != nil { - return nil, err - } - - teams = append(teams, &team) - } - - return teams, nil -} - -func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType { - builder := sq.StatementBuilder - if s.dbType == model.PostgresDBType { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - return builder.RunWith(s.mmDB) -} - -func (s *MattermostAuthLayer) GetUsersByTeam(teamID string, asGuestID string, showEmail, showName bool) ([]*model.User, error) { - query := s.baseUserQuery(showEmail, showName). - Where(sq.Eq{"u.deleteAt": 0}) - - if asGuestID == "" { - query = query. - Join("TeamMembers as tm ON tm.UserID = u.id"). - Where(sq.Eq{"tm.TeamId": teamID}) - } else { - boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false) - if err != nil { - return nil, err - } - - boardsIDs := []string{} - for _, board := range boards { - boardsIDs = append(boardsIDs, board.ID) - } - query = query. - Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID"). - Where(sq.Eq{"bm.BoardId": boardsIDs}) - } - - rows, err := query.Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - users, err := s.usersFromRows(rows) - if err != nil { - return nil, err - } - - return users, nil -} - -func (s *MattermostAuthLayer) GetUsersList(userIDs []string, showEmail, showName bool) ([]*model.User, error) { - query := s.baseUserQuery(showEmail, showName). - Where(sq.Eq{"u.id": userIDs}) - - rows, err := query.Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - users, err := s.usersFromRows(rows) - if err != nil { - return nil, err - } - - if len(users) != len(userIDs) { - return users, model.NewErrNotAllFound("user", userIDs) - } - - return users, nil -} - -func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots, showEmail, showName bool) ([]*model.User, error) { - query := s.baseUserQuery(showEmail, showName). - Where(sq.Eq{"u.deleteAt": 0}). - Where(sq.Or{ - sq.Like{"u.username": "%" + searchQuery + "%"}, - sq.Like{"u.nickname": "%" + searchQuery + "%"}, - sq.Like{"u.firstname": "%" + searchQuery + "%"}, - sq.Like{"u.lastname": "%" + searchQuery + "%"}, - }). - OrderBy("u.username"). - Limit(10) - - if excludeBots { - query = query. - Where(sq.Eq{"b.UserId IS NOT NULL": false}) - } - - if asGuestID == "" { - query = query. - Join("TeamMembers as tm ON tm.UserID = u.id"). - Where(sq.Eq{"tm.TeamId": teamID}) - } else { - boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false) - if err != nil { - return nil, err - } - boardsIDs := []string{} - for _, board := range boards { - boardsIDs = append(boardsIDs, board.ID) - } - query = query. - Join(s.tablePrefix + "board_members as bm ON bm.user_id = u.ID"). - Where(sq.Eq{"bm.board_id": boardsIDs}) - } - - rows, err := query.Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - users, err := s.usersFromRows(rows) - if err != nil { - return nil, err - } - - return users, nil -} - -func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, error) { - users := []*model.User{} - - for rows.Next() { - var user model.User - - err := rows.Scan( - &user.ID, - &user.Username, - &user.Email, - &user.Nickname, - &user.FirstName, - &user.LastName, - &user.CreateAt, - &user.UpdateAt, - &user.DeleteAt, - &user.IsBot, - &user.IsGuest, - ) - if err != nil { - return nil, err - } - - users = append(users, &user) - } - - return users, nil -} - -func (s *MattermostAuthLayer) CloseRows(rows *sql.Rows) { - if err := rows.Close(); err != nil { - s.logger.Error("error closing MattermostAuthLayer row set", mlog.Err(err)) - } -} - -func (s *MattermostAuthLayer) CreatePrivateWorkspace(userID string) (string, error) { - // we emulate a private workspace by creating - // a DM channel from the user to themselves. - channel, err := s.servicesAPI.GetDirectChannel(userID, userID) - if err != nil { - s.logger.Error("error fetching private workspace", mlog.String("userID", userID), mlog.Err(err)) - return "", err - } - - return channel.Id, nil -} - -func mmUserToFbUser(mmUser *mm_model.User) model.User { - authData := "" - if mmUser.AuthData != nil { - authData = *mmUser.AuthData - } - return model.User{ - ID: mmUser.Id, - Username: mmUser.Username, - Email: mmUser.Email, - Password: mmUser.Password, - Nickname: mmUser.Nickname, - FirstName: mmUser.FirstName, - LastName: mmUser.LastName, - MfaSecret: mmUser.MfaSecret, - AuthService: mmUser.AuthService, - AuthData: authData, - CreateAt: mmUser.CreateAt, - UpdateAt: mmUser.UpdateAt, - DeleteAt: mmUser.DeleteAt, - IsBot: mmUser.IsBot, - IsGuest: mmUser.IsGuest(), - Roles: mmUser.Roles, - } -} - -func (s *MattermostAuthLayer) GetFileInfo(id string) (*mm_model.FileInfo, error) { - fileInfo, err := s.servicesAPI.GetFileInfo(id) - if err != nil { - // Not finding fileinfo is fine because we don't have data for - // any existing files already uploaded in Boards before this code - // was deployed. - var appErr *mm_model.AppError - if errors.As(err, &appErr) { - if appErr.StatusCode == http.StatusNotFound { - return nil, model.NewErrNotFound("file info ID=" + id) - } - } - - s.logger.Error("error fetching fileinfo", - mlog.String("id", id), - mlog.Err(err), - ) - return nil, err - } - - return fileInfo, nil -} - -func (s *MattermostAuthLayer) SaveFileInfo(fileInfo *mm_model.FileInfo) error { - query := s.getQueryBuilder(). - Insert("FileInfo"). - Columns( - "Id", - "CreatorId", - "PostId", - "CreateAt", - "UpdateAt", - "DeleteAt", - "Path", - "ThumbnailPath", - "PreviewPath", - "Name", - "Extension", - "Size", - "MimeType", - "Width", - "Height", - "HasPreviewImage", - "MiniPreview", - "Content", - "RemoteId", - "Archived", - ). - Values( - fileInfo.Id, - fileInfo.CreatorId, - fileInfo.PostId, - fileInfo.CreateAt, - fileInfo.UpdateAt, - fileInfo.DeleteAt, - fileInfo.Path, - fileInfo.ThumbnailPath, - fileInfo.PreviewPath, - fileInfo.Name, - fileInfo.Extension, - fileInfo.Size, - fileInfo.MimeType, - fileInfo.Width, - fileInfo.Height, - fileInfo.HasPreviewImage, - fileInfo.MiniPreview, - fileInfo.Content, - fileInfo.RemoteId, - false, - ) - - if _, err := query.Exec(); err != nil { - s.logger.Error( - "failed to save fileinfo", - mlog.String("file_name", fileInfo.Name), - mlog.Int64("size", fileInfo.Size), - mlog.Err(err), - ) - return err - } - - return nil -} - -func (s *MattermostAuthLayer) GetLicense() *mm_model.License { - return s.servicesAPI.GetLicense() -} - -func boardFields(prefix string) []string { //nolint:unparam - fields := []string{ - "id", - "team_id", - "COALESCE(channel_id, '')", - "COALESCE(created_by, '')", - "modified_by", - "type", - "minimum_role", - "title", - "description", - "icon", - "show_description", - "is_template", - "template_version", - "COALESCE(properties, '{}')", - "COALESCE(card_properties, '[]')", - "create_at", - "update_at", - "delete_at", - } - - if prefix == "" { - return fields - } - - prefixedFields := make([]string, len(fields)) - for i, field := range fields { - if strings.HasPrefix(field, "COALESCE(") { - prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1) - } else { - prefixedFields[i] = prefix + field - } - } - return prefixedFields -} - -func (s *MattermostAuthLayer) baseUserQuery(showEmail, showName bool) sq.SelectBuilder { - emailField := "''" - if showEmail { - emailField = "u.email" - } - firstNameField := "''" - lastNameField := "''" - if showName { - firstNameField = "u.firstname" - lastNameField = "u.lastname" - } - - return s.getQueryBuilder(). - Select( - "u.id", - "u.username", - emailField, - "u.nickname", - firstNameField, - lastNameField, - "u.CreateAt as create_at", - "u.UpdateAt as update_at", - "u.DeleteAt as delete_at", - "b.UserId IS NOT NULL AS is_bot", - "u.roles = 'system_guest' as is_guest", - ). - From("Users as u"). - LeftJoin("Bots b ON ( b.UserID = u.id )") -} - -// SearchBoardsForUser returns all boards that match with the -// term that are either private and which the user is a member of, or -// they're open, regardless of the user membership. -// Search is case-insensitive. -func (s *MattermostAuthLayer) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) { - // as we're joining three queries, we need to avoid numbered - // placeholders until the join is done, so we use the default - // question mark placeholder here - builder := s.getQueryBuilder().PlaceholderFormat(sq.Question) - - boardMembersQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b"). - Join(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). - Where(sq.Eq{ - "b.is_template": false, - "bm.user_id": userID, - }) - - teamMembersQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b"). - Join("TeamMembers as tm on tm.teamid=b.team_id"). - Where(sq.Eq{ - "b.is_template": false, - "tm.userID": userID, - "tm.deleteAt": 0, - "b.type": model.BoardTypeOpen, - }) - - channelMembersQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b"). - Join("ChannelMembers as cm on cm.channelId=b.channel_id"). - Where(sq.Eq{ - "b.is_template": false, - "cm.userId": userID, - }) - - if term != "" { - if searchField == model.BoardSearchFieldPropertyName { - var where, whereTerm string - switch s.dbType { - case model.PostgresDBType: - where = "b.properties->? is not null" - whereTerm = term - case model.MysqlDBType: - where = "JSON_EXTRACT(b.properties, ?) IS NOT NULL" - whereTerm = "$." + term - default: - where = "b.properties LIKE ?" - whereTerm = "%\"" + term + "\"%" - } - boardMembersQ = boardMembersQ.Where(where, whereTerm) - teamMembersQ = teamMembersQ.Where(where, whereTerm) - channelMembersQ = channelMembersQ.Where(where, whereTerm) - } else { // model.BoardSearchFieldTitle - // break search query into space separated words - // and search for all words. - // This should later be upgraded to industrial-strength - // word tokenizer, that uses much more than space - // to break words. - conditions := sq.And{} - for _, word := range strings.Split(strings.TrimSpace(term), " ") { - conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"}) - } - - boardMembersQ = boardMembersQ.Where(conditions) - teamMembersQ = teamMembersQ.Where(conditions) - channelMembersQ = channelMembersQ.Where(conditions) - } - } - - teamMembersSQL, teamMembersArgs, err := teamMembersQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUser error getting teamMembersSQL: %w", err) - } - - channelMembersSQL, channelMembersArgs, err := channelMembersQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUser error getting channelMembersSQL: %w", err) - } - - unionQ := boardMembersQ - user, err := s.GetUserByID(userID) - if err != nil { - return nil, err - } - // NOTE: theoretically, could do e.g. `isGuest := !includePublicBoards` - // but that introduces some tight coupling + fragility - if !user.IsGuest { - unionQ = unionQ. - Prefix("("). - Suffix(") UNION ("+channelMembersSQL+")", channelMembersArgs...) - if includePublicBoards { - unionQ = unionQ.Suffix(" UNION ("+teamMembersSQL+")", teamMembersArgs...) - } - } else if includePublicBoards { - unionQ = unionQ. - Prefix("("). - Suffix(") UNION ("+teamMembersSQL+")", teamMembersArgs...) - } - - unionSQL, unionArgs, err := unionQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUser error getting unionSQL: %w", err) - } - - // if we're using postgres, we need to replace the question mark - // placeholder with the numbered dollar one, now that the full - // query is built - if s.dbType == model.PostgresDBType { - var rErr error - unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL) - if rErr != nil { - return nil, fmt.Errorf("SearchBoardsForUser unable to replace unionSQL placeholders: %w", rErr) - } - } - - rows, err := s.mmDB.Query(unionSQL, unionArgs...) - if err != nil { - s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows, false) -} - -// searchBoardsForUserInTeam returns all boards that match with the -// term that are either private and which the user is a member of, or -// they're open, regardless of the user membership. -// Search is case-insensitive. -func (s *MattermostAuthLayer) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) { - // as we're joining three queries, we need to avoid numbered - // placeholders until the join is done, so we use the default - // question mark placeholder here - builder := s.getQueryBuilder().PlaceholderFormat(sq.Question) - - openBoardsQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b"). - Where(sq.Eq{ - "b.is_template": false, - "b.team_id": teamID, - "b.type": model.BoardTypeOpen, - }) - - memberBoardsQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards AS b"). - Join(s.tablePrefix + "board_members AS bm on b.id = bm.board_id"). - Where(sq.Eq{ - "b.is_template": false, - "b.team_id": teamID, - "bm.user_id": userID, - }) - - channelMemberBoardsQ := builder. - Select(boardFields("b.")...). - From(s.tablePrefix + "boards AS b"). - Join("ChannelMembers AS cm on cm.channelId = b.channel_id"). - Where(sq.Eq{ - "b.is_template": false, - "b.team_id": teamID, - "cm.userId": userID, - }) - - if term != "" { - // break search query into space separated words - // and search for all words. - // This should later be upgraded to industrial-strength - // word tokenizer, that uses much more than space - // to break words. - - conditions := sq.And{} - - for _, word := range strings.Split(strings.TrimSpace(term), " ") { - conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"}) - } - - openBoardsQ = openBoardsQ.Where(conditions) - memberBoardsQ = memberBoardsQ.Where(conditions) - channelMemberBoardsQ = channelMemberBoardsQ.Where(conditions) - } - - memberBoardsSQL, memberBoardsArgs, err := memberBoardsQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting memberBoardsSQL: %w", err) - } - - channelMemberBoardsSQL, channelMemberBoardsArgs, err := channelMemberBoardsQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting channelMemberBoardsSQL: %w", err) - } - - unionQ := openBoardsQ. - Prefix("("). - Suffix(") UNION ("+memberBoardsSQL, memberBoardsArgs...). - Suffix(") UNION ("+channelMemberBoardsSQL+")", channelMemberBoardsArgs...) - - unionSQL, unionArgs, err := unionQ.ToSql() - if err != nil { - return nil, fmt.Errorf("SearchBoardsForUserInTeam error getting unionSQL: %w", err) - } - - // if we're using postgres, we need to replace the question mark - // placeholder with the numbered dollar one, now that the full - // query is built - if s.dbType == model.PostgresDBType { - var rErr error - unionSQL, rErr = sq.Dollar.ReplacePlaceholders(unionSQL) - if rErr != nil { - return nil, fmt.Errorf("SearchBoardsForUserInTeam unable to replace unionSQL placeholders: %w", rErr) - } - } - - rows, err := s.mmDB.Query(unionSQL, unionArgs...) - if err != nil { - s.logger.Error(`searchBoardsForUserInTeam ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows, false) -} - -func (s *MattermostAuthLayer) boardsFromRows(rows *sql.Rows, removeDuplicates bool) ([]*model.Board, error) { - boards := []*model.Board{} - idMap := make(map[string]struct{}) - - for rows.Next() { - var board model.Board - var propertiesBytes []byte - var cardPropertiesBytes []byte - - err := rows.Scan( - &board.ID, - &board.TeamID, - &board.ChannelID, - &board.CreatedBy, - &board.ModifiedBy, - &board.Type, - &board.MinimumRole, - &board.Title, - &board.Description, - &board.Icon, - &board.ShowDescription, - &board.IsTemplate, - &board.TemplateVersion, - &propertiesBytes, - &cardPropertiesBytes, - &board.CreateAt, - &board.UpdateAt, - &board.DeleteAt, - ) - if err != nil { - s.logger.Error("boardsFromRows scan error", mlog.Err(err)) - return nil, err - } - - if removeDuplicates { - if _, ok := idMap[board.ID]; ok { - continue - } - idMap[board.ID] = struct{}{} - } - - err = json.Unmarshal(propertiesBytes, &board.Properties) - if err != nil { - s.logger.Error("board properties unmarshal error", mlog.Err(err)) - return nil, err - } - err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties) - if err != nil { - s.logger.Error("board card properties unmarshal error", mlog.Err(err)) - return nil, err - } - - boards = append(boards, &board) - } - - return boards, nil -} - -func (s *MattermostAuthLayer) GetCloudLimits() (*mm_model.ProductLimits, error) { - return s.servicesAPI.GetCloudLimits() -} - -func (s *MattermostAuthLayer) implicitBoardMembershipsFromRows(rows *sql.Rows) ([]*model.BoardMember, error) { - boardMembers := []*model.BoardMember{} - - for rows.Next() { - var boardMember model.BoardMember - - err := rows.Scan( - &boardMember.UserID, - &boardMember.BoardID, - ) - if err != nil { - return nil, err - } - boardMember.Roles = "editor" - boardMember.SchemeEditor = true - boardMember.Synthetic = true - - boardMembers = append(boardMembers, &boardMember) - } - - return boardMembers, nil -} - -func (s *MattermostAuthLayer) GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) { - bm, originalErr := s.Store.GetMemberForBoard(boardID, userID) - // Explicit membership not found - if model.IsErrNotFound(originalErr) { - if userID == model.SystemUserID { - return nil, model.NewErrNotFound(userID) - } - var user *model.User - // No synthetic memberships for guests - user, err := s.GetUserByID(userID) - if err != nil { - return nil, err - } - if user.IsGuest { - return nil, model.NewErrNotFound("user is a guest") - } - - b, boardErr := s.Store.GetBoard(boardID) - if boardErr != nil { - return nil, boardErr - } - if b.ChannelID != "" { - _, memberErr := s.servicesAPI.GetChannelMember(b.ChannelID, userID) - if memberErr != nil { - var appErr *mm_model.AppError - if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound { - // Plugin API returns error if channel member doesn't exist. - // We're fine if it doesn't exist, so its not an error for us. - message := fmt.Sprintf("member BoardID=%s UserID=%s", boardID, userID) - return nil, model.NewErrNotFound(message) - } - - return nil, memberErr - } - - return &model.BoardMember{ - BoardID: boardID, - UserID: userID, - Roles: "editor", - SchemeAdmin: false, - SchemeEditor: true, - SchemeCommenter: false, - SchemeViewer: false, - Synthetic: true, - }, nil - } - if b.Type == model.BoardTypeOpen && b.IsTemplate { - _, memberErr := s.servicesAPI.GetTeamMember(b.TeamID, userID) - if memberErr != nil { - var appErr *mm_model.AppError - if errors.As(memberErr, &appErr) && appErr.StatusCode == http.StatusNotFound { - return nil, model.NewErrNotFound(userID) - } - return nil, memberErr - } - - return &model.BoardMember{ - BoardID: boardID, - UserID: userID, - Roles: "viewer", - SchemeAdmin: false, - SchemeEditor: false, - SchemeCommenter: false, - SchemeViewer: true, - Synthetic: true, - }, nil - } - } - if originalErr != nil { - return nil, originalErr - } - return bm, nil -} - -func (s *MattermostAuthLayer) GetMembersForUser(userID string) ([]*model.BoardMember, error) { - explicitMembers, err := s.Store.GetMembersForUser(userID) - if err != nil { - s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err)) - return nil, err - } - - query := s.getQueryBuilder(). - Select("CM.userID, B.Id"). - From(s.tablePrefix + "boards AS B"). - Join("ChannelMembers AS CM ON B.channel_id=CM.channelId"). - Where(sq.Eq{"CM.userID": userID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - members := []*model.BoardMember{} - existingMembers := map[string]bool{} - for _, m := range explicitMembers { - members = append(members, m) - existingMembers[m.BoardID] = true - } - - // No synthetic memberships for guests - user, err := s.GetUserByID(userID) - if err != nil { - return nil, err - } - if user.IsGuest { - return members, nil - } - - implicitMembers, err := s.implicitBoardMembershipsFromRows(rows) - if err != nil { - return nil, err - } - for _, m := range implicitMembers { - if !existingMembers[m.BoardID] { - members = append(members, m) - } - } - - return members, nil -} - -func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { - explicitMembers, err := s.Store.GetMembersForBoard(boardID) - if err != nil { - s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err)) - return nil, err - } - - query := s.getQueryBuilder(). - Select("CM.userID, B.Id"). - From(s.tablePrefix + "boards AS B"). - Join("ChannelMembers AS CM ON B.channel_id=CM.channelId"). - Join("Users as U on CM.userID = U.id"). - LeftJoin("Bots as bo on U.id = bo.UserID"). - Where(sq.Eq{"B.id": boardID}). - Where(sq.NotEq{"B.channel_id": ""}). - // Filter out guests as they don't have synthetic membership - Where(sq.NotEq{"U.roles": "system_guest"}). - Where(sq.Eq{"bo.UserId IS NOT NULL": false}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - implicitMembers, err := s.implicitBoardMembershipsFromRows(rows) - if err != nil { - return nil, err - } - members := []*model.BoardMember{} - existingMembers := map[string]bool{} - for _, m := range explicitMembers { - members = append(members, m) - existingMembers[m.UserID] = true - } - for _, m := range implicitMembers { - if !existingMembers[m.UserID] { - members = append(members, m) - } - } - - return members, nil -} - -func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) { - if includePublicBoards { - boards, err := s.SearchBoardsForUserInTeam(teamID, "", userID) - if err != nil { - return nil, err - } - return boards, nil - } - - // retrieve only direct memberships for user - // this is usually done for guests. - members, err := s.GetMembersForUser(userID) - if err != nil { - return nil, err - } - boardIDs := []string{} - for _, m := range members { - boardIDs = append(boardIDs, m.BoardID) - } - - boards, err := s.Store.GetBoardsInTeamByIds(boardIDs, teamID) - if model.IsErrNotFound(err) { - if boards == nil { - boards = []*model.Board{} - } - return boards, nil - } - if err != nil { - return nil, err - } - - return boards, nil -} - -func (s *MattermostAuthLayer) SearchUserChannels(teamID, userID, query string) ([]*mm_model.Channel, error) { - channels, err := s.servicesAPI.GetChannelsForTeamForUser(teamID, userID, false) - if err != nil { - return nil, err - } - lowerQuery := strings.ToLower(query) - - result := []*mm_model.Channel{} - count := 0 - for _, channel := range channels { - if channel.Type != mm_model.ChannelTypeDirect && - channel.Type != mm_model.ChannelTypeGroup && - (strings.Contains(strings.ToLower(channel.Name), lowerQuery) || strings.Contains(strings.ToLower(channel.DisplayName), lowerQuery)) { - result = append(result, channel) - count++ - if count >= 10 { - break - } - } - } - return result, nil -} - -func (s *MattermostAuthLayer) GetChannel(teamID, channelID string) (*mm_model.Channel, error) { - channel, err := s.servicesAPI.GetChannelByID(channelID) - if err != nil { - return nil, err - } - return channel, nil -} - -func (s *MattermostAuthLayer) getBoardsBotID() (string, error) { - if boardsBotID == "" { - var err error - boardsBotID, err = s.servicesAPI.EnsureBot(model.GetDefaultFocalboardBot()) - if err != nil { - s.logger.Error("failed to ensure boards bot", mlog.Err(err)) - return "", err - } - } - return boardsBotID, nil -} - -func (s *MattermostAuthLayer) SendMessage(message, postType string, receipts []string) error { - botID, err := s.getBoardsBotID() - if err != nil { - return err - } - - for _, receipt := range receipts { - channel, err := s.servicesAPI.GetDirectChannel(botID, receipt) - if err != nil { - s.logger.Error( - "failed to get DM channel between system bot and user for receipt", - mlog.String("receipt", receipt), - mlog.String("user_id", receipt), - mlog.Err(err), - ) - continue - } - - if err := s.PostMessage(message, postType, channel.Id); err != nil { - s.logger.Error( - "failed to send message to receipt from SendMessage", - mlog.String("receipt", receipt), - mlog.Err(err), - ) - continue - } - } - return nil -} - -func (s *MattermostAuthLayer) PostMessage(message, postType, channelID string) error { - botID, err := s.getBoardsBotID() - if err != nil { - return err - } - - post := &mm_model.Post{ - Message: message, - UserId: botID, - ChannelId: channelID, - Type: postType, - } - - if _, err := s.servicesAPI.CreatePost(post); err != nil { - s.logger.Error( - "failed to send message to receipt from PostMessage", - mlog.Err(err), - ) - } - return nil -} - -func (s *MattermostAuthLayer) GetUserTimezone(userID string) (string, error) { - user, err := s.servicesAPI.GetUserByID(userID) - if err != nil { - return "", err - } - timezone := user.Timezone - return mm_model.GetPreferredTimezone(timezone), nil -} - -func (s *MattermostAuthLayer) CanSeeUser(seerID string, seenID string) (bool, error) { - mmuser, appErr := s.servicesAPI.GetUserByID(seerID) - if appErr != nil { - return false, appErr - } - if !mmuser.IsGuest() { - return true, nil - } - - query := s.getQueryBuilder(). - Select("1"). - From(s.tablePrefix + "board_members AS BM1"). - Join(s.tablePrefix + "board_members AS BM2 ON BM1.BoardID=BM2.BoardID"). - LeftJoin("Bots b ON ( b.UserId = u.id )"). - Where(sq.Or{ - sq.And{ - sq.Eq{"BM1.UserID": seerID}, - sq.Eq{"BM2.UserID": seenID}, - }, - sq.And{ - sq.Eq{"BM1.UserID": seenID}, - sq.Eq{"BM2.UserID": seerID}, - }, - }).Limit(1) - - rows, err := query.Query() - if err != nil { - return false, err - } - defer s.CloseRows(rows) - - for rows.Next() { - return true, err - } - - query = s.getQueryBuilder(). - Select("1"). - From("ChannelMembers AS CM1"). - Join("ChannelMembers AS CM2 ON CM1.BoardID=CM2.BoardID"). - LeftJoin("Bots b ON ( b.UserId = u.id )"). - Where(sq.Or{ - sq.And{ - sq.Eq{"CM1.UserID": seerID}, - sq.Eq{"CM2.UserID": seenID}, - }, - sq.And{ - sq.Eq{"CM1.UserID": seenID}, - sq.Eq{"CM2.UserID": seerID}, - }, - }).Limit(1) - - rows, err = query.Query() - if err != nil { - return false, err - } - defer s.CloseRows(rows) - - for rows.Next() { - return true, err - } - - return false, nil -} diff --git a/server/boards/services/store/mattermostauthlayer/mattermostauthlayer_test.go b/server/boards/services/store/mattermostauthlayer/mattermostauthlayer_test.go deleted file mode 100644 index 247fd7dca0..0000000000 --- a/server/boards/services/store/mattermostauthlayer/mattermostauthlayer_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package mattermostauthlayer - -import ( - "errors" - "testing" - - "github.com/golang/mock/gomock" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/model" - mockservicesapi "github.com/mattermost/mattermost/server/v8/boards/model/mocks" - - "github.com/stretchr/testify/require" -) - -var errTest = errors.New("failed to patch bot") - -func TestGetBoardsBotID(t *testing.T) { - ctrl := gomock.NewController(t) - servicesAPI := mockservicesapi.NewMockServicesAPI(ctrl) - - mmAuthLayer, _ := New("test", nil, nil, mlog.CreateConsoleTestLogger(true, mlog.LvlError), servicesAPI, "") - - servicesAPI.EXPECT().EnsureBot(model.GetDefaultFocalboardBot()).Return("", errTest) - _, err := mmAuthLayer.getBoardsBotID() - require.NotEmpty(t, err) - - servicesAPI.EXPECT().EnsureBot(model.GetDefaultFocalboardBot()).Return("TestBotID", nil).Times(1) - botID, err := mmAuthLayer.getBoardsBotID() - require.Empty(t, err) - require.NotEmpty(t, botID) - require.Equal(t, "TestBotID", botID) - - // Call again, should not call "EnsureBot" - botID, err = mmAuthLayer.getBoardsBotID() - require.Empty(t, err) - require.NotEmpty(t, botID) - require.Equal(t, "TestBotID", botID) -} diff --git a/server/boards/services/store/mockstore/mockstore.go b/server/boards/services/store/mockstore/mockstore.go deleted file mode 100644 index 9b5f61cfd8..0000000000 --- a/server/boards/services/store/mockstore/mockstore.go +++ /dev/null @@ -1,1794 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/services/store (interfaces: Store) - -// Package mockstore is a generated GoMock package. -package mockstore - -import ( - reflect "reflect" - time "time" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/public/model" - model0 "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// MockStore is a mock of Store interface. -type MockStore struct { - ctrl *gomock.Controller - recorder *MockStoreMockRecorder -} - -// MockStoreMockRecorder is the mock recorder for MockStore. -type MockStoreMockRecorder struct { - mock *MockStore -} - -// NewMockStore creates a new mock instance. -func NewMockStore(ctrl *gomock.Controller) *MockStore { - mock := &MockStore{ctrl: ctrl} - mock.recorder = &MockStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStore) EXPECT() *MockStoreMockRecorder { - return m.recorder -} - -// AddUpdateCategoryBoard mocks base method. -func (m *MockStore) AddUpdateCategoryBoard(arg0, arg1 string, arg2 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddUpdateCategoryBoard", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddUpdateCategoryBoard indicates an expected call of AddUpdateCategoryBoard. -func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2) -} - -// CanSeeUser mocks base method. -func (m *MockStore) CanSeeUser(arg0, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CanSeeUser", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CanSeeUser indicates an expected call of CanSeeUser. -func (mr *MockStoreMockRecorder) CanSeeUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSeeUser", reflect.TypeOf((*MockStore)(nil).CanSeeUser), arg0, arg1) -} - -// CleanUpSessions mocks base method. -func (m *MockStore) CleanUpSessions(arg0 int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CleanUpSessions", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// CleanUpSessions indicates an expected call of CleanUpSessions. -func (mr *MockStoreMockRecorder) CleanUpSessions(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpSessions", reflect.TypeOf((*MockStore)(nil).CleanUpSessions), arg0) -} - -// CreateBoardsAndBlocks mocks base method. -func (m *MockStore) CreateBoardsAndBlocks(arg0 *model0.BoardsAndBlocks, arg1 string) (*model0.BoardsAndBlocks, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBoardsAndBlocks", arg0, arg1) - ret0, _ := ret[0].(*model0.BoardsAndBlocks) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateBoardsAndBlocks indicates an expected call of CreateBoardsAndBlocks. -func (mr *MockStoreMockRecorder) CreateBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).CreateBoardsAndBlocks), arg0, arg1) -} - -// CreateBoardsAndBlocksWithAdmin mocks base method. -func (m *MockStore) CreateBoardsAndBlocksWithAdmin(arg0 *model0.BoardsAndBlocks, arg1 string) (*model0.BoardsAndBlocks, []*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBoardsAndBlocksWithAdmin", arg0, arg1) - ret0, _ := ret[0].(*model0.BoardsAndBlocks) - ret1, _ := ret[1].([]*model0.BoardMember) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// CreateBoardsAndBlocksWithAdmin indicates an expected call of CreateBoardsAndBlocksWithAdmin. -func (mr *MockStoreMockRecorder) CreateBoardsAndBlocksWithAdmin(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBoardsAndBlocksWithAdmin", reflect.TypeOf((*MockStore)(nil).CreateBoardsAndBlocksWithAdmin), arg0, arg1) -} - -// CreateCategory mocks base method. -func (m *MockStore) CreateCategory(arg0 model0.Category) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCategory", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateCategory indicates an expected call of CreateCategory. -func (mr *MockStoreMockRecorder) CreateCategory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCategory", reflect.TypeOf((*MockStore)(nil).CreateCategory), arg0) -} - -// CreateSession mocks base method. -func (m *MockStore) CreateSession(arg0 *model0.Session) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSession", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateSession indicates an expected call of CreateSession. -func (mr *MockStoreMockRecorder) CreateSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), arg0) -} - -// CreateSubscription mocks base method. -func (m *MockStore) CreateSubscription(arg0 *model0.Subscription) (*model0.Subscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSubscription", arg0) - ret0, _ := ret[0].(*model0.Subscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateSubscription indicates an expected call of CreateSubscription. -func (mr *MockStoreMockRecorder) CreateSubscription(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockStore)(nil).CreateSubscription), arg0) -} - -// CreateUser mocks base method. -func (m *MockStore) CreateUser(arg0 *model0.User) (*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", arg0) - ret0, _ := ret[0].(*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateUser indicates an expected call of CreateUser. -func (mr *MockStoreMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0) -} - -// DBType mocks base method. -func (m *MockStore) DBType() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DBType") - ret0, _ := ret[0].(string) - return ret0 -} - -// DBType indicates an expected call of DBType. -func (mr *MockStoreMockRecorder) DBType() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBType", reflect.TypeOf((*MockStore)(nil).DBType)) -} - -// DBVersion mocks base method. -func (m *MockStore) DBVersion() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DBVersion") - ret0, _ := ret[0].(string) - return ret0 -} - -// DBVersion indicates an expected call of DBVersion. -func (mr *MockStoreMockRecorder) DBVersion() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBVersion", reflect.TypeOf((*MockStore)(nil).DBVersion)) -} - -// DeleteBlock mocks base method. -func (m *MockStore) DeleteBlock(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBlock", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteBlock indicates an expected call of DeleteBlock. -func (mr *MockStoreMockRecorder) DeleteBlock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlock", reflect.TypeOf((*MockStore)(nil).DeleteBlock), arg0, arg1) -} - -// DeleteBlockRecord mocks base method. -func (m *MockStore) DeleteBlockRecord(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBlockRecord", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteBlockRecord indicates an expected call of DeleteBlockRecord. -func (mr *MockStoreMockRecorder) DeleteBlockRecord(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBlockRecord", reflect.TypeOf((*MockStore)(nil).DeleteBlockRecord), arg0, arg1) -} - -// DeleteBoard mocks base method. -func (m *MockStore) DeleteBoard(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBoard", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteBoard indicates an expected call of DeleteBoard. -func (mr *MockStoreMockRecorder) DeleteBoard(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoard", reflect.TypeOf((*MockStore)(nil).DeleteBoard), arg0, arg1) -} - -// DeleteBoardRecord mocks base method. -func (m *MockStore) DeleteBoardRecord(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBoardRecord", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteBoardRecord indicates an expected call of DeleteBoardRecord. -func (mr *MockStoreMockRecorder) DeleteBoardRecord(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoardRecord", reflect.TypeOf((*MockStore)(nil).DeleteBoardRecord), arg0, arg1) -} - -// DeleteBoardsAndBlocks mocks base method. -func (m *MockStore) DeleteBoardsAndBlocks(arg0 *model0.DeleteBoardsAndBlocks, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteBoardsAndBlocks", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteBoardsAndBlocks indicates an expected call of DeleteBoardsAndBlocks. -func (mr *MockStoreMockRecorder) DeleteBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).DeleteBoardsAndBlocks), arg0, arg1) -} - -// DeleteCategory mocks base method. -func (m *MockStore) DeleteCategory(arg0, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCategory", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteCategory indicates an expected call of DeleteCategory. -func (mr *MockStoreMockRecorder) DeleteCategory(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCategory", reflect.TypeOf((*MockStore)(nil).DeleteCategory), arg0, arg1, arg2) -} - -// DeleteMember mocks base method. -func (m *MockStore) DeleteMember(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteMember", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteMember indicates an expected call of DeleteMember. -func (mr *MockStoreMockRecorder) DeleteMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMember", reflect.TypeOf((*MockStore)(nil).DeleteMember), arg0, arg1) -} - -// DeleteNotificationHint mocks base method. -func (m *MockStore) DeleteNotificationHint(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteNotificationHint", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteNotificationHint indicates an expected call of DeleteNotificationHint. -func (mr *MockStoreMockRecorder) DeleteNotificationHint(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNotificationHint", reflect.TypeOf((*MockStore)(nil).DeleteNotificationHint), arg0) -} - -// DeleteSession mocks base method. -func (m *MockStore) DeleteSession(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSession", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSession indicates an expected call of DeleteSession. -func (mr *MockStoreMockRecorder) DeleteSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSession", reflect.TypeOf((*MockStore)(nil).DeleteSession), arg0) -} - -// DeleteSubscription mocks base method. -func (m *MockStore) DeleteSubscription(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSubscription", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSubscription indicates an expected call of DeleteSubscription. -func (mr *MockStoreMockRecorder) DeleteSubscription(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscription", reflect.TypeOf((*MockStore)(nil).DeleteSubscription), arg0, arg1) -} - -// DropAllTables mocks base method. -func (m *MockStore) DropAllTables() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DropAllTables") - ret0, _ := ret[0].(error) - return ret0 -} - -// DropAllTables indicates an expected call of DropAllTables. -func (mr *MockStoreMockRecorder) DropAllTables() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropAllTables", reflect.TypeOf((*MockStore)(nil).DropAllTables)) -} - -// DuplicateBlock mocks base method. -func (m *MockStore) DuplicateBlock(arg0, arg1, arg2 string, arg3 bool) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DuplicateBlock", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DuplicateBlock indicates an expected call of DuplicateBlock. -func (mr *MockStoreMockRecorder) DuplicateBlock(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateBlock", reflect.TypeOf((*MockStore)(nil).DuplicateBlock), arg0, arg1, arg2, arg3) -} - -// DuplicateBoard mocks base method. -func (m *MockStore) DuplicateBoard(arg0, arg1, arg2 string, arg3 bool) (*model0.BoardsAndBlocks, []*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DuplicateBoard", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model0.BoardsAndBlocks) - ret1, _ := ret[1].([]*model0.BoardMember) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// DuplicateBoard indicates an expected call of DuplicateBoard. -func (mr *MockStoreMockRecorder) DuplicateBoard(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DuplicateBoard", reflect.TypeOf((*MockStore)(nil).DuplicateBoard), arg0, arg1, arg2, arg3) -} - -// GetActiveUserCount mocks base method. -func (m *MockStore) GetActiveUserCount(arg0 int64) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetActiveUserCount", arg0) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetActiveUserCount indicates an expected call of GetActiveUserCount. -func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), arg0) -} - -// GetAllTeams mocks base method. -func (m *MockStore) GetAllTeams() ([]*model0.Team, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAllTeams") - ret0, _ := ret[0].([]*model0.Team) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAllTeams indicates an expected call of GetAllTeams. -func (mr *MockStoreMockRecorder) GetAllTeams() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTeams", reflect.TypeOf((*MockStore)(nil).GetAllTeams)) -} - -// GetBlock mocks base method. -func (m *MockStore) GetBlock(arg0 string) (*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlock", arg0) - ret0, _ := ret[0].(*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlock indicates an expected call of GetBlock. -func (mr *MockStoreMockRecorder) GetBlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0) -} - -// GetBlockCountsByType mocks base method. -func (m *MockStore) GetBlockCountsByType() (map[string]int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockCountsByType") - ret0, _ := ret[0].(map[string]int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlockCountsByType indicates an expected call of GetBlockCountsByType. -func (mr *MockStoreMockRecorder) GetBlockCountsByType() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockCountsByType", reflect.TypeOf((*MockStore)(nil).GetBlockCountsByType)) -} - -// GetBlockHistory mocks base method. -func (m *MockStore) GetBlockHistory(arg0 string, arg1 model0.QueryBlockHistoryOptions) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockHistory", arg0, arg1) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlockHistory indicates an expected call of GetBlockHistory. -func (mr *MockStoreMockRecorder) GetBlockHistory(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistory", reflect.TypeOf((*MockStore)(nil).GetBlockHistory), arg0, arg1) -} - -// GetBlockHistoryDescendants mocks base method. -func (m *MockStore) GetBlockHistoryDescendants(arg0 string, arg1 model0.QueryBlockHistoryOptions) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockHistoryDescendants", arg0, arg1) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlockHistoryDescendants indicates an expected call of GetBlockHistoryDescendants. -func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1) -} - -// GetBlockHistoryNewestChildren mocks base method. -func (m *MockStore) GetBlockHistoryNewestChildren(arg0 string, arg1 model0.QueryBlockHistoryChildOptions) ([]*model0.Block, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlockHistoryNewestChildren", arg0, arg1) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBlockHistoryNewestChildren indicates an expected call of GetBlockHistoryNewestChildren. -func (mr *MockStoreMockRecorder) GetBlockHistoryNewestChildren(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryNewestChildren", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryNewestChildren), arg0, arg1) -} - -// GetBlocks mocks base method. -func (m *MockStore) GetBlocks(arg0 model0.QueryBlocksOptions) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlocks", arg0) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlocks indicates an expected call of GetBlocks. -func (mr *MockStoreMockRecorder) GetBlocks(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocks", reflect.TypeOf((*MockStore)(nil).GetBlocks), arg0) -} - -// GetBlocksByIDs mocks base method. -func (m *MockStore) GetBlocksByIDs(arg0 []string) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlocksByIDs", arg0) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlocksByIDs indicates an expected call of GetBlocksByIDs. -func (mr *MockStoreMockRecorder) GetBlocksByIDs(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByIDs", reflect.TypeOf((*MockStore)(nil).GetBlocksByIDs), arg0) -} - -// GetBlocksComplianceHistory mocks base method. -func (m *MockStore) GetBlocksComplianceHistory(arg0 model0.QueryBlocksComplianceHistoryOptions) ([]*model0.BlockHistory, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlocksComplianceHistory", arg0) - ret0, _ := ret[0].([]*model0.BlockHistory) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBlocksComplianceHistory indicates an expected call of GetBlocksComplianceHistory. -func (mr *MockStoreMockRecorder) GetBlocksComplianceHistory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBlocksComplianceHistory), arg0) -} - -// GetBoard mocks base method. -func (m *MockStore) GetBoard(arg0 string) (*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoard", arg0) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoard indicates an expected call of GetBoard. -func (mr *MockStoreMockRecorder) GetBoard(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoard", reflect.TypeOf((*MockStore)(nil).GetBoard), arg0) -} - -// GetBoardAndCard mocks base method. -func (m *MockStore) GetBoardAndCard(arg0 *model0.Block) (*model0.Board, *model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardAndCard", arg0) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(*model0.Block) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBoardAndCard indicates an expected call of GetBoardAndCard. -func (mr *MockStoreMockRecorder) GetBoardAndCard(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCard", reflect.TypeOf((*MockStore)(nil).GetBoardAndCard), arg0) -} - -// GetBoardAndCardByID mocks base method. -func (m *MockStore) GetBoardAndCardByID(arg0 string) (*model0.Board, *model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardAndCardByID", arg0) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(*model0.Block) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBoardAndCardByID indicates an expected call of GetBoardAndCardByID. -func (mr *MockStoreMockRecorder) GetBoardAndCardByID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardAndCardByID", reflect.TypeOf((*MockStore)(nil).GetBoardAndCardByID), arg0) -} - -// GetBoardCount mocks base method. -func (m *MockStore) GetBoardCount() (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardCount") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardCount indicates an expected call of GetBoardCount. -func (mr *MockStoreMockRecorder) GetBoardCount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardCount", reflect.TypeOf((*MockStore)(nil).GetBoardCount)) -} - -// GetBoardHistory mocks base method. -func (m *MockStore) GetBoardHistory(arg0 string, arg1 model0.QueryBoardHistoryOptions) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardHistory", arg0, arg1) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardHistory indicates an expected call of GetBoardHistory. -func (mr *MockStoreMockRecorder) GetBoardHistory(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardHistory", reflect.TypeOf((*MockStore)(nil).GetBoardHistory), arg0, arg1) -} - -// GetBoardMemberHistory mocks base method. -func (m *MockStore) GetBoardMemberHistory(arg0, arg1 string, arg2 uint64) ([]*model0.BoardMemberHistoryEntry, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardMemberHistory", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model0.BoardMemberHistoryEntry) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardMemberHistory indicates an expected call of GetBoardMemberHistory. -func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardMemberHistory", reflect.TypeOf((*MockStore)(nil).GetBoardMemberHistory), arg0, arg1, arg2) -} - -// GetBoardsComplianceHistory mocks base method. -func (m *MockStore) GetBoardsComplianceHistory(arg0 model0.QueryBoardsComplianceHistoryOptions) ([]*model0.BoardHistory, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardsComplianceHistory", arg0) - ret0, _ := ret[0].([]*model0.BoardHistory) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBoardsComplianceHistory indicates an expected call of GetBoardsComplianceHistory. -func (mr *MockStoreMockRecorder) GetBoardsComplianceHistory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBoardsComplianceHistory), arg0) -} - -// GetBoardsForCompliance mocks base method. -func (m *MockStore) GetBoardsForCompliance(arg0 model0.QueryBoardsForComplianceOptions) ([]*model0.Board, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardsForCompliance", arg0) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetBoardsForCompliance indicates an expected call of GetBoardsForCompliance. -func (mr *MockStoreMockRecorder) GetBoardsForCompliance(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForCompliance", reflect.TypeOf((*MockStore)(nil).GetBoardsForCompliance), arg0) -} - -// GetBoardsForUserAndTeam mocks base method. -func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string, arg2 bool) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam. -func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1, arg2) -} - -// GetBoardsInTeamByIds mocks base method. -func (m *MockStore) GetBoardsInTeamByIds(arg0 []string, arg1 string) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBoardsInTeamByIds", arg0, arg1) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBoardsInTeamByIds indicates an expected call of GetBoardsInTeamByIds. -func (mr *MockStoreMockRecorder) GetBoardsInTeamByIds(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsInTeamByIds", reflect.TypeOf((*MockStore)(nil).GetBoardsInTeamByIds), arg0, arg1) -} - -// GetCardLimitTimestamp mocks base method. -func (m *MockStore) GetCardLimitTimestamp() (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCardLimitTimestamp") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCardLimitTimestamp indicates an expected call of GetCardLimitTimestamp. -func (mr *MockStoreMockRecorder) GetCardLimitTimestamp() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).GetCardLimitTimestamp)) -} - -// GetCategory mocks base method. -func (m *MockStore) GetCategory(arg0 string) (*model0.Category, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCategory", arg0) - ret0, _ := ret[0].(*model0.Category) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCategory indicates an expected call of GetCategory. -func (mr *MockStoreMockRecorder) GetCategory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0) -} - -// GetChannel mocks base method. -func (m *MockStore) GetChannel(arg0, arg1 string) (*model.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannel", arg0, arg1) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChannel indicates an expected call of GetChannel. -func (mr *MockStoreMockRecorder) GetChannel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannel", reflect.TypeOf((*MockStore)(nil).GetChannel), arg0, arg1) -} - -// GetCloudLimits mocks base method. -func (m *MockStore) GetCloudLimits() (*model.ProductLimits, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCloudLimits") - ret0, _ := ret[0].(*model.ProductLimits) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCloudLimits indicates an expected call of GetCloudLimits. -func (mr *MockStoreMockRecorder) GetCloudLimits() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockStore)(nil).GetCloudLimits)) -} - -// GetFileInfo mocks base method. -func (m *MockStore) GetFileInfo(arg0 string) (*model.FileInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfo", arg0) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFileInfo indicates an expected call of GetFileInfo. -func (mr *MockStoreMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockStore)(nil).GetFileInfo), arg0) -} - -// GetLicense mocks base method. -func (m *MockStore) GetLicense() *model.License { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLicense") - ret0, _ := ret[0].(*model.License) - return ret0 -} - -// GetLicense indicates an expected call of GetLicense. -func (mr *MockStoreMockRecorder) GetLicense() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockStore)(nil).GetLicense)) -} - -// GetMemberForBoard mocks base method. -func (m *MockStore) GetMemberForBoard(arg0, arg1 string) (*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMemberForBoard", arg0, arg1) - ret0, _ := ret[0].(*model0.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMemberForBoard indicates an expected call of GetMemberForBoard. -func (mr *MockStoreMockRecorder) GetMemberForBoard(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberForBoard", reflect.TypeOf((*MockStore)(nil).GetMemberForBoard), arg0, arg1) -} - -// GetMembersForBoard mocks base method. -func (m *MockStore) GetMembersForBoard(arg0 string) ([]*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMembersForBoard", arg0) - ret0, _ := ret[0].([]*model0.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMembersForBoard indicates an expected call of GetMembersForBoard. -func (mr *MockStoreMockRecorder) GetMembersForBoard(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForBoard", reflect.TypeOf((*MockStore)(nil).GetMembersForBoard), arg0) -} - -// GetMembersForUser mocks base method. -func (m *MockStore) GetMembersForUser(arg0 string) ([]*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMembersForUser", arg0) - ret0, _ := ret[0].([]*model0.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMembersForUser indicates an expected call of GetMembersForUser. -func (mr *MockStoreMockRecorder) GetMembersForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForUser", reflect.TypeOf((*MockStore)(nil).GetMembersForUser), arg0) -} - -// GetNextNotificationHint mocks base method. -func (m *MockStore) GetNextNotificationHint(arg0 bool) (*model0.NotificationHint, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNextNotificationHint", arg0) - ret0, _ := ret[0].(*model0.NotificationHint) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetNextNotificationHint indicates an expected call of GetNextNotificationHint. -func (mr *MockStoreMockRecorder) GetNextNotificationHint(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextNotificationHint", reflect.TypeOf((*MockStore)(nil).GetNextNotificationHint), arg0) -} - -// GetNotificationHint mocks base method. -func (m *MockStore) GetNotificationHint(arg0 string) (*model0.NotificationHint, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationHint", arg0) - ret0, _ := ret[0].(*model0.NotificationHint) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetNotificationHint indicates an expected call of GetNotificationHint. -func (mr *MockStoreMockRecorder) GetNotificationHint(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationHint", reflect.TypeOf((*MockStore)(nil).GetNotificationHint), arg0) -} - -// GetRegisteredUserCount mocks base method. -func (m *MockStore) GetRegisteredUserCount() (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRegisteredUserCount") - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRegisteredUserCount indicates an expected call of GetRegisteredUserCount. -func (mr *MockStoreMockRecorder) GetRegisteredUserCount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegisteredUserCount", reflect.TypeOf((*MockStore)(nil).GetRegisteredUserCount)) -} - -// GetSession mocks base method. -func (m *MockStore) GetSession(arg0 string, arg1 int64) (*model0.Session, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSession", arg0, arg1) - ret0, _ := ret[0].(*model0.Session) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSession indicates an expected call of GetSession. -func (mr *MockStoreMockRecorder) GetSession(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), arg0, arg1) -} - -// GetSharing mocks base method. -func (m *MockStore) GetSharing(arg0 string) (*model0.Sharing, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSharing", arg0) - ret0, _ := ret[0].(*model0.Sharing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSharing indicates an expected call of GetSharing. -func (mr *MockStoreMockRecorder) GetSharing(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharing", reflect.TypeOf((*MockStore)(nil).GetSharing), arg0) -} - -// GetSubTree2 mocks base method. -func (m *MockStore) GetSubTree2(arg0, arg1 string, arg2 model0.QuerySubtreeOptions) ([]*model0.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubTree2", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model0.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubTree2 indicates an expected call of GetSubTree2. -func (mr *MockStoreMockRecorder) GetSubTree2(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubTree2", reflect.TypeOf((*MockStore)(nil).GetSubTree2), arg0, arg1, arg2) -} - -// GetSubscribersCountForBlock mocks base method. -func (m *MockStore) GetSubscribersCountForBlock(arg0 string) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscribersCountForBlock", arg0) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubscribersCountForBlock indicates an expected call of GetSubscribersCountForBlock. -func (mr *MockStoreMockRecorder) GetSubscribersCountForBlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersCountForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersCountForBlock), arg0) -} - -// GetSubscribersForBlock mocks base method. -func (m *MockStore) GetSubscribersForBlock(arg0 string) ([]*model0.Subscriber, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscribersForBlock", arg0) - ret0, _ := ret[0].([]*model0.Subscriber) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubscribersForBlock indicates an expected call of GetSubscribersForBlock. -func (mr *MockStoreMockRecorder) GetSubscribersForBlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscribersForBlock", reflect.TypeOf((*MockStore)(nil).GetSubscribersForBlock), arg0) -} - -// GetSubscription mocks base method. -func (m *MockStore) GetSubscription(arg0, arg1 string) (*model0.Subscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscription", arg0, arg1) - ret0, _ := ret[0].(*model0.Subscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubscription indicates an expected call of GetSubscription. -func (mr *MockStoreMockRecorder) GetSubscription(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscription", reflect.TypeOf((*MockStore)(nil).GetSubscription), arg0, arg1) -} - -// GetSubscriptions mocks base method. -func (m *MockStore) GetSubscriptions(arg0 string) ([]*model0.Subscription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubscriptions", arg0) - ret0, _ := ret[0].([]*model0.Subscription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubscriptions indicates an expected call of GetSubscriptions. -func (mr *MockStoreMockRecorder) GetSubscriptions(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptions", reflect.TypeOf((*MockStore)(nil).GetSubscriptions), arg0) -} - -// GetSystemSetting mocks base method. -func (m *MockStore) GetSystemSetting(arg0 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSystemSetting", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSystemSetting indicates an expected call of GetSystemSetting. -func (mr *MockStoreMockRecorder) GetSystemSetting(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemSetting", reflect.TypeOf((*MockStore)(nil).GetSystemSetting), arg0) -} - -// GetSystemSettings mocks base method. -func (m *MockStore) GetSystemSettings() (map[string]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSystemSettings") - ret0, _ := ret[0].(map[string]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSystemSettings indicates an expected call of GetSystemSettings. -func (mr *MockStoreMockRecorder) GetSystemSettings() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemSettings", reflect.TypeOf((*MockStore)(nil).GetSystemSettings)) -} - -// GetTeam mocks base method. -func (m *MockStore) GetTeam(arg0 string) (*model0.Team, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeam", arg0) - ret0, _ := ret[0].(*model0.Team) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTeam indicates an expected call of GetTeam. -func (mr *MockStoreMockRecorder) GetTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeam", reflect.TypeOf((*MockStore)(nil).GetTeam), arg0) -} - -// GetTeamBoardsInsights mocks base method. -func (m *MockStore) GetTeamBoardsInsights(arg0 string, arg1 int64, arg2, arg3 int, arg4 []string) (*model0.BoardInsightsList, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamBoardsInsights", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(*model0.BoardInsightsList) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTeamBoardsInsights indicates an expected call of GetTeamBoardsInsights. -func (mr *MockStoreMockRecorder) GetTeamBoardsInsights(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamBoardsInsights", reflect.TypeOf((*MockStore)(nil).GetTeamBoardsInsights), arg0, arg1, arg2, arg3, arg4) -} - -// GetTeamCount mocks base method. -func (m *MockStore) GetTeamCount() (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamCount") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTeamCount indicates an expected call of GetTeamCount. -func (mr *MockStoreMockRecorder) GetTeamCount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamCount", reflect.TypeOf((*MockStore)(nil).GetTeamCount)) -} - -// GetTeamsForUser mocks base method. -func (m *MockStore) GetTeamsForUser(arg0 string) ([]*model0.Team, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamsForUser", arg0) - ret0, _ := ret[0].([]*model0.Team) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTeamsForUser indicates an expected call of GetTeamsForUser. -func (mr *MockStoreMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsForUser", reflect.TypeOf((*MockStore)(nil).GetTeamsForUser), arg0) -} - -// GetTemplateBoards mocks base method. -func (m *MockStore) GetTemplateBoards(arg0, arg1 string) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTemplateBoards", arg0, arg1) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTemplateBoards indicates an expected call of GetTemplateBoards. -func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1) -} - -// GetUsedCardsCount mocks base method. -func (m *MockStore) GetUsedCardsCount() (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsedCardsCount") - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUsedCardsCount indicates an expected call of GetUsedCardsCount. -func (mr *MockStoreMockRecorder) GetUsedCardsCount() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsedCardsCount", reflect.TypeOf((*MockStore)(nil).GetUsedCardsCount)) -} - -// GetUserBoardsInsights mocks base method. -func (m *MockStore) GetUserBoardsInsights(arg0, arg1 string, arg2 int64, arg3, arg4 int, arg5 []string) (*model0.BoardInsightsList, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserBoardsInsights", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].(*model0.BoardInsightsList) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserBoardsInsights indicates an expected call of GetUserBoardsInsights. -func (mr *MockStoreMockRecorder) GetUserBoardsInsights(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserBoardsInsights", reflect.TypeOf((*MockStore)(nil).GetUserBoardsInsights), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// GetUserByEmail mocks base method. -func (m *MockStore) GetUserByEmail(arg0 string) (*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByEmail", arg0) - ret0, _ := ret[0].(*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByEmail indicates an expected call of GetUserByEmail. -func (mr *MockStoreMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockStore)(nil).GetUserByEmail), arg0) -} - -// GetUserByID mocks base method. -func (m *MockStore) GetUserByID(arg0 string) (*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByID", arg0) - ret0, _ := ret[0].(*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByID indicates an expected call of GetUserByID. -func (mr *MockStoreMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockStore)(nil).GetUserByID), arg0) -} - -// GetUserByUsername mocks base method. -func (m *MockStore) GetUserByUsername(arg0 string) (*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByUsername", arg0) - ret0, _ := ret[0].(*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserByUsername indicates an expected call of GetUserByUsername. -func (mr *MockStoreMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), arg0) -} - -// GetUserCategories mocks base method. -func (m *MockStore) GetUserCategories(arg0, arg1 string) ([]model0.Category, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserCategories", arg0, arg1) - ret0, _ := ret[0].([]model0.Category) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserCategories indicates an expected call of GetUserCategories. -func (mr *MockStoreMockRecorder) GetUserCategories(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCategories", reflect.TypeOf((*MockStore)(nil).GetUserCategories), arg0, arg1) -} - -// GetUserCategoryBoards mocks base method. -func (m *MockStore) GetUserCategoryBoards(arg0, arg1 string) ([]model0.CategoryBoards, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserCategoryBoards", arg0, arg1) - ret0, _ := ret[0].([]model0.CategoryBoards) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserCategoryBoards indicates an expected call of GetUserCategoryBoards. -func (mr *MockStoreMockRecorder) GetUserCategoryBoards(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCategoryBoards", reflect.TypeOf((*MockStore)(nil).GetUserCategoryBoards), arg0, arg1) -} - -// GetUserPreferences mocks base method. -func (m *MockStore) GetUserPreferences(arg0 string) (model.Preferences, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserPreferences", arg0) - ret0, _ := ret[0].(model.Preferences) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserPreferences indicates an expected call of GetUserPreferences. -func (mr *MockStoreMockRecorder) GetUserPreferences(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPreferences", reflect.TypeOf((*MockStore)(nil).GetUserPreferences), arg0) -} - -// GetUserTimezone mocks base method. -func (m *MockStore) GetUserTimezone(arg0 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserTimezone", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserTimezone indicates an expected call of GetUserTimezone. -func (mr *MockStoreMockRecorder) GetUserTimezone(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTimezone", reflect.TypeOf((*MockStore)(nil).GetUserTimezone), arg0) -} - -// GetUsersByTeam mocks base method. -func (m *MockStore) GetUsersByTeam(arg0, arg1 string, arg2, arg3 bool) ([]*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersByTeam", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUsersByTeam indicates an expected call of GetUsersByTeam. -func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0, arg1, arg2, arg3) -} - -// GetUsersList mocks base method. -func (m *MockStore) GetUsersList(arg0 []string, arg1, arg2 bool) ([]*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersList", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUsersList indicates an expected call of GetUsersList. -func (mr *MockStoreMockRecorder) GetUsersList(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersList", reflect.TypeOf((*MockStore)(nil).GetUsersList), arg0, arg1, arg2) -} - -// InsertBlock mocks base method. -func (m *MockStore) InsertBlock(arg0 *model0.Block, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBlock", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// InsertBlock indicates an expected call of InsertBlock. -func (mr *MockStoreMockRecorder) InsertBlock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockStore)(nil).InsertBlock), arg0, arg1) -} - -// InsertBlocks mocks base method. -func (m *MockStore) InsertBlocks(arg0 []*model0.Block, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBlocks", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// InsertBlocks indicates an expected call of InsertBlocks. -func (mr *MockStoreMockRecorder) InsertBlocks(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlocks", reflect.TypeOf((*MockStore)(nil).InsertBlocks), arg0, arg1) -} - -// InsertBoard mocks base method. -func (m *MockStore) InsertBoard(arg0 *model0.Board, arg1 string) (*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBoard", arg0, arg1) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertBoard indicates an expected call of InsertBoard. -func (mr *MockStoreMockRecorder) InsertBoard(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBoard", reflect.TypeOf((*MockStore)(nil).InsertBoard), arg0, arg1) -} - -// InsertBoardWithAdmin mocks base method. -func (m *MockStore) InsertBoardWithAdmin(arg0 *model0.Board, arg1 string) (*model0.Board, *model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBoardWithAdmin", arg0, arg1) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(*model0.BoardMember) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// InsertBoardWithAdmin indicates an expected call of InsertBoardWithAdmin. -func (mr *MockStoreMockRecorder) InsertBoardWithAdmin(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBoardWithAdmin", reflect.TypeOf((*MockStore)(nil).InsertBoardWithAdmin), arg0, arg1) -} - -// PatchBlock mocks base method. -func (m *MockStore) PatchBlock(arg0 string, arg1 *model0.BlockPatch, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBlock", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// PatchBlock indicates an expected call of PatchBlock. -func (mr *MockStoreMockRecorder) PatchBlock(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlock", reflect.TypeOf((*MockStore)(nil).PatchBlock), arg0, arg1, arg2) -} - -// PatchBlocks mocks base method. -func (m *MockStore) PatchBlocks(arg0 *model0.BlockPatchBatch, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBlocks", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// PatchBlocks indicates an expected call of PatchBlocks. -func (mr *MockStoreMockRecorder) PatchBlocks(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBlocks", reflect.TypeOf((*MockStore)(nil).PatchBlocks), arg0, arg1) -} - -// PatchBoard mocks base method. -func (m *MockStore) PatchBoard(arg0 string, arg1 *model0.BoardPatch, arg2 string) (*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBoard", arg0, arg1, arg2) - ret0, _ := ret[0].(*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PatchBoard indicates an expected call of PatchBoard. -func (mr *MockStoreMockRecorder) PatchBoard(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBoard", reflect.TypeOf((*MockStore)(nil).PatchBoard), arg0, arg1, arg2) -} - -// PatchBoardsAndBlocks mocks base method. -func (m *MockStore) PatchBoardsAndBlocks(arg0 *model0.PatchBoardsAndBlocks, arg1 string) (*model0.BoardsAndBlocks, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBoardsAndBlocks", arg0, arg1) - ret0, _ := ret[0].(*model0.BoardsAndBlocks) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PatchBoardsAndBlocks indicates an expected call of PatchBoardsAndBlocks. -func (mr *MockStoreMockRecorder) PatchBoardsAndBlocks(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBoardsAndBlocks", reflect.TypeOf((*MockStore)(nil).PatchBoardsAndBlocks), arg0, arg1) -} - -// PatchUserPreferences mocks base method. -func (m *MockStore) PatchUserPreferences(arg0 string, arg1 model0.UserPreferencesPatch) (model.Preferences, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchUserPreferences", arg0, arg1) - ret0, _ := ret[0].(model.Preferences) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PatchUserPreferences indicates an expected call of PatchUserPreferences. -func (mr *MockStoreMockRecorder) PatchUserPreferences(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchUserPreferences", reflect.TypeOf((*MockStore)(nil).PatchUserPreferences), arg0, arg1) -} - -// PostMessage mocks base method. -func (m *MockStore) PostMessage(arg0, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PostMessage", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// PostMessage indicates an expected call of PostMessage. -func (mr *MockStoreMockRecorder) PostMessage(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMessage", reflect.TypeOf((*MockStore)(nil).PostMessage), arg0, arg1, arg2) -} - -// RefreshSession mocks base method. -func (m *MockStore) RefreshSession(arg0 *model0.Session) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RefreshSession", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RefreshSession indicates an expected call of RefreshSession. -func (mr *MockStoreMockRecorder) RefreshSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshSession", reflect.TypeOf((*MockStore)(nil).RefreshSession), arg0) -} - -// RemoveDefaultTemplates mocks base method. -func (m *MockStore) RemoveDefaultTemplates(arg0 []*model0.Board) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveDefaultTemplates", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveDefaultTemplates indicates an expected call of RemoveDefaultTemplates. -func (mr *MockStoreMockRecorder) RemoveDefaultTemplates(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDefaultTemplates", reflect.TypeOf((*MockStore)(nil).RemoveDefaultTemplates), arg0) -} - -// ReorderCategories mocks base method. -func (m *MockStore) ReorderCategories(arg0, arg1 string, arg2 []string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReorderCategories", arg0, arg1, arg2) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReorderCategories indicates an expected call of ReorderCategories. -func (mr *MockStoreMockRecorder) ReorderCategories(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderCategories", reflect.TypeOf((*MockStore)(nil).ReorderCategories), arg0, arg1, arg2) -} - -// ReorderCategoryBoards mocks base method. -func (m *MockStore) ReorderCategoryBoards(arg0 string, arg1 []string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReorderCategoryBoards", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReorderCategoryBoards indicates an expected call of ReorderCategoryBoards. -func (mr *MockStoreMockRecorder) ReorderCategoryBoards(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReorderCategoryBoards", reflect.TypeOf((*MockStore)(nil).ReorderCategoryBoards), arg0, arg1) -} - -// RunDataRetention mocks base method. -func (m *MockStore) RunDataRetention(arg0, arg1 int64) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunDataRetention", arg0, arg1) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RunDataRetention indicates an expected call of RunDataRetention. -func (mr *MockStoreMockRecorder) RunDataRetention(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunDataRetention", reflect.TypeOf((*MockStore)(nil).RunDataRetention), arg0, arg1) -} - -// SaveFileInfo mocks base method. -func (m *MockStore) SaveFileInfo(arg0 *model.FileInfo) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveFileInfo", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveFileInfo indicates an expected call of SaveFileInfo. -func (mr *MockStoreMockRecorder) SaveFileInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveFileInfo", reflect.TypeOf((*MockStore)(nil).SaveFileInfo), arg0) -} - -// SaveMember mocks base method. -func (m *MockStore) SaveMember(arg0 *model0.BoardMember) (*model0.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveMember", arg0) - ret0, _ := ret[0].(*model0.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SaveMember indicates an expected call of SaveMember. -func (mr *MockStoreMockRecorder) SaveMember(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveMember", reflect.TypeOf((*MockStore)(nil).SaveMember), arg0) -} - -// SearchBoardsForUser mocks base method. -func (m *MockStore) SearchBoardsForUser(arg0 string, arg1 model0.BoardSearchField, arg2 string, arg3 bool) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SearchBoardsForUser indicates an expected call of SearchBoardsForUser. -func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1, arg2, arg3) -} - -// SearchBoardsForUserInTeam mocks base method. -func (m *MockStore) SearchBoardsForUserInTeam(arg0, arg1, arg2 string) ([]*model0.Board, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchBoardsForUserInTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model0.Board) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SearchBoardsForUserInTeam indicates an expected call of SearchBoardsForUserInTeam. -func (mr *MockStoreMockRecorder) SearchBoardsForUserInTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUserInTeam", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUserInTeam), arg0, arg1, arg2) -} - -// SearchUserChannels mocks base method. -func (m *MockStore) SearchUserChannels(arg0, arg1, arg2 string) ([]*model.Channel, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchUserChannels", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SearchUserChannels indicates an expected call of SearchUserChannels. -func (mr *MockStoreMockRecorder) SearchUserChannels(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUserChannels", reflect.TypeOf((*MockStore)(nil).SearchUserChannels), arg0, arg1, arg2) -} - -// SearchUsersByTeam mocks base method. -func (m *MockStore) SearchUsersByTeam(arg0, arg1, arg2 string, arg3, arg4, arg5 bool) ([]*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].([]*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SearchUsersByTeam indicates an expected call of SearchUsersByTeam. -func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// SendMessage mocks base method. -func (m *MockStore) SendMessage(arg0, arg1 string, arg2 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMessage", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// SendMessage indicates an expected call of SendMessage. -func (mr *MockStoreMockRecorder) SendMessage(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockStore)(nil).SendMessage), arg0, arg1, arg2) -} - -// SetBoardVisibility mocks base method. -func (m *MockStore) SetBoardVisibility(arg0, arg1, arg2 string, arg3 bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetBoardVisibility", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetBoardVisibility indicates an expected call of SetBoardVisibility. -func (mr *MockStoreMockRecorder) SetBoardVisibility(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBoardVisibility", reflect.TypeOf((*MockStore)(nil).SetBoardVisibility), arg0, arg1, arg2, arg3) -} - -// SetSystemSetting mocks base method. -func (m *MockStore) SetSystemSetting(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetSystemSetting", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetSystemSetting indicates an expected call of SetSystemSetting. -func (mr *MockStoreMockRecorder) SetSystemSetting(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSystemSetting", reflect.TypeOf((*MockStore)(nil).SetSystemSetting), arg0, arg1) -} - -// Shutdown mocks base method. -func (m *MockStore) Shutdown() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Shutdown") - ret0, _ := ret[0].(error) - return ret0 -} - -// Shutdown indicates an expected call of Shutdown. -func (mr *MockStoreMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStore)(nil).Shutdown)) -} - -// UndeleteBlock mocks base method. -func (m *MockStore) UndeleteBlock(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UndeleteBlock", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UndeleteBlock indicates an expected call of UndeleteBlock. -func (mr *MockStoreMockRecorder) UndeleteBlock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBlock", reflect.TypeOf((*MockStore)(nil).UndeleteBlock), arg0, arg1) -} - -// UndeleteBoard mocks base method. -func (m *MockStore) UndeleteBoard(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UndeleteBoard", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UndeleteBoard indicates an expected call of UndeleteBoard. -func (mr *MockStoreMockRecorder) UndeleteBoard(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBoard", reflect.TypeOf((*MockStore)(nil).UndeleteBoard), arg0, arg1) -} - -// UpdateCardLimitTimestamp mocks base method. -func (m *MockStore) UpdateCardLimitTimestamp(arg0 int) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateCardLimitTimestamp", arg0) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateCardLimitTimestamp indicates an expected call of UpdateCardLimitTimestamp. -func (mr *MockStoreMockRecorder) UpdateCardLimitTimestamp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).UpdateCardLimitTimestamp), arg0) -} - -// UpdateCategory mocks base method. -func (m *MockStore) UpdateCategory(arg0 model0.Category) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateCategory", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateCategory indicates an expected call of UpdateCategory. -func (mr *MockStoreMockRecorder) UpdateCategory(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCategory", reflect.TypeOf((*MockStore)(nil).UpdateCategory), arg0) -} - -// UpdateSession mocks base method. -func (m *MockStore) UpdateSession(arg0 *model0.Session) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateSession", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateSession indicates an expected call of UpdateSession. -func (mr *MockStoreMockRecorder) UpdateSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSession", reflect.TypeOf((*MockStore)(nil).UpdateSession), arg0) -} - -// UpdateSubscribersNotifiedAt mocks base method. -func (m *MockStore) UpdateSubscribersNotifiedAt(arg0 string, arg1 int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateSubscribersNotifiedAt", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateSubscribersNotifiedAt indicates an expected call of UpdateSubscribersNotifiedAt. -func (mr *MockStoreMockRecorder) UpdateSubscribersNotifiedAt(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubscribersNotifiedAt", reflect.TypeOf((*MockStore)(nil).UpdateSubscribersNotifiedAt), arg0, arg1) -} - -// UpdateUser mocks base method. -func (m *MockStore) UpdateUser(arg0 *model0.User) (*model0.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(*model0.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockStoreMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0) -} - -// UpdateUserPassword mocks base method. -func (m *MockStore) UpdateUserPassword(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserPassword", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateUserPassword indicates an expected call of UpdateUserPassword. -func (mr *MockStoreMockRecorder) UpdateUserPassword(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserPassword), arg0, arg1) -} - -// UpdateUserPasswordByID mocks base method. -func (m *MockStore) UpdateUserPasswordByID(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserPasswordByID", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateUserPasswordByID indicates an expected call of UpdateUserPasswordByID. -func (mr *MockStoreMockRecorder) UpdateUserPasswordByID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPasswordByID", reflect.TypeOf((*MockStore)(nil).UpdateUserPasswordByID), arg0, arg1) -} - -// UpsertNotificationHint mocks base method. -func (m *MockStore) UpsertNotificationHint(arg0 *model0.NotificationHint, arg1 time.Duration) (*model0.NotificationHint, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertNotificationHint", arg0, arg1) - ret0, _ := ret[0].(*model0.NotificationHint) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpsertNotificationHint indicates an expected call of UpsertNotificationHint. -func (mr *MockStoreMockRecorder) UpsertNotificationHint(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationHint", reflect.TypeOf((*MockStore)(nil).UpsertNotificationHint), arg0, arg1) -} - -// UpsertSharing mocks base method. -func (m *MockStore) UpsertSharing(arg0 model0.Sharing) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertSharing", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertSharing indicates an expected call of UpsertSharing. -func (mr *MockStoreMockRecorder) UpsertSharing(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0) -} - -// UpsertTeamSettings mocks base method. -func (m *MockStore) UpsertTeamSettings(arg0 model0.Team) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertTeamSettings", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertTeamSettings indicates an expected call of UpsertTeamSettings. -func (mr *MockStoreMockRecorder) UpsertTeamSettings(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTeamSettings", reflect.TypeOf((*MockStore)(nil).UpsertTeamSettings), arg0) -} - -// UpsertTeamSignupToken mocks base method. -func (m *MockStore) UpsertTeamSignupToken(arg0 model0.Team) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertTeamSignupToken", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertTeamSignupToken indicates an expected call of UpsertTeamSignupToken. -func (mr *MockStoreMockRecorder) UpsertTeamSignupToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTeamSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertTeamSignupToken), arg0) -} diff --git a/server/boards/services/store/sqlstore/blocks.go b/server/boards/services/store/sqlstore/blocks.go deleted file mode 100644 index 2828f19041..0000000000 --- a/server/boards/services/store/sqlstore/blocks.go +++ /dev/null @@ -1,1010 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "fmt" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - sq "github.com/Masterminds/squirrel" - _ "github.com/lib/pq" // postgres driver - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - maxSearchDepth = 50 - descClause = " DESC " -) - -type ErrEmptyBoardID struct{} - -func (re ErrEmptyBoardID) Error() string { - return "boardID is empty" -} - -type ErrLimitExceeded struct{ max int } - -func (le ErrLimitExceeded) Error() string { - return fmt.Sprintf("limit exceeded (max=%d)", le.max) -} - -func (s *SQLStore) timestampToCharField(name string, as string) string { - switch s.dbType { - case model.MysqlDBType: - return fmt.Sprintf("date_format(%s, '%%Y-%%m-%%d %%H:%%i:%%S') AS %s", name, as) - case model.PostgresDBType: - return fmt.Sprintf("to_char(%s, 'YYYY-MM-DD HH:MI:SS.MS') AS %s", name, as) - default: - return fmt.Sprintf("%s AS %s", name, as) - } -} - -func (s *SQLStore) blockFields(tableAlias string) []string { - if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") { - tableAlias += "." - } - - return []string{ - tableAlias + "id", - tableAlias + "parent_id", - tableAlias + "created_by", - tableAlias + "modified_by", - tableAlias + s.escapeField("schema"), - tableAlias + "type", - tableAlias + "title", - "COALESCE(" + tableAlias + "fields, '{}')", - s.timestampToCharField(tableAlias+"insert_at", "insertAt"), - tableAlias + "create_at", - tableAlias + "update_at", - tableAlias + "delete_at", - "COALESCE(" + tableAlias + "board_id, '0')", - } -} - -func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]*model.Block, error) { - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks") - - if opts.BoardID != "" { - query = query.Where(sq.Eq{"board_id": opts.BoardID}) - } - - if opts.ParentID != "" { - query = query.Where(sq.Eq{"parent_id": opts.ParentID}) - } - - if opts.BlockType != "" && opts.BlockType != model.TypeUnknown { - query = query.Where(sq.Eq{"type": opts.BlockType}) - } - - if opts.Page != 0 { - query = query.Offset(uint64(opts.Page * opts.PerPage)) - } - - if opts.PerPage > 0 { - query = query.Limit(uint64(opts.PerPage)) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBlocks ERROR`, mlog.Err(err)) - - return nil, err - } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) -} - -func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) { - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": ids}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlocksByIDs ERROR`, mlog.Err(err)) - - return nil, err - } - defer s.CloseRows(rows) - - blocks, err := s.blocksFromRows(rows) - if err != nil { - return nil, err - } - - if len(blocks) != len(ids) { - return blocks, model.NewErrNotAllFound("block", ids) - } - - return blocks, nil -} - -// getSubTree2 returns blocks within 2 levels of the given blockID. -func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) { - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks"). - Where(sq.Or{sq.Eq{"id": blockID}, sq.Eq{"parent_id": blockID}}). - Where(sq.Eq{"board_id": boardID}). - OrderBy("insert_at, update_at") - - if opts.BeforeUpdateAt != 0 { - query = query.Where(sq.LtOrEq{"update_at": opts.BeforeUpdateAt}) - } - - if opts.AfterUpdateAt != 0 { - query = query.Where(sq.GtOrEq{"update_at": opts.AfterUpdateAt}) - } - - if opts.Limit != 0 { - query = query.Limit(opts.Limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getSubTree ERROR`, mlog.Err(err)) - - return nil, err - } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) -} - -func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]*model.Block, error) { - results := []*model.Block{} - - for rows.Next() { - var block model.Block - var fieldsJSON string - var modifiedBy sql.NullString - var insertAt sql.NullString - - err := rows.Scan( - &block.ID, - &block.ParentID, - &block.CreatedBy, - &modifiedBy, - &block.Schema, - &block.Type, - &block.Title, - &fieldsJSON, - &insertAt, - &block.CreateAt, - &block.UpdateAt, - &block.DeleteAt, - &block.BoardID) - if err != nil { - // handle this error - s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err)) - - return nil, err - } - - if modifiedBy.Valid { - block.ModifiedBy = modifiedBy.String - } - - err = json.Unmarshal([]byte(fieldsJSON), &block.Fields) - if err != nil { - // handle this error - s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err)) - - return nil, err - } - - results = append(results, &block) - } - - return results, nil -} - -func (s *SQLStore) insertBlock(db sq.BaseRunner, block *model.Block, userID string) error { - if block.BoardID == "" { - return ErrEmptyBoardID{} - } - - fieldsJSON, err := json.Marshal(block.Fields) - if err != nil { - return err - } - - existingBlock, err := s.getBlock(db, block.ID) - if err != nil && !model.IsErrNotFound(err) { - return err - } - - block.UpdateAt = utils.GetMillis() - block.ModifiedBy = userID - - insertQuery := s.getQueryBuilder(db).Insert(""). - Columns( - "channel_id", - "id", - "parent_id", - "created_by", - "modified_by", - s.escapeField("schema"), - "type", - "title", - "fields", - "create_at", - "update_at", - "delete_at", - "board_id", - ) - - insertQueryValues := map[string]interface{}{ - "channel_id": "", - "id": block.ID, - "parent_id": block.ParentID, - s.escapeField("schema"): block.Schema, - "type": block.Type, - "title": block.Title, - "fields": fieldsJSON, - "delete_at": block.DeleteAt, - "created_by": userID, - "modified_by": block.ModifiedBy, - "create_at": utils.GetMillis(), - "update_at": block.UpdateAt, - "board_id": block.BoardID, - } - - if existingBlock != nil { - // block with ID exists, so this is an update operation - query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks"). - Where(sq.Eq{"id": block.ID}). - Where(sq.Eq{"board_id": block.BoardID}). - Set("parent_id", block.ParentID). - Set("modified_by", block.ModifiedBy). - Set(s.escapeField("schema"), block.Schema). - Set("type", block.Type). - Set("title", block.Title). - Set("fields", fieldsJSON). - Set("update_at", block.UpdateAt). - Set("delete_at", block.DeleteAt) - - if _, err := query.Exec(); err != nil { - s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err)) - - return err - } - } else { - block.CreatedBy = userID - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks") - if _, err := query.Exec(); err != nil { - return err - } - } - - // writing block history - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history") - if _, err := query.Exec(); err != nil { - return err - } - - return nil -} - -func (s *SQLStore) patchBlock(db sq.BaseRunner, blockID string, blockPatch *model.BlockPatch, userID string) error { - existingBlock, err := s.getBlock(db, blockID) - if err != nil { - return err - } - - block := blockPatch.Patch(existingBlock) - return s.insertBlock(db, block, userID) -} - -func (s *SQLStore) patchBlocks(db sq.BaseRunner, blockPatches *model.BlockPatchBatch, userID string) error { - for i, blockID := range blockPatches.BlockIDs { - err := s.patchBlock(db, blockID, &blockPatches.BlockPatches[i], userID) - if err != nil { - return err - } - } - return nil -} - -func (s *SQLStore) insertBlocks(db sq.BaseRunner, blocks []*model.Block, userID string) error { - for _, block := range blocks { - if block.BoardID == "" { - return ErrEmptyBoardID{} - } - } - for i := range blocks { - err := s.insertBlock(db, blocks[i], userID) - if err != nil { - return err - } - } - return nil -} - -func (s *SQLStore) deleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error { - return s.deleteBlockAndChildren(db, blockID, modifiedBy, false) -} - -func (s *SQLStore) deleteBlockAndChildren(db sq.BaseRunner, blockID string, modifiedBy string, keepChildren bool) error { - block, err := s.getBlock(db, blockID) - if model.IsErrNotFound(err) { - s.logger.Warn("deleteBlock block not found", mlog.String("block_id", blockID)) - return nil // deleting non-exiting block is not considered an error (for now) - } - if err != nil { - return err - } - - fieldsJSON, err := json.Marshal(block.Fields) - if err != nil { - return err - } - - now := utils.GetMillis() - insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix+"blocks_history"). - Columns( - "board_id", - "id", - "parent_id", - s.escapeField("schema"), - "type", - "title", - "fields", - "modified_by", - "create_at", - "update_at", - "delete_at", - "created_by", - ). - Values( - block.BoardID, - block.ID, - block.ParentID, - block.Schema, - block.Type, - block.Title, - fieldsJSON, - modifiedBy, - block.CreateAt, - now, - now, - block.CreatedBy, - ) - - if _, err := insertQuery.Exec(); err != nil { - return err - } - - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}) - - if _, err := deleteQuery.Exec(); err != nil { - return err - } - - if keepChildren { - return nil - } - - return s.deleteBlockChildren(db, block.BoardID, block.ID, modifiedBy) -} - -func (s *SQLStore) undeleteBlock(db sq.BaseRunner, blockID string, modifiedBy string) error { - blocks, err := s.getBlockHistory(db, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) - if err != nil { - return err - } - - if len(blocks) == 0 { - s.logger.Warn("undeleteBlock block not found", mlog.String("block_id", blockID)) - return nil // undeleting non-exiting block is not considered an error (for now) - } - block := blocks[0] - - if block.DeleteAt == 0 { - s.logger.Warn("undeleteBlock block not deleted", mlog.String("block_id", block.ID)) - return nil // undeleting not deleted block is not considered an error (for now) - } - - fieldsJSON, err := json.Marshal(block.Fields) - if err != nil { - return err - } - - now := utils.GetMillis() - columns := []string{ - "board_id", - "channel_id", - "id", - "parent_id", - s.escapeField("schema"), - "type", - "title", - "fields", - "modified_by", - "create_at", - "update_at", - "delete_at", - "created_by", - } - - values := []interface{}{ - block.BoardID, - "", - block.ID, - block.ParentID, - block.Schema, - block.Type, - block.Title, - fieldsJSON, - modifiedBy, - block.CreateAt, - now, - 0, - block.CreatedBy, - } - insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks_history"). - Columns(columns...). - Values(values...) - insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks"). - Columns(columns...). - Values(values...) - - if _, err := insertHistoryQuery.Exec(); err != nil { - return err - } - - if _, err := insertQuery.Exec(); err != nil { - return err - } - - return s.undeleteBlockChildren(db, block.BoardID, block.ID, modifiedBy) -} - -func (s *SQLStore) getBlockCountsByType(db sq.BaseRunner) (map[string]int64, error) { - query := s.getQueryBuilder(db). - Select( - "type", - "COUNT(*) AS count", - ). - From(s.tablePrefix + "blocks"). - GroupBy("type") - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlockCountsByType ERROR`, mlog.Err(err)) - - return nil, err - } - defer s.CloseRows(rows) - - m := make(map[string]int64) - - for rows.Next() { - var blockType string - var count int64 - - err := rows.Scan(&blockType, &count) - if err != nil { - s.logger.Error("Failed to fetch block count", mlog.Err(err)) - return nil, err - } - m[blockType] = count - } - return m, nil -} - -func (s *SQLStore) getBoardCount(db sq.BaseRunner) (int64, error) { - query := s.getQueryBuilder(db). - Select("COUNT(*) AS count"). - From(s.tablePrefix + "boards"). - Where(sq.Eq{"delete_at": 0}). - Where(sq.Eq{"is_template": false}) - - row := query.QueryRow() - - var count int64 - err := row.Scan(&count) - if err != nil { - return 0, err - } - - return count, nil -} - -func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, error) { - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlock ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - blocks, err := s.blocksFromRows(rows) - if err != nil { - return nil, err - } - - if len(blocks) == 0 { - return nil, model.NewErrNotFound("block ID=" + blockID) - } - - return blocks[0], nil -} - -func (s *SQLStore) getBlockHistory(db sq.BaseRunner, blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) { - var order string - if opts.Descending { - order = descClause - } - - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks_history"). - Where(sq.Eq{"id": blockID}). - OrderBy("insert_at " + order + ", update_at" + order) - - if opts.BeforeUpdateAt != 0 { - query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt}) - } - - if opts.AfterUpdateAt != 0 { - query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt}) - } - - if opts.Limit != 0 { - query = query.Limit(opts.Limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlockHistory ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) -} - -func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) { - var order string - if opts.Descending { - order = descClause - } - - query := s.getQueryBuilder(db). - Select(s.blockFields("")...). - From(s.tablePrefix + "blocks_history"). - Where(sq.Eq{"board_id": boardID}). - OrderBy("insert_at " + order + ", update_at" + order) - - if opts.BeforeUpdateAt != 0 { - query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt}) - } - - if opts.AfterUpdateAt != 0 { - query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt}) - } - - if opts.Limit != 0 { - query = query.Limit(opts.Limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlockHistoryDescendants ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) -} - -// getBlockHistoryNewestChildren returns the newest (latest) version child blocks for the -// specified parent from the blocks_history table. This includes any deleted children. -func (s *SQLStore) getBlockHistoryNewestChildren(db sq.BaseRunner, parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) { - // as we're joining 2 queries, we need to avoid numbered - // placeholders until the join is done, so we use the default - // question mark placeholder here - builder := s.getQueryBuilder(db).PlaceholderFormat(sq.Question) - - sub := builder. - Select("bh2.id", "MAX(bh2.insert_at) AS max_insert_at"). - From(s.tablePrefix + "blocks_history AS bh2"). - Where(sq.Eq{"bh2.parent_id": parentID}). - GroupBy("bh2.id") - - if opts.AfterUpdateAt != 0 { - sub = sub.Where(sq.Gt{"bh2.update_at": opts.AfterUpdateAt}) - } - - if opts.BeforeUpdateAt != 0 { - sub = sub.Where(sq.Lt{"bh2.update_at": opts.BeforeUpdateAt}) - } - - subQuery, subArgs, err := sub.ToSql() - if err != nil { - return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate subquery: %w", err) - } - - query := s.getQueryBuilder(db). - Select(s.blockFields("bh")...). - From(s.tablePrefix+"blocks_history AS bh"). - InnerJoin("("+subQuery+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", subArgs...) - - if opts.Page != 0 { - query = query.Offset(uint64(opts.Page * opts.PerPage)) - } - - if opts.PerPage > 0 { - // limit+1 to detect if more records available - query = query.Limit(uint64(opts.PerPage + 1)) - } - - sql, args, err := query.ToSql() - if err != nil { - return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate sql: %w", err) - } - - // if we're using postgres, we need to replace the question mark - // placeholder with the numbered dollar one, now that the full - // query is built - if s.dbType == model.PostgresDBType { - var rErr error - sql, rErr = sq.Dollar.ReplacePlaceholders(sql) - if rErr != nil { - return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to replace sql placeholders: %w", rErr) - } - } - - rows, err := db.Query(sql, args...) - if err != nil { - s.logger.Error(`getBlockHistoryNewestChildren ERROR`, mlog.Err(err)) - return nil, false, err - } - defer s.CloseRows(rows) - - blocks, err := s.blocksFromRows(rows) - if err != nil { - return nil, false, err - } - - hasMore := false - if opts.PerPage > 0 && len(blocks) > opts.PerPage { - blocks = blocks[:opts.PerPage] - hasMore = true - } - return blocks, hasMore, nil -} - -// getBoardAndCardByID returns the first parent of type `card` and first parent of type `board` for the block specified by ID. -// `board` and/or `card` may return nil without error if the block does not belong to a board or card. -func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board *model.Board, card *model.Block, err error) { - // use block_history to fetch block in case it was deleted and no longer exists in blocks table. - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: true, - } - - blocks, err := s.getBlockHistory(db, blockID, opts) - if err != nil { - return nil, nil, err - } - - if len(blocks) == 0 { - return nil, nil, model.NewErrNotFound("block history BlockID=" + blockID) - } - - return s.getBoardAndCard(db, blocks[0]) -} - -// getBoardAndCard returns the first parent of type `card` and and the `board` for the specified block. -// `board` and/or `card` may return nil without error if the block does not belong to a board or card. -func (s *SQLStore) getBoardAndCard(db sq.BaseRunner, block *model.Block) (board *model.Board, card *model.Block, err error) { - var count int // don't let invalid blocks hierarchy cause infinite loop. - iter := block - - // use block_history to fetch blocks in case they were deleted and no longer exist in blocks table. - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: true, - } - - for { - count++ - if card == nil && iter.Type == model.TypeCard { - card = iter - } - - if iter.ParentID == "" || card != nil || count > maxSearchDepth { - break - } - - blocks, err2 := s.getBlockHistory(db, iter.ParentID, opts) - if err2 != nil { - return nil, nil, err2 - } - if len(blocks) == 0 { - return board, card, nil - } - iter = blocks[0] - } - board, err = s.getBoard(db, block.BoardID) - if err != nil { - return nil, nil, err - } - return board, card, nil -} - -func (s *SQLStore) replaceBlockID(db sq.BaseRunner, currentID, newID, workspaceID string) error { - runUpdateForBlocksAndHistory := func(query sq.UpdateBuilder) error { - if _, err := query.Table(s.tablePrefix + "blocks").Exec(); err != nil { - return err - } - - if _, err := query.Table(s.tablePrefix + "blocks_history").Exec(); err != nil { - return err - } - - return nil - } - - baseQuery := s.getQueryBuilder(db). - Where(sq.Eq{"workspace_id": workspaceID}) - - // update ID - updateIDQ := baseQuery.Update(""). - Set("id", newID). - Where(sq.Eq{"id": currentID}) - - if errID := runUpdateForBlocksAndHistory(updateIDQ); errID != nil { - s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errID)) - return errID - } - - // update BoardID - updateBoardIDQ := baseQuery.Update(""). - Set("board_id", newID). - Where(sq.Eq{"board_id": currentID}) - - if errBoardID := runUpdateForBlocksAndHistory(updateBoardIDQ); errBoardID != nil { - s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errBoardID)) - return errBoardID - } - - // update ParentID - updateParentIDQ := baseQuery.Update(""). - Set("parent_id", newID). - Where(sq.Eq{"parent_id": currentID}) - - if errParentID := runUpdateForBlocksAndHistory(updateParentIDQ); errParentID != nil { - s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errParentID)) - return errParentID - } - - // update parent contentOrder - updateContentOrder := baseQuery.Update("") - if s.dbType == model.PostgresDBType { - updateContentOrder = updateContentOrder. - Set("fields", sq.Expr("REPLACE(fields::text, ?, ?)::json", currentID, newID)). - Where(sq.Like{"fields->>'contentOrder'": "%" + currentID + "%"}). - Where(sq.Eq{"type": model.TypeCard}) - } else { - updateContentOrder = updateContentOrder. - Set("fields", sq.Expr("REPLACE(fields, ?, ?)", currentID, newID)). - Where(sq.Like{"fields": "%" + currentID + "%"}). - Where(sq.Eq{"type": model.TypeCard}) - } - - if errParentID := runUpdateForBlocksAndHistory(updateContentOrder); errParentID != nil { - s.logger.Error(`replaceBlockID ERROR`, mlog.Err(errParentID)) - return errParentID - } - - return nil -} - -func (s *SQLStore) duplicateBlock(db sq.BaseRunner, boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) { - blocks, err := s.getSubTree2(db, boardID, blockID, model.QuerySubtreeOptions{}) - if err != nil { - return nil, err - } - if len(blocks) == 0 { - message := fmt.Sprintf("block subtree BoardID=%s BlockID=%s", boardID, blockID) - return nil, model.NewErrNotFound(message) - } - - var rootBlock *model.Block - allBlocks := []*model.Block{} - for _, block := range blocks { - if block.Type == model.TypeComment { - continue - } - if block.ID == blockID { - if block.Fields == nil { - block.Fields = make(map[string]interface{}) - } - block.Fields["isTemplate"] = asTemplate - rootBlock = block - } else { - allBlocks = append(allBlocks, block) - } - } - allBlocks = append([]*model.Block{rootBlock}, allBlocks...) - - allBlocks = model.GenerateBlockIDs(allBlocks, nil) - if err := s.insertBlocks(db, allBlocks, userID); err != nil { - return nil, err - } - return allBlocks, nil -} - -func (s *SQLStore) deleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error { - now := utils.GetMillis() - - selectQuery := s.getQueryBuilder(db). - Select( - "board_id", - "id", - "parent_id", - s.escapeField("schema"), - "type", - "title", - "fields", - "'"+modifiedBy+"'", - "create_at", - s.castInt(now, "update_at"), - s.castInt(now, "delete_at"), - "created_by", - ). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"board_id": boardID}) - - if parentID != "" { - selectQuery = selectQuery.Where(sq.Eq{"parent_id": parentID}) - } - - insertQuery := s.getQueryBuilder(db). - Insert(s.tablePrefix+"blocks_history"). - Columns( - "board_id", - "id", - "parent_id", - s.escapeField("schema"), - "type", - "title", - "fields", - "modified_by", - "create_at", - "update_at", - "delete_at", - "created_by", - ).Select(selectQuery) - - if _, err := insertQuery.Exec(); err != nil { - return err - } - - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "blocks"). - Where(sq.Eq{"board_id": boardID}) - - if parentID != "" { - deleteQuery = deleteQuery.Where(sq.Eq{"parent_id": parentID}) - } - - if _, err := deleteQuery.Exec(); err != nil { - return err - } - - return nil -} - -func (s *SQLStore) undeleteBlockChildren(db sq.BaseRunner, boardID string, parentID string, modifiedBy string) error { - if boardID == "" { - return ErrEmptyBoardID{} - } - - where := fmt.Sprintf("board_id='%s'", boardID) - if parentID != "" { - where += fmt.Sprintf(" AND parent_id='%s'", parentID) - } - - selectQuery := s.getQueryBuilder(db). - Select( - "bh.board_id", - "'' AS channel_id", - "bh.id", - "bh.parent_id", - "bh.schema", - "bh.type", - "bh.title", - "bh.fields", - "'"+modifiedBy+"' AS modified_by", - "bh.create_at", - s.castInt(utils.GetMillis(), "update_at"), - s.castInt(0, "delete_at"), - "bh.created_by", - ). - From(fmt.Sprintf(` - %sblocks_history AS bh, - (SELECT id, max(insert_at) AS max_insert_at FROM %sblocks_history WHERE %s GROUP BY id) AS sub`, - s.tablePrefix, s.tablePrefix, where)). - Where("bh.id=sub.id"). - Where("bh.insert_at=sub.max_insert_at"). - Where(sq.NotEq{"bh.delete_at": 0}) - - columns := []string{ - "board_id", - "channel_id", - "id", - "parent_id", - s.escapeField("schema"), - "type", - "title", - "fields", - "modified_by", - "create_at", - "update_at", - "delete_at", - "created_by", - } - - insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks"). - Columns(columns...). - Select(selectQuery) - - insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "blocks_history"). - Columns(columns...). - Select(selectQuery) - - sql, args, err := insertQuery.ToSql() - s.logger.Trace("undeleteBlockChildren - insertQuery", - mlog.String("sql", sql), - mlog.Array("args", args), - mlog.Err(err), - ) - - sql, args, err = insertHistoryQuery.ToSql() - s.logger.Trace("undeleteBlockChildren - insertHistoryQuery", - mlog.String("sql", sql), - mlog.Array("args", args), - mlog.Err(err), - ) - - // insert into blocks table must happen before history table, otherwise the history - // table will be changed and the second query will fail to find the same records. - result, err := insertQuery.Exec() - if err != nil { - return err - } - rowsAffected, _ := result.RowsAffected() - s.logger.Debug("undeleteBlockChildren - insertQuery", mlog.Int64("rows_affected", rowsAffected)) - - result, err = insertHistoryQuery.Exec() - if err != nil { - return err - } - rowsAffected, _ = result.RowsAffected() - s.logger.Debug("undeleteBlockChildren - insertHistoryQuery", mlog.Int64("rows_affected", rowsAffected)) - - return nil -} diff --git a/server/boards/services/store/sqlstore/board.go b/server/boards/services/store/sqlstore/board.go deleted file mode 100644 index ee1aaacc35..0000000000 --- a/server/boards/services/store/sqlstore/board.go +++ /dev/null @@ -1,905 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - //nolint:gosec - "crypto/md5" - "database/sql" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func boardFields(tableAlias string) []string { - if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") { - tableAlias += "." - } - - return []string{ - tableAlias + "id", - tableAlias + "team_id", - "COALESCE(" + tableAlias + "channel_id, '')", - "COALESCE(" + tableAlias + "created_by, '')", - tableAlias + "modified_by", - tableAlias + "type", - tableAlias + "minimum_role", - tableAlias + "title", - tableAlias + "description", - tableAlias + "icon", - tableAlias + "show_description", - tableAlias + "is_template", - tableAlias + "template_version", - "COALESCE(" + tableAlias + "properties, '{}')", - "COALESCE(" + tableAlias + "card_properties, '[]')", - tableAlias + "create_at", - tableAlias + "update_at", - tableAlias + "delete_at", - } -} - -func boardHistoryFields() []string { - fields := []string{ - "id", - "team_id", - "COALESCE(channel_id, '')", - "COALESCE(created_by, '')", - "COALESCE(modified_by, '')", - "type", - "minimum_role", - "COALESCE(title, '')", - "COALESCE(description, '')", - "COALESCE(icon, '')", - "COALESCE(show_description, false)", - "COALESCE(is_template, false)", - "template_version", - "COALESCE(properties, '{}')", - "COALESCE(card_properties, '[]')", - "COALESCE(create_at, 0)", - "COALESCE(update_at, 0)", - "COALESCE(delete_at, 0)", - } - - return fields -} - -var boardMemberFields = []string{ - "COALESCE(B.minimum_role, '')", - "BM.board_id", - "BM.user_id", - "BM.roles", - "BM.scheme_admin", - "BM.scheme_editor", - "BM.scheme_commenter", - "BM.scheme_viewer", -} - -func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) { - boards := []*model.Board{} - - for rows.Next() { - var board model.Board - var propertiesBytes []byte - var cardPropertiesBytes []byte - - err := rows.Scan( - &board.ID, - &board.TeamID, - &board.ChannelID, - &board.CreatedBy, - &board.ModifiedBy, - &board.Type, - &board.MinimumRole, - &board.Title, - &board.Description, - &board.Icon, - &board.ShowDescription, - &board.IsTemplate, - &board.TemplateVersion, - &propertiesBytes, - &cardPropertiesBytes, - &board.CreateAt, - &board.UpdateAt, - &board.DeleteAt, - ) - if err != nil { - s.logger.Error("boardsFromRows scan error", mlog.Err(err)) - return nil, err - } - - err = json.Unmarshal(propertiesBytes, &board.Properties) - if err != nil { - s.logger.Error("board properties unmarshal error", mlog.Err(err)) - return nil, err - } - err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties) - if err != nil { - s.logger.Error("board card properties unmarshal error", mlog.Err(err)) - return nil, err - } - - boards = append(boards, &board) - } - - return boards, nil -} - -func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, error) { - boardMembers := []*model.BoardMember{} - - for rows.Next() { - var boardMember model.BoardMember - - err := rows.Scan( - &boardMember.MinimumRole, - &boardMember.BoardID, - &boardMember.UserID, - &boardMember.Roles, - &boardMember.SchemeAdmin, - &boardMember.SchemeEditor, - &boardMember.SchemeCommenter, - &boardMember.SchemeViewer, - ) - if err != nil { - return nil, err - } - - boardMembers = append(boardMembers, &boardMember) - } - - return boardMembers, nil -} - -func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) { - boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{} - - for rows.Next() { - var boardMemberHistoryEntry model.BoardMemberHistoryEntry - var insertAt sql.NullString - - err := rows.Scan( - &boardMemberHistoryEntry.BoardID, - &boardMemberHistoryEntry.UserID, - &boardMemberHistoryEntry.Action, - &insertAt, - ) - if err != nil { - return nil, err - } - - // parse the insert_at timestamp which is different based on database type. - dateTemplate := "2006-01-02T15:04:05Z0700" - if s.dbType == model.MysqlDBType { - dateTemplate = "2006-01-02 15:04:05.000000" - } - ts, err := time.Parse(dateTemplate, insertAt.String) - if err != nil { - return nil, fmt.Errorf("cannot parse datetime '%s' for board_members_history scan: %w", insertAt.String, err) - } - boardMemberHistoryEntry.InsertAt = ts - - boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry) - } - - return boardMemberHistoryEntries, nil -} - -func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) { - boards, err := s.getBoardsByCondition(db, conditions...) - if err != nil { - return nil, err - } - - return boards[0], nil -} - -func (s *SQLStore) getBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) { - return s.getBoardsFieldsByCondition(db, boardFields(""), conditions...) -} - -func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string, conditions ...interface{}) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(fields...). - From(s.tablePrefix + "boards") - for _, c := range conditions { - query = query.Where(c) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBoardsFieldsByCondition ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - boards, err := s.boardsFromRows(rows) - if err != nil { - return nil, err - } - - if len(boards) == 0 { - return nil, model.NewErrNotFound("boards") - } - - return boards, nil -} - -func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, error) { - return s.getBoardByCondition(db, sq.Eq{"id": boardID}) -} - -func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(boardFields("b.")...). - Distinct(). - From(s.tablePrefix + "boards as b"). - LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). - Where(sq.Eq{"b.team_id": teamID}). - Where(sq.Eq{"b.is_template": false}) - - if includePublicBoards { - query = query.Where(sq.Or{ - sq.Eq{"b.type": model.BoardTypeOpen}, - sq.Eq{"bm.user_id": userID}, - }) - } else { - query = query.Where(sq.Or{ - sq.Eq{"bm.user_id": userID}, - }) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBoardsForUserAndTeam ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows) -} - -func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, teamID string) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b"). - Where(sq.Eq{"b.team_id": teamID}). - Where(sq.Eq{"b.id": boardIDs}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBoardsInTeamByIds ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - boards, err := s.boardsFromRows(rows) - if err != nil { - return nil, err - } - - if len(boards) != len(boardIDs) { - s.logger.Warn("getBoardsInTeamByIds mismatched number of boards found", - mlog.Int("len(boards)", len(boards)), - mlog.Int("len(boardIDs)", len(boardIDs)), - ) - return boards, model.NewErrNotAllFound("board", boardIDs) - } - - return boards, nil -} - -func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) { - // Generate tracking IDs for in-built templates - if board.IsTemplate && board.TeamID == model.GlobalTeamID { - //nolint:gosec - // we don't need cryptographically secure hash, so MD5 is fine - board.Properties["trackingTemplateId"] = fmt.Sprintf("%x", md5.Sum([]byte(board.Title))) - } - - propertiesBytes, err := s.MarshalJSONB(board.Properties) - if err != nil { - s.logger.Error( - "failed to marshal board.Properties", - mlog.String("board_id", board.ID), - mlog.String("board.Properties", fmt.Sprintf("%v", board.Properties)), - mlog.Err(err), - ) - return nil, err - } - - cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties) - if err != nil { - s.logger.Error( - "failed to marshal board.CardProperties", - mlog.String("board_id", board.ID), - mlog.String("board.CardProperties", fmt.Sprintf("%v", board.CardProperties)), - mlog.Err(err), - ) - return nil, err - } - - existingBoard, err := s.getBoard(db, board.ID) - if err != nil && !model.IsErrNotFound(err) { - return nil, fmt.Errorf("insertBoard error occurred while fetching existing board %s: %w", board.ID, err) - } - - insertQuery := s.getQueryBuilder(db).Insert(""). - Columns(boardFields("")...) - - now := utils.GetMillis() - board.ModifiedBy = userID - board.UpdateAt = now - - insertQueryValues := map[string]interface{}{ - "id": board.ID, - "team_id": board.TeamID, - "channel_id": board.ChannelID, - "created_by": board.CreatedBy, - "modified_by": board.ModifiedBy, - "type": board.Type, - "title": board.Title, - "minimum_role": board.MinimumRole, - "description": board.Description, - "icon": board.Icon, - "show_description": board.ShowDescription, - "is_template": board.IsTemplate, - "template_version": board.TemplateVersion, - "properties": propertiesBytes, - "card_properties": cardPropertiesBytes, - "create_at": board.CreateAt, - "update_at": board.UpdateAt, - "delete_at": board.DeleteAt, - } - - if existingBoard != nil { - query := s.getQueryBuilder(db).Update(s.tablePrefix+"boards"). - Where(sq.Eq{"id": board.ID}). - Set("modified_by", board.ModifiedBy). - Set("type", board.Type). - Set("channel_id", board.ChannelID). - Set("minimum_role", board.MinimumRole). - Set("title", board.Title). - Set("description", board.Description). - Set("icon", board.Icon). - Set("show_description", board.ShowDescription). - Set("is_template", board.IsTemplate). - Set("template_version", board.TemplateVersion). - Set("properties", propertiesBytes). - Set("card_properties", cardPropertiesBytes). - Set("update_at", board.UpdateAt). - Set("delete_at", board.DeleteAt) - - if _, err := query.Exec(); err != nil { - s.logger.Error(`InsertBoard error occurred while updating existing board`, mlog.String("boardID", board.ID), mlog.Err(err)) - return nil, fmt.Errorf("insertBoard error occurred while updating existing board %s: %w", board.ID, err) - } - } else { - board.CreatedBy = userID - board.CreateAt = now - insertQueryValues["created_by"] = board.CreatedBy - insertQueryValues["create_at"] = board.CreateAt - - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards") - if _, err := query.Exec(); err != nil { - return nil, fmt.Errorf("insertBoard error occurred while inserting board %s: %w", board.ID, err) - } - } - - // writing board history - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history") - if _, err := query.Exec(); err != nil { - s.logger.Error("failed to insert board history", mlog.String("board_id", board.ID), mlog.Err(err)) - return nil, fmt.Errorf("failed to insert board %s history: %w", board.ID, err) - } - - return board, nil -} - -func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) { - existingBoard, err := s.getBoard(db, boardID) - if err != nil { - return nil, err - } - - board := boardPatch.Patch(existingBoard) - return s.insertBoard(db, board, userID) -} - -func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error { - return s.deleteBoardAndChildren(db, boardID, userID, false) -} - -func (s *SQLStore) deleteBoardAndChildren(db sq.BaseRunner, boardID, userID string, keepChildren bool) error { - now := utils.GetMillis() - - board, err := s.getBoard(db, boardID) - if err != nil { - return err - } - - propertiesBytes, err := s.MarshalJSONB(board.Properties) - if err != nil { - return err - } - cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties) - if err != nil { - return err - } - - insertQueryValues := map[string]interface{}{ - "id": board.ID, - "team_id": board.TeamID, - "channel_id": board.ChannelID, - "created_by": board.CreatedBy, - "modified_by": userID, - "type": board.Type, - "minimum_role": board.MinimumRole, - "title": board.Title, - "description": board.Description, - "icon": board.Icon, - "show_description": board.ShowDescription, - "is_template": board.IsTemplate, - "template_version": board.TemplateVersion, - "properties": propertiesBytes, - "card_properties": cardPropertiesBytes, - "create_at": board.CreateAt, - "update_at": now, - "delete_at": now, - } - - // writing board history - insertQuery := s.getQueryBuilder(db).Insert(""). - Columns(boardHistoryFields()...) - - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history") - if _, err := query.Exec(); err != nil { - return err - } - - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "boards"). - Where(sq.Eq{"id": boardID}). - Where(sq.Eq{"COALESCE(team_id, '0')": board.TeamID}) - - if _, err := deleteQuery.Exec(); err != nil { - return err - } - - if keepChildren { - return nil - } - - return s.deleteBlockChildren(db, boardID, "", userID) -} - -func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) { - newBoard, err := s.insertBoard(db, board, userID) - if err != nil { - return nil, nil, err - } - - bm := &model.BoardMember{ - BoardID: newBoard.ID, - UserID: newBoard.CreatedBy, - SchemeAdmin: true, - SchemeEditor: true, - } - - nbm, err := s.saveMember(db, bm) - if err != nil { - return nil, nil, fmt.Errorf("cannot save member %s while inserting board %s: %w", bm.UserID, bm.BoardID, err) - } - - return newBoard, nbm, nil -} - -func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.BoardMember, error) { - queryValues := map[string]interface{}{ - "board_id": bm.BoardID, - "user_id": bm.UserID, - "roles": "", - "scheme_admin": bm.SchemeAdmin, - "scheme_editor": bm.SchemeEditor, - "scheme_commenter": bm.SchemeCommenter, - "scheme_viewer": bm.SchemeViewer, - } - - oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID) - if err != nil && !model.IsErrNotFound(err) { - return nil, err - } - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix + "board_members"). - SetMap(queryValues) - - if s.dbType == model.MysqlDBType { - query = query.Suffix( - "ON DUPLICATE KEY UPDATE scheme_admin = ?, scheme_editor = ?, scheme_commenter = ?, scheme_viewer = ?", - bm.SchemeAdmin, bm.SchemeEditor, bm.SchemeCommenter, bm.SchemeViewer) - } else { - query = query.Suffix( - `ON CONFLICT (board_id, user_id) - DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor, - scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`, - ) - } - - if _, err := query.Exec(); err != nil { - return nil, err - } - - if oldMember == nil { - addToMembersHistory := s.getQueryBuilder(db). - Insert(s.tablePrefix+"board_members_history"). - Columns("board_id", "user_id", "action"). - Values(bm.BoardID, bm.UserID, "created") - - if _, err := addToMembersHistory.Exec(); err != nil { - return nil, err - } - } - - return bm, nil -} - -func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error { - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "board_members"). - Where(sq.Eq{"board_id": boardID}). - Where(sq.Eq{"user_id": userID}) - - result, err := deleteQuery.Exec() - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected > 0 { - addToMembersHistory := s.getQueryBuilder(db). - Insert(s.tablePrefix+"board_members_history"). - Columns("board_id", "user_id", "action"). - Values(boardID, userID, "deleted") - - if _, err := addToMembersHistory.Exec(); err != nil { - return err - } - } - - return nil -} - -func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) { - query := s.getQueryBuilder(db). - Select(boardMemberFields...). - From(s.tablePrefix + "board_members AS BM"). - LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id"). - Where(sq.Eq{"BM.board_id": boardID}). - Where(sq.Eq{"BM.user_id": userID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getMemberForBoard ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - members, err := s.boardMembersFromRows(rows) - if err != nil { - return nil, err - } - - if len(members) == 0 { - message := fmt.Sprintf("board member BoardID=%s UserID=%s", boardID, userID) - return nil, model.NewErrNotFound(message) - } - - return members[0], nil -} - -func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) { - query := s.getQueryBuilder(db). - Select(boardMemberFields...). - From(s.tablePrefix + "board_members AS BM"). - LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id"). - Where(sq.Eq{"BM.user_id": userID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - members, err := s.boardMembersFromRows(rows) - if err != nil { - return nil, err - } - - return members, nil -} - -func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) { - query := s.getQueryBuilder(db). - Select(boardMemberFields...). - From(s.tablePrefix + "board_members AS BM"). - LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id"). - Where(sq.Eq{"BM.board_id": boardID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardMembersFromRows(rows) -} - -// searchBoardsForUser returns all boards that match with the -// term that are either private and which the user is a member of, or -// they're open, regardless of the user membership. -// Search is case-insensitive. -func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(boardFields("b.")...). - Distinct(). - From(s.tablePrefix + "boards as b"). - LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). - Where(sq.Eq{"b.is_template": false}) - - if includePublicBoards { - query = query.Where(sq.Or{ - sq.Eq{"b.type": model.BoardTypeOpen}, - sq.Eq{"bm.user_id": userID}, - }) - } else { - query = query.Where(sq.Or{ - sq.Eq{"bm.user_id": userID}, - }) - } - - if term != "" { - if searchField == model.BoardSearchFieldPropertyName { - switch s.dbType { - case model.PostgresDBType: - where := "b.properties->? is not null" - query = query.Where(where, term) - case model.MysqlDBType: - where := "JSON_EXTRACT(b.properties, ?) IS NOT NULL" - query = query.Where(where, "$."+term) - default: - where := "b.properties LIKE ?" - query = query.Where(where, "%\""+term+"\"%") - } - } else { // model.BoardSearchFieldTitle - // break search query into space separated words - // and search for all words. - // This should later be upgraded to industrial-strength - // word tokenizer, that uses much more than space - // to break words. - conditions := sq.And{} - for _, word := range strings.Split(strings.TrimSpace(term), " ") { - conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"}) - } - query = query.Where(conditions) - } - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows) -} - -// searchBoardsForUserInTeam returns all boards that match with the -// term that are either private and which the user is a member of, or -// they're open, regardless of the user membership. -// Search is case-insensitive. -func (s *SQLStore) searchBoardsForUserInTeam(db sq.BaseRunner, teamID, term, userID string) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(boardFields("b.")...). - Distinct(). - From(s.tablePrefix + "boards as b"). - LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id"). - Where(sq.Eq{"b.is_template": false}). - Where(sq.Eq{"b.team_id": teamID}). - Where(sq.Or{ - sq.Eq{"b.type": model.BoardTypeOpen}, - sq.And{ - sq.Eq{"b.type": model.BoardTypePrivate}, - sq.Eq{"bm.user_id": userID}, - }, - }) - - if term != "" { - // break search query into space separated words - // and search for all words. - // This should later be upgraded to industrial-strength - // word tokenizer, that uses much more than space - // to break words. - - conditions := sq.And{} - - for _, word := range strings.Split(strings.TrimSpace(term), " ") { - conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"}) - } - - query = query.Where(conditions) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows) -} - -func (s *SQLStore) getBoardHistory(db sq.BaseRunner, boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) { - var order string - if opts.Descending { - order = " DESC " - } - - query := s.getQueryBuilder(db). - Select(boardHistoryFields()...). - From(s.tablePrefix + "boards_history"). - Where(sq.Eq{"id": boardID}). - OrderBy("insert_at " + order + ", update_at" + order) - - if opts.BeforeUpdateAt != 0 { - query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt}) - } - - if opts.AfterUpdateAt != 0 { - query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt}) - } - - if opts.Limit != 0 { - query = query.Limit(opts.Limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBoardHistory ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.boardsFromRows(rows) -} - -func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy string) error { - boards, err := s.getBoardHistory(db, boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true}) - if err != nil { - return err - } - - if len(boards) == 0 { - s.logger.Warn("undeleteBlock board not found", mlog.String("board_id", boardID)) - return nil // undeleting non-existing board is not considered an error (for now) - } - board := boards[0] - - if board.DeleteAt == 0 { - s.logger.Warn("undeleteBlock board not deleted", mlog.String("board_id", board.ID)) - return nil // undeleting not deleted board is not considered an error (for now) - } - - propertiesJSON, err := s.MarshalJSONB(board.Properties) - if err != nil { - return err - } - - cardPropertiesJSON, err := s.MarshalJSONB(board.CardProperties) - if err != nil { - return err - } - - now := utils.GetMillis() - columns := []string{ - "id", - "team_id", - "channel_id", - "created_by", - "modified_by", - "type", - "title", - "minimum_role", - "description", - "icon", - "show_description", - "is_template", - "template_version", - "properties", - "card_properties", - "create_at", - "update_at", - "delete_at", - } - - values := []interface{}{ - board.ID, - board.TeamID, - "", - board.CreatedBy, - modifiedBy, - board.Type, - board.Title, - board.MinimumRole, - board.Description, - board.Icon, - board.ShowDescription, - board.IsTemplate, - board.TemplateVersion, - propertiesJSON, - cardPropertiesJSON, - board.CreateAt, - now, - 0, - } - insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards_history"). - Columns(columns...). - Values(values...) - insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards"). - Columns(columns...). - Values(values...) - - if _, err := insertHistoryQuery.Exec(); err != nil { - return err - } - - if _, err := insertQuery.Exec(); err != nil { - return err - } - - return s.undeleteBlockChildren(db, board.ID, "", modifiedBy) -} - -func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) { - query := s.getQueryBuilder(db). - Select("board_id", "user_id", "action", "insert_at"). - From(s.tablePrefix + "board_members_history"). - Where(sq.Eq{"board_id": boardID}). - Where(sq.Eq{"user_id": userID}). - OrderBy("insert_at DESC") - - if limit > 0 { - query = query.Limit(limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows) - if err != nil { - return nil, err - } - - return memberHistory, nil -} diff --git a/server/boards/services/store/sqlstore/board_insights.go b/server/boards/services/store/sqlstore/board_insights.go deleted file mode 100644 index 927716c78e..0000000000 --- a/server/boards/services/store/sqlstore/board_insights.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - "strings" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - sq "github.com/Masterminds/squirrel" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) getTeamBoardsInsights(db sq.BaseRunner, teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) { - boardsHistoryQuery := s.getQueryBuilder(db). - Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by"). - From(s.tablePrefix + "boards_history as boards_history"). - Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id"). - Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}). - Where(sq.Eq{"boards.team_id": teamID}). - Where(sq.Eq{"boards.id": boardIDs}). - Where(sq.NotEq{"boards_history.modified_by": "system"}). - Where(sq.Eq{"boards.delete_at": 0}). - GroupBy("boards.id, boards_history.id, boards_history.modified_by") - - blocksHistoryQuery := s.getQueryBuilder(db). - Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by"). - Prefix("UNION ALL"). - From(s.tablePrefix + "blocks_history as blocks_history"). - Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id"). - Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}). - Where(sq.Eq{"boards.team_id": teamID}). - Where(sq.Eq{"boards.id": boardIDs}). - Where(sq.NotEq{"blocks_history.modified_by": "system"}). - Where(sq.Eq{"boards.delete_at": 0}). - GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by") - - boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery) - - insightsQuery := s.getQueryBuilder(db).Select( - fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")), - ). - FromSelect(boardsActivity, "boards_and_blocks_history"). - GroupBy("id, title, icon, created_by"). - OrderBy("activity_count desc"). - Offset(uint64(offset)). - Limit(uint64(limit)) - - rows, err := insightsQuery.Query() - if err != nil { - s.logger.Error(`Team insights query ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - boardsInsights, err := boardsInsightsFromRows(rows) - if err != nil { - return nil, err - } - boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit) - - return boardInsightsPaginated, nil -} - -func (s *SQLStore) getUserBoardsInsights(db sq.BaseRunner, teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) { - boardsHistoryQuery := s.getQueryBuilder(db). - Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by"). - From(s.tablePrefix + "boards_history as boards_history"). - Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id"). - Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}). - Where(sq.Eq{"boards.team_id": teamID}). - Where(sq.Eq{"boards.id": boardIDs}). - Where(sq.NotEq{"boards_history.modified_by": "system"}). - Where(sq.Eq{"boards.delete_at": 0}). - GroupBy("boards.id, boards_history.id, boards_history.modified_by") - - blocksHistoryQuery := s.getQueryBuilder(db). - Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by"). - Prefix("UNION ALL"). - From(s.tablePrefix + "blocks_history as blocks_history"). - Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id"). - Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}). - Where(sq.Eq{"boards.team_id": teamID}). - Where(sq.Eq{"boards.id": boardIDs}). - Where(sq.NotEq{"blocks_history.modified_by": "system"}). - Where(sq.Eq{"boards.delete_at": 0}). - GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by") - - boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery) - - insightsQuery := s.getQueryBuilder(db).Select( - fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")), - ). - FromSelect(boardsActivity, "boards_and_blocks_history"). - GroupBy("id, title, icon, created_by"). - OrderBy("activity_count desc") - - userQuery := s.getQueryBuilder(db).Select("*"). - FromSelect(insightsQuery, "boards_and_blocks_history_for_user"). - Where(sq.Or{ - sq.Eq{ - "created_by": userID, - }, - sq.Expr(s.elementInColumn("active_users"), userID), - }). - Offset(uint64(offset)). - Limit(uint64(limit)) - - rows, err := userQuery.Query() - - if err != nil { - s.logger.Error(`Team insights query ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - boardsInsights, err := boardsInsightsFromRows(rows) - if err != nil { - return nil, err - } - boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit) - - return boardInsightsPaginated, nil -} - -func boardsInsightsFromRows(rows *sql.Rows) ([]*model.BoardInsight, error) { - boardsInsights := []*model.BoardInsight{} - for rows.Next() { - var boardInsight model.BoardInsight - var activeUsersString string - err := rows.Scan( - &boardInsight.BoardID, - &boardInsight.Title, - &boardInsight.Icon, - &boardInsight.ActivityCount, - &activeUsersString, - &boardInsight.CreatedBy, - ) - // split activeUsersString into slice - boardInsight.ActiveUsers = strings.Split(activeUsersString, ",") - if err != nil { - return nil, err - } - boardsInsights = append(boardsInsights, &boardInsight) - } - return boardsInsights, nil -} diff --git a/server/boards/services/store/sqlstore/boards_and_blocks.go b/server/boards/services/store/sqlstore/boards_and_blocks.go deleted file mode 100644 index f17f1d485f..0000000000 --- a/server/boards/services/store/sqlstore/boards_and_blocks.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -type BlockDoesntBelongToBoardsErr struct { - blockID string -} - -func (e BlockDoesntBelongToBoardsErr) Error() string { - return fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", e.blockID) -} - -func (s *SQLStore) createBoardsAndBlocksWithAdmin(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - newBab, err := s.createBoardsAndBlocks(db, bab, userID) - if err != nil { - return nil, nil, err - } - - members := []*model.BoardMember{} - for _, board := range newBab.Boards { - bm := &model.BoardMember{ - BoardID: board.ID, - UserID: board.CreatedBy, - SchemeAdmin: true, - SchemeEditor: true, - } - - nbm, err := s.saveMember(db, bm) - if err != nil { - return nil, nil, err - } - - members = append(members, nbm) - } - - return newBab, members, nil -} - -func (s *SQLStore) createBoardsAndBlocks(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { - boards := []*model.Board{} - blocks := []*model.Block{} - - for _, board := range bab.Boards { - newBoard, err := s.insertBoard(db, board, userID) - if err != nil { - return nil, err - } - - boards = append(boards, newBoard) - } - - for _, block := range bab.Blocks { - b := block - err := s.insertBlock(db, b, userID) - if err != nil { - return nil, err - } - - blocks = append(blocks, block) - } - - newBab := &model.BoardsAndBlocks{ - Boards: boards, - Blocks: blocks, - } - - return newBab, nil -} - -func (s *SQLStore) patchBoardsAndBlocks(db sq.BaseRunner, pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { - bab := &model.BoardsAndBlocks{} - for i, boardID := range pbab.BoardIDs { - board, err := s.patchBoard(db, boardID, pbab.BoardPatches[i], userID) - if err != nil { - return nil, err - } - bab.Boards = append(bab.Boards, board) - } - - for i, blockID := range pbab.BlockIDs { - if err := s.patchBlock(db, blockID, pbab.BlockPatches[i], userID); err != nil { - return nil, err - } - block, err := s.getBlock(db, blockID) - if err != nil { - return nil, err - } - bab.Blocks = append(bab.Blocks, block) - } - - return bab, nil -} - -// deleteBoardsAndBlocks deletes all the boards and blocks entities of -// the DeleteBoardsAndBlocks struct, making sure that all the blocks -// belong to the boards in the struct. -func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoardsAndBlocks, userID string) error { - boardIDMap := map[string]bool{} - for _, boardID := range dbab.Boards { - boardIDMap[boardID] = true - } - - // delete the blocks first, since deleting the board will clean up any children and we'll get - // not found errors when deleting the blocks after. - for _, blockID := range dbab.Blocks { - block, err := s.getBlock(db, blockID) - if err != nil { - return err - } - - if _, ok := boardIDMap[block.BoardID]; !ok { - return BlockDoesntBelongToBoardsErr{blockID} - } - - if err := s.deleteBlock(db, blockID, userID); err != nil { - return err - } - } - - for _, boardID := range dbab.Boards { - if err := s.deleteBoard(db, boardID, userID); err != nil { - return err - } - } - - return nil -} - -func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - bab := &model.BoardsAndBlocks{ - Boards: []*model.Board{}, - Blocks: []*model.Block{}, - } - - board, err := s.getBoard(db, boardID) - if err != nil { - return nil, nil, err - } - - // todo: server localization - if asTemplate == board.IsTemplate { - // board -> board or template -> template - board.Title += " copy" - } else if asTemplate { - // template from board - board.Title = "New board template" - } - - // make new board private - board.Type = "P" - board.IsTemplate = asTemplate - board.CreatedBy = userID - board.ChannelID = "" - - if toTeam != "" { - board.TeamID = toTeam - } - - bab.Boards = []*model.Board{board} - blocks, err := s.getBlocks(db, model.QueryBlocksOptions{BoardID: boardID}) - if err != nil { - return nil, nil, err - } - newBlocks := []*model.Block{} - for _, b := range blocks { - if b.Type != model.TypeComment { - newBlocks = append(newBlocks, b) - } - } - bab.Blocks = newBlocks - - bab, err = model.GenerateBoardsAndBlocksIDs(bab, nil) - if err != nil { - return nil, nil, err - } - - return s.createBoardsAndBlocksWithAdmin(db, bab, userID) -} diff --git a/server/boards/services/store/sqlstore/boards_migrator.go b/server/boards/services/store/sqlstore/boards_migrator.go deleted file mode 100644 index c4599c0ba3..0000000000 --- a/server/boards/services/store/sqlstore/boards_migrator.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "bytes" - "context" - "database/sql" - "fmt" - "path/filepath" - "text/template" - - "github.com/mattermost/morph" - "github.com/mattermost/morph/drivers" - "github.com/mattermost/morph/drivers/mysql" - "github.com/mattermost/morph/drivers/postgres" - embedded "github.com/mattermost/morph/sources/embedded" - "github.com/mgdelacroix/foundation" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/db" - mmSqlStore "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -var tablePrefix = "focalboard_" - -type BoardsMigrator struct { - connString string - driverName string - db *sql.DB - store *SQLStore - morphEngine *morph.Morph - morphDriver drivers.Driver -} - -func NewBoardsMigrator(store *SQLStore) *BoardsMigrator { - return &BoardsMigrator{ - connString: store.connectionString, - driverName: store.dbType, - store: store, - } -} - -func (bm *BoardsMigrator) runMattermostMigrations() error { - assets := db.Assets() - assetsList, err := assets.ReadDir(filepath.Join("migrations", bm.driverName)) - if err != nil { - return err - } - - assetNames := make([]string, len(assetsList)) - for i, entry := range assetsList { - assetNames[i] = entry.Name() - } - - src, err := embedded.WithInstance(&embedded.AssetSource{ - Names: assetNames, - AssetFunc: func(name string) ([]byte, error) { - return assets.ReadFile(filepath.Join("migrations", bm.driverName, name)) - }, - }) - if err != nil { - return err - } - - driver, err := bm.getDriver() - if err != nil { - return err - } - - options := []morph.EngineOption{ - morph.SetStatementTimeoutInSeconds(1000000), - } - - engine, err := morph.New(context.Background(), driver, src, options...) - if err != nil { - return err - } - defer engine.Close() - - return engine.ApplyAll() -} - -func (bm *BoardsMigrator) getDriver() (drivers.Driver, error) { - var driver drivers.Driver - var err error - switch bm.driverName { - case model.PostgresDBType: - driver, err = postgres.WithInstance(bm.db) - if err != nil { - return nil, err - } - case model.MysqlDBType: - driver, err = mysql.WithInstance(bm.db) - if err != nil { - return nil, err - } - } - - return driver, nil -} - -func (bm *BoardsMigrator) getMorphConnection() (*morph.Morph, drivers.Driver, error) { - driver, err := bm.getDriver() - if err != nil { - return nil, nil, err - } - - assetsList, err := Assets.ReadDir("migrations") - if err != nil { - return nil, nil, err - } - assetNamesForDriver := make([]string, len(assetsList)) - for i, dirEntry := range assetsList { - assetNamesForDriver[i] = dirEntry.Name() - } - - params := map[string]interface{}{ - "prefix": tablePrefix, - "postgres": bm.driverName == model.PostgresDBType, - "mysql": bm.driverName == model.MysqlDBType, - "plugin": true, // TODO: to be removed - "singleUser": false, - } - - migrationAssets := &embedded.AssetSource{ - Names: assetNamesForDriver, - AssetFunc: func(name string) ([]byte, error) { - asset, mErr := Assets.ReadFile("migrations/" + name) - if mErr != nil { - return nil, mErr - } - - tmpl, pErr := template.New("sql").Funcs(bm.store.GetTemplateHelperFuncs()).Parse(string(asset)) - if pErr != nil { - return nil, pErr - } - buffer := bytes.NewBufferString("") - - err = tmpl.Execute(buffer, params) - if err != nil { - return nil, err - } - - return buffer.Bytes(), nil - }, - } - - src, err := embedded.WithInstance(migrationAssets) - if err != nil { - return nil, nil, err - } - - engine, err := morph.New(context.Background(), driver, src, morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", tablePrefix))) - if err != nil { - return nil, nil, err - } - - return engine, driver, nil -} - -func (bm *BoardsMigrator) Setup() error { - var err error - if bm.driverName == model.MysqlDBType { - bm.connString, err = mmSqlStore.ResetReadTimeout(bm.connString) - if err != nil { - return err - } - - bm.connString, err = mmSqlStore.AppendMultipleStatementsFlag(bm.connString) - if err != nil { - return err - } - } - - var dbErr error - bm.db, dbErr = sql.Open(bm.driverName, bm.connString) - if dbErr != nil { - return dbErr - } - - if err2 := bm.db.Ping(); err2 != nil { - return err2 - } - - if err3 := bm.runMattermostMigrations(); err3 != nil { - return err3 - } - - storeParams := Params{ - DBType: bm.driverName, - ConnectionString: bm.connString, - TablePrefix: tablePrefix, - Logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug), - DB: bm.db, - IsPlugin: true, // TODO: to be removed - SkipMigrations: true, - } - bm.store, err = New(storeParams) - if err != nil { - return err - } - - morphEngine, morphDriver, err := bm.getMorphConnection() - if err != nil { - return err - } - bm.morphEngine = morphEngine - bm.morphDriver = morphDriver - - return nil -} - -func (bm *BoardsMigrator) MigrateToStep(step int) error { - applied, err := bm.morphDriver.AppliedMigrations() - if err != nil { - return err - } - currentVersion := len(applied) - - if _, err := bm.morphEngine.Apply(step - currentVersion); err != nil { - return err - } - - return nil -} - -func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor { - return map[int]foundation.Interceptor{ - 18: bm.store.RunDeletedMembershipBoardsMigration, - 35: func() error { - return bm.store.RunDeDuplicateCategoryBoardsMigration(35) - }, - } -} - -func (bm *BoardsMigrator) TearDown() error { - if err := bm.morphEngine.Close(); err != nil { - return err - } - - if err := bm.db.Close(); err != nil { - return err - } - - return nil -} - -func (bm *BoardsMigrator) DriverName() string { - return bm.driverName -} - -func (bm *BoardsMigrator) DB() *sql.DB { - return bm.db -} diff --git a/server/boards/services/store/sqlstore/category.go b/server/boards/services/store/sqlstore/category.go deleted file mode 100644 index 7f0e0a636a..0000000000 --- a/server/boards/services/store/sqlstore/category.go +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const categorySortOrderGap = 10 - -func (s *SQLStore) categoryFields() []string { - return []string{ - "id", - "name", - "user_id", - "team_id", - "create_at", - "update_at", - "delete_at", - "collapsed", - "COALESCE(sort_order, 0)", - "type", - } -} - -func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, error) { - query := s.getQueryBuilder(db). - Select(s.categoryFields()...). - From(s.tablePrefix + "categories"). - Where(sq.Eq{"id": id}) - - rows, err := query.Query() - if err != nil { - s.logger.Error("getCategory error", mlog.Err(err)) - return nil, err - } - - categories, err := s.categoriesFromRows(rows) - if err != nil { - s.logger.Error("getCategory row scan error", mlog.Err(err)) - return nil, err - } - - if len(categories) == 0 { - return nil, model.NewErrNotFound("category ID=" + id) - } - - return &categories[0], nil -} - -func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) error { - // A new category should always end up at the top. - // So we first insert the provided category, then bump up - // existing user-team categories' order - - // creating provided category - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"categories"). - Columns( - "id", - "name", - "user_id", - "team_id", - "create_at", - "update_at", - "delete_at", - "collapsed", - "sort_order", - "type", - ). - Values( - category.ID, - category.Name, - category.UserID, - category.TeamID, - category.CreateAt, - category.UpdateAt, - category.DeleteAt, - category.Collapsed, - category.SortOrder, - category.Type, - ) - - _, err := query.Exec() - if err != nil { - s.logger.Error("Error creating category", mlog.String("category name", category.Name), mlog.Err(err)) - return err - } - - // bumping up order of existing categories - updateQuery := s.getQueryBuilder(db). - Update(s.tablePrefix+"categories"). - Set("sort_order", sq.Expr(fmt.Sprintf("sort_order + %d", categorySortOrderGap))). - Where( - sq.Eq{ - "user_id": category.UserID, - "team_id": category.TeamID, - "delete_at": 0, - }, - ) - - if _, err := updateQuery.Exec(); err != nil { - s.logger.Error( - "createCategory failed to update sort order of existing user-team categories", - mlog.String("user_id", category.UserID), - mlog.String("team_id", category.TeamID), - mlog.Err(err), - ) - - return err - } - - return nil -} - -func (s *SQLStore) updateCategory(db sq.BaseRunner, category model.Category) error { - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"categories"). - Set("name", category.Name). - Set("update_at", category.UpdateAt). - Set("collapsed", category.Collapsed). - Where(sq.Eq{ - "id": category.ID, - "delete_at": 0, - }) - - _, err := query.Exec() - if err != nil { - s.logger.Error("Error updating category", mlog.String("category_id", category.ID), mlog.String("category_name", category.Name), mlog.Err(err)) - return err - } - return nil -} - -func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID string) error { - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"categories"). - Set("delete_at", utils.GetMillis()). - Where(sq.Eq{ - "id": categoryID, - "user_id": userID, - "team_id": teamID, - "delete_at": 0, - }) - - _, err := query.Exec() - if err != nil { - s.logger.Error( - "Error updating category", - mlog.String("category_id", categoryID), - mlog.String("user_id", userID), - mlog.String("team_id", teamID), - mlog.Err(err), - ) - return err - } - return nil -} - -func (s *SQLStore) getUserCategories(db sq.BaseRunner, userID, teamID string) ([]model.Category, error) { - query := s.getQueryBuilder(db). - Select(s.categoryFields()...). - From(s.tablePrefix+"categories"). - Where(sq.Eq{ - "user_id": userID, - "team_id": teamID, - "delete_at": 0, - }). - OrderBy("sort_order", "name") - - rows, err := query.Query() - if err != nil { - s.logger.Error("getUserCategories error", mlog.Err(err)) - return nil, err - } - - return s.categoriesFromRows(rows) -} - -func (s *SQLStore) categoriesFromRows(rows *sql.Rows) ([]model.Category, error) { - var categories []model.Category - - for rows.Next() { - category := model.Category{} - err := rows.Scan( - &category.ID, - &category.Name, - &category.UserID, - &category.TeamID, - &category.CreateAt, - &category.UpdateAt, - &category.DeleteAt, - &category.Collapsed, - &category.SortOrder, - &category.Type, - ) - - if err != nil { - s.logger.Error("categoriesFromRows row parsing error", mlog.Err(err)) - return nil, err - } - - categories = append(categories, category) - } - - return categories, nil -} - -func (s *SQLStore) reorderCategories(db sq.BaseRunner, userID, teamID string, newCategoryOrder []string) ([]string, error) { - if len(newCategoryOrder) == 0 { - return nil, nil - } - - updateCase := sq.Case("id") - for i, categoryID := range newCategoryOrder { - updateCase = updateCase.When("'"+categoryID+"'", sq.Expr(fmt.Sprintf("%d", i*categorySortOrderGap))) - } - updateCase = updateCase.Else("sort_order") - - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"categories"). - Set("sort_order", updateCase). - Where(sq.Eq{ - "user_id": userID, - "team_id": teamID, - }) - - if _, err := query.Exec(); err != nil { - s.logger.Error( - "reorderCategories failed to update category order", - mlog.String("user_id", userID), - mlog.String("team_id", teamID), - mlog.Err(err), - ) - - return nil, err - } - - return newCategoryOrder, nil -} diff --git a/server/boards/services/store/sqlstore/category_boards.go b/server/boards/services/store/sqlstore/category_boards.go deleted file mode 100644 index 7d14f8779c..0000000000 --- a/server/boards/services/store/sqlstore/category_boards.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) getUserCategoryBoards(db sq.BaseRunner, userID, teamID string) ([]model.CategoryBoards, error) { - categories, err := s.getUserCategories(db, userID, teamID) - if err != nil { - return nil, err - } - - userCategoryBoards := []model.CategoryBoards{} - for _, category := range categories { - boardMetadata, err := s.getCategoryBoardAttributes(db, category.ID) - if err != nil { - return nil, err - } - - userCategoryBoard := model.CategoryBoards{ - Category: category, - BoardMetadata: boardMetadata, - } - - userCategoryBoards = append(userCategoryBoards, userCategoryBoard) - } - - return userCategoryBoards, nil -} - -func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID string) ([]model.CategoryBoardMetadata, error) { - query := s.getQueryBuilder(db). - Select("board_id, COALESCE(hidden, false)"). - From(s.tablePrefix + "category_boards"). - Where(sq.Eq{ - "category_id": categoryID, - }). - OrderBy("sort_order") - - rows, err := query.Query() - if err != nil { - s.logger.Error("getCategoryBoards error fetching categoryblocks", mlog.String("categoryID", categoryID), mlog.Err(err)) - return nil, err - } - - return s.categoryBoardsFromRows(rows) -} - -func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID, categoryID string, boardIDsParam []string) error { - // we need to de-duplicate this array as Postgres failes to - // handle upsert if there are multiple incoming rows - // that conflict the same existing row. - // For example, having the entry "1" in DB and trying to upsert "1" and "1" will fail - // as there are multiple duplicates of the same "1". - // - // Source: https://stackoverflow.com/questions/42994373/postgresql-on-conflict-cannot-affect-row-a-second-time - boardIDs := utils.DedupeStringArr(boardIDsParam) - - if len(boardIDs) == 0 { - return nil - } - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"category_boards"). - Columns( - "id", - "user_id", - "category_id", - "board_id", - "create_at", - "update_at", - "sort_order", - "hidden", - ) - - now := utils.GetMillis() - for _, boardID := range boardIDs { - query = query.Values( - utils.NewID(utils.IDTypeNone), - userID, - categoryID, - boardID, - now, - now, - 0, - false, - ) - } - - if s.dbType == model.MysqlDBType { - query = query.Suffix( - "ON DUPLICATE KEY UPDATE category_id = ?", - categoryID, - ) - } else { - query = query.Suffix( - `ON CONFLICT (user_id, board_id) - DO UPDATE SET category_id = EXCLUDED.category_id, update_at = EXCLUDED.update_at`, - ) - } - - if _, err := query.Exec(); err != nil { - return fmt.Errorf( - "store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w", - userID, categoryID, len(boardIDs), err, - ) - } - - return nil -} - -func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]model.CategoryBoardMetadata, error) { - metadata := []model.CategoryBoardMetadata{} - - for rows.Next() { - datum := model.CategoryBoardMetadata{} - err := rows.Scan(&datum.BoardID, &datum.Hidden) - - if err != nil { - s.logger.Error("categoryBoardsFromRows row scan error", mlog.Err(err)) - return nil, err - } - - metadata = append(metadata, datum) - } - - return metadata, nil -} - -func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, newBoardsOrder []string) ([]string, error) { - if len(newBoardsOrder) == 0 { - return nil, nil - } - - updateCase := sq.Case("board_id") - for i, boardID := range newBoardsOrder { - updateCase = updateCase.When("'"+boardID+"'", sq.Expr(fmt.Sprintf("%d", i+model.CategoryBoardsSortOrderGap))) - } - updateCase.Else("sort_order") - - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"category_boards"). - Set("sort_order", updateCase). - Where(sq.Eq{ - "category_id": categoryID, - }) - - if _, err := query.Exec(); err != nil { - s.logger.Error( - "reorderCategoryBoards failed to update category board order", - mlog.String("category_id", categoryID), - mlog.Err(err), - ) - - return nil, err - } - - return newBoardsOrder, nil -} - -func (s *SQLStore) setBoardVisibility(db sq.BaseRunner, userID, categoryID, boardID string, visible bool) error { - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"category_boards"). - Set("hidden", !visible). - Where(sq.Eq{ - "user_id": userID, - "category_id": categoryID, - "board_id": boardID, - }) - - if _, err := query.Exec(); err != nil { - s.logger.Error( - "SQLStore setBoardVisibility: failed to update board visibility", - mlog.String("user_id", userID), - mlog.String("board_id", boardID), - mlog.Bool("visible", visible), - mlog.Err(err), - ) - - return err - } - - return nil -} diff --git a/server/boards/services/store/sqlstore/cloud.go b/server/boards/services/store/sqlstore/cloud.go deleted file mode 100644 index fea8b2fea4..0000000000 --- a/server/boards/services/store/sqlstore/cloud.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "errors" - "strconv" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -var ErrInvalidCardLimitValue = errors.New("card limit value is invalid") - -// activeCardsQuery applies the necessary filters to the query for it -// to fetch an active cards window if the cardLimit is set, or all the -// active cards if it's 0. -func (s *SQLStore) activeCardsQuery(builder sq.StatementBuilderType, selectStr string, cardLimit int) sq.SelectBuilder { - query := builder. - Select(selectStr). - From(s.tablePrefix + "blocks b"). - Join(s.tablePrefix + "boards bd on b.board_id=bd.id"). - Where(sq.Eq{ - "b.delete_at": 0, - "b.type": model.TypeCard, - "bd.is_template": false, - }) - - if cardLimit != 0 { - query = query. - Limit(1). - Offset(uint64(cardLimit - 1)) - } - - return query -} - -// getUsedCardsCount returns the amount of active cards in the server. -func (s *SQLStore) getUsedCardsCount(db sq.BaseRunner) (int, error) { - row := s.activeCardsQuery(s.getQueryBuilder(db), "count(b.id)", 0). - QueryRow() - - var usedCards int - err := row.Scan(&usedCards) - if err != nil { - return 0, err - } - - return usedCards, nil -} - -// getCardLimitTimestamp returns the timestamp value from the -// system_settings table or zero if it doesn't exist. -func (s *SQLStore) getCardLimitTimestamp(db sq.BaseRunner) (int64, error) { - scanner := s.getQueryBuilder(db). - Select("value"). - From(s.tablePrefix + "system_settings"). - Where(sq.Eq{"id": store.CardLimitTimestampSystemKey}). - QueryRow() - - var result string - err := scanner.Scan(&result) - if errors.Is(sql.ErrNoRows, err) { - return 0, nil - } - if err != nil { - return 0, err - } - - cardLimitTimestamp, err := strconv.Atoi(result) - if err != nil { - return 0, ErrInvalidCardLimitValue - } - - return int64(cardLimitTimestamp), nil -} - -// updateCardLimitTimestamp updates the card limit value in the -// system_settings table with the timestamp of the nth last updated -// card, being nth the value of the cardLimit parameter. If cardLimit -// is zero, the timestamp will be set to zero. -func (s *SQLStore) updateCardLimitTimestamp(db sq.BaseRunner, cardLimit int) (int64, error) { - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"system_settings"). - Columns("id", "value") - - var value any = 0 //nolint:revive - if cardLimit != 0 { - value = s.activeCardsQuery(sq.StatementBuilder, "b.update_at", cardLimit). - OrderBy("b.update_at DESC"). - Prefix("COALESCE((").Suffix("), 0)") - } - query = query.Values(store.CardLimitTimestampSystemKey, value) - - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET value = EXCLUDED.value`, - ) - } - - result, err := query.Exec() - if err != nil { - return 0, err - } - - if _, err := result.RowsAffected(); err != nil { - return 0, err - } - - return s.getCardLimitTimestamp(db) -} diff --git a/server/boards/services/store/sqlstore/compliance.go b/server/boards/services/store/sqlstore/compliance.go deleted file mode 100644 index 6d6e5316ca..0000000000 --- a/server/boards/services/store/sqlstore/compliance.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) getBoardsForCompliance(db sq.BaseRunner, opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) { - query := s.getQueryBuilder(db). - Select(boardFields("b.")...). - From(s.tablePrefix + "boards as b") - - if opts.TeamID != "" { - query = query.Where(sq.Eq{"b.team_id": opts.TeamID}) - } - - if opts.Page != 0 { - query = query.Offset(uint64(opts.Page * opts.PerPage)) - } - - if opts.PerPage > 0 { - // N+1 to check if there's a next page for pagination - query = query.Limit(uint64(opts.PerPage) + 1) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBoardsForCompliance ERROR`, mlog.Err(err)) - return nil, false, err - } - defer s.CloseRows(rows) - - boards, err := s.boardsFromRows(rows) - if err != nil { - return nil, false, err - } - - var hasMore bool - if opts.PerPage > 0 && len(boards) > opts.PerPage { - boards = boards[0:opts.PerPage] - hasMore = true - } - return boards, hasMore, nil -} - -func (s *SQLStore) getBoardsComplianceHistory(db sq.BaseRunner, opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) { - queryDescendentLastUpdate := s.getQueryBuilder(db). - Select("MAX(blk1.update_at)"). - From(s.tablePrefix + "blocks_history as blk1"). - Where("blk1.board_id=bh.id") - - if !opts.IncludeDeleted { - queryDescendentLastUpdate.Where(sq.Eq{"blk1.delete_at": 0}) - } - - sqlDescendentLastUpdate, _, _ := queryDescendentLastUpdate.ToSql() - - queryDescendentFirstUpdate := s.getQueryBuilder(db). - Select("MIN(blk2.update_at)"). - From(s.tablePrefix + "blocks_history as blk2"). - Where("blk2.board_id=bh.id") - - if !opts.IncludeDeleted { - queryDescendentFirstUpdate.Where(sq.Eq{"blk2.delete_at": 0}) - } - - sqlDescendentFirstUpdate, _, _ := queryDescendentFirstUpdate.ToSql() - - query := s.getQueryBuilder(db). - Select( - "bh.id", - "bh.team_id", - "CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted", - "COALESCE(("+sqlDescendentLastUpdate+"),0) as decendentLastUpdateAt", - "COALESCE(("+sqlDescendentFirstUpdate+"),0) as decendentFirstUpdateAt", - "bh.created_by", - "bh.modified_by", - ). - From(s.tablePrefix + "boards_history as bh") - - if !opts.IncludeDeleted { - // filtering out deleted boards; join with boards table to ensure no history - // for deleted boards are returned. Deleted boards won't exist in boards table. - query = query.Join(s.tablePrefix + "boards as b ON b.id=bh.id") - } - - query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}). - GroupBy("bh.id", "bh.team_id", "bh.delete_at", "bh.created_by", "bh.modified_by"). - OrderBy("decendentLastUpdateAt desc", "bh.id") - - if opts.TeamID != "" { - query = query.Where(sq.Eq{"bh.team_id": opts.TeamID}) - } - - if opts.Page != 0 { - query = query.Offset(uint64(opts.Page * opts.PerPage)) - } - - if opts.PerPage > 0 { - // N+1 to check if there's a next page for pagination - query = query.Limit(uint64(opts.PerPage) + 1) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBoardsComplianceHistory ERROR`, mlog.Err(err)) - return nil, false, err - } - defer s.CloseRows(rows) - - history, err := s.boardsHistoryFromRows(rows) - if err != nil { - return nil, false, err - } - - var hasMore bool - if opts.PerPage > 0 && len(history) > opts.PerPage { - history = history[0:opts.PerPage] - hasMore = true - } - return history, hasMore, nil -} - -func (s *SQLStore) getBlocksComplianceHistory(db sq.BaseRunner, opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) { - query := s.getQueryBuilder(db). - Select( - "bh.id", - "brd.team_id", - "bh.board_id", - "bh.type", - "CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted", - "max(bh.update_at) as lastUpdateAt", - "min(bh.update_at) as firstUpdateAt", - "bh.created_by", - "bh.modified_by", - ). - From(s.tablePrefix + "blocks_history as bh"). - Join(s.tablePrefix + "boards_history as brd on brd.id=bh.board_id") - - if !opts.IncludeDeleted { - // filtering out deleted blocks; join with blocks table to ensure no history - // for deleted blocks are returned. Deleted blocks won't exist in blocks table. - query = query.Join(s.tablePrefix + "blocks as b ON b.id=bh.id") - } - - query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}). - GroupBy("bh.id", "brd.team_id", "bh.board_id", "bh.type", "bh.delete_at", "bh.created_by", "bh.modified_by"). - OrderBy("lastUpdateAt desc", "bh.id") - - if opts.TeamID != "" { - query = query.Where(sq.Eq{"brd.team_id": opts.TeamID}) - } - - if opts.BoardID != "" { - query = query.Where(sq.Eq{"bh.board_id": opts.BoardID}) - } - - if opts.Page != 0 { - query = query.Offset(uint64(opts.Page * opts.PerPage)) - } - - if opts.PerPage > 0 { - // N+1 to check if there's a next page for pagination - query = query.Limit(uint64(opts.PerPage) + 1) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlocksComplianceHistory ERROR`, mlog.Err(err)) - return nil, false, err - } - defer s.CloseRows(rows) - - history, err := s.blocksHistoryFromRows(rows) - if err != nil { - return nil, false, err - } - - var hasMore bool - if opts.PerPage > 0 && len(history) > opts.PerPage { - history = history[0:opts.PerPage] - hasMore = true - } - return history, hasMore, nil -} - -func (s *SQLStore) boardsHistoryFromRows(rows *sql.Rows) ([]*model.BoardHistory, error) { - history := []*model.BoardHistory{} - - for rows.Next() { - boardHistory := &model.BoardHistory{} - - err := rows.Scan( - &boardHistory.ID, - &boardHistory.TeamID, - &boardHistory.IsDeleted, - &boardHistory.DescendantLastUpdateAt, - &boardHistory.DescendantFirstUpdateAt, - &boardHistory.CreatedBy, - &boardHistory.LastModifiedBy, - ) - if err != nil { - s.logger.Error("boardsHistoryFromRows scan error", mlog.Err(err)) - return nil, err - } - - history = append(history, boardHistory) - } - return history, nil -} - -func (s *SQLStore) blocksHistoryFromRows(rows *sql.Rows) ([]*model.BlockHistory, error) { - history := []*model.BlockHistory{} - - for rows.Next() { - blockHistory := &model.BlockHistory{} - - err := rows.Scan( - &blockHistory.ID, - &blockHistory.TeamID, - &blockHistory.BoardID, - &blockHistory.Type, - &blockHistory.IsDeleted, - &blockHistory.LastUpdateAt, - &blockHistory.FirstUpdateAt, - &blockHistory.CreatedBy, - &blockHistory.LastModifiedBy, - ) - if err != nil { - s.logger.Error("blocksHistoryFromRows scan error", mlog.Err(err)) - return nil, err - } - - history = append(history, blockHistory) - } - return history, nil -} diff --git a/server/boards/services/store/sqlstore/data_migrations.go b/server/boards/services/store/sqlstore/data_migrations.go deleted file mode 100644 index 0b95084b40..0000000000 --- a/server/boards/services/store/sqlstore/data_migrations.go +++ /dev/null @@ -1,885 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "context" - "fmt" - "os" - "strconv" - - sq "github.com/Masterminds/squirrel" - "github.com/wiggin77/merror" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const ( - // we group the inserts on batches of 1000 because PostgreSQL - // supports a limit of around 64K values (not rows) on an insert - // query, so we want to stay safely below. - CategoryInsertBatch = 1000 - - TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete" - UniqueIDsMigrationKey = "UniqueIDsMigrationComplete" - CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete" - TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete" - DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete" - DeDuplicateCategoryBoardTableMigrationKey = "DeDuplicateCategoryBoardTableComplete" -) - -func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]*model.Block, error) { - subquery, _, _ := s.getQueryBuilder(db). - Select("id"). - From(s.tablePrefix + "blocks"). - Having("count(id) > 1"). - GroupBy("id"). - ToSql() - - blocksFields := []string{ - "id", - "parent_id", - "root_id", - "created_by", - "modified_by", - s.escapeField("schema"), - "type", - "title", - "COALESCE(fields, '{}')", - s.timestampToCharField("insert_at", "insertAt"), - "create_at", - "update_at", - "delete_at", - "COALESCE(workspace_id, '0')", - } - - rows, err := s.getQueryBuilder(db). - Select(blocksFields...). - From(s.tablePrefix + "blocks"). - Where(fmt.Sprintf("id IN (%s)", subquery)). - Query() - if err != nil { - s.logger.Error(`getBlocksWithSameID ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - return s.blocksFromRows(rows) -} - -func (s *SQLStore) RunUniqueIDsMigration() error { - setting, err := s.GetSystemSetting(UniqueIDsMigrationKey) - if err != nil { - return fmt.Errorf("cannot get migration state: %w", err) - } - - // If the migration is already completed, do not run it again. - if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { - return nil - } - - s.logger.Debug("Running Unique IDs migration") - - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - - blocks, err := s.getBlocksWithSameID(tx) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "getBlocksWithSameID")) - } - return fmt.Errorf("cannot get blocks with same ID: %w", err) - } - - blocksByID := map[string][]*model.Block{} - for _, block := range blocks { - blocksByID[block.ID] = append(blocksByID[block.ID], block) - } - - for _, blocks := range blocksByID { - for i, block := range blocks { - if i == 0 { - // do nothing for the first ID, only updating the others - continue - } - - newID := utils.NewID(model.BlockType2IDType(block.Type)) - if err := s.replaceBlockID(tx, block.ID, newID, block.WorkspaceID); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "replaceBlockID")) - } - return fmt.Errorf("cannot replace blockID %s: %w", block.ID, err) - } - } - } - - if err := s.setSystemSetting(tx, UniqueIDsMigrationKey, strconv.FormatBool(true)); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) - } - return fmt.Errorf("cannot mark migration as completed: %w", err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("cannot commit unique IDs transaction: %w", err) - } - - s.logger.Debug("Unique IDs migration finished successfully") - return nil -} - -// RunCategoryUUIDIDMigration takes care of deriving the categories -// from the boards and its memberships. The name references UUID -// because of the preexisting purpose of this migration, and has been -// preserved for compatibility with already migrated instances. -func (s *SQLStore) RunCategoryUUIDIDMigration() error { - setting, err := s.GetSystemSetting(CategoryUUIDIDMigrationKey) - if err != nil { - return fmt.Errorf("cannot get migration state: %w", err) - } - - // If the migration is already completed, do not run it again. - if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { - return nil - } - - s.logger.Debug("Running category UUID ID migration") - - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - - if s.isPlugin { - if err := s.createCategories(tx); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("category UUIDs insert categories transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) - } - return err - } - - if err := s.createCategoryBoards(tx); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("category UUIDs insert category boards transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) - } - return err - } - } - - if err := s.setSystemSetting(tx, CategoryUUIDIDMigrationKey, strconv.FormatBool(true)); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("category UUIDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting")) - } - return fmt.Errorf("cannot mark migration as completed: %w", err) - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("cannot commit category UUIDs transaction: %w", err) - } - - s.logger.Debug("category UUIDs migration finished successfully") - return nil -} - -func (s *SQLStore) createCategories(db sq.BaseRunner) error { - rows, err := s.getQueryBuilder(db). - Select("c.DisplayName, cm.UserId, c.TeamId, cm.ChannelId"). - From(s.tablePrefix + "boards boards"). - Join("ChannelMembers cm on boards.channel_id = cm.ChannelId"). - Join("Channels c on cm.ChannelId = c.id and (c.Type = 'O' or c.Type = 'P')"). - GroupBy("cm.UserId, c.TeamId, cm.ChannelId, c.DisplayName"). - Query() - - if err != nil { - s.logger.Error("get boards data error", mlog.Err(err)) - return err - } - defer s.CloseRows(rows) - - initQuery := func() sq.InsertBuilder { - return s.getQueryBuilder(db). - Insert(s.tablePrefix+"categories"). - Columns( - "id", - "name", - "user_id", - "team_id", - "channel_id", - "create_at", - "update_at", - "delete_at", - ) - } - // query will accumulate the insert values until the limit is - // reached, and then it will be stored and reset - query := initQuery() - // queryList stores those queries that already reached the limit - // to be run when all the data is processed - queryList := []sq.InsertBuilder{} - counter := 0 - now := model.GetMillis() - - for rows.Next() { - var displayName string - var userID string - var teamID string - var channelID string - - err := rows.Scan( - &displayName, - &userID, - &teamID, - &channelID, - ) - if err != nil { - return fmt.Errorf("cannot scan result while trying to create categories: %w", err) - } - - query = query.Values( - utils.NewID(utils.IDTypeNone), - displayName, - userID, - teamID, - channelID, - now, - 0, - 0, - ) - - counter++ - if counter%CategoryInsertBatch == 0 { - queryList = append(queryList, query) - query = initQuery() - } - } - - if counter%CategoryInsertBatch != 0 { - queryList = append(queryList, query) - } - - for _, q := range queryList { - if _, err := q.Exec(); err != nil { - return fmt.Errorf("cannot create category values: %w", err) - } - } - - return nil -} - -func (s *SQLStore) createCategoryBoards(db sq.BaseRunner) error { - rows, err := s.getQueryBuilder(db). - Select("categories.user_id, categories.id, boards.id"). - From(s.tablePrefix + "categories categories"). - Join(s.tablePrefix + "boards boards on categories.channel_id = boards.channel_id AND boards.is_template = false"). - Query() - - if err != nil { - s.logger.Error("get categories data error", mlog.Err(err)) - return err - } - defer s.CloseRows(rows) - - initQuery := func() sq.InsertBuilder { - return s.getQueryBuilder(db). - Insert(s.tablePrefix+"category_boards"). - Columns( - "id", - "user_id", - "category_id", - "board_id", - "create_at", - "update_at", - "delete_at", - ) - } - // query will accumulate the insert values until the limit is - // reached, and then it will be stored and reset - query := initQuery() - // queryList stores those queries that already reached the limit - // to be run when all the data is processed - queryList := []sq.InsertBuilder{} - counter := 0 - now := model.GetMillis() - - for rows.Next() { - var userID string - var categoryID string - var boardID string - - err := rows.Scan( - &userID, - &categoryID, - &boardID, - ) - if err != nil { - return fmt.Errorf("cannot scan result while trying to create category boards: %w", err) - } - - query = query.Values( - utils.NewID(utils.IDTypeNone), - userID, - categoryID, - boardID, - now, - 0, - 0, - ) - - counter++ - if counter%CategoryInsertBatch == 0 { - queryList = append(queryList, query) - query = initQuery() - } - } - - if counter%CategoryInsertBatch != 0 { - queryList = append(queryList, query) - } - - for _, q := range queryList { - if _, err := q.Exec(); err != nil { - return fmt.Errorf("cannot create category boards values: %w", err) - } - } - - return nil -} - -// We no longer support boards existing in DMs and private -// group messages. This function migrates all boards -// belonging to a DM to the best possible team. -func (s *SQLStore) RunTeamLessBoardsMigration() error { - if !s.isPlugin { - return nil - } - - setting, err := s.GetSystemSetting(TeamLessBoardsMigrationKey) - if err != nil { - return fmt.Errorf("cannot get teamless boards migration state: %w", err) - } - - // If the migration is already completed, do not run it again. - if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { - return nil - } - - boards, err := s.getDMBoards(s.db) - if err != nil { - return err - } - - s.logger.Debug("Migrating teamless boards to a team", mlog.Int("count", len(boards))) - - // cache for best suitable team for a DM. Since a DM can - // contain multiple boards, caching this avoids - // duplicate queries for the same DM. - channelToTeamCache := map[string]string{} - - tx, err := s.db.BeginTx(context.Background(), nil) - if err != nil { - s.logger.Error("error starting transaction in runTeamLessBoardsMigration", mlog.Err(err)) - return err - } - - for i := range boards { - // check the cache first - teamID, ok := channelToTeamCache[boards[i].ChannelID] - - // query DB if entry not found in cache - if !ok { - teamID, err = s.getBestTeamForBoard(s.db, boards[i]) - if err != nil { - // don't let one board's error spoil - // the mood for others - s.logger.Error("could not find the best team for board during team less boards migration. Continuing", mlog.String("boardID", boards[i].ID)) - continue - } - } - - channelToTeamCache[boards[i].ChannelID] = teamID - boards[i].TeamID = teamID - - query := s.getQueryBuilder(tx). - Update(s.tablePrefix+"boards"). - Set("team_id", teamID). - Set("type", model.BoardTypePrivate). - Where(sq.Eq{"id": boards[i].ID}) - - if _, err := query.Exec(); err != nil { - s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err)) - return err - } - } - - if err := s.setSystemSetting(tx, TeamLessBoardsMigrationKey, strconv.FormatBool(true)); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runTeamLessBoardsMigration")) - } - return fmt.Errorf("cannot mark migration as completed: %w", err) - } - - if err := tx.Commit(); err != nil { - s.logger.Error("failed to commit runTeamLessBoardsMigration transaction", mlog.Err(err)) - return err - } - - return nil -} - -func (s *SQLStore) getDMBoards(tx sq.BaseRunner) ([]*model.Board, error) { - conditions := sq.And{ - sq.Eq{"team_id": ""}, - sq.Or{ - sq.Eq{"type": "D"}, - sq.Eq{"type": "G"}, - }, - } - - boards, err := s.getLegacyBoardsByCondition(tx, conditions) - if err != nil && model.IsErrNotFound(err) { - return []*model.Board{}, nil - } - - return boards, err -} - -// The destination is selected as the first team where all members -// of the DM are a part of. If no such team exists, -// we use the first team to which DM creator belongs to. -func (s *SQLStore) getBestTeamForBoard(tx sq.BaseRunner, board *model.Board) (string, error) { - userTeams, err := s.getBoardUserTeams(tx, board) - if err != nil { - return "", err - } - - teams := [][]interface{}{} - for _, userTeam := range userTeams { - userTeamInterfaces := make([]interface{}, len(userTeam)) - for i := range userTeam { - userTeamInterfaces[i] = userTeam[i] - } - teams = append(teams, userTeamInterfaces) - } - - commonTeams := utils.Intersection(teams...) - var teamID string - if len(commonTeams) > 0 { - teamID = commonTeams[0].(string) - } else { - // no common teams found. Let's try finding the best suitable team - if board.Type == "D" { - // get DM's creator and pick one of their team - channel, err := (s.servicesAPI).GetChannelByID(board.ChannelID) - if err != nil { - s.logger.Error("failed to fetch DM channel for board", - mlog.String("board_id", board.ID), - mlog.String("channel_id", board.ChannelID), - mlog.Err(err), - ) - return "", err - } - - if _, ok := userTeams[channel.CreatorId]; !ok { - s.logger.Error("channel creator not found in user teams", - mlog.String("board_id", board.ID), - mlog.String("channel_id", board.ChannelID), - mlog.String("creator_id", channel.CreatorId), - ) - err := fmt.Errorf("%w board_id: %s, channel_id: %s, creator_id: %s", errChannelCreatorNotInTeam, board.ID, board.ChannelID, channel.CreatorId) - return "", err - } - - teamID = userTeams[channel.CreatorId][0] - } else if board.Type == "G" { - // pick the team that has the most users as members - teamFrequency := map[string]int{} - highestFrequencyTeam := "" - highestFrequencyTeamFrequency := -1 - - for _, teams := range userTeams { - for _, teamID := range teams { - teamFrequency[teamID]++ - - if teamFrequency[teamID] > highestFrequencyTeamFrequency { - highestFrequencyTeamFrequency = teamFrequency[teamID] - highestFrequencyTeam = teamID - } - } - } - - teamID = highestFrequencyTeam - } - } - - return teamID, nil -} - -func (s *SQLStore) getBoardUserTeams(tx sq.BaseRunner, board *model.Board) (map[string][]string, error) { - query := s.getQueryBuilder(tx). - Select("tm.UserId", "tm.TeamId"). - From("ChannelMembers cm"). - Join("TeamMembers tm ON cm.UserId = tm.UserId"). - Join("Teams t ON tm.TeamId = t.Id"). - Where(sq.Eq{ - "cm.ChannelId": board.ChannelID, - "t.DeleteAt": 0, - "tm.DeleteAt": 0, - }) - - rows, err := query.Query() - if err != nil { - s.logger.Error("failed to fetch user teams for board", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err)) - return nil, err - } - - defer rows.Close() - - userTeams := map[string][]string{} - - for rows.Next() { - var userID, teamID string - err := rows.Scan(&userID, &teamID) - if err != nil { - s.logger.Error("getBoardUserTeams failed to scan SQL query result", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err)) - return nil, err - } - - userTeams[userID] = append(userTeams[userID], teamID) - } - - return userTeams, nil -} - -func (s *SQLStore) RunDeletedMembershipBoardsMigration() error { - if !s.isPlugin { - return nil - } - - setting, err := s.GetSystemSetting(DeletedMembershipBoardsMigrationKey) - if err != nil { - return fmt.Errorf("cannot get deleted membership boards migration state: %w", err) - } - - // If the migration is already completed, do not run it again. - if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { - return nil - } - - boards, err := s.getDeletedMembershipBoards(s.db) - if err != nil { - return err - } - - if len(boards) == 0 { - s.logger.Debug("No boards with owner not anymore on their team found, marking runDeletedMembershipBoardsMigration as done") - if sErr := s.SetSystemSetting(DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); sErr != nil { - return fmt.Errorf("cannot mark migration as completed: %w", sErr) - } - return nil - } - - s.logger.Debug("Migrating boards with owner not anymore on their team", mlog.Int("count", len(boards))) - - tx, err := s.db.BeginTx(context.Background(), nil) - if err != nil { - s.logger.Error("error starting transaction in runDeletedMembershipBoardsMigration", mlog.Err(err)) - return err - } - - for i := range boards { - teamID, err := s.getBestTeamForBoard(s.db, boards[i]) - if err != nil { - // don't let one board's error spoil - // the mood for others - s.logger.Error("could not find the best team for board during deleted membership boards migration. Continuing", mlog.String("boardID", boards[i].ID)) - continue - } - - boards[i].TeamID = teamID - - query := s.getQueryBuilder(tx). - Update(s.tablePrefix+"boards"). - Set("team_id", teamID). - Where(sq.Eq{"id": boards[i].ID}) - - if _, err := query.Exec(); err != nil { - s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err)) - return err - } - } - - if err := s.setSystemSetting(tx, DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runDeletedMembershipBoardsMigration")) - } - return fmt.Errorf("cannot mark migration as completed: %w", err) - } - - if err := tx.Commit(); err != nil { - s.logger.Error("failed to commit runDeletedMembershipBoardsMigration transaction", mlog.Err(err)) - return err - } - - return nil -} - -// getDeletedMembershipBoards retrieves those boards whose creator is -// associated to the board's team with a deleted team membership. -func (s *SQLStore) getDeletedMembershipBoards(tx sq.BaseRunner) ([]*model.Board, error) { - rows, err := s.getQueryBuilder(tx). - Select(legacyBoardFields("b.")...). - From(s.tablePrefix + "boards b"). - Join("TeamMembers tm ON b.created_by = tm.UserId"). - Where("b.team_id = tm.TeamId"). - Where(sq.NotEq{"tm.DeleteAt": 0}). - Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - boards, err := s.boardsFromRows(rows) - if err != nil { - return nil, err - } - - return boards, err -} - -func (s *SQLStore) RunFixCollationsAndCharsetsMigration() error { - // This is for MySQL only - if s.dbType != model.MysqlDBType { - return nil - } - - // get collation and charSet setting that Channels is using. - // when personal server or unit testing, no channels tables exist so just set to a default. - var collation string - var charSet string - var err error - if !s.isPlugin || os.Getenv("FOCALBOARD_UNIT_TESTING") == "1" { - collation = "utf8mb4_general_ci" - charSet = "utf8mb4" - } else { - collation, charSet, err = s.getCollationAndCharset("Channels") - if err != nil { - return err - } - } - - // get all FocalBoard tables - tableNames, err := s.getFocalBoardTableNames() - if err != nil { - return err - } - - merr := merror.New() - - // alter each table if there is a collation or charset mismatch - for _, name := range tableNames { - tableCollation, tableCharSet, err := s.getCollationAndCharset(name) - if err != nil { - return err - } - - if collation == tableCollation && charSet == tableCharSet { - // nothing to do - continue - } - - s.logger.Warn( - "found collation/charset mismatch, fixing table", - mlog.String("tableName", name), - mlog.String("tableCollation", tableCollation), - mlog.String("tableCharSet", tableCharSet), - mlog.String("collation", collation), - mlog.String("charSet", charSet), - ) - - sql := fmt.Sprintf("ALTER TABLE %s CONVERT TO CHARACTER SET '%s' COLLATE '%s'", name, charSet, collation) - result, err := s.db.Exec(sql) - if err != nil { - merr.Append(err) - continue - } - num, err := result.RowsAffected() - if err != nil { - merr.Append(err) - } - if num > 0 { - s.logger.Debug("table collation and/or charSet fixed", - mlog.String("table_name", name), - ) - } - } - return merr.ErrorOrNil() -} - -func (s *SQLStore) getFocalBoardTableNames() ([]string, error) { - if s.dbType != model.MysqlDBType { - return nil, newErrInvalidDBType("getFocalBoardTableNames requires MySQL") - } - - query := s.getQueryBuilder(s.db). - Select("table_name"). - From("information_schema.tables"). - Where(sq.Like{"table_name": s.tablePrefix + "%"}). - Where("table_schema=(SELECT DATABASE())") - - rows, err := query.Query() - if err != nil { - return nil, fmt.Errorf("error fetching FocalBoard table names: %w", err) - } - defer rows.Close() - - names := make([]string, 0) - - for rows.Next() { - var tableName string - - err := rows.Scan(&tableName) - if err != nil { - return nil, fmt.Errorf("cannot scan result while fetching table names: %w", err) - } - - names = append(names, tableName) - } - - return names, nil -} - -func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, error) { - if s.dbType != model.MysqlDBType { - return "", "", newErrInvalidDBType("getCollationAndCharset requires MySQL") - } - - query := s.getQueryBuilder(s.db). - Select("table_collation"). - From("information_schema.tables"). - Where(sq.Eq{"table_name": tableName}). - Where("table_schema=(SELECT DATABASE())") - - row := query.QueryRow() - - var collation string - err := row.Scan(&collation) - if err != nil { - return "", "", fmt.Errorf("error fetching collation for table %s: %w", tableName, err) - } - - // obtains the charset from the first column that has it set - query = s.getQueryBuilder(s.db). - Select("CHARACTER_SET_NAME"). - From("information_schema.columns"). - Where(sq.Eq{ - "table_name": tableName, - }). - Where("table_schema=(SELECT DATABASE())"). - Where(sq.NotEq{"CHARACTER_SET_NAME": "NULL"}). - Limit(1) - - row = query.QueryRow() - - var charSet string - err = row.Scan(&charSet) - if err != nil { - return "", "", fmt.Errorf("error fetching charSet: %w", err) - } - - return collation, charSet, nil -} - -func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error { - setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey) - if err != nil { - return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err) - } - - // If the migration is already completed, do not run it again. - if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun { - return nil - } - - if currentMigration >= (deDuplicateCategoryBoards + 1) { - // if the migration for which we're fixing the data is already applied, - // no need to check fix anything - - if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) - } - return nil - } - - needed, err := s.doesDuplicateCategoryBoardsExist() - if err != nil { - return err - } - - if !needed { - if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) - } - } - - if s.dbType == model.MysqlDBType { - return s.runMySQLDeDuplicateCategoryBoardsMigration() - } else if s.dbType == model.PostgresDBType { - return s.runPostgresDeDuplicateCategoryBoardsMigration() - } - - if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil { - return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr) - } - - return nil -} - -func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) { - subQuery := s.getQueryBuilder(s.db). - Select("user_id", "board_id", "count(*) AS count"). - From(s.tablePrefix+"category_boards"). - GroupBy("user_id", "board_id"). - Having("count(*) > 1") - - query := s.getQueryBuilder(s.db). - Select("COUNT(user_id)"). - FromSelect(subQuery, "duplicate_dataset") - - row := query.QueryRow() - - count := 0 - if err := row.Scan(&count); err != nil { - s.logger.Error("Error occurred reading number of duplicate records in category_boards table", mlog.Err(err)) - return false, err - } - - return count > 0, nil -} - -func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error { - query := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " + - "(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)" - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) - } - - return nil -} - -func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error { - query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " + - "FROM " + s.tablePrefix + "category_boards) " + - "DELETE FROM " + s.tablePrefix + "category_boards USING duplicates " + - "WHERE " + s.tablePrefix + "category_boards.id = duplicates.id AND duplicates.rownum > 1;" - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err)) - } - - return nil -} diff --git a/server/boards/services/store/sqlstore/data_migrations_test.go b/server/boards/services/store/sqlstore/data_migrations_test.go deleted file mode 100644 index 91f77036df..0000000000 --- a/server/boards/services/store/sqlstore/data_migrations_test.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore/migrationstests" - "github.com/mgdelacroix/foundation" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetBlocksWithSameID(t *testing.T) { - t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") - - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - container1 := "1" - container2 := "2" - container3 := "3" - - block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"} - block3 := &model.Block{ID: "block-id-3", BoardID: "board-id-3"} - - block4 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block5 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"} - - block6 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block7 := &model.Block{ID: "block-id-7", BoardID: "board-id-7"} - block8 := &model.Block{ID: "block-id-8", BoardID: "board-id-8"} - - for _, block := range []*model.Block{block1, block2, block3} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - for _, block := range []*model.Block{block4, block5} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - for _, block := range []*model.Block{block6, block7, block8} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container3, block, "user-id") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - blocksWithDuplicatedID := []*model.Block{block1, block2, block4, block5, block6} - - blocks, err := sqlStore.getBlocksWithSameID(sqlStore.db) - require.NoError(t, err) - - // we process the found blocks to remove extra information and be - // able to compare both expected and found sets - foundBlocks := []*model.Block{} - for _, foundBlock := range blocks { - foundBlocks = append(foundBlocks, &model.Block{ID: foundBlock.ID, BoardID: foundBlock.BoardID}) - } - - require.ElementsMatch(t, blocksWithDuplicatedID, foundBlocks) - }) -} - -func TestReplaceBlockID(t *testing.T) { - t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") - - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - container1 := "1" - container2 := "2" - - // blocks from team1 - block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} - block3 := &model.Block{ID: "block-id-3", BoardID: "block-id-1"} - block4 := &model.Block{ID: "block-id-4", BoardID: "block-id-2"} - block5 := &model.Block{ID: "block-id-5", BoardID: "block-id-1", ParentID: "block-id-1"} - block8 := &model.Block{ - ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard, - Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}}, - } - - // blocks from team2. They're identical to blocks 1 and 2, - // but they shouldn't change - block6 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block7 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} - block9 := &model.Block{ - ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard, - Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}}, - } - - for _, block := range []*model.Block{block1, block2, block3, block4, block5, block8} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - for _, block := range []*model.Block{block6, block7, block9} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - currentID := "block-id-1" - newID := "new-id-1" - err := sqlStore.replaceBlockID(sqlStore.db, currentID, newID, "1") - require.NoError(t, err) - - newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, newID) - require.NoError(t, err) - newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID) - require.NoError(t, err) - newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID) - require.NoError(t, err) - newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block5.ID) - require.NoError(t, err) - newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID) - require.NoError(t, err) - newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block7.ID) - require.NoError(t, err) - newBlock8, err := sqlStore.GetBlock(block8.ID) - require.NoError(t, err) - newBlock9, err := sqlStore.GetBlock(block9.ID) - require.NoError(t, err) - - require.Equal(t, newID, newBlock1.ID) - require.Equal(t, newID, newBlock2.ParentID) - require.Equal(t, newID, newBlock3.BoardID) - require.Equal(t, newID, newBlock5.BoardID) - require.Equal(t, newID, newBlock5.ParentID) - require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[0], newID) - require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[1], "block-id-2") - - require.Equal(t, currentID, newBlock6.ID) - require.Equal(t, currentID, newBlock7.ParentID) - require.Equal(t, newBlock9.Fields["contentOrder"].([]interface{})[0], "block-id-1") - require.Equal(t, newBlock9.Fields["contentOrder"].([]interface{})[1], "block-id-2") - }) -} - -func TestRunUniqueIDsMigration(t *testing.T) { - t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests") - - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - // we need to mark the migration as not done so we can run it - // again with the test data - keyErr := sqlStore.SetSystemSetting(UniqueIDsMigrationKey, "false") - require.NoError(t, keyErr) - - container1 := "1" - container2 := "2" - container3 := "3" - - // blocks from workspace1. They shouldn't change, as the first - // duplicated ID is preserved - block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} - block3 := &model.Block{ID: "block-id-3", BoardID: "block-id-1"} - - // blocks from workspace2. They're identical to blocks 1, 2 and 3, - // and they should change - block4 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"} - block5 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"} - block6 := &model.Block{ID: "block-id-6", BoardID: "block-id-1", ParentID: "block-id-2"} - - // block from workspace3. It should change as well - block7 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"} - - for _, block := range []*model.Block{block1, block2, block3} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id-2") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - for _, block := range []*model.Block{block4, block5, block6} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id-2") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - for _, block := range []*model.Block{block7} { - err := sqlStore.insertLegacyBlock(sqlStore.db, container3, block, "user-id-2") - require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - } - - err := sqlStore.RunUniqueIDsMigration() - require.NoError(t, err) - - // blocks from workspace 1 haven't changed, so we can simply fetch them - newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block1.ID) - require.NoError(t, err) - require.NotNil(t, newBlock1) - newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID) - require.NoError(t, err) - require.NotNil(t, newBlock2) - newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID) - require.NoError(t, err) - require.NotNil(t, newBlock3) - - // first two blocks from workspace 2 have changed, so we fetch - // them through the third one, which points to the new IDs - newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID) - require.NoError(t, err) - require.NotNil(t, newBlock6) - newBlock4, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.BoardID) - require.NoError(t, err) - require.NotNil(t, newBlock4) - newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.ParentID) - require.NoError(t, err) - require.NotNil(t, newBlock5) - - // block from workspace 3 changed as well, so we shouldn't be able - // to fetch it - newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container3, block7.ID) - require.NoError(t, err) - require.Nil(t, newBlock7) - - // workspace 1 block links are maintained - require.Equal(t, newBlock1.ID, newBlock2.ParentID) - require.Equal(t, newBlock1.ID, newBlock3.BoardID) - - // workspace 2 first two block IDs have changed - require.NotEqual(t, block4.ID, newBlock4.BoardID) - require.NotEqual(t, block5.ID, newBlock5.ParentID) - }) -} - -func TestCheckForMismatchedCollation(t *testing.T) { - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - if sqlStore.dbType != model.MysqlDBType { - return - } - - // make sure all collations are consistent. - tableNames, err := sqlStore.getFocalBoardTableNames() - require.NoError(t, err) - - sqlCollation := "SELECT table_collation FROM information_schema.tables WHERE table_name=? and table_schema=(SELECT DATABASE())" - stmtCollation, err := sqlStore.db.Prepare(sqlCollation) - require.NoError(t, err) - defer stmtCollation.Close() - - var collation string - - // make sure the correct charset is applied to each table. - for i, name := range tableNames { - row := stmtCollation.QueryRow(name) - - var actualCollation string - err = row.Scan(&actualCollation) - require.NoError(t, err) - - if collation == "" { - collation = actualCollation - } - - assert.Equalf(t, collation, actualCollation, "for table_name='%s', index=%d", name, i) - } - }) -} - -func TestRunDeDuplicateCategoryBoardsMigration(t *testing.T) { - RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - th, tearDown := migrationstests.SetupTestHelper(t, f) - defer tearDown() - - th.F().MigrateToStepSkippingLastInterceptor(35). - ExecFile("./fixtures/testDeDuplicateCategoryBoardsMigration.sql") - - th.F().RunInterceptor(35) - - // verifying count of rows - var count int - countQuery := "SELECT COUNT(*) FROM focalboard_category_boards" - row := th.F().DB().QueryRow(countQuery) - err := row.Scan(&count) - assert.NoError(t, err) - assert.Equal(t, 4, count) - }) -} diff --git a/server/boards/services/store/sqlstore/data_retention.go b/server/boards/services/store/sqlstore/data_retention.go deleted file mode 100644 index 85992d8a92..0000000000 --- a/server/boards/services/store/sqlstore/data_retention.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package sqlstore - -import ( - "database/sql" - "strings" - "time" - - "github.com/pkg/errors" - - sq "github.com/Masterminds/squirrel" - _ "github.com/lib/pq" // postgres driver - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type RetentionTableDeletionInfo struct { - Table string - PrimaryKeys []string - BoardIDColumn string -} - -func (s *SQLStore) runDataRetention(db sq.BaseRunner, globalRetentionDate int64, batchSize int64) (int64, error) { - s.logger.Info("Start Boards Data Retention", - mlog.String("Global Retention Date", time.Unix(globalRetentionDate/1000, 0).String()), - mlog.Int64("Raw Date", globalRetentionDate)) - deleteTables := []RetentionTableDeletionInfo{ - { - Table: "blocks", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "board_id", - }, - { - Table: "blocks_history", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "board_id", - }, - { - Table: "boards", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "id", - }, - { - Table: "boards_history", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "id", - }, - { - Table: "board_members", - PrimaryKeys: []string{"board_id"}, - BoardIDColumn: "board_id", - }, - { - Table: "board_members_history", - PrimaryKeys: []string{"board_id"}, - BoardIDColumn: "board_id", - }, - { - Table: "sharing", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "id", - }, - { - Table: "category_boards", - PrimaryKeys: []string{"id"}, - BoardIDColumn: "board_id", - }, - } - - subBuilder := s.getQueryBuilder(db). - Select("board_id, MAX(update_at) AS maxDate"). - From(s.tablePrefix + "blocks"). - GroupBy("board_id") - - subQuery, _, _ := subBuilder.ToSql() - - builder := s.getQueryBuilder(db). - Select("id"). - From(s.tablePrefix + "boards"). - LeftJoin("( " + subQuery + " ) As subquery ON (subquery.board_id = id)"). - Where(sq.Lt{"maxDate": globalRetentionDate}). - Where(sq.NotEq{"team_id": "0"}). - Where(sq.Eq{"is_template": false}) - - rows, err := builder.Query() - if err != nil { - s.logger.Error(`dataRetention subquery ERROR`, mlog.Err(err)) - return 0, err - } - defer s.CloseRows(rows) - deleteIds, err := idsFromRows(rows) - if err != nil { - return 0, err - } - - totalAffected := 0 - if len(deleteIds) > 0 { - for _, table := range deleteTables { - affected, err := s.genericRetentionPoliciesDeletion(db, table, deleteIds, batchSize) - if err != nil { - return int64(totalAffected), err - } - totalAffected += int(affected) - } - } - s.logger.Info("Complete Boards Data Retention", - mlog.Int("Total deletion ids", len(deleteIds)), - mlog.Int("TotalAffected", totalAffected)) - return int64(totalAffected), nil -} - -func idsFromRows(rows *sql.Rows) ([]string, error) { - deleteIds := []string{} - for rows.Next() { - var boardID string - err := rows.Scan( - &boardID, - ) - if err != nil { - return nil, err - } - deleteIds = append(deleteIds, boardID) - } - return deleteIds, nil -} - -// genericRetentionPoliciesDeletion actually executes the DELETE query -// using a sq.SelectBuilder which selects the rows to delete. -func (s *SQLStore) genericRetentionPoliciesDeletion( - db sq.BaseRunner, - info RetentionTableDeletionInfo, - deleteIds []string, - batchSize int64, -) (int64, error) { - whereClause := info.BoardIDColumn + " IN ('" + strings.Join(deleteIds, "','") + "')" - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + info.Table). - Where(whereClause) - - if batchSize > 0 { - deleteQuery.Limit(uint64(batchSize)) - primaryKeysStr := "(" + strings.Join(info.PrimaryKeys, ",") + ")" - if s.dbType != model.MysqlDBType { - selectQuery := s.getQueryBuilder(db). - Select(primaryKeysStr). - From(s.tablePrefix + info.Table). - Where(whereClause). - Limit(uint64(batchSize)) - - selectString, _, _ := selectQuery.ToSql() - - deleteQuery = s.getQueryBuilder(db). - Delete(s.tablePrefix + info.Table). - Where(primaryKeysStr + " IN (" + selectString + ")") - } - } - - var totalRowsAffected int64 - var batchRowsAffected int64 - for { - result, err := deleteQuery.Exec() - if err != nil { - return 0, errors.Wrap(err, "failed to delete "+info.Table) - } - - batchRowsAffected, err = result.RowsAffected() - if err != nil { - return 0, errors.Wrap(err, "failed to get rows affected for "+info.Table) - } - totalRowsAffected += batchRowsAffected - if batchRowsAffected != batchSize { - break - } - } - return totalRowsAffected, nil -} diff --git a/server/boards/services/store/sqlstore/file.go b/server/boards/services/store/sqlstore/file.go deleted file mode 100644 index 2c279c8f3c..0000000000 --- a/server/boards/services/store/sqlstore/file.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "errors" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mm_model.FileInfo) error { - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"file_info"). - Columns( - "id", - "create_at", - "name", - "extension", - "size", - "delete_at", - "path", - "archived", - ). - Values( - fileInfo.Id, - fileInfo.CreateAt, - fileInfo.Name, - fileInfo.Extension, - fileInfo.Size, - fileInfo.DeleteAt, - fileInfo.Path, - false, - ) - - if _, err := query.Exec(); err != nil { - s.logger.Error( - "failed to save fileinfo", - mlog.String("file_name", fileInfo.Name), - mlog.Int64("size", fileInfo.Size), - mlog.Err(err), - ) - return err - } - - return nil -} - -func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mm_model.FileInfo, error) { - query := s.getQueryBuilder(db). - Select( - "id", - "create_at", - "delete_at", - "name", - "extension", - "size", - "archived", - "path", - ). - From(s.tablePrefix + "file_info"). - Where(sq.Eq{"Id": id}) - - row := query.QueryRow() - - fileInfo := mm_model.FileInfo{} - - err := row.Scan( - &fileInfo.Id, - &fileInfo.CreateAt, - &fileInfo.DeleteAt, - &fileInfo.Name, - &fileInfo.Extension, - &fileInfo.Size, - &fileInfo.Archived, - &fileInfo.Path, - ) - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, model.NewErrNotFound("file info ID=" + id) - } - - s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err)) - return nil, err - } - - return &fileInfo, nil -} diff --git a/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql b/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql deleted file mode 100644 index 69a7dc9bde..0000000000 --- a/server/boards/services/store/sqlstore/fixtures/testDeDuplicateCategoryBoardsMigration.sql +++ /dev/null @@ -1,9 +0,0 @@ -INSERT INTO focalboard_category_boards(id, user_id, category_id, board_id, create_at, update_at, sort_order) -VALUES - ('id_1', 'user_id_1', 'category_id_1', 'board_id_1', 0, 0, 0), - ('id_2', 'user_id_1', 'category_id_2', 'board_id_1', 0, 0, 0), - ('id_3', 'user_id_1', 'category_id_3', 'board_id_1', 0, 0, 0), - ('id_4', 'user_id_2', 'category_id_4', 'board_id_2', 0, 0, 0), - ('id_5', 'user_id_2', 'category_id_5', 'board_id_2', 0, 0, 0), - ('id_6', 'user_id_3', 'category_id_6', 'board_id_3', 0, 0, 0), - ('id_7', 'user_id_4', 'category_id_6', 'board_id_4', 0, 0, 0); diff --git a/server/boards/services/store/sqlstore/legacy_blocks.go b/server/boards/services/store/sqlstore/legacy_blocks.go deleted file mode 100644 index 0d8cf2cfcd..0000000000 --- a/server/boards/services/store/sqlstore/legacy_blocks.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "strings" - - "github.com/mattermost/mattermost/server/v8/boards/utils" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func legacyBoardFields(prefix string) []string { - // substitute new columns with `"\"\""` (empty string) so as to allow - // row scan to continue to work with new models. - - fields := []string{ - "id", - "team_id", - "COALESCE(channel_id, '')", - "COALESCE(created_by, '')", - "modified_by", - "type", - "''", // substitute for minimum_role column. - "title", - "description", - "icon", - "show_description", - "is_template", - "template_version", - "COALESCE(properties, '{}')", - "COALESCE(card_properties, '[]')", - "create_at", - "update_at", - "delete_at", - } - - if prefix == "" { - return fields - } - - prefixedFields := make([]string, len(fields)) - for i, field := range fields { - switch { - case strings.HasPrefix(field, "COALESCE("): - prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1) - case field == "''": - prefixedFields[i] = field - default: - prefixedFields[i] = prefix + field - } - } - return prefixedFields -} - -// legacyBlocksFromRows is the old getBlock version that still uses -// the old block model. This method is kept to enable the unique IDs -// data migration. -// -//nolint:unused -func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]*model.Block, error) { - results := []*model.Block{} - - for rows.Next() { - var block model.Block - var fieldsJSON string - var modifiedBy sql.NullString - var insertAt string - - err := rows.Scan( - &block.ID, - &block.ParentID, - &block.BoardID, - &block.CreatedBy, - &modifiedBy, - &block.Schema, - &block.Type, - &block.Title, - &fieldsJSON, - &insertAt, - &block.CreateAt, - &block.UpdateAt, - &block.DeleteAt, - &block.WorkspaceID) - if err != nil { - // handle this error - s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err)) - - return nil, err - } - - if modifiedBy.Valid { - block.ModifiedBy = modifiedBy.String - } - - err = json.Unmarshal([]byte(fieldsJSON), &block.Fields) - if err != nil { - // handle this error - s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err)) - - return nil, err - } - - results = append(results, &block) - } - - return results, nil -} - -// getLegacyBlock is the old getBlock version that still uses the old -// block model. This method is kept to enable the unique IDs data -// migration. -// -//nolint:unused -func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID string) (*model.Block, error) { - query := s.getQueryBuilder(db). - Select( - "id", - "parent_id", - "root_id", - "created_by", - "modified_by", - s.escapeField("schema"), - "type", - "title", - "COALESCE(fields, '{}')", - "insert_at", - "create_at", - "update_at", - "delete_at", - "COALESCE(workspace_id, '0')", - ). - From(s.tablePrefix + "blocks"). - Where(sq.Eq{"id": blockID}). - Where(sq.Eq{"coalesce(workspace_id, '0')": workspaceID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`GetBlock ERROR`, mlog.Err(err)) - return nil, err - } - - blocks, err := s.legacyBlocksFromRows(rows) - if err != nil { - return nil, err - } - - if len(blocks) == 0 { - return nil, nil - } - - return blocks[0], nil -} - -// insertLegacyBlock is the old insertBlock version that still uses -// the old block model. This method is kept to enable the unique IDs -// data migration. -// -//nolint:unused -func (s *SQLStore) insertLegacyBlock(db sq.BaseRunner, workspaceID string, block *model.Block, userID string) error { - if block.BoardID == "" { - return ErrEmptyBoardID{} - } - - fieldsJSON, err := json.Marshal(block.Fields) - if err != nil { - return err - } - - existingBlock, err := s.getLegacyBlock(db, workspaceID, block.ID) - if err != nil { - return err - } - - block.UpdateAt = utils.GetMillis() - block.ModifiedBy = userID - - insertQuery := s.getQueryBuilder(db).Insert(""). - Columns( - "workspace_id", - "id", - "parent_id", - "root_id", - "created_by", - "modified_by", - s.escapeField("schema"), - "type", - "title", - "fields", - "create_at", - "update_at", - "delete_at", - ) - - insertQueryValues := map[string]interface{}{ - "workspace_id": workspaceID, - "id": block.ID, - "parent_id": block.ParentID, - "root_id": block.BoardID, - s.escapeField("schema"): block.Schema, - "type": block.Type, - "title": block.Title, - "fields": fieldsJSON, - "delete_at": block.DeleteAt, - "created_by": block.CreatedBy, - "modified_by": block.ModifiedBy, - "create_at": block.CreateAt, - "update_at": block.UpdateAt, - } - - if existingBlock != nil { - // block with ID exists, so this is an update operation - query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks"). - Where(sq.Eq{"id": block.ID}). - Where(sq.Eq{"COALESCE(workspace_id, '0')": workspaceID}). - Set("parent_id", block.ParentID). - Set("root_id", block.BoardID). - Set("modified_by", block.ModifiedBy). - Set(s.escapeField("schema"), block.Schema). - Set("type", block.Type). - Set("title", block.Title). - Set("fields", fieldsJSON). - Set("update_at", block.UpdateAt). - Set("delete_at", block.DeleteAt) - - if _, err := query.Exec(); err != nil { - s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err)) - return err - } - } else { - block.CreatedBy = userID - block.CreateAt = utils.GetMillis() - - insertQueryValues["created_by"] = block.CreatedBy - insertQueryValues["create_at"] = block.CreateAt - insertQueryValues["update_at"] = block.UpdateAt - insertQueryValues["modified_by"] = block.ModifiedBy - - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks") - if _, err := query.Exec(); err != nil { - return err - } - } - - // writing block history - query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history") - if _, err := query.Exec(); err != nil { - return err - } - - return nil -} - -func (s *SQLStore) getLegacyBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) { - return s.getBoardsFieldsByCondition(db, legacyBoardFields(""), conditions...) -} diff --git a/server/boards/services/store/sqlstore/main_test.go b/server/boards/services/store/sqlstore/main_test.go deleted file mode 100644 index ed81fa3ce7..0000000000 --- a/server/boards/services/store/sqlstore/main_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "os" - "testing" -) - -func TestMain(m *testing.M) { - mainStoreTypes = initStores(false) - - status := m.Run() - - for _, st := range mainStoreTypes { - _ = st.Store.Shutdown() - _ = st.Logger.Shutdown() - } - - os.Exit(status) -} diff --git a/server/boards/services/store/sqlstore/migrate.go b/server/boards/services/store/sqlstore/migrate.go deleted file mode 100644 index 066a80115b..0000000000 --- a/server/boards/services/store/sqlstore/migrate.go +++ /dev/null @@ -1,658 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "bytes" - "context" - "database/sql" - "embed" - "errors" - "fmt" - "strings" - - "text/template" - - sq "github.com/Masterminds/squirrel" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" - - "github.com/mattermost/morph" - drivers "github.com/mattermost/morph/drivers" - mysql "github.com/mattermost/morph/drivers/mysql" - postgres "github.com/mattermost/morph/drivers/postgres" - embedded "github.com/mattermost/morph/sources/embedded" - - _ "github.com/lib/pq" // postgres driver - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -//go:embed migrations/*.sql -var Assets embed.FS - -const ( - uniqueIDsMigrationRequiredVersion = 14 - teamLessBoardsMigrationRequiredVersion = 18 - categoriesUUIDIDMigrationRequiredVersion = 20 - deDuplicateCategoryBoards = 35 - - tempSchemaMigrationTableName = "temp_schema_migration" -) - -var errChannelCreatorNotInTeam = errors.New("channel creator not found in user teams") - -// migrations in MySQL need to run with the multiStatements flag -// enabled, so this method creates a new connection ensuring that it's -// enabled. -func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { - connectionString := s.connectionString - if s.dbType == model.MysqlDBType { - var err error - connectionString, err = sqlstore.ResetReadTimeout(connectionString) - if err != nil { - return nil, err - } - - connectionString, err = sqlstore.AppendMultipleStatementsFlag(connectionString) - if err != nil { - return nil, err - } - } - - var settings mm_model.SqlSettings - settings.SetDefaults(false) - if s.configFn != nil { - settings = s.configFn().SqlSettings - } - *settings.DriverName = s.dbType - - db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts) - if err != nil { - return nil, err - } - - return db, nil -} - -func (s *SQLStore) Migrate() error { - if err := s.EnsureSchemaMigrationFormat(); err != nil { - return err - } - defer func() { - // the old schema migration table deletion happens after the - // migrations have run, to be able to recover its information - // in case there would be errors during the process. - if err := s.deleteOldSchemaMigrationTable(); err != nil { - s.logger.Error("cannot delete the old schema migration table", mlog.Err(err)) - } - }() - - var driver drivers.Driver - var err error - var db *sql.DB - s.logger.Debug("Getting migrations connection") - db, err = s.getMigrationConnection() - if err != nil { - return err - } - - defer func() { - s.logger.Debug("Closing migrations connection") - db.Close() - }() - - if s.dbType == model.PostgresDBType { - driver, err = postgres.WithInstance(db) - if err != nil { - return err - } - } - - if s.dbType == model.MysqlDBType { - driver, err = mysql.WithInstance(db) - if err != nil { - return err - } - } - - assetsList, err := Assets.ReadDir("migrations") - if err != nil { - return err - } - assetNamesForDriver := make([]string, len(assetsList)) - for i, dirEntry := range assetsList { - assetNamesForDriver[i] = dirEntry.Name() - } - - params := map[string]interface{}{ - "prefix": s.tablePrefix, - "postgres": s.dbType == model.PostgresDBType, - "mysql": s.dbType == model.MysqlDBType, - "plugin": s.isPlugin, - "singleUser": s.isSingleUser, - } - - migrationAssets := &embedded.AssetSource{ - Names: assetNamesForDriver, - AssetFunc: func(name string) ([]byte, error) { - asset, mErr := Assets.ReadFile("migrations/" + name) - if mErr != nil { - return nil, mErr - } - - tmpl, pErr := template.New("sql").Funcs(s.GetTemplateHelperFuncs()).Parse(string(asset)) - if pErr != nil { - return nil, pErr - } - - buffer := bytes.NewBufferString("") - - err = tmpl.Execute(buffer, params) - if err != nil { - return nil, err - } - - s.logger.Trace("migration template", - mlog.String("name", name), - mlog.String("sql", buffer.String()), - ) - - return buffer.Bytes(), nil - }, - } - - src, err := embedded.WithInstance(migrationAssets) - if err != nil { - return err - } - - opts := []morph.EngineOption{ - morph.WithLock("boards-lock-key"), - morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", s.tablePrefix)), - morph.SetStatementTimeoutInSeconds(1000000), - } - - s.logger.Debug("Creating migration engine") - engine, err := morph.New(context.Background(), driver, src, opts...) - if err != nil { - return err - } - defer func() { - s.logger.Debug("Closing migration engine") - engine.Close() - }() - - return s.runMigrationSequence(engine, driver) -} - -// runMigrationSequence executes all the migrations in order, both -// plain SQL and data migrations. -func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driver) error { - if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, uniqueIDsMigrationRequiredVersion); mErr != nil { - return mErr - } - - if mErr := s.RunUniqueIDsMigration(); mErr != nil { - return fmt.Errorf("error running unique IDs migration: %w", mErr) - } - - if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, teamLessBoardsMigrationRequiredVersion); mErr != nil { - return mErr - } - - if mErr := s.RunTeamLessBoardsMigration(); mErr != nil { - return fmt.Errorf("error running teamless boards migration: %w", mErr) - } - - if mErr := s.RunDeletedMembershipBoardsMigration(); mErr != nil { - return fmt.Errorf("error running deleted membership boards migration: %w", mErr) - } - - if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, categoriesUUIDIDMigrationRequiredVersion); mErr != nil { - return mErr - } - - if mErr := s.RunCategoryUUIDIDMigration(); mErr != nil { - return fmt.Errorf("error running categoryID migration: %w", mErr) - } - - appliedMigrations, err := driver.AppliedMigrations() - if err != nil { - return err - } - - if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, deDuplicateCategoryBoards); mErr != nil { - return mErr - } - - currentMigrationVersion := len(appliedMigrations) - if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); mErr != nil { - return mErr - } - - s.logger.Debug("== Applying all remaining migrations ====================", - mlog.Int("current_version", len(appliedMigrations)), - ) - - if err := engine.ApplyAll(); err != nil { - return err - } - - // always run the collations & charset fix-ups - if mErr := s.RunFixCollationsAndCharsetsMigration(); mErr != nil { - return fmt.Errorf("error running fix collations and charsets migration: %w", mErr) - } - return nil -} - -func (s *SQLStore) ensureMigrationsAppliedUpToVersion(engine *morph.Morph, driver drivers.Driver, version int) error { - applied, err := driver.AppliedMigrations() - if err != nil { - return err - } - currentVersion := len(applied) - - s.logger.Debug("== Ensuring migrations applied up to version ====================", - mlog.Int("version", version), - mlog.Int("current_version", currentVersion)) - - // if the target version is below or equal to the current one, do - // not migrate either because is not needed (both are equal) or - // because it would downgrade the database (is below) - if version <= currentVersion { - s.logger.Debug("-- There is no need of applying any migration --------------------") - return nil - } - - for _, migration := range applied { - s.logger.Debug("-- Found applied migration --------------------", mlog.Uint32("version", migration.Version), mlog.String("name", migration.Name)) - } - - if _, err = engine.Apply(version - currentVersion); err != nil { - return err - } - - return nil -} - -func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap { - funcs := template.FuncMap{ - "addColumnIfNeeded": s.genAddColumnIfNeeded, - "dropColumnIfNeeded": s.genDropColumnIfNeeded, - "createIndexIfNeeded": s.genCreateIndexIfNeeded, - "renameTableIfNeeded": s.genRenameTableIfNeeded, - "renameColumnIfNeeded": s.genRenameColumnIfNeeded, - "doesTableExist": s.doesTableExist, - "doesColumnExist": s.doesColumnExist, - "addConstraintIfNeeded": s.genAddConstraintIfNeeded, - } - return funcs -} - -func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constraint string) (string, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - normTableName := normalizeTablename(s.schemaName, tableName) - - switch s.dbType { - case model.MysqlDBType: - vars := map[string]string{ - "schema": s.schemaName, - "table_name": tableName, - "norm_table_name": normTableName, - "column_name": columnName, - "data_type": datatype, - "constraint": constraint, - } - return replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - AND column_name = '[[column_name]]' - ) > 0, - 'SELECT 1;', - 'ALTER TABLE [[norm_table_name]] ADD COLUMN [[column_name]] [[data_type]] [[constraint]];' - )); - PREPARE addColumnIfNeeded FROM @stmt; - EXECUTE addColumnIfNeeded; - DEALLOCATE PREPARE addColumnIfNeeded; - `, vars), nil - case model.PostgresDBType: - return fmt.Sprintf("\nALTER TABLE %s ADD COLUMN IF NOT EXISTS %s %s %s;\n", normTableName, columnName, datatype, constraint), nil - default: - return "", ErrUnsupportedDatabaseType - } -} - -func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - normTableName := normalizeTablename(s.schemaName, tableName) - - switch s.dbType { - case model.MysqlDBType: - vars := map[string]string{ - "schema": s.schemaName, - "table_name": tableName, - "norm_table_name": normTableName, - "column_name": columnName, - } - return replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - AND column_name = '[[column_name]]' - ) > 0, - 'ALTER TABLE [[norm_table_name]] DROP COLUMN [[column_name]];', - 'SELECT 1;' - )); - PREPARE dropColumnIfNeeded FROM @stmt; - EXECUTE dropColumnIfNeeded; - DEALLOCATE PREPARE dropColumnIfNeeded; - `, vars), nil - case model.PostgresDBType: - return fmt.Sprintf("\nALTER TABLE %s DROP COLUMN IF EXISTS %s;\n", normTableName, columnName), nil - default: - return "", ErrUnsupportedDatabaseType - } -} - -func (s *SQLStore) genCreateIndexIfNeeded(tableName, columns string) (string, error) { - indexName := getIndexName(tableName, columns) - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - normTableName := normalizeTablename(s.schemaName, tableName) - - switch s.dbType { - case model.MysqlDBType: - vars := map[string]string{ - "schema": s.schemaName, - "table_name": tableName, - "norm_table_name": normTableName, - "index_name": indexName, - "columns": columns, - } - return replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(index_name) FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - AND index_name = '[[index_name]]' - ) > 0, - 'SELECT 1;', - 'CREATE INDEX [[index_name]] ON [[norm_table_name]] ([[columns]]);' - )); - PREPARE createIndexIfNeeded FROM @stmt; - EXECUTE createIndexIfNeeded; - DEALLOCATE PREPARE createIndexIfNeeded; - `, vars), nil - case model.PostgresDBType: - return fmt.Sprintf("\nCREATE INDEX IF NOT EXISTS %s ON %s (%s);\n", indexName, normTableName, columns), nil - default: - return "", ErrUnsupportedDatabaseType - } -} - -func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (string, error) { - oldTableName = addPrefixIfNeeded(oldTableName, s.tablePrefix) - newTableName = addPrefixIfNeeded(newTableName, s.tablePrefix) - - normOldTableName := normalizeTablename(s.schemaName, oldTableName) - - vars := map[string]string{ - "schema": s.schemaName, - "table_name": newTableName, - "norm_old_table_name": normOldTableName, - "new_table_name": newTableName, - } - - switch s.dbType { - case model.MysqlDBType: - return replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - ) > 0, - 'SELECT 1;', - 'RENAME TABLE [[norm_old_table_name]] TO [[new_table_name]];' - )); - PREPARE renameTableIfNeeded FROM @stmt; - EXECUTE renameTableIfNeeded; - DEALLOCATE PREPARE renameTableIfNeeded; - `, vars), nil - case model.PostgresDBType: - return replaceVars(` - do $$ - begin - if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES - WHERE table_name = '[[new_table_name]]' - AND table_schema = '[[schema]]' - ) = 0 then - ALTER TABLE [[norm_old_table_name]] RENAME TO [[new_table_name]]; - end if; - end$$; - `, vars), nil - default: - return "", ErrUnsupportedDatabaseType - } -} - -func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnName, dataType string) (string, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - normTableName := normalizeTablename(s.schemaName, tableName) - - vars := map[string]string{ - "schema": s.schemaName, - "table_name": tableName, - "norm_table_name": normTableName, - "old_column_name": oldColumnName, - "new_column_name": newColumnName, - "data_type": dataType, - } - - switch s.dbType { - case model.MysqlDBType: - return replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - AND column_name = '[[new_column_name]]' - ) > 0, - 'SELECT 1;', - 'ALTER TABLE [[norm_table_name]] CHANGE [[old_column_name]] [[new_column_name]] [[data_type]];' - )); - PREPARE renameColumnIfNeeded FROM @stmt; - EXECUTE renameColumnIfNeeded; - DEALLOCATE PREPARE renameColumnIfNeeded; - `, vars), nil - case model.PostgresDBType: - return replaceVars(` - do $$ - begin - if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = '[[table_name]]' - AND table_schema = '[[schema]]' - AND column_name = '[[new_column_name]]' - ) = 0 then - ALTER TABLE [[norm_table_name]] RENAME COLUMN [[old_column_name]] TO [[new_column_name]]; - end if; - end$$; - `, vars), nil - default: - return "", ErrUnsupportedDatabaseType - } -} - -func (s *SQLStore) doesTableExist(tableName string) (bool, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - - query := s.getQueryBuilder(s.db). - Select("table_name"). - From("INFORMATION_SCHEMA.TABLES"). - Where(sq.Eq{ - "table_name": tableName, - "table_schema": s.schemaName, - }) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`doesTableExist ERROR`, mlog.Err(err)) - return false, err - } - defer s.CloseRows(rows) - - exists := rows.Next() - sql, _, _ := query.ToSql() - - s.logger.Trace("doesTableExist", - mlog.String("table", tableName), - mlog.Bool("exists", exists), - mlog.String("sql", sql), - ) - return exists, nil -} - -func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - - query := s.getQueryBuilder(s.db). - Select("table_name"). - From("INFORMATION_SCHEMA.COLUMNS"). - Where(sq.Eq{ - "table_name": tableName, - "table_schema": s.schemaName, - "column_name": columnName, - }) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`doesColumnExist ERROR`, mlog.Err(err)) - return false, err - } - defer s.CloseRows(rows) - - exists := rows.Next() - sql, _, _ := query.ToSql() - - s.logger.Trace("doesColumnExist", - mlog.String("table", tableName), - mlog.String("column", columnName), - mlog.Bool("exists", exists), - mlog.String("sql", sql), - ) - return exists, nil -} - -func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) { - tableName = addPrefixIfNeeded(tableName, s.tablePrefix) - normTableName := normalizeTablename(s.schemaName, tableName) - - var query string - - vars := map[string]string{ - "schema": s.schemaName, - "constraint_name": constraintName, - "constraint_type": constraintType, - "table_name": tableName, - "constraint_definition": constraintDefinition, - "norm_table_name": normTableName, - } - - switch s.dbType { - case model.MysqlDBType: - query = replaceVars(` - SET @stmt = (SELECT IF( - ( - SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE constraint_schema = '[[schema]]' - AND constraint_name = '[[constraint_name]]' - AND constraint_type = '[[constraint_type]]' - AND table_name = '[[table_name]]' - ) > 0, - 'SELECT 1;', - 'ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];' - )); - PREPARE addConstraintIfNeeded FROM @stmt; - EXECUTE addConstraintIfNeeded; - DEALLOCATE PREPARE addConstraintIfNeeded; - `, vars) - case model.PostgresDBType: - query = replaceVars(` - DO - $$ - BEGIN - IF NOT EXISTS ( - SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE constraint_schema = '[[schema]]' - AND constraint_name = '[[constraint_name]]' - AND constraint_type = '[[constraint_type]]' - AND table_name = '[[table_name]]' - ) THEN - ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]]; - END IF; - END; - $$ - LANGUAGE plpgsql; - `, vars) - } - - return query, nil -} - -func addPrefixIfNeeded(s, prefix string) string { - if !strings.HasPrefix(s, prefix) { - return prefix + s - } - return s -} - -func normalizeTablename(schemaName, tableName string) string { - if schemaName != "" && !strings.HasPrefix(tableName, schemaName+".") { - tableName = schemaName + "." + tableName - } - return tableName -} - -func getIndexName(tableName string, columns string) string { - var sb strings.Builder - - _, _ = sb.WriteString("idx_") - _, _ = sb.WriteString(tableName) - - // allow developers to separate column names with spaces and/or commas - columns = strings.ReplaceAll(columns, ",", " ") - cols := strings.Split(columns, " ") - - for _, s := range cols { - sub := strings.TrimSpace(s) - if sub == "" { - continue - } - - _, _ = sb.WriteString("_") - _, _ = sb.WriteString(s) - } - return sb.String() -} - -// replaceVars replaces instances of variable placeholders with the -// values provided via a map. Variable placeholders are of the form -// `[[var_name]]`. -func replaceVars(s string, vars map[string]string) string { - for key, val := range vars { - placeholder := "[[" + key + "]]" - val = strings.ReplaceAll(val, "'", "\\'") - s = strings.ReplaceAll(s, placeholder, val) - } - return s -} diff --git a/server/boards/services/store/sqlstore/migrations/000001_init.down.sql b/server/boards/services/store/sqlstore/migrations/000001_init.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000001_init.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000001_init.up.sql b/server/boards/services/store/sqlstore/migrations/000001_init.up.sql deleted file mode 100644 index 3717f9d1b1..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000001_init.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}blocks ( - id VARCHAR(36), - {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} - {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} - parent_id VARCHAR(36), - {{if .mysql}}`schema`{{else}}schema{{end}} BIGINT, - type TEXT, - title TEXT, - fields {{if .postgres}}JSON{{else}}TEXT{{end}}, - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - PRIMARY KEY (id, insert_at) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.down.sql b/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.up.sql b/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.up.sql deleted file mode 100644 index 394f7b015c..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000002_system_settings_table.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}system_settings ( - id VARCHAR(100), - value TEXT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.down.sql b/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.up.sql b/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.up.sql deleted file mode 100644 index 500ce16268..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000003_blocks_rootid.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "blocks" "root_id" "varchar(36)" ""}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000004_auth_table.down.sql b/server/boards/services/store/sqlstore/migrations/000004_auth_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000004_auth_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000004_auth_table.up.sql b/server/boards/services/store/sqlstore/migrations/000004_auth_table.up.sql deleted file mode 100644 index ea9eb738d6..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000004_auth_table.up.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}users ( - id VARCHAR(100), - username VARCHAR(100), - email VARCHAR(255), - password VARCHAR(100), - mfa_secret VARCHAR(100), - auth_service VARCHAR(20), - auth_data VARCHAR(255), - props {{if .postgres}}JSON{{else}}TEXT{{end}}, - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -CREATE TABLE IF NOT EXISTS {{.prefix}}sessions ( - id VARCHAR(100), - token VARCHAR(100), - user_id VARCHAR(100), - props {{if .postgres}}JSON{{else}}TEXT{{end}}, - create_at BIGINT, - update_at BIGINT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.down.sql b/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.up.sql b/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.up.sql deleted file mode 100644 index 411f7968d3..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000005_blocks_modifiedby.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "blocks" "modified_by" "varchar(36)" ""}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000006_sharing_table.down.sql b/server/boards/services/store/sqlstore/migrations/000006_sharing_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000006_sharing_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000006_sharing_table.up.sql b/server/boards/services/store/sqlstore/migrations/000006_sharing_table.up.sql deleted file mode 100644 index 2f21ca030e..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000006_sharing_table.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}sharing ( - id VARCHAR(36), - enabled BOOLEAN, - token VARCHAR(100), - modified_by VARCHAR(36), - update_at BIGINT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.down.sql b/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.up.sql b/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.up.sql deleted file mode 100644 index 9ce73a9957..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000007_workspaces_table.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}workspaces ( - id VARCHAR(36), - signup_token VARCHAR(100) NOT NULL, - settings {{if .postgres}}JSON{{else}}TEXT{{end}}, - modified_by VARCHAR(36), - update_at BIGINT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000008_teams.down.sql b/server/boards/services/store/sqlstore/migrations/000008_teams.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000008_teams.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000008_teams.up.sql b/server/boards/services/store/sqlstore/migrations/000008_teams.up.sql deleted file mode 100644 index 133f56bf1f..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000008_teams.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "blocks" "workspace_id" "varchar(36)" ""}} - -{{ addColumnIfNeeded "sharing" "workspace_id" "varchar(36)" ""}} - -{{ addColumnIfNeeded "sessions" "auth_service" "varchar(20)" ""}} - -UPDATE {{.prefix}}blocks SET workspace_id = '0' WHERE workspace_id = '' OR workspace_id IS NULL; diff --git a/server/boards/services/store/sqlstore/migrations/000009_blocks_history.down.sql b/server/boards/services/store/sqlstore/migrations/000009_blocks_history.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000009_blocks_history.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000009_blocks_history.up.sql b/server/boards/services/store/sqlstore/migrations/000009_blocks_history.up.sql deleted file mode 100644 index af4ed74690..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000009_blocks_history.up.sql +++ /dev/null @@ -1,40 +0,0 @@ -{{- /* Only perform this migration if the blocks_history table does not already exist */ -}} - -{{- /* doesTableExist tableName */ -}} -{{if doesTableExist "blocks_history" }} - - SELECT 1; - -{{else}} - -{{- /* renameTableIfNeeded oldTableName newTableName */ -}} -{{ renameTableIfNeeded "blocks" "blocks_history" }} - -CREATE TABLE IF NOT EXISTS {{.prefix}}blocks ( - id VARCHAR(36), - {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} - {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} - parent_id VARCHAR(36), - {{if .mysql}}`schema`{{else}}schema{{end}} BIGINT, - type TEXT, - title TEXT, - fields {{if .postgres}}JSON{{else}}TEXT{{end}}, - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - root_id VARCHAR(36), - modified_by VARCHAR(36), - workspace_id VARCHAR(36), - PRIMARY KEY (workspace_id,id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{if .mysql}} -INSERT IGNORE INTO {{.prefix}}blocks (SELECT * FROM {{.prefix}}blocks_history ORDER BY insert_at DESC); -{{end}} -{{if .postgres}} -INSERT INTO {{.prefix}}blocks (SELECT * FROM {{.prefix}}blocks_history ORDER BY insert_at DESC) ON CONFLICT DO NOTHING; -{{end}} - -{{end}} - -DELETE FROM {{.prefix}}blocks where delete_at > 0; diff --git a/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.down.sql b/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.up.sql b/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.up.sql deleted file mode 100644 index ba5143127a..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000010_blocks_created_by.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint) */ -}} -{{ addColumnIfNeeded "blocks" "created_by" "varchar(36)" ""}} -{{ addColumnIfNeeded "blocks_history" "created_by" "varchar(36)" ""}} - -UPDATE {{.prefix}}blocks SET created_by = - COALESCE(NULLIF((select modified_by from {{.prefix}}blocks_history where {{.prefix}}blocks_history.id = {{.prefix}}blocks.id ORDER BY {{.prefix}}blocks_history.insert_at ASC limit 1), ''), 'system') -WHERE created_by IS NULL; diff --git a/server/boards/services/store/sqlstore/migrations/000011_match_collation.down.sql b/server/boards/services/store/sqlstore/migrations/000011_match_collation.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000011_match_collation.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000011_match_collation.up.sql b/server/boards/services/store/sqlstore/migrations/000011_match_collation.up.sql deleted file mode 100644 index c593b63535..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000011_match_collation.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -{{- /* All tables have collation fixed via code at startup so this migration is no longer needed. */ -}} -{{- /* See https://github.com/mattermost/focalboard/pull/4002 */ -}} - -SELECT 1; - - - diff --git a/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.down.sql b/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.up.sql b/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.up.sql deleted file mode 100644 index 21be1cb35e..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000012_match_column_collation.up.sql +++ /dev/null @@ -1,54 +0,0 @@ -{{if and .mysql .plugin}} - -- this migration applies collation on column level. - -- collation of mattermost's Channels table - SET @mattermostCollation = (SELECT table_collation from information_schema.tables WHERE table_name = 'Channels' AND table_schema = (SELECT DATABASE())); - -- charset of mattermost's CHannels table's Name column - SET @mattermostCharset = (SELECT CHARACTER_SET_NAME from information_schema.columns WHERE table_name = 'Channels' AND table_schema = (SELECT DATABASE()) AND COLUMN_NAME = 'Name'); - - -- blocks - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}blocks CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- blocks history - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}blocks_history CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- sessions - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}sessions CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- sharing - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}sharing CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- system settings - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}system_settings CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- users - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}users CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; - - -- workspaces - SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}workspaces CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation); - PREPARE stmt FROM @updateCollationQuery; - EXECUTE stmt; - DEALLOCATE PREPARE stmt; -{{else}} - -- We need a query here otherwise the migration will result - -- in an empty query when the if condition is false. - -- Empty query causes a "Query was empty" error. - SELECT 1; -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.down.sql b/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.up.sql b/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.up.sql deleted file mode 100644 index 461cb95b21..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000013_millisecond_timestamps.up.sql +++ /dev/null @@ -1,18 +0,0 @@ - -UPDATE {{.prefix}}users SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000 - WHERE create_at < 1000000000000; - -UPDATE {{.prefix}}blocks SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000 - WHERE create_at < 1000000000000; - -UPDATE {{.prefix}}blocks_history SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000 - WHERE create_at < 1000000000000; - -UPDATE {{.prefix}}workspaces SET update_at = update_at*1000 - WHERE update_at < 1000000000000; - -UPDATE {{.prefix}}sharing SET update_at = update_at*1000 - WHERE update_at < 1000000000000; - -UPDATE {{.prefix}}sessions SET create_at = create_at*1000, update_at = update_at*1000 - WHERE create_at < 1000000000000; diff --git a/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.down.sql b/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.up.sql b/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.up.sql deleted file mode 100644 index dfdd29c4f2..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000014_add_not_null_constraint.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -UPDATE {{.prefix}}blocks SET created_by = 'system' where created_by IS NULL; -UPDATE {{.prefix}}blocks SET modified_by = 'system' where modified_by IS NULL; - -{{if .mysql}} -ALTER TABLE {{.prefix}}blocks MODIFY created_by varchar(36) NOT NULL; -ALTER TABLE {{.prefix}}blocks MODIFY modified_by varchar(36) NOT NULL; -{{end}} - -{{if .postgres}} -ALTER TABLE {{.prefix}}blocks ALTER COLUMN created_by set NOT NULL; -ALTER TABLE {{.prefix}}blocks ALTER COLUMN modified_by set NOT NULL; -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.down.sql b/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.up.sql b/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.up.sql deleted file mode 100644 index 9aa6293015..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000015_blocks_history_no_nulls.up.sql +++ /dev/null @@ -1,105 +0,0 @@ -{{if .mysql}} - -UPDATE {{.prefix}}blocks_history AS bh SET bh.parent_id='' WHERE bh.parent_id IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.schema=1 WHERE bh.schema IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.type='' WHERE bh.type IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.title='' WHERE bh.title IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.fields='' WHERE bh.fields IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.create_at=0 WHERE bh.create_at IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.root_id='' WHERE bh.root_id IS NULL; -UPDATE {{.prefix}}blocks_history AS bh SET bh.created_by='system' WHERE bh.created_by IS NULL; - -{{else}} - -/* parent_id */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET parent_id = COALESCE( - (SELECT bh2.parent_id - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.parent_id IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '') -WHERE parent_id IS NULL; - -/* schema */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET schema = COALESCE( - (SELECT bh2.schema - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.schema IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , 1) -WHERE schema IS NULL; - -/* type */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET type = COALESCE( - (SELECT bh2.type - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.type IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '') -WHERE type IS NULL; - -/* title */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET title = COALESCE( - (SELECT bh2.title - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.title IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '') -WHERE title IS NULL; - -/* fields */ -{{if .postgres}} - UPDATE {{.prefix}}blocks_history AS bh1 - SET fields = COALESCE( - (SELECT bh2.fields - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.fields IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '{}'::json) - WHERE fields IS NULL; -{{else}} - UPDATE {{.prefix}}blocks_history AS bh1 - SET fields = COALESCE( - (SELECT bh2.fields - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.fields IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '') - WHERE fields IS NULL; -{{end}} - -/* create_at */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET create_at = COALESCE( - (SELECT bh2.create_at - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.create_at IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , bh1.update_at) -WHERE create_at IS NULL; - -/* root_id */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET root_id = COALESCE( - (SELECT bh2.root_id - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.root_id IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , '') -WHERE root_id IS NULL; - -/* created_by */ -UPDATE {{.prefix}}blocks_history AS bh1 - SET created_by = COALESCE( - (SELECT bh2.created_by - FROM {{.prefix}}blocks_history AS bh2 - WHERE bh1.id = bh2.id AND bh2.created_by IS NOT NULL - ORDER BY bh2.insert_at ASC limit 1) - , 'system') -WHERE created_by IS NULL; - -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.down.sql b/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.up.sql b/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.up.sql deleted file mode 100644 index 0443dbf52b..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000016_subscriptions_table.up.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}subscriptions ( - block_type VARCHAR(10), - block_id VARCHAR(36), - workspace_id VARCHAR(36), - subscriber_type VARCHAR(10), - subscriber_id VARCHAR(36), - notified_at BIGINT, - create_at BIGINT, - delete_at BIGINT, - PRIMARY KEY (block_id, subscriber_id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -CREATE TABLE IF NOT EXISTS {{.prefix}}notification_hints ( - block_type VARCHAR(10), - block_id VARCHAR(36), - workspace_id VARCHAR(36), - modified_by_id VARCHAR(36), - create_at BIGINT, - notify_at BIGINT, - PRIMARY KEY (block_id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - diff --git a/server/boards/services/store/sqlstore/migrations/000017_add_file_info.down.sql b/server/boards/services/store/sqlstore/migrations/000017_add_file_info.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000017_add_file_info.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000017_add_file_info.up.sql b/server/boards/services/store/sqlstore/migrations/000017_add_file_info.up.sql deleted file mode 100644 index 19933d2c85..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000017_add_file_info.up.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}file_info ( - id varchar(26) NOT NULL, - create_at BIGINT NOT NULL, - delete_at BIGINT, - name TEXT NOT NULL, - extension VARCHAR(50) NOT NULL, - size BIGINT NOT NULL, - archived BOOLEAN -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; diff --git a/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.down.sql b/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.up.sql b/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.up.sql deleted file mode 100644 index 31e58c060d..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000018_add_teams_and_boards.up.sql +++ /dev/null @@ -1,288 +0,0 @@ -{{- /* renameTableIfNeeded oldTableName newTableName string */ -}} -{{ renameTableIfNeeded "workspaces" "teams" }} - -{{- /* renameColumnIfNeeded tableName oldColumnName newColumnName dataType */ -}} -{{ renameColumnIfNeeded "blocks" "workspace_id" "channel_id" "varchar(36)" }} -{{ renameColumnIfNeeded "blocks_history" "workspace_id" "channel_id" "varchar(36)" }} - -{{- /* dropColumnIfNeeded tableName columnName */ -}} -{{ dropColumnIfNeeded "blocks" "workspace_id" }} -{{ dropColumnIfNeeded "blocks_history" "workspace_id" }} - -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "blocks" "board_id" "varchar(36)" ""}} -{{ addColumnIfNeeded "blocks_history" "board_id" "varchar(36)" ""}} - -{{- /* cleanup incorrect data format in column calculations */ -}} -{{- /* then move from 'board' type to 'view' type*/ -}} -{{if .mysql}} -UPDATE {{.prefix}}blocks SET fields = JSON_SET(fields, '$.columnCalculations', JSON_OBJECT()) WHERE JSON_EXTRACT(fields, '$.columnCalculations') = JSON_ARRAY(); - -UPDATE {{.prefix}}blocks b - JOIN ( - SELECT id, JSON_EXTRACT(fields, '$.columnCalculations') as board_calculations from {{.prefix}}blocks - WHERE JSON_EXTRACT(fields, '$.columnCalculations') <> JSON_OBJECT() - ) AS s on s.id = b.root_id - SET fields = JSON_SET(fields, '$.columnCalculations', JSON_ARRAY(s.board_calculations)) - WHERE JSON_EXTRACT(b.fields, '$.viewType') = 'table' - AND b.type = 'view'; -{{end}} - -{{if .postgres}} -UPDATE {{.prefix}}blocks SET fields = fields::jsonb - 'columnCalculations' || '{"columnCalculations": {}}' WHERE fields->>'columnCalculations' = '[]'; - -WITH subquery AS ( - SELECT id, fields->'columnCalculations' as board_calculations from {{.prefix}}blocks - WHERE fields ->> 'columnCalculations' <> '{}') -UPDATE {{.prefix}}blocks b - SET fields = b.fields::jsonb|| json_build_object('columnCalculations', s.board_calculations::jsonb)::jsonb - FROM subquery AS s - WHERE s.id = b.root_id - AND b.fields ->> 'viewType' = 'table' - AND b.type = 'view'; -{{end}} - -{{- /* TODO: Migrate the columnCalculations at app level and remove it from the boards and boards_history tables */ -}} - - -{{- /* add boards tables */ -}} -CREATE TABLE IF NOT EXISTS {{.prefix}}boards ( - id VARCHAR(36) NOT NULL PRIMARY KEY, - - {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} - {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} - - team_id VARCHAR(36) NOT NULL, - channel_id VARCHAR(36), - created_by VARCHAR(36), - modified_by VARCHAR(36), - type VARCHAR(1) NOT NULL, - title TEXT NOT NULL, - description TEXT, - icon VARCHAR(256), - show_description BOOLEAN, - is_template BOOLEAN, - template_version INT DEFAULT 0, - {{if .mysql}} - properties JSON, - card_properties JSON, - {{end}} - {{if .postgres}} - properties JSONB, - card_properties JSONB, - {{end}} - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "boards" "team_id, is_template" }} -{{ createIndexIfNeeded "boards" "channel_id" }} - -CREATE TABLE IF NOT EXISTS {{.prefix}}boards_history ( - id VARCHAR(36) NOT NULL, - - {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} - {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} - - team_id VARCHAR(36) NOT NULL, - channel_id VARCHAR(36), - created_by VARCHAR(36), - modified_by VARCHAR(36), - type VARCHAR(1) NOT NULL, - title TEXT NOT NULL, - description TEXT, - icon VARCHAR(256), - show_description BOOLEAN, - is_template BOOLEAN, - template_version INT DEFAULT 0, - {{if .mysql}} - properties JSON, - card_properties JSON, - {{end}} - {{if .postgres}} - properties JSONB, - card_properties JSONB, - {{end}} - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - - PRIMARY KEY (id, insert_at) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - - -{{- /* migrate board blocks to boards table */ -}} -{{if .plugin}} - {{if .postgres}} - INSERT INTO {{.prefix}}boards ( - SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, - COALESCE(B.title, ''), - COALESCE((B.fields->>'description')::text, ''), - B.fields->>'icon', - COALESCE((fields->'showDescription')::text::boolean, false), - COALESCE((fields->'isTemplate')::text::boolean, false), - COALESCE((B.fields->'templateVer')::text::int, 0), - '{}', B.fields->'cardProperties', B.create_at, - B.update_at, B.delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'' {{end}} - FROM {{.prefix}}blocks AS B - INNER JOIN channels AS C ON C.Id=B.channel_id - WHERE B.type='board' - ); - INSERT INTO {{.prefix}}boards_history ( - SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, - COALESCE(B.title, ''), - COALESCE((B.fields->>'description')::text, ''), - B.fields->>'icon', - COALESCE((fields->'showDescription')::text::boolean, false), - COALESCE((fields->'isTemplate')::text::boolean, false), - COALESCE((B.fields->'templateVer')::text::int, 0), - '{}', B.fields->'cardProperties', B.create_at, - B.update_at, B.delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'' {{end}} - FROM {{.prefix}}blocks_history AS B - INNER JOIN channels AS C ON C.Id=B.channel_id - WHERE B.type='board' - ); - {{end}} - {{if .mysql}} - INSERT INTO {{.prefix}}boards ( - SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type, - COALESCE(B.title, ''), - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''), - JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')), - COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0), - '{}', JSON_EXTRACT(B.fields, '$.cardProperties'), B.create_at, - B.update_at, B.delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'' {{end}} - FROM {{.prefix}}blocks AS B - INNER JOIN Channels AS C ON C.Id=B.channel_id - WHERE B.type='board' - ); - INSERT INTO {{.prefix}}boards_history ( - SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type, - COALESCE(B.title, ''), - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''), - JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')), - COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0), - '{}', JSON_EXTRACT(B.fields, '$.cardProperties'), B.create_at, - B.update_at, B.delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'' {{end}} - FROM {{.prefix}}blocks_history AS B - INNER JOIN Channels AS C ON C.Id=B.channel_id - WHERE B.type='board' - ); - {{end}} -{{else}} - {{if .postgres}} - INSERT INTO {{.prefix}}boards ( - SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', - COALESCE(B.title, ''), - COALESCE((fields->>'description')::text, ''), - B.fields->>'icon', - COALESCE((fields->'showDescription')::text::boolean, false), - COALESCE((fields->'isTemplate')::text::boolean, false), - COALESCE((B.fields->'templateVer')::text::int, 0), - '{}', fields->'cardProperties', create_at, - update_at, delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'editor' {{end}} - FROM {{.prefix}}blocks AS B - WHERE type='board' - ); - INSERT INTO {{.prefix}}boards_history ( - SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', - COALESCE(B.title, ''), - COALESCE((fields->>'description')::text, ''), - B.fields->>'icon', - COALESCE((fields->'showDescription')::text::boolean, false), - COALESCE((fields->'isTemplate')::text::boolean, false), - COALESCE((B.fields->'templateVer')::text::int, 0), - '{}', fields->'cardProperties', create_at, - update_at, delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'editor' {{end}} - FROM {{.prefix}}blocks_history AS B - WHERE type='board' - ); - {{end}} - - {{if .mysql}} - INSERT INTO {{.prefix}}boards ( - SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', - COALESCE(B.title, ''), - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''), - JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), - COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0), - '{}', JSON_EXTRACT(fields, '$.cardProperties'), create_at, - update_at, delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'editor' {{end}} - FROM {{.prefix}}blocks AS B - WHERE type='board' - ); - INSERT INTO {{.prefix}}boards_history ( - SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', - COALESCE(B.title, ''), - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''), - JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), - COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true', - COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0), - '{}', JSON_EXTRACT(fields, '$.cardProperties'), create_at, - update_at, delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'editor' {{end}} - FROM {{.prefix}}blocks_history AS B - WHERE type='board' - ); - {{end}} -{{end}} - - -{{- /* Update block references to boards*/ -}} -UPDATE {{.prefix}}blocks SET board_id=root_id WHERE board_id IS NULL OR board_id=''; -UPDATE {{.prefix}}blocks_history SET board_id=root_id WHERE board_id IS NULL OR board_id=''; - -{{- /* Remove boards, including templates */ -}} -DELETE FROM {{.prefix}}blocks WHERE type = 'board'; -DELETE FROM {{.prefix}}blocks_history WHERE type = 'board'; - -{{- /* add board_members (only if boards_members doesn't already exist) */ -}} -{{if not (doesTableExist "board_members") }} -CREATE TABLE IF NOT EXISTS {{.prefix}}board_members ( - board_id VARCHAR(36) NOT NULL, - user_id VARCHAR(36) NOT NULL, - roles VARCHAR(64), - scheme_admin BOOLEAN, - scheme_editor BOOLEAN, - scheme_commenter BOOLEAN, - scheme_viewer BOOLEAN, - PRIMARY KEY (board_id, user_id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{- /* if we're in plugin, migrate channel memberships to the board */ -}} -{{if .plugin}} -INSERT INTO {{.prefix}}board_members ( - SELECT B.Id, CM.UserId, CM.Roles, TRUE, TRUE, FALSE, FALSE - FROM {{.prefix}}boards AS B - INNER JOIN ChannelMembers as CM ON CM.ChannelId=B.channel_id - WHERE CM.SchemeAdmin=True OR (CM.UserId=B.created_by) -); -{{end}} - -{{- /* if we're in personal server or desktop, create memberships for everyone */ -}} -{{if and (not .plugin) (not .singleUser)}} -{{- /* for personal server, create a membership per user and board */ -}} -INSERT INTO {{.prefix}}board_members - SELECT B.id, U.id, '', B.created_by=U.id, TRUE, FALSE, FALSE - FROM {{.prefix}}boards AS B, {{.prefix}}users AS U; -{{end}} - -{{if and (not .plugin) .singleUser}} -{{- /* for personal desktop, as we don't have users, create a membership */ -}} -{{- /* per board with a fixed user id */ -}} -INSERT INTO {{.prefix}}board_members - SELECT B.id, 'single-user', '', TRUE, TRUE, FALSE, FALSE - FROM {{.prefix}}boards AS B; -{{end}} -{{end}} - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "board_members" "user_id" }} diff --git a/server/boards/services/store/sqlstore/migrations/000019_populate_categories.down.sql b/server/boards/services/store/sqlstore/migrations/000019_populate_categories.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000019_populate_categories.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000019_populate_categories.up.sql b/server/boards/services/store/sqlstore/migrations/000019_populate_categories.up.sql deleted file mode 100644 index 6b59957c61..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000019_populate_categories.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}categories ( - id varchar(36) NOT NULL, - name varchar(100) NOT NULL, - user_id varchar(36) NOT NULL, - team_id varchar(36) NOT NULL, - channel_id varchar(36), - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - PRIMARY KEY (id) - ) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "categories" "user_id, team_id" }} - diff --git a/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.down.sql b/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.up.sql b/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.up.sql deleted file mode 100644 index 39a0d4cdc9..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000020_populate_category_blocks.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}category_boards ( - id varchar(36) NOT NULL, - user_id varchar(36) NOT NULL, - category_id varchar(36) NOT NULL, - board_id VARCHAR(36) NOT NULL, - create_at BIGINT, - update_at BIGINT, - delete_at BIGINT, - PRIMARY KEY (id) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "category_boards" "category_id" }} diff --git a/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.down.sql b/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.up.sql b/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.up.sql deleted file mode 100644 index b0179a349b..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000021_create_boards_members_history.up.sql +++ /dev/null @@ -1,23 +0,0 @@ -{{- /* Only perform this migration if the board_members_history table does not already exist */ -}} -{{if doesTableExist "board_members_history" }} - -SELECT 1; - -{{else}} - -CREATE TABLE IF NOT EXISTS {{.prefix}}board_members_history ( - board_id VARCHAR(36) NOT NULL, - user_id VARCHAR(36) NOT NULL, - action VARCHAR(10), - {{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}} - {{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}} - PRIMARY KEY (board_id, user_id, insert_at) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -INSERT INTO {{.prefix}}board_members_history (board_id, user_id, action) SELECT board_id, user_id, 'created' from {{.prefix}}board_members; - -{{end}} - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "board_members_history" "user_id" }} -{{ createIndexIfNeeded "board_members_history" "board_id, user_id" }} diff --git a/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.down.sql b/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.up.sql b/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.up.sql deleted file mode 100644 index f91f0c2e3a..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000022_create_default_board_role.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "boards" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}} -{{ addColumnIfNeeded "boards_history" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}} - -UPDATE {{.prefix}}boards SET minimum_role = 'editor' WHERE minimum_role IS NULL OR minimum_role=''; -UPDATE {{.prefix}}boards_history SET minimum_role = 'editor' WHERE minimum_role IS NULL OR minimum_role=''; diff --git a/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.down.sql b/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.up.sql b/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.up.sql deleted file mode 100644 index a85da0e5b5..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000023_persist_category_collapsed_state.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "categories" "collapsed" "boolean" "default false"}} diff --git a/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.down.sql b/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.up.sql b/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.up.sql deleted file mode 100644 index a5b5ad9bc9..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000024_mark_existsing_categories_collapsed.up.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE {{.prefix}}categories SET collapsed = true; diff --git a/server/boards/services/store/sqlstore/migrations/000025_indexes_update.down.sql b/server/boards/services/store/sqlstore/migrations/000025_indexes_update.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000025_indexes_update.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000025_indexes_update.up.sql b/server/boards/services/store/sqlstore/migrations/000025_indexes_update.up.sql deleted file mode 100644 index e7d7548e72..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000025_indexes_update.up.sql +++ /dev/null @@ -1,16 +0,0 @@ -{{- /* delete old blocks PK and add id as the new one */ -}} -{{if .mysql}} -ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY; -ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id); -{{end}} - -{{if .postgres}} -ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1; -ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id); -{{end}} - -{{- /* most block searches use board_id or a combination of board and parent ids */ -}} -{{ createIndexIfNeeded "blocks" "board_id, parent_id" }} - -{{- /* get subscriptions is used once per board page load */ -}} -{{ createIndexIfNeeded "subscriptions" "subscriber_id" }} diff --git a/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.down.sql b/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.up.sql b/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.up.sql deleted file mode 100644 index 43e694c5b7..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000026_create_preferences_table.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE IF NOT EXISTS {{.prefix}}preferences -( - userid VARCHAR(36) NOT NULL, - category VARCHAR(32) NOT NULL, - name VARCHAR(32) NOT NULL, - value TEXT NULL, - PRIMARY KEY (userid, category, name) -) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; - -{{- /* createIndexIfNeeded tableName columns */ -}} -{{ createIndexIfNeeded "preferences" "category" }} -{{ createIndexIfNeeded "preferences" "name" }} diff --git a/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.down.sql b/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.up.sql b/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.up.sql deleted file mode 100644 index b52a9a9042..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000027_migrate_user_props_to_preferences.up.sql +++ /dev/null @@ -1,54 +0,0 @@ -{{if .plugin}} - {{- /* For plugin mode, we need to write into Mattermost's `Preferences` table, hence, no use of `prefix`. */ -}} - - {{if .postgres}} - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace((Props->'focalboard_welcomePageViewed')::varchar, '"', '') FROM Users WHERE Props->'focalboard_welcomePageViewed' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((Props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') FROM Users WHERE Props->'hiddenBoardIDs' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace((Props->'focalboard_tourCategory')::varchar, '"', '') FROM Users WHERE Props->'focalboard_tourCategory' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace((Props->'focalboard_onboardingTourStep')::varchar, '"', '') FROM Users WHERE Props->'focalboard_onboardingTourStep' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace((Props->'focalboard_onboardingTourStarted')::varchar, '"', '') FROM Users WHERE Props->'focalboard_onboardingTourStarted' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace((Props->'focalboard_version72MessageCanceled')::varchar, '"', '') FROM Users WHERE Props->'focalboard_version72MessageCanceled' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace((Props->'focalboard_lastWelcomeVersion')::varchar, '"', '') FROM Users WHERE Props->'focalboard_lastWelcomeVersion' IS NOT NULL ON CONFLICT DO NOTHING; - - UPDATE Users SET props = (props - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion') WHERE jsonb_typeof(props) = 'object'; - {{end}} - - {{if .mysql}} - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(Props, '$."focalboard_welcomePageViewed"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_welcomePageViewed') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(Props, '$."hiddenBoardIDs"'), '"[', '['), ']"', ']'), '\\"', '"') FROM Users WHERE JSON_EXTRACT(Props, '$.hiddenBoardIDs') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(Props, '$."focalboard_tourCategory"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_tourCategory') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStep"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStep') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStarted"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStarted') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(Props, '$."focalboard_version72MessageCanceled"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_version72MessageCanceled') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(Props, '$."focalboard_lastWelcomeVersion"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_lastWelcomeVersion') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - - UPDATE Users SET Props = JSON_REMOVE(Props, '$."focalboard_welcomePageViewed"', '$."hiddenBoardIDs"', '$."focalboard_tourCategory"', '$."focalboard_onboardingTourStep"', '$."focalboard_onboardingTourStarted"', '$."focalboard_version72MessageCanceled"', '$."focalboard_lastWelcomeVersion"'); - {{end}} -{{else}} - {{- /* For personal server, we need to write to Focalboard's preferences table, hence the use of `prefix`. */ -}} - - {{if .postgres}} - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace((Props->'focalboard_welcomePageViewed')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_welcomePageViewed' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((Props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') from {{.prefix}}users WHERE Props->'hiddenBoardIDs' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace((Props->'focalboard_tourCategory')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_tourCategory' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace((Props->'focalboard_onboardingTourStep')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_onboardingTourStep' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace((Props->'focalboard_onboardingTourStarted')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_onboardingTourStarted' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace((Props->'focalboard_version72MessageCanceled')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_version72MessageCanceled' IS NOT NULL ON CONFLICT DO NOTHING; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace((Props->'focalboard_lastWelcomeVersion')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_lastWelcomeVersion' IS NOT NULL ON CONFLICT DO NOTHING; - - UPDATE {{.prefix}}users SET props = (props::jsonb - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion')::json WHERE jsonb_typeof(props::jsonb) = 'object'; - {{end}} - - {{if .mysql}} - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(Props, '$."focalboard_welcomePageViewed"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_welcomePageViewed') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(Props, '$."hiddenBoardIDs"'), '"[', '['), ']"', ']'), '\\"', '"') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.hiddenBoardIDs') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(Props, '$."focalboard_tourCategory"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_tourCategory') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStep"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStep') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStarted"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStarted') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(Props, '$."focalboard_version72MessageCanceled"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_version72MessageCanceled') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(Props, '$."focalboard_lastWelcomeVersion"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_lastWelcomeVersion') IS NOT NULL ON DUPLICATE KEY UPDATE value = value; - - UPDATE {{.prefix}}users SET Props = JSON_REMOVE(Props, '$."focalboard_welcomePageViewed"', '$."hiddenBoardIDs"', '$."focalboard_tourCategory"', '$."focalboard_onboardingTourStep"', '$."focalboard_onboardingTourStarted"', '$."focalboard_version72MessageCanceled"', '$."focalboard_lastWelcomeVersion"'); - {{end}} - -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.down.sql b/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.up.sql b/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.up.sql deleted file mode 100644 index 11661829f9..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000028_remove_template_channel_link.up.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE {{.prefix}}boards SET channel_id = '' WHERE is_template; diff --git a/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.down.sql b/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.down.sql deleted file mode 100644 index e0ac49d1ec..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.up.sql b/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.up.sql deleted file mode 100644 index 36471b36f5..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000029_add_category_type_field.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "categories" "type" "varchar(64)" ""}} - -UPDATE {{.prefix}}categories SET type = 'custom' WHERE type IS NULL; diff --git a/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.down.sql b/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.up.sql b/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.up.sql deleted file mode 100644 index d3e86526bf..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000030_add_category_sort_order.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "categories" "sort_order" "BIGINT" ""}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.down.sql b/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.up.sql b/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.up.sql deleted file mode 100644 index d655939edd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000031_add_category_boards_sort_order.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}} -{{ addColumnIfNeeded "category_boards" "sort_order" "BIGINT" ""}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.down.sql b/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.up.sql b/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.up.sql deleted file mode 100644 index c7b6450151..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000032_move_boards_category_to_end.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -{{- /* To move Boards category to to the last value, we just need a relatively large value. */ -}} -{{- /* Assigning 10x total number of categories works perfectly. The sort_order is anyways updated */ -}} -{{- /* when the user manually DNDs a category. */ -}} - -{{if .postgres}} -UPDATE {{.prefix}}categories SET sort_order = (10 * (SELECT COUNT(*) FROM {{.prefix}}categories)) WHERE lower(name) = 'boards'; -{{end}} - -{{if .mysql}} -{{- /* MySQL doesn't allow referencing the same table in subquery and update query like Postgres, */ -}} -{{- /* So we save the subquery result in a variable to use later. */ -}} -SET @focalboard_numCategories = (SELECT COUNT(*) FROM {{.prefix}}categories); -UPDATE {{.prefix}}categories SET sort_order = (10 * @focalboard_numCategories) WHERE lower(name) = 'boards'; -SET @focalboard_numCategories = NULL; -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.down.sql b/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.up.sql b/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.up.sql deleted file mode 100644 index 1714ace0cb..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000033_remove_deleted_category_boards.up.sql +++ /dev/null @@ -1 +0,0 @@ -DELETE FROM {{.prefix}}category_boards WHERE delete_at > 0; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.down.sql b/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.up.sql b/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.up.sql deleted file mode 100644 index 215b9c762e..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000034_category_boards_remove_unused_delete_at_column.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -{{ if or .postgres .mysql }} - {{ dropColumnIfNeeded "category_boards" "delete_at" }} -{{end}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.down.sql b/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.up.sql b/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.up.sql deleted file mode 100644 index 47140be283..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000035_add_hidden_board_column.up.sql +++ /dev/null @@ -1 +0,0 @@ -{{ addColumnIfNeeded "category_boards" "hidden" "boolean" "" }} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.down.sql b/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql b/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql deleted file mode 100644 index cf9f103542..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000036_category_board_add_unique_constraint.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -{{if or .mysql .postgres}} -{{ addConstraintIfNeeded "category_boards" "unique_user_category_board" "UNIQUE" "UNIQUE(user_id, board_id)"}} -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.down.sql b/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.up.sql b/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.up.sql deleted file mode 100644 index b9fc6181c7..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000037_hidden_boards_from_preferences.up.sql +++ /dev/null @@ -1,41 +0,0 @@ -{{if .plugin}} - {{if .mysql}} - UPDATE {{.prefix}}category_boards AS fcb - JOIN Preferences p - ON fcb.user_id = p.userid - AND p.category = 'focalboard' - AND p.name = 'hiddenBoardIDs' - SET hidden = true - WHERE p.value LIKE concat('%', fcb.board_id, '%'); - {{end}} - - {{if .postgres}} - UPDATE {{.prefix}}category_boards as fcb - SET hidden = true - FROM preferences p - WHERE p.userid = fcb.user_id - AND p.category = 'focalboard' - AND p.name = 'hiddenBoardIDs' - AND p.value like ('%' || fcb.board_id || '%'); - {{end}} -{{else}} - {{if .mysql}} - UPDATE {{.prefix}}category_boards AS fcb - JOIN {{.prefix}}preferences p - ON fcb.user_id = p.userid - AND p.category = 'focalboard' - AND p.name = 'hiddenBoardIDs' - SET hidden = true - WHERE p.value LIKE concat('%', fcb.board_id, '%'); - {{end}} - - {{if .postgres}} - UPDATE {{.prefix}}category_boards as fcb - SET hidden = true - FROM {{.prefix}}preferences p - WHERE p.userid = fcb.user_id - AND p.category = 'focalboard' - AND p.name = 'hiddenBoardIDs' - AND p.value like ('%' || fcb.board_id || '%'); - {{end}} -{{end}} diff --git a/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.down.sql b/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.up.sql b/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.up.sql deleted file mode 100644 index f86eb0c9e8..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000038_delete_hiddenBoardIDs_from_preferences.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -{{if .plugin}} - DELETE FROM Preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs'; -{{else}} - DELETE FROM {{.prefix}}preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs'; -{{end}} \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql b/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql deleted file mode 100644 index 027b7d63fd..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql b/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql deleted file mode 100644 index 24dd49b575..0000000000 --- a/server/boards/services/store/sqlstore/migrations/000039_add_path_to_file_info.up.sql +++ /dev/null @@ -1 +0,0 @@ -{{ addColumnIfNeeded "file_info" "path" "varchar(512)" "" }} diff --git a/server/boards/services/store/sqlstore/migrations/README.md b/server/boards/services/store/sqlstore/migrations/README.md deleted file mode 100644 index 21cb9d8698..0000000000 --- a/server/boards/services/store/sqlstore/migrations/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Migration Scripts - -These scripts are executed against the current database on server start-up. Any scripts previously executed are skipped, however these scripts are designed to be idempotent for Postgres and MySQL. To correct common problems with schema and data migrations the `focalboard_schema_migrations` table can be cleared of all records and the server restarted. - -The following built-in variables are available: - -| Name | Syntax | Description | -| ----- | ----- | ----- | -| schemaName | {{ .schemaName }} | Returns the database/schema name (e.g. `mattermost_`, `mattermost_test`, `public`, ...) | -| prefix | {{ .prefix }} | Returns the table name prefix (e.g. `focalboard_`) | -| postgres | {{if .postgres }} ... {{end}} | Returns true if the current database is Postgres. | -| sqlite | {{if .sqlite }} ... {{end}} | Returns true if the current database is Sqlite3. | -| mysql | {{if .mysql }} ... {{end}} | Returns true if the current database is MySQL. | -| plugin | {{if .plugin }} ... {{end}} | Returns true if the server is currently running as a plugin (or product). In others words this is true if the server is not running as stand-alone or personal server. | -| singleUser | {{if .singleUser }} ... {{end}} | Returns true if the server is currently running in single user mode. | - -To help with creating scripts that are idempotent some template functions have been added to the migration engine. - -| Name | Syntax | Description | -| ----- | ----- | ----- | -| addColumnIfNeeded | {{ addColumnIfNeeded schemaName tableName columnName datatype constraint }} | Adds column to table only if column doesn't already exist. | -| dropColumnIfNeeded | {{ dropColumnIfNeeded schemaName tableName columnName }} | Drops column from table if the column exists. | -| createIndexIfNeeded | {{ createIndexIfNeeded schemaName tableName columns }} | Creates an index if it does not already exist. The index name follows the existing convention of using `idx_` plus the table name and all columns separated by underscores. | -| renameTableIfNeeded | {{ renameTableIfNeeded schemaName oldTableName newTableName }} | Renames the table if the new table name does not exist. | -| renameColumnIfNeeded | {{ renameColumnIfNeeded schemaName tableName oldVolumnName newColumnName datatype }} | Renames a column if the new column name does not exist. | -| doesTableExist | {{if doesTableExist schemaName tableName }} ... {{end}} | Returns true if the table exists. Typically used in a `if` statement to conditionally include a section of script. Currently the existence of the table is determined before any scripts are executed (limitation of Morph). | -| doesColumnExist | {{if doesTableExist schemaName tableName columnName }} ... {{end}} | Returns true if the column exists. Typically used in a `if` statement to conditionally include a section of script. Currently the existence of the column is determined before any scripts are executed (limitation of Morph). | - -**Note, table names should not include table prefix or schema name.** - -## Examples - -```bash -{{ addColumnIfNeeded .schemaName "categories" "type" "varchar(64)" ""}} -{{ addColumnIfNeeded .schemaName "boards_history" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}} -``` - -```bash -{{ dropColumnIfNeeded .schemaName "blocks_history" "workspace_id" }} -``` - -```bash -{{ createIndexIfNeeded .schemaName "boards" "team_id, is_template" }} -``` - -```bash -{{ renameTableIfNeeded .schemaName "blocks" "blocks_history" }} -``` - -```bash -{{ renameColumnIfNeeded .schemaName "blocks_history" "workspace_id" "channel_id" "varchar(36)" }} -``` - -```bash -{{if doesTableExist .schemaName "blocks_history" }} - SELECT 'table exists'; -{{end}} - -{{if not (doesTableExist .schemaName "blocks_history") }} - SELECT 1; -{{end}} -``` - -```bash -{{if doesColumnExist .schemaName "boards_history" "minimum_role"}} - UPDATE ... - {{end}} -``` diff --git a/server/boards/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go b/server/boards/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go deleted file mode 100644 index f98a12ba1a..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/deleted_membership_boards_migration_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func TestDeletedMembershipBoardsMigration(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("should detect a board linked to a team in which the owner has a deleted membership and restore it", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStepSkippingLastInterceptor(18). - ExecFile("./fixtures/deletedMembershipBoardsMigrationFixtures.sql") - - boardGroupChannel := struct { - Created_By string - Team_ID string - }{} - boardDirectMessage := struct { - Created_By string - Team_ID string - }{} - - th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'") - require.Equal(t, "user-one", boardGroupChannel.Created_By) - require.Equal(t, "team-one", boardGroupChannel.Team_ID) - - th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'") - require.Equal(t, "user-one", boardDirectMessage.Created_By) - require.Equal(t, "team-one", boardDirectMessage.Team_ID) - - th.f.RunInterceptor(18) - - th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'") - require.Equal(t, "user-one", boardGroupChannel.Created_By) - require.Equal(t, "team-three", boardGroupChannel.Team_ID) - - th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'") - require.Equal(t, "user-one", boardDirectMessage.Created_By) - require.Equal(t, "team-three", boardDirectMessage.Team_ID) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/deletedMembershipBoardsMigrationFixtures.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/deletedMembershipBoardsMigrationFixtures.sql deleted file mode 100644 index a9f2b89bc5..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/deletedMembershipBoardsMigrationFixtures.sql +++ /dev/null @@ -1,47 +0,0 @@ -INSERT INTO Teams -(Id, Name, Type, DeleteAt) -VALUES -('team-one', 'team-one', 'O', 0), -('team-two', 'team-two', 'O', 0), -('team-three', 'team-three', 'O', 0); - -INSERT INTO Channels -(Id, DeleteAt, TeamId, Type, Name, CreatorId) -VALUES -('group-channel', 0, 'team-one', 'G', 'group-channel', 'user-one'), -('direct-channel', 0, 'team-one', 'D', 'direct-channel', 'user-one'); - -INSERT INTO Users -(Id, Username, Email) -VALUES -('user-one', 'john-doe', 'john-doe@sample.com'), -('user-two', 'jane-doe', 'jane-doe@sample.com'); - -INSERT INTO focalboard_boards -(id, team_id, channel_id, created_by, modified_by, type, title, description, icon, show_description, is_template, create_at, update_at, delete_at) -VALUES -('board-group-channel', 'team-one', 'group-channel', 'user-one', 'user-one', 'P', 'Group Channel Board', '', '', false, false, 123, 123, 0), -('board-direct-channel', 'team-one', 'direct-channel', 'user-one', 'user-one', 'P', 'Direct Channel Board', '', '', false, false, 123, 123, 0); - -INSERT INTO focalboard_board_members -(board_id, user_id, scheme_admin) -VALUES -('board-group-channel', 'user-one', true), -('board-direct-channel', 'user-one', true); - -INSERT INTO TeamMembers -(TeamId, UserId, DeleteAt, SchemeAdmin) -VALUES -('team-one', 'user-one', 123, true), -('team-one', 'user-two', 123, true), -('team-two', 'user-one', 123, true), -('team-two', 'user-two', 123, true), -('team-three', 'user-one', 0, true), -('team-three', 'user-two', 0, true); - -INSERT INTO ChannelMembers -(ChannelId, UserId, SchemeUser, SchemeAdmin) -VALUES -('group-channel', 'user-one', true, true), -('group-channel', 'two-one', true, false), -('direct-channel', 'user-one', true, true); diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test18AddTeamsAndBoardsSQLMigrationFixtures.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test18AddTeamsAndBoardsSQLMigrationFixtures.sql deleted file mode 100644 index aa6d984707..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test18AddTeamsAndBoardsSQLMigrationFixtures.sql +++ /dev/null @@ -1,10 +0,0 @@ -INSERT INTO Channels (Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, Name, CreatorId) VALUES ('chan-id', 123, 123, 0, 'team-id', 'O', 'channel', 'user-id'); - -INSERT INTO focalboard_blocks -(id, workspace_id, root_id, parent_id, created_by, modified_by, type, title, create_at, update_at, delete_at, fields) -VALUES -('board-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'board', 'My Board', 123, 123, 0, '{"columnCalculations": {"__title":"countUniqueValue"}}'), -('card-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'card', 'A card', 123, 123, 0, '{}'), -('view-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'view', 'A view', 123, 123, 0, '{"viewType":"table"}'), -('view-id2', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'view', 'A view2', 123, 123, 0, '{"viewType":"board"}'), -('board-id2', 'chan-id', 'board-id2', 'board-id2', 'user-id', 'user-id', 'board', 'My Board Two', 123, 123, 0, '{"description": "My Description","showDescription":true,"isTemplate":true,"templateVer":1,"columnCalculations":[]}'); diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test28RemoveTemplateChannelLink.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test28RemoveTemplateChannelLink.sql deleted file mode 100644 index c2e45c736c..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test28RemoveTemplateChannelLink.sql +++ /dev/null @@ -1,5 +0,0 @@ -INSERT INTO focalboard_boards -(id, title, type, is_template, channel_id, team_id) -VALUES -('board-id', 'Board', 'O', false, 'linked-channel', 'team-id'), -('template-id', 'Template', 'O', true, 'linked-channel', 'team-id'); diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_deleted_data.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_deleted_data.sql deleted file mode 100644 index c5593660bc..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_deleted_data.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO focalboard_category_boards values -('id-1', 'user_id-1', 'category-id-1', 'board-id-1', 1672988834402, 1672988834402, 0, 0), -('id-2', 'user_id-1', 'category-id-2', 'board-id-1', 1672988834402, 1672988834402, 0, 0), -('id-3', 'user_id-2', 'category-id-3', 'board-id-2', 1672988834402, 1672988834402, 1672988834402, 0), -('id-4', 'user_id-2', 'category-id-3', 'board-id-4', 1672988834402, 1672988834402, 0, 0), -('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 1672988834402, 0); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_no_deleted_data.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_no_deleted_data.sql deleted file mode 100644 index 5f4337bb2c..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test33_with_no_deleted_data.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO focalboard_category_boards values -('id-1', 'user_id-1', 'category-id-1', 'board-id-1', 1672988834402, 1672988834402, 0, 0), -('id-2', 'user_id-1', 'category-id-2', 'board-id-1', 1672988834402, 1672988834402, 0, 0), -('id-3', 'user_id-2', 'category-id-3', 'board-id-2', 1672988834402, 1672988834402, 0, 0), -('id-4', 'user_id-2', 'category-id-3', 'board-id-4', 1672988834402, 1672988834402, 0, 0), -('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 0, 0); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test34_drop_delete_at_column.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test34_drop_delete_at_column.sql deleted file mode 100644 index 4ab1ccbc88..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test34_drop_delete_at_column.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE focalboard_category_boards DROP COLUMN delete_at; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test35_add_hidden_column.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test35_add_hidden_column.sql deleted file mode 100644 index 1896a04166..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test35_add_hidden_column.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE focalboard_category_boards ADD COLUMN hidden boolean; \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test36_add_unique_constraint.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test36_add_unique_constraint.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data.sql deleted file mode 100644 index 8f75da3157..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data.sql +++ /dev/null @@ -1,10 +0,0 @@ -INSERT INTO focalboard_category_boards VALUES -('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false), -('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false), -('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false), -('id-4', 'user-id-2', 'category-id-3', 'board-id-4', 1672889246832, 1672889246832, 0, false), -('id-5', 'user-id-3', 'category-id-4', 'board-id-5', 1672889246832, 1672889246832, 0, false); - -INSERT INTO Preferences VALUES -('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'), -('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]'); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_no_hidden_boards.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_no_hidden_boards.sql deleted file mode 100644 index 2986bad282..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_no_hidden_boards.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO focalboard_category_boards VALUES -('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false), -('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false), -('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false), -('id-4', 'user-id-2', 'category-id-3', 'board-id-4', 1672889246832, 1672889246832, 0, false), -('id-5', 'user-id-3', 'category-id-4', 'board-id-5', 1672889246832, 1672889246832, 0, false); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_preference_but_no_hidden_board.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_preference_but_no_hidden_board.sql deleted file mode 100644 index 943932e543..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test37_valid_data_preference_but_no_hidden_board.sql +++ /dev/null @@ -1,10 +0,0 @@ -INSERT INTO focalboard_category_boards VALUES -('id-1', 'user-id-1', 'category-id-1', 'board-id-1', 1672889246832, 1672889246832, 0, false), -('id-2', 'user-id-1', 'category-id-2', 'board-id-2', 1672889246832, 1672889246832, 0, false), -('id-3', 'user-id-2', 'category-id-3', 'board-id-3', 1672889246832, 1672889246832, 0, false), -('id-4', 'user-id-2', 'category-id-3', 'board-id-4', 1672889246832, 1672889246832, 0, false), -('id-5', 'user-id-3', 'category-id-4', 'board-id-5', 1672889246832, 1672889246832, 0, false); - -INSERT INTO Preferences VALUES -('user-id-1', 'focalboard', 'hiddenBoardIDs', ''), -('user-id-2', 'focalboard', 'hiddenBoardIDs', ''); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/fixtures/test38_add_preferences.sql b/server/boards/services/store/sqlstore/migrationstests/fixtures/test38_add_preferences.sql deleted file mode 100644 index 600be118af..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/fixtures/test38_add_preferences.sql +++ /dev/null @@ -1,5 +0,0 @@ -INSERT INTO Preferences VALUES -('user-id-1', 'focalboard', 'hiddenBoardIDs', '["board-id-1"]'), -('user-id-2', 'focalboard', 'hiddenBoardIDs', '["board-id-3", "board-id-4"]'), -('user-id-3', 'lorem', 'lorem', ''), -('user-id-4', 'ipsum', 'ipsum', ''); \ No newline at end of file diff --git a/server/boards/services/store/sqlstore/migrationstests/helpers.go b/server/boards/services/store/sqlstore/migrationstests/helpers.go deleted file mode 100644 index a674d5f988..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/helpers.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mgdelacroix/foundation" -) - -type TestHelper struct { - t *testing.T - f *foundation.Foundation -} - -func (th *TestHelper) IsPostgres() bool { - return th.f.DB().DriverName() == "postgres" -} - -func (th *TestHelper) IsMySQL() bool { - return th.f.DB().DriverName() == "mysql" -} - -func (th *TestHelper) F() *foundation.Foundation { - return th.f -} - -func SetupTestHelper(t *testing.T, f *foundation.Foundation) (*TestHelper, func()) { - th := &TestHelper{t, f} - - tearDown := func() { - th.f.TearDown() - } - - return th, tearDown -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migrate_34_test.go b/server/boards/services/store/sqlstore/migrationstests/migrate_34_test.go deleted file mode 100644 index 4a6152ebf2..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migrate_34_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test34DropDeleteAtColumn(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("column exists", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(34) - - // migration 34 only works for MySQL and PostgreSQL - if th.IsMySQL() { - var count int - query := "SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() AND table_name = 'focalboard_category_boards' AND column_name = 'delete_at'" - th.f.DB().Get(&count, query) - require.Equal(t, 0, count) - } else if th.IsPostgres() { - var count int - query := "select count(*) from information_schema.columns where table_name = 'focalboard_category_boards' and column_name = 'delete_at'" - th.f.DB().Get(&count, query) - require.Equal(t, 0, count) - } - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("column already deleted", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(33). - ExecFile("./fixtures/test34_drop_delete_at_column.sql") - - th.f.MigrateToStep(34) - - if th.IsMySQL() { - var count int - query := "SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE WHERE table_schema = DATABASE() table_name = 'focalboard_category_boards' AND column_name = 'delete_at'" - th.f.DB().Get(&count, query) - require.Equal(t, 0, count) - } else if th.IsPostgres() { - var count int - query := "select count(*) from information_schema.columns where table_name = 'focalboard_category_boards' and column_name = 'delete_at'" - th.f.DB().Get(&count, query) - require.Equal(t, 0, count) - } - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration35_test.go b/server/boards/services/store/sqlstore/migrationstests/migration35_test.go deleted file mode 100644 index 52d1c0116d..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration35_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" -) - -func Test35AddHiddenColumnToCategoryBoards(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("base case - column doesn't already exist", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - th.f.MigrateToStep(35) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("column already exist", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(34). - ExecFile("./fixtures/test35_add_hidden_column.sql") - - th.f.MigrateToStep(35) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration36_test.go b/server/boards/services/store/sqlstore/migrationstests/migration36_test.go deleted file mode 100644 index f8dfc08284..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration36_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test36AddUniqueConstraintToCategoryBoards(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("constraint doesn't alreadt exists", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(36) - - // verifying if constraint has been added - - var schema string - if th.IsMySQL() { - schema = "DATABASE()" - } else if th.IsPostgres() { - schema = "'public'" - } - - var count int - query := "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + - "WHERE constraint_schema = " + schema + " " + - "AND constraint_name = 'unique_user_category_board' " + - "AND constraint_type = 'UNIQUE' " + - "AND table_name = 'focalboard_category_boards'" - - th.f.DB().Get(&count, query) - - require.Equal(t, 1, count) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("constraint already exists", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(35) - - if th.IsMySQL() { - th.f.DB().Exec("alter table focalboard_category_boards add constraint unique_user_category_board UNIQUE(user_id, board_id);") - } else if th.IsPostgres() { - th.f.DB().Exec("ALTER TABLE focalboard_category_boards ADD CONSTRAINT unique_user_category_board UNIQUE(user_id, board_id);") - } - - th.f.MigrateToStep(36) - - var schema string - if th.IsMySQL() { - schema = "DATABASE()" - } else if th.IsPostgres() { - schema = "'public'" - } - - var count int - query := "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + - "WHERE constraint_schema = " + schema + " " + - "AND constraint_name = 'unique_user_category_board' " + - "AND constraint_type = 'UNIQUE' " + - "AND table_name = 'focalboard_category_boards'" - th.f.DB().Get(&count, query) - require.Equal(t, 1, count) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration37_test.go b/server/boards/services/store/sqlstore/migrationstests/migration37_test.go deleted file mode 100644 index 7e23620644..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration37_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test37MigrateHiddenBoardIDTest(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("no existing hidden boards exist", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - th.f.MigrateToStep(37) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("existsing category boards with some hidden boards", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(36). - ExecFile("./fixtures/test37_valid_data.sql") - - th.f.MigrateToStep(37) - - type categoryBoard struct { - User_ID string - Category_ID string - Board_ID string - Hidden bool - } - - var hiddenCategoryBoards []categoryBoard - - query := "SELECT user_id, category_id, board_id, hidden FROM focalboard_category_boards WHERE hidden = true" - err := th.f.DB().Select(&hiddenCategoryBoards, query) - require.NoError(t, err) - require.Equal(t, 3, len(hiddenCategoryBoards)) - require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-1", Category_ID: "category-id-1", Board_ID: "board-id-1", Hidden: true}) - require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-3", Hidden: true}) - require.Contains(t, hiddenCategoryBoards, categoryBoard{User_ID: "user-id-2", Category_ID: "category-id-3", Board_ID: "board-id-4", Hidden: true}) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("no hidden boards", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(36). - ExecFile("./fixtures/test37_valid_data_no_hidden_boards.sql") - - th.f.MigrateToStep(37) - - var count int - query := "SELECT count(*) FROM focalboard_category_boards WHERE hidden = true" - err := th.f.DB().Get(&count, query) - require.NoError(t, err) - require.Equal(t, 0, count) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("preference but no hidden board", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(36). - ExecFile("./fixtures/test37_valid_data_preference_but_no_hidden_board.sql") - - th.f.MigrateToStep(37) - - var count int - query := "SELECT count(*) FROM focalboard_category_boards WHERE hidden = true" - err := th.f.DB().Get(&count, query) - require.NoError(t, err) - require.Equal(t, 0, count) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration38_test.go b/server/boards/services/store/sqlstore/migrationstests/migration38_test.go deleted file mode 100644 index aa6f7807c1..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration38_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test38RemoveHiddenBoardIDsFromPreferences(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("no data exist", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - th.f.MigrateToStep(38) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("some data exist", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - th.f.MigrateToStep(37). - ExecFile("./fixtures/test38_add_preferences.sql") - - // verify existing data count - var count int - countQuery := "SELECT COUNT(*) FROM Preferences" - err := th.f.DB().Get(&count, countQuery) - require.NoError(t, err) - require.Equal(t, 4, count) - - th.f.MigrateToStep(38) - - // now the count should be 0 - err = th.f.DB().Get(&count, countQuery) - require.NoError(t, err) - require.Equal(t, 2, count) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration_18_test.go b/server/boards/services/store/sqlstore/migrationstests/migration_18_test.go deleted file mode 100644 index c42e88d202..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration_18_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "encoding/json" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test18AddTeamsAndBoardsSQLMigration(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("should migrate a block of type board to the boards table", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(17). - ExecFile("./fixtures/test18AddTeamsAndBoardsSQLMigrationFixtures.sql") - - board := struct { - ID string - Title string - Type string - Fields string - Description string - Show_Description bool - Is_Template bool - Template_Version int - }{} - - // we check first that the board is inside the blocks table as - // a block of board type and columnCalculations exists if Fields - err := th.f.DB().Get(&board, "SELECT id, title, type, fields FROM focalboard_blocks WHERE id = 'board-id'") - require.NoError(t, err) - require.Equal(t, "My Board", board.Title) - require.Equal(t, "board", board.Type) - require.Contains(t, board.Fields, "columnCalculations") - - // we check another board is inside the blocks table as - // a block of board type and has several different properties in boards - err = th.f.DB().Get(&board, "SELECT id, title, type, fields FROM focalboard_blocks WHERE id = 'board-id2'") - require.NoError(t, err) - require.Equal(t, "My Board Two", board.Title) - require.Equal(t, "board", board.Type) - require.Contains(t, board.Fields, "description") - require.Contains(t, board.Fields, "showDescription") - require.Contains(t, board.Fields, "isTemplate") - require.Contains(t, board.Fields, "templateVer") - - // then we run the migration - th.f.MigrateToStep(18) - - // we assert that the board is now in the boards table - bErr := th.f.DB().Get(&board, "SELECT id, title, type FROM focalboard_boards WHERE id = 'board-id'") - require.NoError(t, bErr) - require.Equal(t, "My Board", board.Title) - require.Equal(t, "O", board.Type) - - card := struct { - Title string - Type string - Parent_ID string - Board_ID string - }{} - - // we fetch the card to ensure that the card is still in the blocks table - cErr := th.f.DB().Get(&card, "SELECT title, type, parent_id, board_id FROM focalboard_blocks WHERE id = 'card-id'") - require.NoError(t, cErr) - require.Equal(t, "A card", card.Title) - require.Equal(t, "card", card.Type) - require.Equal(t, board.ID, card.Parent_ID) - require.Equal(t, board.ID, card.Board_ID) - - // we assert that the board is now a board and properties from JSON Fields - dErr := th.f.DB().Get(&board, "SELECT id, title, type, description, show_description, is_template, template_version FROM focalboard_boards WHERE id = 'board-id2'") - require.NoError(t, dErr) - require.Equal(t, "My Board Two", board.Title) - require.Equal(t, "O", board.Type) - require.Equal(t, "My Description", board.Description) - require.Equal(t, true, board.Show_Description) - require.Equal(t, true, board.Is_Template) - require.Equal(t, 1, board.Template_Version) - - view := struct { - Title string - Type string - Parent_ID string - Board_ID string - Fields string - }{} - - // we fetch the views to ensure that the calculation columns exist on views - eErr := th.f.DB().Get(&view, "SELECT title, type, parent_id, board_id, fields FROM focalboard_blocks WHERE id = 'view-id'") - require.NoError(t, eErr) - require.Contains(t, view.Fields, "columnCalculations") - var fields map[string]interface{} - - // Make sure a valid JSON object - json.Unmarshal([]byte(view.Fields), &fields) - require.NotNil(t, fields["columnCalculations"]) - require.NotEmpty(t, fields["columnCalculations"]) - - // Board View should not have columnCalculations - fErr := th.f.DB().Get(&view, "SELECT title, type, parent_id, board_id, fields FROM focalboard_blocks WHERE id = 'view-id2'") - require.NoError(t, fErr) - require.NotContains(t, view.Fields, "columnCalculations") - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration_28_test.go b/server/boards/services/store/sqlstore/migrationstests/migration_28_test.go deleted file mode 100644 index 5e424754bd..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration_28_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -func Test28RemoveTemplateChannelLink(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("should correctly remove the channel link from templates", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(27). - ExecFile("./fixtures/test28RemoveTemplateChannelLink.sql") - - // first we check that the data has the expected shape - board := struct { - ID string - Is_template bool - Channel_id string - }{} - - template := struct { - ID string - Is_template bool - Channel_id string - }{} - - bErr := th.f.DB().Get(&board, "SELECT id, is_template, channel_id FROM focalboard_boards WHERE id = 'board-id'") - require.NoError(t, bErr) - require.False(t, board.Is_template) - require.Equal(t, "linked-channel", board.Channel_id) - - tErr := th.f.DB().Get(&template, "SELECT id, is_template, channel_id FROM focalboard_boards WHERE id = 'template-id'") - require.NoError(t, tErr) - require.True(t, template.Is_template) - require.Equal(t, "linked-channel", template.Channel_id) - - // we apply the migration - th.f.MigrateToStep(28) - - // then we reuse the structs to load again the data and check - // that the changes were correctly applied - bErr = th.f.DB().Get(&board, "SELECT id, is_template, channel_id FROM focalboard_boards WHERE id = 'board-id'") - require.NoError(t, bErr) - require.False(t, board.Is_template) - require.Equal(t, "linked-channel", board.Channel_id) - - tErr = th.f.DB().Get(&template, "SELECT id, is_template, channel_id FROM focalboard_boards WHERE id = 'template-id'") - require.NoError(t, tErr) - require.True(t, template.Is_template) - require.Empty(t, template.Channel_id) - }) - }) -} diff --git a/server/boards/services/store/sqlstore/migrationstests/migration_33_test.go b/server/boards/services/store/sqlstore/migrationstests/migration_33_test.go deleted file mode 100644 index 9c6ce57cd4..0000000000 --- a/server/boards/services/store/sqlstore/migrationstests/migration_33_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package migrationstests - -import ( - "testing" - - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/sqlstore" -) - -func Test33RemoveDeletedCategoryBoards(t *testing.T) { - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("base case - no data in table", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - th.f.MigrateToStep(33) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("existing data - 2 soft deleted records", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(32). - ExecFile("./fixtures/test33_with_deleted_data.sql") - - // cound total records - var count int - err := th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards") - require.NoError(t, err) - require.Equal(t, 5, count) - - // now we run the migration - th.f.MigrateToStep(33) - - // and verify record count again. - // The soft deleted records should have been removed from the DB now - err = th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards") - require.NoError(t, err) - require.Equal(t, 3, count) - }) - }) - - sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) { - t.Run("existing data - no soft deleted records", func(t *testing.T) { - th, tearDown := SetupTestHelper(t, f) - defer tearDown() - - th.f.MigrateToStep(32). - ExecFile("./fixtures/test33_with_no_deleted_data.sql") - - // cound total records - var count int - err := th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards") - require.NoError(t, err) - require.Equal(t, 5, count) - - // now we run the migration - th.f.MigrateToStep(33) - - // and verify record count again. - // Since there were no soft-deleted records, nothing should have been - // deleted from the database. - err = th.f.DB().Get(&count, "SELECT COUNT(*) FROM focalboard_category_boards") - require.NoError(t, err) - require.Equal(t, 5, count) - - }) - }) -} diff --git a/server/boards/services/store/sqlstore/notificationhints.go b/server/boards/services/store/sqlstore/notificationhints.go deleted file mode 100644 index be9a7ca520..0000000000 --- a/server/boards/services/store/sqlstore/notificationhints.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - "time" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var notificationHintFields = []string{ - "block_type", - "block_id", - "modified_by_id", - "create_at", - "notify_at", -} - -func valuesForNotificationHint(hint *model.NotificationHint) []interface{} { - return []interface{}{ - hint.BlockType, - hint.BlockID, - hint.ModifiedByID, - hint.CreateAt, - hint.NotifyAt, - } -} - -func (s *SQLStore) notificationHintFromRows(rows *sql.Rows) ([]*model.NotificationHint, error) { - hints := []*model.NotificationHint{} - - for rows.Next() { - var hint model.NotificationHint - err := rows.Scan( - &hint.BlockType, - &hint.BlockID, - &hint.ModifiedByID, - &hint.CreateAt, - &hint.NotifyAt, - ) - if err != nil { - return nil, err - } - hints = append(hints, &hint) - } - return hints, nil -} - -// upsertNotificationHint creates or updates a notification hint. When updating the `notify_at` is set -// to the current time plus `notifyFreq`. -func (s *SQLStore) upsertNotificationHint(db sq.BaseRunner, hint *model.NotificationHint, notifyFreq time.Duration) (*model.NotificationHint, error) { - if err := hint.IsValid(); err != nil { - return nil, err - } - - hint.CreateAt = utils.GetMillis() - - notifyAt := utils.GetMillisForTime(time.Now().Add(notifyFreq)) - hint.NotifyAt = notifyAt - - query := s.getQueryBuilder(db).Insert(s.tablePrefix + "notification_hints"). - Columns(notificationHintFields...). - Values(valuesForNotificationHint(hint)...) - - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE notify_at = ?", notifyAt) - } else { - query = query.Suffix("ON CONFLICT (block_id) DO UPDATE SET notify_at = ?", notifyAt) - } - - if _, err := query.Exec(); err != nil { - s.logger.Error("Cannot upsert notification hint", - mlog.String("block_id", hint.BlockID), - mlog.Err(err), - ) - return nil, err - } - return hint, nil -} - -// deleteNotificationHint deletes the notification hint for the specified block. -func (s *SQLStore) deleteNotificationHint(db sq.BaseRunner, blockID string) error { - query := s.getQueryBuilder(db). - Delete(s.tablePrefix + "notification_hints"). - Where(sq.Eq{"block_id": blockID}) - - result, err := query.Exec() - if err != nil { - return err - } - - count, err := result.RowsAffected() - if err != nil { - return err - } - - if count == 0 { - return model.NewErrNotFound("notification hint BlockID=" + blockID) - } - - return nil -} - -// getNotificationHint fetches the notification hint for the specified block. -func (s *SQLStore) getNotificationHint(db sq.BaseRunner, blockID string) (*model.NotificationHint, error) { - query := s.getQueryBuilder(db). - Select(notificationHintFields...). - From(s.tablePrefix + "notification_hints"). - Where(sq.Eq{"block_id": blockID}) - - rows, err := query.Query() - if err != nil { - s.logger.Error("Cannot fetch notification hint", - mlog.String("block_id", blockID), - mlog.Err(err), - ) - return nil, err - } - defer s.CloseRows(rows) - - hint, err := s.notificationHintFromRows(rows) - if err != nil { - s.logger.Error("Cannot get notification hint", - mlog.String("block_id", blockID), - mlog.Err(err), - ) - return nil, err - } - if len(hint) == 0 { - return nil, model.NewErrNotFound("notification hint BlockID=" + blockID) - } - return hint[0], nil -} - -// getNextNotificationHint fetches the next scheduled notification hint. If remove is true -// then the hint is removed from the database as well, as if popping from a stack. -func (s *SQLStore) getNextNotificationHint(db sq.BaseRunner, remove bool) (*model.NotificationHint, error) { - selectQuery := s.getQueryBuilder(db). - Select(notificationHintFields...). - From(s.tablePrefix + "notification_hints"). - OrderBy("notify_at"). - Limit(1) - - rows, err := selectQuery.Query() - if err != nil { - s.logger.Error("Cannot fetch next notification hint", - mlog.Err(err), - ) - return nil, err - } - defer s.CloseRows(rows) - - hints, err := s.notificationHintFromRows(rows) - if err != nil { - s.logger.Error("Cannot get next notification hint", - mlog.Err(err), - ) - return nil, err - } - if len(hints) == 0 { - return nil, model.NewErrNotFound("next notification hint") - } - - hint := hints[0] - - if remove { - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "notification_hints"). - Where(sq.Eq{"block_id": hint.BlockID}) - - result, err := deleteQuery.Exec() - if err != nil { - return nil, fmt.Errorf("cannot delete while getting next notification hint: %w", err) - } - rows, err := result.RowsAffected() - if err != nil { - return nil, fmt.Errorf("cannot verify delete while getting next notification hint: %w", err) - } - if rows == 0 { - // another node likely has grabbed this hint for processing concurrently; let that node handle it - // and we'll return an error here so we try again. - return nil, model.NewErrNotFound("notification hint") - } - } - - return hint, nil -} diff --git a/server/boards/services/store/sqlstore/params.go b/server/boards/services/store/sqlstore/params.go deleted file mode 100644 index 9ed79ef887..0000000000 --- a/server/boards/services/store/sqlstore/params.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// servicesAPI is the interface required my the Params to interact with the mattermost-server. -// You can use plugin-api or product-api adapter implementations. -type servicesAPI interface { - GetChannelByID(string) (*mm_model.Channel, error) -} - -type Params struct { - DBType string - ConnectionString string - TablePrefix string - Logger mlog.LoggerIFace - DB *sql.DB - IsPlugin bool - IsSingleUser bool - ServicesAPI servicesAPI - SkipMigrations bool - ConfigFn func() *mm_model.Config -} - -func (p Params) CheckValid() error { - return nil -} - -type ErrStoreParam struct { - name string - issue string -} - -func (e ErrStoreParam) Error() string { - return fmt.Sprintf("invalid store params: %s %s", e.name, e.issue) -} diff --git a/server/boards/services/store/sqlstore/public_methods.go b/server/boards/services/store/sqlstore/public_methods.go deleted file mode 100644 index 85450811e2..0000000000 --- a/server/boards/services/store/sqlstore/public_methods.go +++ /dev/null @@ -1,907 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by "make generate" from the Store interface -// DO NOT EDIT - -// To add a public method, create an entry in the Store interface, -// prefix it with a @withTransaction comment if you need it to be -// transactional and then add a private method in the store itself -// with db sq.BaseRunner as the first parameter before running `make -// generate` - -package sqlstore - -import ( - "context" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, boardIDs []string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.addUpdateCategoryBoard(tx, userID, categoryID, boardIDs) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "AddUpdateCategoryBoard")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) CanSeeUser(seerID string, seenID string) (bool, error) { - return s.canSeeUser(s.db, seerID, seenID) - -} - -func (s *SQLStore) CleanUpSessions(expireTime int64) error { - return s.cleanUpSessions(s.db, expireTime) - -} - -func (s *SQLStore) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, txErr - } - result, err := s.createBoardsAndBlocks(tx, bab, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocks")) - } - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return result, nil - -} - -func (s *SQLStore) CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, nil, txErr - } - result, resultVar1, err := s.createBoardsAndBlocksWithAdmin(tx, bab, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateBoardsAndBlocksWithAdmin")) - } - return nil, nil, err - } - - if err := tx.Commit(); err != nil { - return nil, nil, err - } - - return result, resultVar1, nil - -} - -func (s *SQLStore) CreateCategory(category model.Category) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.createCategory(tx, category) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "CreateCategory")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) CreateSession(session *model.Session) error { - return s.createSession(s.db, session) - -} - -func (s *SQLStore) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { - return s.createSubscription(s.db, sub) - -} - -func (s *SQLStore) CreateUser(user *model.User) (*model.User, error) { - return s.createUser(s.db, user) - -} - -func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.deleteBlock(tx, blockID, modifiedBy) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBlock")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) DeleteBlockRecord(blockID string, modifiedBy string) error { - return s.deleteBlockRecord(s.db, blockID, modifiedBy) - -} - -func (s *SQLStore) DeleteBoard(boardID string, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.deleteBoard(tx, boardID, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoard")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) DeleteBoardRecord(boardID string, modifiedBy string) error { - return s.deleteBoardRecord(s.db, boardID, modifiedBy) - -} - -func (s *SQLStore) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.deleteBoardsAndBlocks(tx, dbab, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DeleteBoardsAndBlocks")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) DeleteCategory(categoryID string, userID string, teamID string) error { - return s.deleteCategory(s.db, categoryID, userID, teamID) - -} - -func (s *SQLStore) DeleteMember(boardID string, userID string) error { - return s.deleteMember(s.db, boardID, userID) - -} - -func (s *SQLStore) DeleteNotificationHint(blockID string) error { - return s.deleteNotificationHint(s.db, blockID) - -} - -func (s *SQLStore) DeleteSession(sessionID string) error { - return s.deleteSession(s.db, sessionID) - -} - -func (s *SQLStore) DeleteSubscription(blockID string, subscriberID string) error { - return s.deleteSubscription(s.db, blockID, subscriberID) - -} - -func (s *SQLStore) DropAllTables() error { - return s.dropAllTables(s.db) - -} - -func (s *SQLStore) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, txErr - } - result, err := s.duplicateBlock(tx, boardID, blockID, userID, asTemplate) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBlock")) - } - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return result, nil - -} - -func (s *SQLStore) DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, nil, txErr - } - result, resultVar1, err := s.duplicateBoard(tx, boardID, userID, toTeam, asTemplate) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "DuplicateBoard")) - } - return nil, nil, err - } - - if err := tx.Commit(); err != nil { - return nil, nil, err - } - - return result, resultVar1, nil - -} - -func (s *SQLStore) GetActiveUserCount(updatedSecondsAgo int64) (int, error) { - return s.getActiveUserCount(s.db, updatedSecondsAgo) - -} - -func (s *SQLStore) GetAllTeams() ([]*model.Team, error) { - return s.getAllTeams(s.db) - -} - -func (s *SQLStore) GetBlock(blockID string) (*model.Block, error) { - return s.getBlock(s.db, blockID) - -} - -func (s *SQLStore) GetBlockCountsByType() (map[string]int64, error) { - return s.getBlockCountsByType(s.db) - -} - -func (s *SQLStore) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) { - return s.getBlockHistory(s.db, blockID, opts) - -} - -func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) { - return s.getBlockHistoryDescendants(s.db, boardID, opts) - -} - -func (s *SQLStore) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) { - return s.getBlockHistoryNewestChildren(s.db, parentID, opts) - -} - -func (s *SQLStore) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) { - return s.getBlocks(s.db, opts) - -} - -func (s *SQLStore) GetBlocksByIDs(ids []string) ([]*model.Block, error) { - return s.getBlocksByIDs(s.db, ids) - -} - -func (s *SQLStore) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) { - return s.getBlocksComplianceHistory(s.db, opts) - -} - -func (s *SQLStore) GetBoard(id string) (*model.Board, error) { - return s.getBoard(s.db, id) - -} - -func (s *SQLStore) GetBoardAndCard(block *model.Block) (*model.Board, *model.Block, error) { - return s.getBoardAndCard(s.db, block) - -} - -func (s *SQLStore) GetBoardAndCardByID(blockID string) (*model.Board, *model.Block, error) { - return s.getBoardAndCardByID(s.db, blockID) - -} - -func (s *SQLStore) GetBoardCount() (int64, error) { - return s.getBoardCount(s.db) - -} - -func (s *SQLStore) GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) { - return s.getBoardHistory(s.db, boardID, opts) - -} - -func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) { - return s.getBoardMemberHistory(s.db, boardID, userID, limit) - -} - -func (s *SQLStore) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) { - return s.getBoardsComplianceHistory(s.db, opts) - -} - -func (s *SQLStore) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) { - return s.getBoardsForCompliance(s.db, opts) - -} - -func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) { - return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards) - -} - -func (s *SQLStore) GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error) { - return s.getBoardsInTeamByIds(s.db, boardIDs, teamID) - -} - -func (s *SQLStore) GetCardLimitTimestamp() (int64, error) { - return s.getCardLimitTimestamp(s.db) - -} - -func (s *SQLStore) GetCategory(id string) (*model.Category, error) { - return s.getCategory(s.db, id) - -} - -func (s *SQLStore) GetChannel(teamID string, channelID string) (*mm_model.Channel, error) { - return s.getChannel(s.db, teamID, channelID) - -} - -func (s *SQLStore) GetCloudLimits() (*mm_model.ProductLimits, error) { - return s.getCloudLimits(s.db) - -} - -func (s *SQLStore) GetFileInfo(id string) (*mm_model.FileInfo, error) { - return s.getFileInfo(s.db, id) - -} - -func (s *SQLStore) GetLicense() *mm_model.License { - return s.getLicense(s.db) - -} - -func (s *SQLStore) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) { - return s.getMemberForBoard(s.db, boardID, userID) - -} - -func (s *SQLStore) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { - return s.getMembersForBoard(s.db, boardID) - -} - -func (s *SQLStore) GetMembersForUser(userID string) ([]*model.BoardMember, error) { - return s.getMembersForUser(s.db, userID) - -} - -func (s *SQLStore) GetNextNotificationHint(remove bool) (*model.NotificationHint, error) { - return s.getNextNotificationHint(s.db, remove) - -} - -func (s *SQLStore) GetNotificationHint(blockID string) (*model.NotificationHint, error) { - return s.getNotificationHint(s.db, blockID) - -} - -func (s *SQLStore) GetRegisteredUserCount() (int, error) { - return s.getRegisteredUserCount(s.db) - -} - -func (s *SQLStore) GetSession(token string, expireTime int64) (*model.Session, error) { - return s.getSession(s.db, token, expireTime) - -} - -func (s *SQLStore) GetSharing(rootID string) (*model.Sharing, error) { - return s.getSharing(s.db, rootID) - -} - -func (s *SQLStore) GetSubTree2(boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) { - return s.getSubTree2(s.db, boardID, blockID, opts) - -} - -func (s *SQLStore) GetSubscribersCountForBlock(blockID string) (int, error) { - return s.getSubscribersCountForBlock(s.db, blockID) - -} - -func (s *SQLStore) GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) { - return s.getSubscribersForBlock(s.db, blockID) - -} - -func (s *SQLStore) GetSubscription(blockID string, subscriberID string) (*model.Subscription, error) { - return s.getSubscription(s.db, blockID, subscriberID) - -} - -func (s *SQLStore) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) { - return s.getSubscriptions(s.db, subscriberID) - -} - -func (s *SQLStore) GetSystemSetting(key string) (string, error) { - return s.getSystemSetting(s.db, key) - -} - -func (s *SQLStore) GetSystemSettings() (map[string]string, error) { - return s.getSystemSettings(s.db) - -} - -func (s *SQLStore) GetTeam(ID string) (*model.Team, error) { - return s.getTeam(s.db, ID) - -} - -func (s *SQLStore) GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) { - return s.getTeamBoardsInsights(s.db, teamID, since, offset, limit, boardIDs) - -} - -func (s *SQLStore) GetTeamCount() (int64, error) { - return s.getTeamCount(s.db) - -} - -func (s *SQLStore) GetTeamsForUser(userID string) ([]*model.Team, error) { - return s.getTeamsForUser(s.db, userID) - -} - -func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Board, error) { - return s.getTemplateBoards(s.db, teamID, userID) - -} - -func (s *SQLStore) GetUsedCardsCount() (int, error) { - return s.getUsedCardsCount(s.db) - -} - -func (s *SQLStore) GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) { - return s.getUserBoardsInsights(s.db, teamID, userID, since, offset, limit, boardIDs) - -} - -func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) { - return s.getUserByEmail(s.db, email) - -} - -func (s *SQLStore) GetUserByID(userID string) (*model.User, error) { - return s.getUserByID(s.db, userID) - -} - -func (s *SQLStore) GetUserByUsername(username string) (*model.User, error) { - return s.getUserByUsername(s.db, username) - -} - -func (s *SQLStore) GetUserCategories(userID string, teamID string) ([]model.Category, error) { - return s.getUserCategories(s.db, userID, teamID) - -} - -func (s *SQLStore) GetUserCategoryBoards(userID string, teamID string) ([]model.CategoryBoards, error) { - return s.getUserCategoryBoards(s.db, userID, teamID) - -} - -func (s *SQLStore) GetUserPreferences(userID string) (mm_model.Preferences, error) { - return s.getUserPreferences(s.db, userID) - -} - -func (s *SQLStore) GetUserTimezone(userID string) (string, error) { - return s.getUserTimezone(s.db, userID) - -} - -func (s *SQLStore) GetUsersByTeam(teamID string, asGuestID string, showEmail bool, showName bool) ([]*model.User, error) { - return s.getUsersByTeam(s.db, teamID, asGuestID, showEmail, showName) - -} - -func (s *SQLStore) GetUsersList(userIDs []string, showEmail bool, showName bool) ([]*model.User, error) { - return s.getUsersList(s.db, userIDs, showEmail, showName) - -} - -func (s *SQLStore) InsertBlock(block *model.Block, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.insertBlock(tx, block, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlock")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) InsertBlocks(blocks []*model.Block, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.insertBlocks(tx, blocks, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBlocks")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) InsertBoard(board *model.Board, userID string) (*model.Board, error) { - return s.insertBoard(s.db, board, userID) - -} - -func (s *SQLStore) InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, nil, txErr - } - result, resultVar1, err := s.insertBoardWithAdmin(tx, board, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "InsertBoardWithAdmin")) - } - return nil, nil, err - } - - if err := tx.Commit(); err != nil { - return nil, nil, err - } - - return result, resultVar1, nil - -} - -func (s *SQLStore) PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.patchBlock(tx, blockID, blockPatch, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlock")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.patchBlocks(tx, blockPatches, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBlocks")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, txErr - } - result, err := s.patchBoard(tx, boardID, boardPatch, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoard")) - } - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return result, nil - -} - -func (s *SQLStore) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return nil, txErr - } - result, err := s.patchBoardsAndBlocks(tx, pbab, userID) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "PatchBoardsAndBlocks")) - } - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return result, nil - -} - -func (s *SQLStore) PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) { - return s.patchUserPreferences(s.db, userID, patch) - -} - -func (s *SQLStore) PostMessage(message string, postType string, channelID string) error { - return s.postMessage(s.db, message, postType, channelID) - -} - -func (s *SQLStore) RefreshSession(session *model.Session) error { - return s.refreshSession(s.db, session) - -} - -func (s *SQLStore) RemoveDefaultTemplates(boards []*model.Board) error { - return s.removeDefaultTemplates(s.db, boards) - -} - -func (s *SQLStore) ReorderCategories(userID string, teamID string, newCategoryOrder []string) ([]string, error) { - return s.reorderCategories(s.db, userID, teamID, newCategoryOrder) - -} - -func (s *SQLStore) ReorderCategoryBoards(categoryID string, newBoardsOrder []string) ([]string, error) { - return s.reorderCategoryBoards(s.db, categoryID, newBoardsOrder) - -} - -func (s *SQLStore) RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error) { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return 0, txErr - } - result, err := s.runDataRetention(tx, globalRetentionDate, batchSize) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "RunDataRetention")) - } - return 0, err - } - - if err := tx.Commit(); err != nil { - return 0, err - } - - return result, nil - -} - -func (s *SQLStore) SaveFileInfo(fileInfo *mm_model.FileInfo) error { - return s.saveFileInfo(s.db, fileInfo) - -} - -func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) { - return s.saveMember(s.db, bm) - -} - -func (s *SQLStore) SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) { - return s.searchBoardsForUser(s.db, term, searchField, userID, includePublicBoards) - -} - -func (s *SQLStore) SearchBoardsForUserInTeam(teamID string, term string, userID string) ([]*model.Board, error) { - return s.searchBoardsForUserInTeam(s.db, teamID, term, userID) - -} - -func (s *SQLStore) SearchUserChannels(teamID string, userID string, query string) ([]*mm_model.Channel, error) { - return s.searchUserChannels(s.db, teamID, userID, query) - -} - -func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots bool, showEmail bool, showName bool) ([]*model.User, error) { - return s.searchUsersByTeam(s.db, teamID, searchQuery, asGuestID, excludeBots, showEmail, showName) - -} - -func (s *SQLStore) SendMessage(message string, postType string, receipts []string) error { - return s.sendMessage(s.db, message, postType, receipts) - -} - -func (s *SQLStore) SetBoardVisibility(userID string, categoryID string, boardID string, visible bool) error { - return s.setBoardVisibility(s.db, userID, categoryID, boardID, visible) - -} - -func (s *SQLStore) SetSystemSetting(key string, value string) error { - return s.setSystemSetting(s.db, key, value) - -} - -func (s *SQLStore) UndeleteBlock(blockID string, modifiedBy string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.undeleteBlock(tx, blockID, modifiedBy) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "UndeleteBlock")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) UndeleteBoard(boardID string, modifiedBy string) error { - tx, txErr := s.db.BeginTx(context.Background(), nil) - if txErr != nil { - return txErr - } - err := s.undeleteBoard(tx, boardID, modifiedBy) - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "UndeleteBoard")) - } - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil - -} - -func (s *SQLStore) UpdateCardLimitTimestamp(cardLimit int) (int64, error) { - return s.updateCardLimitTimestamp(s.db, cardLimit) - -} - -func (s *SQLStore) UpdateCategory(category model.Category) error { - return s.updateCategory(s.db, category) - -} - -func (s *SQLStore) UpdateSession(session *model.Session) error { - return s.updateSession(s.db, session) - -} - -func (s *SQLStore) UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error { - return s.updateSubscribersNotifiedAt(s.db, blockID, notifiedAt) - -} - -func (s *SQLStore) UpdateUser(user *model.User) (*model.User, error) { - return s.updateUser(s.db, user) - -} - -func (s *SQLStore) UpdateUserPassword(username string, password string) error { - return s.updateUserPassword(s.db, username, password) - -} - -func (s *SQLStore) UpdateUserPasswordByID(userID string, password string) error { - return s.updateUserPasswordByID(s.db, userID, password) - -} - -func (s *SQLStore) UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) { - return s.upsertNotificationHint(s.db, hint, notificationFreq) - -} - -func (s *SQLStore) UpsertSharing(sharing model.Sharing) error { - return s.upsertSharing(s.db, sharing) - -} - -func (s *SQLStore) UpsertTeamSettings(team model.Team) error { - return s.upsertTeamSettings(s.db, team) - -} - -func (s *SQLStore) UpsertTeamSignupToken(team model.Team) error { - return s.upsertTeamSignupToken(s.db, team) - -} diff --git a/server/boards/services/store/sqlstore/schema_table_migration.go b/server/boards/services/store/sqlstore/schema_table_migration.go deleted file mode 100644 index de67adea8e..0000000000 --- a/server/boards/services/store/sqlstore/schema_table_migration.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "bytes" - "fmt" - "io" - "strings" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/morph/models" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// EnsureSchemaMigrationFormat checks the schema migrations table -// format and, if it's not using the new shape, it migrates the old -// one's status before initializing the migrations engine. -func (s *SQLStore) EnsureSchemaMigrationFormat() error { - migrationNeeded, err := s.isSchemaMigrationNeeded() - if err != nil { - return err - } - - if !migrationNeeded { - s.logger.Info("Schema migration table is in correct format") - return nil - } - - s.logger.Info("Migrating schema migration to new format") - - legacySchemaVersion, err := s.getLegacySchemaVersion() - if err != nil { - return err - } - - migrations, err := getEmbeddedMigrations() - if err != nil { - return err - } - filteredMigrations := filterMigrations(migrations, legacySchemaVersion) - - if err := s.createTempSchemaTable(); err != nil { - return err - } - - s.logger.Info("Populating the temporal schema table", mlog.Uint32("legacySchemaVersion", legacySchemaVersion), mlog.Int("migrations", len(filteredMigrations))) - - if err := s.populateTempSchemaTable(filteredMigrations); err != nil { - return err - } - - if err := s.useNewSchemaTable(); err != nil { - return err - } - - return nil -} - -// getEmbeddedMigrations returns a list of the embedded migrations -// using the morph migration format. The migrations do not have the -// contents set, as the goal is to obtain a list of them. -func getEmbeddedMigrations() ([]*models.Migration, error) { - assetsList, err := Assets.ReadDir("migrations") - if err != nil { - return nil, err - } - - migrations := []*models.Migration{} - for _, f := range assetsList { - m, err := models.NewMigration(io.NopCloser(&bytes.Buffer{}), f.Name()) - if err != nil { - return nil, err - } - - if m.Direction != models.Up { - continue - } - - migrations = append(migrations, m) - } - - return migrations, nil -} - -// filterMigrations takes the whole list of migrations parsed from the -// embedded directory and returns a filtered list that only contains -// one migration per version and those migrations that have already -// run based on the legacySchemaVersion. -func filterMigrations(migrations []*models.Migration, legacySchemaVersion uint32) []*models.Migration { - filteredMigrations := []*models.Migration{} - for _, migration := range migrations { - // we only take into account up migrations to avoid duplicates - if migration.Direction != models.Up { - continue - } - - // we're only interested on registering migrations that - // already run, so we skip those above the legacy version - if migration.Version > legacySchemaVersion { - continue - } - - filteredMigrations = append(filteredMigrations, migration) - } - - return filteredMigrations -} - -func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { - // Check if `name` column exists on schema version table. - // This column exists only for the new schema version table. - - query := s.getQueryBuilder(s.db). - Select("COLUMN_NAME"). - From("information_schema.COLUMNS"). - Where(sq.Eq{ - "TABLE_NAME": s.tablePrefix + "schema_migrations", - }) - - switch s.dbType { - case model.MysqlDBType: - query = query.Where(sq.Eq{"TABLE_SCHEMA": s.schemaName}) - case model.PostgresDBType: - query = query.Where("table_schema = current_schema()") - } - - rows, err := query.Query() - if err != nil { - s.logger.Error("failed to fetch columns in schema_migrations table", mlog.Err(err)) - return false, err - } - - defer s.CloseRows(rows) - - data := []string{} - for rows.Next() { - var columnName string - - err := rows.Scan(&columnName) - if err != nil { - s.logger.Error("error scanning rows from schema_migrations table definition", mlog.Err(err)) - return false, err - } - - data = append(data, columnName) - } - - if len(data) == 0 { - // if no data then table does not exist and therefore a schema migration is not needed. - return false, nil - } - - for _, columnName := range data { - // look for a column named 'name', if found then no migration is needed - if strings.ToLower(columnName) == "name" { - return false, nil - } - } - - return true, nil -} - -func (s *SQLStore) getLegacySchemaVersion() (uint32, error) { - query := s.getQueryBuilder(s.db). - Select("version"). - From(s.tablePrefix + "schema_migrations") - - row := query.QueryRow() - - var version uint32 - if err := row.Scan(&version); err != nil { - s.logger.Error("error fetching legacy schema version", mlog.Err(err)) - return version, err - } - - return version, nil -} - -func (s *SQLStore) createTempSchemaTable() error { - // squirrel doesn't support DDL query in query builder - // so, we need to use a plain old string - query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (Version bigint NOT NULL, Name varchar(64) NOT NULL, PRIMARY KEY (Version))", s.tablePrefix+tempSchemaMigrationTableName) - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("failed to create temporary schema migration table", mlog.Err(err)) - s.logger.Error("createTempSchemaTable error " + err.Error()) - return err - } - - return nil -} - -func (s *SQLStore) populateTempSchemaTable(migrations []*models.Migration) error { - query := s.getQueryBuilder(s.db). - Insert(s.tablePrefix+tempSchemaMigrationTableName). - Columns("Version", "Name") - - for _, migration := range migrations { - s.logger.Info("-- Registering migration", mlog.Uint32("version", migration.Version), mlog.String("name", migration.Name)) - query = query.Values(migration.Version, migration.Name) - } - - if _, err := query.Exec(); err != nil { - s.logger.Error("failed to insert migration records into temporary schema table", mlog.Err(err)) - return err - } - - return nil -} - -func (s *SQLStore) useNewSchemaTable() error { - // first delete the old table, then - // rename the new table to old table's name - - // renaming old schema migration table. Will delete later once the migration is - // complete, just in case. - var query string - if s.dbType == model.MysqlDBType { - query = fmt.Sprintf("RENAME TABLE `%sschema_migrations` TO `%sschema_migrations_old_temp`", s.tablePrefix, s.tablePrefix) - } else { - query = fmt.Sprintf("ALTER TABLE %sschema_migrations RENAME TO %sschema_migrations_old_temp", s.tablePrefix, s.tablePrefix) - } - - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("failed to rename old schema migration table", mlog.Err(err)) - return err - } - - // renaming new temp table to old table's name - if s.dbType == model.MysqlDBType { - query = fmt.Sprintf("RENAME TABLE `%s%s` TO `%sschema_migrations`", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix) - } else { - query = fmt.Sprintf("ALTER TABLE %s%s RENAME TO %sschema_migrations", s.tablePrefix, tempSchemaMigrationTableName, s.tablePrefix) - } - - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("failed to rename temp schema table", mlog.Err(err)) - return err - } - - return nil -} - -func (s *SQLStore) deleteOldSchemaMigrationTable() error { - query := "DROP TABLE IF EXISTS " + s.tablePrefix + "schema_migrations_old_temp" - if _, err := s.db.Exec(query); err != nil { - s.logger.Error("failed to delete old temp schema migrations table", mlog.Err(err)) - return err - } - - return nil -} diff --git a/server/boards/services/store/sqlstore/schema_table_migration_test.go b/server/boards/services/store/sqlstore/schema_table_migration_test.go deleted file mode 100644 index b161520f8b..0000000000 --- a/server/boards/services/store/sqlstore/schema_table_migration_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/mattermost/morph/models" - "github.com/stretchr/testify/require" -) - -func TestGetEmbeddedMigrations(t *testing.T) { - t.Run("should find migrations on the embedded assets", func(t *testing.T) { - migrations, err := getEmbeddedMigrations() - require.NoError(t, err) - require.NotEmpty(t, migrations) - }) -} - -func TestFilterMigrations(t *testing.T) { - migrations := []*models.Migration{ - {Direction: models.Up, Version: 1}, - {Direction: models.Down, Version: 1}, - {Direction: models.Up, Version: 2}, - {Direction: models.Down, Version: 2}, - {Direction: models.Up, Version: 3}, - {Direction: models.Down, Version: 3}, - {Direction: models.Up, Version: 4}, - {Direction: models.Down, Version: 4}, - } - - t.Run("only up migrations should be included", func(t *testing.T) { - filteredMigrations := filterMigrations(migrations, 4) - require.Len(t, filteredMigrations, 4) - for _, migration := range filteredMigrations { - require.Equal(t, models.Up, migration.Direction) - } - }) - - t.Run("only migrations below or equal to the legacy schema version should be included", func(t *testing.T) { - testCases := []struct { - Name string - LegacyVersion uint32 - ExpectedVersions []uint32 - }{ - {"All should be included", 4, []uint32{1, 2, 3, 4}}, - {"Only half should be included", 2, []uint32{1, 2}}, - {"Three including the third should be included", 3, []uint32{1, 2, 3}}, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - filteredMigrations := filterMigrations(migrations, tc.LegacyVersion) - require.Len(t, filteredMigrations, int(tc.LegacyVersion)) - - versions := make([]uint32, len(filteredMigrations)) - for i, migration := range filteredMigrations { - versions[i] = migration.Version - } - - require.ElementsMatch(t, versions, tc.ExpectedVersions) - }) - } - }) - - t.Run("migrations should be included even if they're not sorted", func(t *testing.T) { - unsortedMigrations := []*models.Migration{ - {Direction: models.Up, Version: 4}, - {Direction: models.Down, Version: 4}, - {Direction: models.Up, Version: 1}, - {Direction: models.Down, Version: 2}, - {Direction: models.Down, Version: 1}, - {Direction: models.Up, Version: 3}, - {Direction: models.Down, Version: 3}, - {Direction: models.Up, Version: 2}, - } - - testCases := []struct { - Name string - LegacyVersion uint32 - ExpectedVersions []uint32 - }{ - {"All should be included", 4, []uint32{1, 2, 3, 4}}, - {"Only half should be included", 2, []uint32{1, 2}}, - {"Three including the third should be included", 3, []uint32{1, 2, 3}}, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - filteredMigrations := filterMigrations(unsortedMigrations, tc.LegacyVersion) - require.Len(t, filteredMigrations, int(tc.LegacyVersion)) - - versions := make([]uint32, len(filteredMigrations)) - for i, migration := range filteredMigrations { - versions[i] = migration.Version - } - - require.ElementsMatch(t, versions, tc.ExpectedVersions) - }) - } - }) -} diff --git a/server/boards/services/store/sqlstore/session.go b/server/boards/services/store/sqlstore/session.go deleted file mode 100644 index ac633b7643..0000000000 --- a/server/boards/services/store/sqlstore/session.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "encoding/json" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -// GetActiveUserCount returns the number of users with active sessions within N seconds ago. -func (s *SQLStore) getActiveUserCount(db sq.BaseRunner, updatedSecondsAgo int64) (int, error) { - query := s.getQueryBuilder(db). - Select("count(distinct user_id)"). - From(s.tablePrefix + "sessions"). - Where(sq.Gt{"update_at": utils.GetMillis() - utils.SecondsToMillis(updatedSecondsAgo)}) - - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - return 0, err - } - - return count, nil -} - -func (s *SQLStore) getSession(db sq.BaseRunner, token string, expireTimeSeconds int64) (*model.Session, error) { - query := s.getQueryBuilder(db). - Select("id", "token", "user_id", "auth_service", "props"). - From(s.tablePrefix + "sessions"). - Where(sq.Eq{"token": token}). - Where(sq.Gt{"update_at": utils.GetMillis() - utils.SecondsToMillis(expireTimeSeconds)}) - - row := query.QueryRow() - session := model.Session{} - - var propsBytes []byte - err := row.Scan(&session.ID, &session.Token, &session.UserID, &session.AuthService, &propsBytes) - if err != nil { - return nil, err - } - - err = json.Unmarshal(propsBytes, &session.Props) - if err != nil { - return nil, err - } - - return &session, nil -} - -func (s *SQLStore) createSession(db sq.BaseRunner, session *model.Session) error { - now := utils.GetMillis() - - propsBytes, err := json.Marshal(session.Props) - if err != nil { - return err - } - - query := s.getQueryBuilder(db).Insert(s.tablePrefix+"sessions"). - Columns("id", "token", "user_id", "auth_service", "props", "create_at", "update_at"). - Values(session.ID, session.Token, session.UserID, session.AuthService, propsBytes, now, now) - - _, err = query.Exec() - return err -} - -func (s *SQLStore) refreshSession(db sq.BaseRunner, session *model.Session) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db).Update(s.tablePrefix+"sessions"). - Where(sq.Eq{"token": session.Token}). - Set("update_at", now) - - _, err := query.Exec() - return err -} - -func (s *SQLStore) updateSession(db sq.BaseRunner, session *model.Session) error { - now := utils.GetMillis() - - propsBytes, err := json.Marshal(session.Props) - if err != nil { - return err - } - - query := s.getQueryBuilder(db).Update(s.tablePrefix+"sessions"). - Where(sq.Eq{"token": session.Token}). - Set("update_at", now). - Set("props", propsBytes) - - _, err = query.Exec() - return err -} - -func (s *SQLStore) deleteSession(db sq.BaseRunner, sessionID string) error { - query := s.getQueryBuilder(db).Delete(s.tablePrefix + "sessions"). - Where(sq.Eq{"id": sessionID}) - - _, err := query.Exec() - return err -} - -func (s *SQLStore) cleanUpSessions(db sq.BaseRunner, expireTimeSeconds int64) error { - query := s.getQueryBuilder(db).Delete(s.tablePrefix + "sessions"). - Where(sq.Lt{"update_at": utils.GetMillis() - utils.SecondsToMillis(expireTimeSeconds)}) - - _, err := query.Exec() - return err -} diff --git a/server/boards/services/store/sqlstore/sharing.go b/server/boards/services/store/sqlstore/sharing.go deleted file mode 100644 index 78aba6082e..0000000000 --- a/server/boards/services/store/sqlstore/sharing.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - sq "github.com/Masterminds/squirrel" -) - -func (s *SQLStore) upsertSharing(db sq.BaseRunner, sharing model.Sharing) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"sharing"). - Columns( - "id", - "enabled", - "token", - "modified_by", - "update_at", - ). - Values( - sharing.ID, - sharing.Enabled, - sharing.Token, - sharing.ModifiedBy, - now, - ) - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE enabled = ?, token = ?, modified_by = ?, update_at = ?", - sharing.Enabled, sharing.Token, sharing.ModifiedBy, now) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET enabled = EXCLUDED.enabled, token = EXCLUDED.token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, - ) - } - - _, err := query.Exec() - return err -} - -func (s *SQLStore) getSharing(db sq.BaseRunner, boardID string) (*model.Sharing, error) { - query := s.getQueryBuilder(db). - Select( - "id", - "enabled", - "token", - "modified_by", - "update_at", - ). - From(s.tablePrefix + "sharing"). - Where(sq.Eq{"id": boardID}) - row := query.QueryRow() - sharing := model.Sharing{} - - err := row.Scan( - &sharing.ID, - &sharing.Enabled, - &sharing.Token, - &sharing.ModifiedBy, - &sharing.UpdateAt, - ) - if err != nil { - return nil, err - } - - return &sharing, nil -} diff --git a/server/boards/services/store/sqlstore/sqlstore.go b/server/boards/services/store/sqlstore/sqlstore.go deleted file mode 100644 index e14e1e1264..0000000000 --- a/server/boards/services/store/sqlstore/sqlstore.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - "net/url" - "strings" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// SQLStore is a SQL database. -type SQLStore struct { - db *sql.DB - dbType string - tablePrefix string - connectionString string - isPlugin bool - isSingleUser bool - logger mlog.LoggerIFace - servicesAPI servicesAPI - isBinaryParam bool - schemaName string - configFn func() *mm_model.Config -} - -// New creates a new SQL implementation of the store. -func New(params Params) (*SQLStore, error) { - if err := params.CheckValid(); err != nil { - return nil, err - } - - params.Logger.Info("connectDatabase", mlog.String("dbType", params.DBType)) - store := &SQLStore{ - // TODO: add replica DB support too. - db: params.DB, - dbType: params.DBType, - tablePrefix: params.TablePrefix, - connectionString: params.ConnectionString, - logger: params.Logger, - isPlugin: params.IsPlugin, - isSingleUser: params.IsSingleUser, - servicesAPI: params.ServicesAPI, - configFn: params.ConfigFn, - } - - var err error - store.isBinaryParam, err = store.computeBinaryParam() - if err != nil { - params.Logger.Error(`Cannot compute binary parameter`, mlog.Err(err)) - return nil, err - } - - store.schemaName, err = store.GetSchemaName() - if err != nil { - params.Logger.Error(`Cannot get schema name`, mlog.Err(err)) - return nil, err - } - - if !params.SkipMigrations { - if mErr := store.Migrate(); mErr != nil { - params.Logger.Error(`Table creation / migration failed`, mlog.Err(mErr)) - - return nil, mErr - } - } - return store, nil -} - -func (s *SQLStore) IsMariaDB() bool { - if s.dbType != model.MysqlDBType { - return false - } - - row := s.db.QueryRow("SELECT Version()") - - var version string - if err := row.Scan(&version); err != nil { - s.logger.Error("error checking database version", mlog.Err(err)) - return false - } - - return strings.Contains(strings.ToLower(version), "mariadb") -} - -// computeBinaryParam returns whether the data source uses binary_parameters -// when using Postgres. -func (s *SQLStore) computeBinaryParam() (bool, error) { - if s.dbType != model.PostgresDBType { - return false, nil - } - - url, err := url.Parse(s.connectionString) - if err != nil { - return false, err - } - return url.Query().Get("binary_parameters") == "yes", nil -} - -// Shutdown close the connection with the store. -func (s *SQLStore) Shutdown() error { - return s.db.Close() -} - -// DBHandle returns the raw sql.DB handle. -// It is used by the mattermostauthlayer to run their own -// raw SQL queries. -func (s *SQLStore) DBHandle() *sql.DB { - return s.db -} - -// DBType returns the DB driver used for the store. -func (s *SQLStore) DBType() string { - return s.dbType -} - -func (s *SQLStore) getQueryBuilder(db sq.BaseRunner) sq.StatementBuilderType { - builder := sq.StatementBuilder - if s.dbType == model.PostgresDBType { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - return builder.RunWith(db) -} - -func (s *SQLStore) escapeField(fieldName string) string { //nolint:unparam - if s.dbType == model.MysqlDBType { - return "`" + fieldName + "`" - } - if s.dbType == model.PostgresDBType { - return "\"" + fieldName + "\"" - } - return fieldName -} - -func (s *SQLStore) concatenationSelector(field string, delimiter string) string { - if s.dbType == model.PostgresDBType { - return fmt.Sprintf("string_agg(%s, '%s')", field, delimiter) - } - if s.dbType == model.MysqlDBType { - return fmt.Sprintf("GROUP_CONCAT(%s SEPARATOR '%s')", field, delimiter) - } - return "" -} - -func (s *SQLStore) elementInColumn(column string) string { - if s.dbType == model.MysqlDBType { - return fmt.Sprintf("instr(%s, ?) > 0", column) - } - if s.dbType == model.PostgresDBType { - return fmt.Sprintf("position(? in %s) > 0", column) - } - return "" -} - -func (s *SQLStore) getLicense(db sq.BaseRunner) *mm_model.License { - return nil -} - -func (s *SQLStore) getCloudLimits(db sq.BaseRunner) (*mm_model.ProductLimits, error) { - return nil, nil -} - -func (s *SQLStore) searchUserChannels(db sq.BaseRunner, teamID, userID, query string) ([]*mm_model.Channel, error) { - return nil, store.NewNotSupportedError("search user channels not supported on standalone mode") -} - -func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mm_model.Channel, error) { - return nil, store.NewNotSupportedError("get channel not supported on standalone mode") -} - -func (s *SQLStore) DBVersion() string { - var version string - var row *sql.Row - - switch s.dbType { - case model.MysqlDBType: - row = s.db.QueryRow("SELECT VERSION()") - case model.PostgresDBType: - row = s.db.QueryRow("SHOW server_version") - default: - return "" - } - - if err := row.Scan(&version); err != nil { - s.logger.Error("error checking database version", mlog.Err(err)) - return "" - } - - return version -} - -// dropAllTables deletes the contents of all the database tables -// except the schema_migrations table with the intention of cleaning -// the state for the next text to execute without having to run the -// migrations. -func (s *SQLStore) dropAllTables(db sq.BaseRunner) error { - if s.DBType() == model.PostgresDBType { - _, err := db.Exec(`DO - $func$ - BEGIN - EXECUTE - (SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE' - FROM pg_class - WHERE relkind = 'r' -- only tables - AND relnamespace = 'public'::regnamespace - AND NOT relname = 'schema_migrations' - ); - END - $func$;`) - if err != nil { - return err - } - } else { - rows, err := db.Query(`show tables`) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var table string - if err := rows.Scan(&table); err != nil { - return err - } - - if table != s.tablePrefix+"schema_migrations" { - if _, err := db.Exec(`TRUNCATE TABLE ` + table); err != nil { - return err - } - } - } - } - - return nil -} diff --git a/server/boards/services/store/sqlstore/sqlstore_test.go b/server/boards/services/store/sqlstore/sqlstore_test.go deleted file mode 100644 index 02e514baf7..0000000000 --- a/server/boards/services/store/sqlstore/sqlstore_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/services/store/storetests" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func TestSQLStore(t *testing.T) { - t.Run("BlocksStore", func(t *testing.T) { storetests.StoreTestBlocksStore(t, RunStoreTests) }) - t.Run("SharingStore", func(t *testing.T) { storetests.StoreTestSharingStore(t, RunStoreTests) }) - t.Run("SystemStore", func(t *testing.T) { storetests.StoreTestSystemStore(t, RunStoreTests) }) - t.Run("UserStore", func(t *testing.T) { storetests.StoreTestUserStore(t, RunStoreTests) }) - t.Run("SessionStore", func(t *testing.T) { storetests.StoreTestSessionStore(t, RunStoreTests) }) - t.Run("TeamStore", func(t *testing.T) { storetests.StoreTestTeamStore(t, RunStoreTests) }) - t.Run("BoardStore", func(t *testing.T) { storetests.StoreTestBoardStore(t, RunStoreTests) }) - t.Run("BoardsAndBlocksStore", func(t *testing.T) { storetests.StoreTestBoardsAndBlocksStore(t, RunStoreTests) }) - t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, RunStoreTests) }) - t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, RunStoreTests) }) - t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, RunStoreTests) }) - t.Run("CloudStore", func(t *testing.T) { storetests.StoreTestCloudStore(t, RunStoreTests) }) - t.Run("StoreTestFileStore", func(t *testing.T) { storetests.StoreTestFileStore(t, RunStoreTests) }) - t.Run("StoreTestCategoryStore", func(t *testing.T) { storetests.StoreTestCategoryStore(t, RunStoreTests) }) - t.Run("StoreTestCategoryBoardsStore", func(t *testing.T) { storetests.StoreTestCategoryBoardsStore(t, RunStoreTests) }) - t.Run("BoardsInsightsStore", func(t *testing.T) { storetests.StoreTestBoardsInsightsStore(t, RunStoreTests) }) - t.Run("ComplianceHistoryStore", func(t *testing.T) { storetests.StoreTestComplianceHistoryStore(t, RunStoreTests) }) -} - -// tests for utility functions inside sqlstore.go - -func TestConcatenationSelector(t *testing.T) { - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - concatenationString := sqlStore.concatenationSelector("a", ",") - switch sqlStore.dbType { - case model.MysqlDBType: - require.Equal(t, concatenationString, "GROUP_CONCAT(a SEPARATOR ',')") - case model.PostgresDBType: - require.Equal(t, concatenationString, "string_agg(a, ',')") - } - }) -} - -func TestElementInColumn(t *testing.T) { - RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) { - inLiteral := sqlStore.elementInColumn("test_column") - switch sqlStore.dbType { - case model.MysqlDBType: - require.Equal(t, inLiteral, "instr(test_column, ?) > 0") - case model.PostgresDBType: - require.Equal(t, inLiteral, "position(? in test_column) > 0") - } - }) -} diff --git a/server/boards/services/store/sqlstore/subscriptions.go b/server/boards/services/store/sqlstore/subscriptions.go deleted file mode 100644 index bf0893cad2..0000000000 --- a/server/boards/services/store/sqlstore/subscriptions.go +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var subscriptionFields = []string{ - "block_type", - "block_id", - "subscriber_type", - "subscriber_id", - "notified_at", - "create_at", - "delete_at", -} - -func valuesForSubscription(sub *model.Subscription) []interface{} { - return []interface{}{ - sub.BlockType, - sub.BlockID, - sub.SubscriberType, - sub.SubscriberID, - sub.NotifiedAt, - sub.CreateAt, - sub.DeleteAt, - } -} - -func (s *SQLStore) subscriptionsFromRows(rows *sql.Rows) ([]*model.Subscription, error) { - subscriptions := []*model.Subscription{} - - for rows.Next() { - var sub model.Subscription - err := rows.Scan( - &sub.BlockType, - &sub.BlockID, - &sub.SubscriberType, - &sub.SubscriberID, - &sub.NotifiedAt, - &sub.CreateAt, - &sub.DeleteAt, - ) - if err != nil { - return nil, err - } - subscriptions = append(subscriptions, &sub) - } - return subscriptions, nil -} - -// createSubscription creates a new subscription, or returns an existing subscription -// for the block & subscriber. -func (s *SQLStore) createSubscription(db sq.BaseRunner, sub *model.Subscription) (*model.Subscription, error) { - if err := sub.IsValid(); err != nil { - return nil, err - } - - now := model.GetMillis() - - subAdd := *sub - subAdd.NotifiedAt = now // notified_at set so first notification doesn't pick up all history - subAdd.CreateAt = now - subAdd.DeleteAt = 0 - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix + "subscriptions"). - Columns(subscriptionFields...). - Values(valuesForSubscription(&subAdd)...) - - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE delete_at = 0, notified_at = ?", now) - } else { - query = query.Suffix("ON CONFLICT (block_id,subscriber_id) DO UPDATE SET delete_at = 0, notified_at = ?", now) - } - - if _, err := query.Exec(); err != nil { - s.logger.Error("Cannot create subscription", - mlog.String("block_id", sub.BlockID), - mlog.String("subscriber_id", sub.SubscriberID), - mlog.Err(err), - ) - return nil, err - } - return &subAdd, nil -} - -// deleteSubscription soft deletes the subscription for a specific block and subscriber. -func (s *SQLStore) deleteSubscription(db sq.BaseRunner, blockID string, subscriberID string) error { - now := model.GetMillis() - - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"subscriptions"). - Set("delete_at", now). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"subscriber_id": subscriberID}) - - result, err := query.Exec() - if err != nil { - return err - } - - count, err := result.RowsAffected() - if err != nil { - return err - } - - if count == 0 { - message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID) - return model.NewErrNotFound(message) - } - - return nil -} - -// getSubscription fetches the subscription for a specific block and subscriber. -func (s *SQLStore) getSubscription(db sq.BaseRunner, blockID string, subscriberID string) (*model.Subscription, error) { - query := s.getQueryBuilder(db). - Select(subscriptionFields...). - From(s.tablePrefix + "subscriptions"). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"subscriber_id": subscriberID}). - Where(sq.Eq{"delete_at": 0}) - - rows, err := query.Query() - if err != nil { - s.logger.Error("Cannot fetch subscription for block & subscriber", - mlog.String("block_id", blockID), - mlog.String("subscriber_id", subscriberID), - mlog.Err(err), - ) - return nil, err - } - defer s.CloseRows(rows) - - subscriptions, err := s.subscriptionsFromRows(rows) - if err != nil { - s.logger.Error("Cannot get subscription for block & subscriber", - mlog.String("block_id", blockID), - mlog.String("subscriber_id", subscriberID), - mlog.Err(err), - ) - return nil, err - } - if len(subscriptions) == 0 { - message := fmt.Sprintf("subscription BlockID=%s SubscriberID=%s", blockID, subscriberID) - return nil, model.NewErrNotFound(message) - } - return subscriptions[0], nil -} - -// getSubscriptions fetches all subscriptions for a specific subscriber. -func (s *SQLStore) getSubscriptions(db sq.BaseRunner, subscriberID string) ([]*model.Subscription, error) { - query := s.getQueryBuilder(db). - Select(subscriptionFields...). - From(s.tablePrefix + "subscriptions"). - Where(sq.Eq{"subscriber_id": subscriberID}). - Where(sq.Eq{"delete_at": 0}) - - rows, err := query.Query() - if err != nil { - s.logger.Error("Cannot fetch subscriptions for subscriber", - mlog.String("subscriber_id", subscriberID), - mlog.Err(err), - ) - return nil, err - } - defer s.CloseRows(rows) - - return s.subscriptionsFromRows(rows) -} - -// getSubscribersForBlock fetches all subscribers for a block. -func (s *SQLStore) getSubscribersForBlock(db sq.BaseRunner, blockID string) ([]*model.Subscriber, error) { - query := s.getQueryBuilder(db). - Select( - "subscriber_type", - "subscriber_id", - "notified_at", - ). - From(s.tablePrefix + "subscriptions"). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"delete_at": 0}). - OrderBy("notified_at") - - rows, err := query.Query() - if err != nil { - s.logger.Error("Cannot fetch subscribers for block", - mlog.String("block_id", blockID), - mlog.Err(err), - ) - return nil, err - } - defer s.CloseRows(rows) - - subscribers := []*model.Subscriber{} - - for rows.Next() { - var sub model.Subscriber - err := rows.Scan( - &sub.SubscriberType, - &sub.SubscriberID, - &sub.NotifiedAt, - ) - if err != nil { - return nil, err - } - subscribers = append(subscribers, &sub) - } - return subscribers, nil -} - -// getSubscribersCountForBlock returns a count of all subscribers for a block. -func (s *SQLStore) getSubscribersCountForBlock(db sq.BaseRunner, blockID string) (int, error) { - query := s.getQueryBuilder(db). - Select("count(subscriber_id)"). - From(s.tablePrefix + "subscriptions"). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"delete_at": 0}) - - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - s.logger.Error("Cannot count subscribers for block", - mlog.String("block_id", blockID), - mlog.Err(err), - ) - return 0, err - } - return count, nil -} - -// updateSubscribersNotifiedAt updates the notified_at field of all subscribers for a block. -func (s *SQLStore) updateSubscribersNotifiedAt(db sq.BaseRunner, blockID string, notifiedAt int64) error { - query := s.getQueryBuilder(db). - Update(s.tablePrefix+"subscriptions"). - Set("notified_at", notifiedAt). - Where(sq.Eq{"block_id": blockID}). - Where(sq.Eq{"delete_at": 0}) - - if _, err := query.Exec(); err != nil { - s.logger.Error("UpdateSubscribersNotifiedAt error occurred while updating subscriber(s)", - mlog.String("blockID", blockID), - mlog.Err(err), - ) - return err - } - return nil -} diff --git a/server/boards/services/store/sqlstore/system.go b/server/boards/services/store/sqlstore/system.go deleted file mode 100644 index 923bab523e..0000000000 --- a/server/boards/services/store/sqlstore/system.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -func (s *SQLStore) getSystemSetting(db sq.BaseRunner, key string) (string, error) { - scanner := s.getQueryBuilder(db). - Select("value"). - From(s.tablePrefix + "system_settings"). - Where(sq.Eq{"id": key}). - QueryRow() - - var result string - err := scanner.Scan(&result) - if err != nil && !model.IsErrNotFound(err) { - return "", err - } - - return result, nil -} - -func (s *SQLStore) getSystemSettings(db sq.BaseRunner) (map[string]string, error) { - query := s.getQueryBuilder(db).Select("*").From(s.tablePrefix + "system_settings") - - rows, err := query.Query() - if err != nil { - return nil, err - } - defer s.CloseRows(rows) - - results := map[string]string{} - - for rows.Next() { - var id string - var value string - - err := rows.Scan(&id, &value) - if err != nil { - return nil, err - } - - results[id] = value - } - - return results, nil -} - -func (s *SQLStore) setSystemSetting(db sq.BaseRunner, id, value string) error { - query := s.getQueryBuilder(db).Insert(s.tablePrefix+"system_settings").Columns("id", "value").Values(id, value) - - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value) - } else { - query = query.Suffix("ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value") - } - - _, err := query.Exec() - if err != nil { - return err - } - - return nil -} diff --git a/server/boards/services/store/sqlstore/team.go b/server/boards/services/store/sqlstore/team.go deleted file mode 100644 index 8c5332b9ec..0000000000 --- a/server/boards/services/store/sqlstore/team.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - - sq "github.com/Masterminds/squirrel" -) - -var ( - teamFields = []string{ - "id", - "signup_token", - "COALESCE(settings, '{}')", - "modified_by", - "update_at", - } -) - -func (s *SQLStore) upsertTeamSignupToken(db sq.BaseRunner, team model.Team) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"teams"). - Columns( - "id", - "signup_token", - "modified_by", - "update_at", - ). - Values( - team.ID, - team.SignupToken, - team.ModifiedBy, - now, - ) - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE signup_token = ?, modified_by = ?, update_at = ?", - team.SignupToken, team.ModifiedBy, now) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET signup_token = EXCLUDED.signup_token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, - ) - } - - _, err := query.Exec() - return err -} - -func (s *SQLStore) upsertTeamSettings(db sq.BaseRunner, team model.Team) error { - now := utils.GetMillis() - signupToken := utils.NewID(utils.IDTypeToken) - - settingsJSON, err := json.Marshal(team.Settings) - if err != nil { - return err - } - - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"teams"). - Columns( - "id", - "signup_token", - "settings", - "modified_by", - "update_at", - ). - Values( - team.ID, - signupToken, - settingsJSON, - team.ModifiedBy, - now, - ) - if s.dbType == model.MysqlDBType { - query = query.Suffix("ON DUPLICATE KEY UPDATE settings = ?, modified_by = ?, update_at = ?", settingsJSON, team.ModifiedBy, now) - } else { - query = query.Suffix( - `ON CONFLICT (id) - DO UPDATE SET settings = EXCLUDED.settings, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at`, - ) - } - - _, err = query.Exec() - return err -} - -func (s *SQLStore) getTeam(db sq.BaseRunner, id string) (*model.Team, error) { - var settingsJSON string - - query := s.getQueryBuilder(db). - Select( - "id", - "signup_token", - "COALESCE(settings, '{}')", - "modified_by", - "update_at", - ). - From(s.tablePrefix + "teams"). - Where(sq.Eq{"id": id}) - row := query.QueryRow() - team := model.Team{} - - err := row.Scan( - &team.ID, - &team.SignupToken, - &settingsJSON, - &team.ModifiedBy, - &team.UpdateAt, - ) - if err != nil { - return nil, err - } - - err = json.Unmarshal([]byte(settingsJSON), &team.Settings) - if err != nil { - s.logger.Error(`ERROR GetTeam settings json.Unmarshal`, mlog.Err(err)) - return nil, err - } - - return &team, nil -} - -func (s *SQLStore) getTeamsForUser(db sq.BaseRunner, _ string) ([]*model.Team, error) { - return s.getAllTeams(db) -} - -func (s *SQLStore) getTeamCount(db sq.BaseRunner) (int64, error) { - query := s.getQueryBuilder(db). - Select( - "COUNT(*) AS count", - ). - From(s.tablePrefix + "teams") - - rows, err := query.Query() - if err != nil { - s.logger.Error("ERROR GetTeamCount", mlog.Err(err)) - return 0, err - } - defer s.CloseRows(rows) - - var count int64 - - rows.Next() - err = rows.Scan(&count) - if err != nil { - s.logger.Error("Failed to fetch team count", mlog.Err(err)) - return 0, err - } - return count, nil -} - -func (s *SQLStore) teamsFromRows(rows *sql.Rows) ([]*model.Team, error) { - teams := []*model.Team{} - - for rows.Next() { - var team model.Team - var settingsBytes []byte - - err := rows.Scan( - &team.ID, - &team.SignupToken, - &settingsBytes, - &team.ModifiedBy, - &team.UpdateAt, - ) - if err != nil { - return nil, err - } - - err = json.Unmarshal(settingsBytes, &team.Settings) - if err != nil { - return nil, err - } - - teams = append(teams, &team) - } - - return teams, nil -} - -func (s *SQLStore) getAllTeams(db sq.BaseRunner) ([]*model.Team, error) { - query := s.getQueryBuilder(db). - Select(teamFields...). - From(s.tablePrefix + "teams") - rows, err := query.Query() - if err != nil { - s.logger.Error("ERROR GetAllTeams", mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - teams, err := s.teamsFromRows(rows) - if err != nil { - return nil, err - } - - return teams, nil -} diff --git a/server/boards/services/store/sqlstore/templates.go b/server/boards/services/store/sqlstore/templates.go deleted file mode 100644 index d1925fb8d1..0000000000 --- a/server/boards/services/store/sqlstore/templates.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "errors" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ( - ErrUnsupportedDatabaseType = errors.New("database type is unsupported") -) - -// removeDefaultTemplates deletes all the default templates and their children. -func (s *SQLStore) removeDefaultTemplates(db sq.BaseRunner, boards []*model.Board) error { - count := 0 - for _, board := range boards { - if board.CreatedBy != model.SystemUserID { - continue - } - // default template deletion does not need to go to blocks_history - deleteQuery := s.getQueryBuilder(db). - Delete(s.tablePrefix + "boards"). - Where(sq.Eq{"id": board.ID}). - Where(sq.Eq{"is_template": true}) - - if _, err := deleteQuery.Exec(); err != nil { - return fmt.Errorf("cannot delete default template %s: %w", board.ID, err) - } - - deleteQuery = s.getQueryBuilder(db). - Delete(s.tablePrefix + "blocks"). - Where(sq.Or{ - sq.Eq{"parent_id": board.ID}, - sq.Eq{"root_id": board.ID}, - sq.Eq{"board_id": board.ID}, - }) - - if _, err := deleteQuery.Exec(); err != nil { - return fmt.Errorf("cannot delete default template %s: %w", board.ID, err) - } - - s.logger.Trace("removed default template block", - mlog.String("board_id", board.ID), - ) - count++ - } - - s.logger.Debug("Removed default templates", mlog.Int("count", count)) - - return nil -} - -// getTemplateBoards fetches all template boards . -func (s *SQLStore) getTemplateBoards(db sq.BaseRunner, teamID, userID string) ([]*model.Board, error) { - query := s.getQueryBuilder(db). - Select(boardFields("")...). - From(s.tablePrefix+"boards as b"). - LeftJoin(s.tablePrefix+"board_members as bm on b.id = bm.board_id and bm.user_id = ?", userID). - Where(sq.Eq{"is_template": true}). - Where(sq.Eq{"b.team_id": teamID}). - Where(sq.Or{ - // this is to include public templates even if there is not board_member entry - sq.And{ - sq.Eq{"bm.board_id": nil}, - sq.Eq{"b.type": model.BoardTypeOpen}, - }, - sq.And{ - sq.NotEq{"bm.board_id": nil}, - }, - }) - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getTemplateBoards ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - userTemplates, err := s.boardsFromRows(rows) - if err != nil { - return nil, err - } - - return userTemplates, nil -} diff --git a/server/boards/services/store/sqlstore/testlib.go b/server/boards/services/store/sqlstore/testlib.go deleted file mode 100644 index 0010e6f42f..0000000000 --- a/server/boards/services/store/sqlstore/testlib.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - "os" - "testing" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mgdelacroix/foundation" - "github.com/stretchr/testify/require" -) - -type storeType struct { - Name string - ConnString string - Store store.Store - Logger *mlog.Logger -} - -var mainStoreTypes []*storeType - -func NewStoreType(name string, driver string, skipMigrations bool) *storeType { - settings := storetest.MakeSqlSettings(driver, false) - connectionString := *settings.DataSource - - logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) - - sqlDB, err := sql.Open(driver, connectionString) - if err != nil { - panic(fmt.Sprintf("cannot open database: %s", err)) - } - err = sqlDB.Ping() - if err != nil { - panic(fmt.Sprintf("cannot ping database: %s", err)) - } - - storeParams := Params{ - DBType: driver, - ConnectionString: connectionString, - SkipMigrations: skipMigrations, - TablePrefix: "focalboard_", - Logger: logger, - DB: sqlDB, - IsPlugin: false, // ToDo: to be removed - } - - store, err := New(storeParams) - if err != nil { - panic(fmt.Sprintf("cannot create store: %s", err)) - } - - return &storeType{name, connectionString, store, logger} -} - -func initStores(skipMigrations bool) []*storeType { - var storeTypes []*storeType - - if os.Getenv("IS_CI") == "true" { - switch os.Getenv("MM_SQLSETTINGS_DRIVERNAME") { - case "mysql": - storeTypes = append(storeTypes, NewStoreType("MySQL", model.MysqlDBType, skipMigrations)) - case "postgres": - storeTypes = append(storeTypes, NewStoreType("PostgreSQL", model.PostgresDBType, skipMigrations)) - } - } else { - storeTypes = append(storeTypes, - NewStoreType("PostgreSQL", model.PostgresDBType, skipMigrations), - NewStoreType("MySQL", model.MysqlDBType, skipMigrations), - ) - } - - return storeTypes -} - -func RunStoreTests(t *testing.T, f func(*testing.T, store.Store)) { - for _, st := range mainStoreTypes { - st := st - require.NoError(t, st.Store.DropAllTables()) - t.Run(st.Name, func(t *testing.T) { - f(t, st.Store) - }) - } -} - -func RunStoreTestsWithSqlStore(t *testing.T, f func(*testing.T, *SQLStore)) { - for _, st := range mainStoreTypes { - st := st - sqlstore := st.Store.(*SQLStore) - require.NoError(t, sqlstore.DropAllTables()) - t.Run(st.Name, func(t *testing.T) { - f(t, sqlstore) - }) - } -} - -// RunStoreTestsWithFoundation executes a test for all store types. It -// requires a new instance of each store type as migration tests -// cannot reuse old stores with already run migrations -func RunStoreTestsWithFoundation(t *testing.T, f func(*testing.T, *foundation.Foundation)) { - storeTypes := initStores(true) - - for _, st := range storeTypes { - st := st - t.Run(st.Name, func(t *testing.T) { - sqlstore := st.Store.(*SQLStore) - f(t, foundation.New(t, NewBoardsMigrator(sqlstore))) - }) - require.NoError(t, st.Store.Shutdown()) - require.NoError(t, st.Logger.Shutdown()) - } -} diff --git a/server/boards/services/store/sqlstore/user.go b/server/boards/services/store/sqlstore/user.go deleted file mode 100644 index 182ccf3749..0000000000 --- a/server/boards/services/store/sqlstore/user.go +++ /dev/null @@ -1,420 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "errors" - "fmt" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -var ( - errUnsupportedOperation = errors.New("unsupported operation") -) - -type UserNotFoundError struct { - id string -} - -func (unf UserNotFoundError) Error() string { - return fmt.Sprintf("user not found (%s)", unf.id) -} - -func (s *SQLStore) getRegisteredUserCount(db sq.BaseRunner) (int, error) { - query := s.getQueryBuilder(db). - Select("count(*)"). - From(s.tablePrefix + "users"). - Where(sq.Eq{"delete_at": 0}) - row := query.QueryRow() - - var count int - err := row.Scan(&count) - if err != nil { - return 0, err - } - - return count, nil -} - -func (s *SQLStore) getUserByCondition(db sq.BaseRunner, condition sq.Eq) (*model.User, error) { - users, err := s.getUsersByCondition(db, condition, 0) - if err != nil { - return nil, err - } - - if len(users) == 0 { - return nil, model.NewErrNotFound("user") - } - - return users[0], nil -} - -func (s *SQLStore) getUsersByCondition(db sq.BaseRunner, condition interface{}, limit uint64) ([]*model.User, error) { - query := s.getQueryBuilder(db). - Select( - "id", - "username", - "email", - "password", - "mfa_secret", - "auth_service", - "auth_data", - "create_at", - "update_at", - "delete_at", - ). - From(s.tablePrefix + "users"). - Where(sq.Eq{"delete_at": 0}). - Where(condition) - - if limit != 0 { - query = query.Limit(limit) - } - - rows, err := query.Query() - if err != nil { - s.logger.Error(`getUsersByCondition ERROR`, mlog.Err(err)) - return nil, err - } - defer s.CloseRows(rows) - - users, err := s.usersFromRows(rows) - if err != nil { - return nil, err - } - - if len(users) == 0 { - return nil, model.NewErrNotFound("user") - } - - return users, nil -} - -func (s *SQLStore) getUserByID(db sq.BaseRunner, userID string) (*model.User, error) { - return s.getUserByCondition(db, sq.Eq{"id": userID}) -} - -func (s *SQLStore) getUsersList(db sq.BaseRunner, userIDs []string, _, _ bool) ([]*model.User, error) { - users, err := s.getUsersByCondition(db, sq.Eq{"id": userIDs}, 0) - if err != nil { - return nil, err - } - - if len(users) != len(userIDs) { - return users, model.NewErrNotAllFound("user", userIDs) - } - - return users, nil -} - -func (s *SQLStore) getUserByEmail(db sq.BaseRunner, email string) (*model.User, error) { - return s.getUserByCondition(db, sq.Eq{"email": email}) -} - -func (s *SQLStore) getUserByUsername(db sq.BaseRunner, username string) (*model.User, error) { - return s.getUserByCondition(db, sq.Eq{"username": username}) -} - -func (s *SQLStore) createUser(db sq.BaseRunner, user *model.User) (*model.User, error) { - now := utils.GetMillis() - user.CreateAt = now - user.UpdateAt = now - user.DeleteAt = 0 - - query := s.getQueryBuilder(db).Insert(s.tablePrefix+"users"). - Columns("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "create_at", "update_at", "delete_at"). - Values(user.ID, user.Username, user.Email, user.Password, user.MfaSecret, user.AuthService, user.AuthData, user.CreateAt, user.UpdateAt, user.DeleteAt) - - _, err := query.Exec() - return user, err -} - -func (s *SQLStore) updateUser(db sq.BaseRunner, user *model.User) (*model.User, error) { - now := utils.GetMillis() - user.UpdateAt = now - - query := s.getQueryBuilder(db).Update(s.tablePrefix+"users"). - Set("username", user.Username). - Set("email", user.Email). - Set("update_at", user.UpdateAt). - Where(sq.Eq{"id": user.ID}) - - result, err := query.Exec() - if err != nil { - return nil, err - } - - rowCount, err := result.RowsAffected() - if err != nil { - return nil, err - } - - if rowCount < 1 { - return nil, UserNotFoundError{user.ID} - } - - return user, nil -} - -func (s *SQLStore) updateUserPassword(db sq.BaseRunner, username, password string) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db).Update(s.tablePrefix+"users"). - Set("password", password). - Set("update_at", now). - Where(sq.Eq{"username": username}) - - result, err := query.Exec() - if err != nil { - return err - } - - rowCount, err := result.RowsAffected() - if err != nil { - return err - } - - if rowCount < 1 { - return UserNotFoundError{username} - } - - return nil -} - -func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password string) error { - now := utils.GetMillis() - - query := s.getQueryBuilder(db).Update(s.tablePrefix+"users"). - Set("password", password). - Set("update_at", now). - Where(sq.Eq{"id": userID}) - - result, err := query.Exec() - if err != nil { - return err - } - - rowCount, err := result.RowsAffected() - if err != nil { - return err - } - - if rowCount < 1 { - return UserNotFoundError{userID} - } - - return nil -} - -func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string, _, _ bool) ([]*model.User, error) { - users, err := s.getUsersByCondition(db, nil, 0) - if model.IsErrNotFound(err) { - return []*model.User{}, nil - } - - return users, err -} - -func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string, _, _, _ bool) ([]*model.User, error) { - users, err := s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10) - if model.IsErrNotFound(err) { - return []*model.User{}, nil - } - - return users, err -} - -func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) { - users := []*model.User{} - - for rows.Next() { - var user model.User - - err := rows.Scan( - &user.ID, - &user.Username, - &user.Email, - &user.Password, - &user.MfaSecret, - &user.AuthService, - &user.AuthData, - &user.CreateAt, - &user.UpdateAt, - &user.DeleteAt, - ) - if err != nil { - return nil, err - } - - users = append(users, &user) - } - - return users, nil -} - -func (s *SQLStore) patchUserPreferences(db sq.BaseRunner, userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) { - preferences, err := s.getUserPreferences(db, userID) - if err != nil { - return nil, err - } - - if len(patch.UpdatedFields) > 0 { - for key, value := range patch.UpdatedFields { - preference := mm_model.Preference{ - UserId: userID, - Category: model.PreferencesCategoryFocalboard, - Name: key, - Value: value, - } - - if err := s.updateUserPreference(db, preference); err != nil { - return nil, err - } - - newPreferences := mm_model.Preferences{} - for _, existingPreference := range preferences { - if preference.Name != existingPreference.Name { - newPreferences = append(newPreferences, existingPreference) - } - } - newPreferences = append(newPreferences, preference) - preferences = newPreferences - } - } - - if len(patch.DeletedFields) > 0 { - for _, key := range patch.DeletedFields { - preference := mm_model.Preference{ - UserId: userID, - Category: model.PreferencesCategoryFocalboard, - Name: key, - } - - if err := s.deleteUserPreference(db, preference); err != nil { - return nil, err - } - - newPreferences := mm_model.Preferences{} - for _, existingPreference := range preferences { - if preference.Name != existingPreference.Name { - newPreferences = append(newPreferences, existingPreference) - } - } - preferences = newPreferences - } - } - - return preferences, nil -} - -func (s *SQLStore) updateUserPreference(db sq.BaseRunner, preference mm_model.Preference) error { - query := s.getQueryBuilder(db). - Insert(s.tablePrefix+"preferences"). - Columns("UserId", "Category", "Name", "Value"). - Values(preference.UserId, preference.Category, preference.Name, preference.Value) - - switch s.dbType { - case model.MysqlDBType: - query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value)) - case model.PostgresDBType: - query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value)) - default: - return store.NewErrNotImplemented("failed to update preference because of missing driver") - } - - if _, err := query.Exec(); err != nil { - return fmt.Errorf("failed to upsert user preference in database: userID: %s name: %s value: %s error: %w", preference.UserId, preference.Name, preference.Value, err) - } - - return nil -} - -func (s *SQLStore) deleteUserPreference(db sq.BaseRunner, preference mm_model.Preference) error { - query := s.getQueryBuilder(db). - Delete(s.tablePrefix + "preferences"). - Where(sq.Eq{"UserId": preference.UserId}). - Where(sq.Eq{"Category": preference.Category}). - Where(sq.Eq{"Name": preference.Name}) - - if _, err := query.Exec(); err != nil { - return fmt.Errorf("failed to delete user preference from database: %w", err) - } - - return nil -} - -func (s *SQLStore) canSeeUser(db sq.BaseRunner, seerID string, seenID string) (bool, error) { - return true, nil -} - -func (s *SQLStore) sendMessage(db sq.BaseRunner, message, postType string, receipts []string) error { - return errUnsupportedOperation -} - -func (s *SQLStore) postMessage(db sq.BaseRunner, message, postType string, channel string) error { - return errUnsupportedOperation -} - -func (s *SQLStore) getUserTimezone(_ sq.BaseRunner, _ string) (string, error) { - return "", errUnsupportedOperation -} - -func (s *SQLStore) getUserPreferences(db sq.BaseRunner, userID string) (mm_model.Preferences, error) { - query := s.getQueryBuilder(db). - Select("userid", "category", "name", "value"). - From(s.tablePrefix + "preferences"). - Where(sq.Eq{ - "userid": userID, - "category": model.PreferencesCategoryFocalboard, - }) - - rows, err := query.Query() - if err != nil { - s.logger.Error("failed to fetch user preferences", mlog.String("user_id", userID), mlog.Err(err)) - return nil, err - } - - defer rows.Close() - - preferences, err := s.preferencesFromRows(rows) - if err != nil { - return nil, err - } - - return preferences, nil -} - -func (s *SQLStore) preferencesFromRows(rows *sql.Rows) ([]mm_model.Preference, error) { - preferences := []mm_model.Preference{} - - for rows.Next() { - var preference mm_model.Preference - - err := rows.Scan( - &preference.UserId, - &preference.Category, - &preference.Name, - &preference.Value, - ) - - if err != nil { - s.logger.Error("failed to scan row for user preference", mlog.Err(err)) - return nil, err - } - - preferences = append(preferences, preference) - } - - return preferences, nil -} diff --git a/server/boards/services/store/sqlstore/util.go b/server/boards/services/store/sqlstore/util.go deleted file mode 100644 index d910de4d8f..0000000000 --- a/server/boards/services/store/sqlstore/util.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (s *SQLStore) CloseRows(rows *sql.Rows) { - if err := rows.Close(); err != nil { - s.logger.Error("error closing MattermostAuthLayer row set", mlog.Err(err)) - } -} - -func (s *SQLStore) IsErrNotFound(err error) bool { - return model.IsErrNotFound(err) -} - -func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) { - b, err := json.Marshal(data) - if err != nil { - return nil, err - } - - if s.isBinaryParam { - b = append([]byte{0x01}, b...) - } - - return b, nil -} - -func PrepareNewTestDatabase(dbType string) (connectionString string, err error) { - if dbType == "" { - dbType = model.PostgresDBType - } - - var dbName string - var rootUser string - - // docker unit tests take priority over any DSN env vars - var template string - switch dbType { - case model.MysqlDBType: - template = "%s:mostest@tcp(localhost:3306)/%s?charset=utf8mb4,utf8&writeTimeout=30s" - rootUser = "root" - case model.PostgresDBType: - template = "postgres://%s:mostest@localhost:5432/%s?sslmode=disable\u0026connect_timeout=10" - rootUser = "mmuser" - default: - return "", newErrInvalidDBType(dbType) - } - - connectionString = fmt.Sprintf(template, rootUser, "mattermost_test") - - // create a new database each run - sqlDB, err := sql.Open(dbType, connectionString) - if err != nil { - return "", fmt.Errorf("cannot connect to %s database: %w", dbType, err) - } - defer sqlDB.Close() - - err = sqlDB.Ping() - if err != nil { - return "", fmt.Errorf("cannot ping %s database: %w", dbType, err) - } - - dbName = "testdb_" + utils.NewID(utils.IDTypeNone)[:8] - _, err = sqlDB.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)) - if err != nil { - return "", fmt.Errorf("cannot create %s database %s: %w", dbType, dbName, err) - } - - if dbType != model.PostgresDBType { - _, err = sqlDB.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO mmuser;", dbName)) - if err != nil { - return "", fmt.Errorf("cannot grant permissions on %s database %s: %w", dbType, dbName, err) - } - } - - connectionString = fmt.Sprintf(template, "mmuser", dbName) - - return connectionString, nil -} - -type ErrInvalidDBType struct { - dbType string -} - -func newErrInvalidDBType(dbType string) error { - return ErrInvalidDBType{ - dbType: dbType, - } -} - -func (e ErrInvalidDBType) Error() string { - return "unsupported database type: " + e.dbType -} - -// deleteBoardRecord deletes a boards record without deleting any child records in the blocks table. -// FOR UNIT TESTING ONLY. -func (s *SQLStore) deleteBoardRecord(db sq.BaseRunner, boardID string, modifiedBy string) error { - return s.deleteBoardAndChildren(db, boardID, modifiedBy, true) -} - -// deleteBlockRecord deletes a blocks record without deleting any child records in the blocks table. -// FOR UNIT TESTING ONLY. -func (s *SQLStore) deleteBlockRecord(db sq.BaseRunner, blockID, modifiedBy string) error { - return s.deleteBlockAndChildren(db, blockID, modifiedBy, true) -} - -func (s *SQLStore) castInt(val int64, as string) string { - if s.dbType == model.MysqlDBType { - return fmt.Sprintf("cast(%d as unsigned) AS %s", val, as) - } - return fmt.Sprintf("cast(%d as bigint) AS %s", val, as) -} - -func (s *SQLStore) GetSchemaName() (string, error) { - var query sq.SelectBuilder - - switch s.dbType { - case model.MysqlDBType: - query = s.getQueryBuilder(s.db).Select("DATABASE()") - case model.PostgresDBType: - query = s.getQueryBuilder(s.db).Select("current_schema()") - default: - return "", ErrUnsupportedDatabaseType - } - - scanner := query.QueryRow() - - var result string - err := scanner.Scan(&result) - if err != nil && !model.IsErrNotFound(err) { - return "", err - } - - return result, nil -} diff --git a/server/boards/services/store/store.go b/server/boards/services/store/store.go deleted file mode 100644 index eb0c961f19..0000000000 --- a/server/boards/services/store/store.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../../copyright.txt -destination=mockstore/mockstore.go -package mockstore . Store -//go:generate go run ./generators/main.go -package store - -import ( - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -const CardLimitTimestampSystemKey = "card_limit_timestamp" - -// Store represents the abstraction of the data storage. -type Store interface { - GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) - GetBlocksByIDs(ids []string) ([]*model.Block, error) - GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) - // @withTransaction - InsertBlock(block *model.Block, userID string) error - // @withTransaction - DeleteBlock(blockID string, modifiedBy string) error - // @withTransaction - InsertBlocks(blocks []*model.Block, userID string) error - // @withTransaction - UndeleteBlock(blockID string, modifiedBy string) error - // @withTransaction - UndeleteBoard(boardID string, modifiedBy string) error - GetBlockCountsByType() (map[string]int64, error) - GetBoardCount() (int64, error) - GetBlock(blockID string) (*model.Block, error) - // @withTransaction - PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error - GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) - GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error) - GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) - GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) - GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) - GetBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) - // @withTransaction - DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) - // @withTransaction - DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) - // @withTransaction - PatchBlocks(blockPatches *model.BlockPatchBatch, userID string) error - - Shutdown() error - - GetSystemSetting(key string) (string, error) - GetSystemSettings() (map[string]string, error) - SetSystemSetting(key, value string) error - - GetRegisteredUserCount() (int, error) - GetUserByID(userID string) (*model.User, error) - GetUsersList(userIDs []string, showEmail, showName bool) ([]*model.User, error) - GetUserByEmail(email string) (*model.User, error) - GetUserByUsername(username string) (*model.User, error) - CreateUser(user *model.User) (*model.User, error) - UpdateUser(user *model.User) (*model.User, error) - UpdateUserPassword(username, password string) error - UpdateUserPasswordByID(userID, password string) error - GetUsersByTeam(teamID string, asGuestID string, showEmail, showName bool) ([]*model.User, error) - SearchUsersByTeam(teamID string, searchQuery string, asGuestID string, excludeBots bool, showEmail, showName bool) ([]*model.User, error) - PatchUserPreferences(userID string, patch model.UserPreferencesPatch) (mm_model.Preferences, error) - GetUserPreferences(userID string) (mm_model.Preferences, error) - - GetActiveUserCount(updatedSecondsAgo int64) (int, error) - GetSession(token string, expireTime int64) (*model.Session, error) - CreateSession(session *model.Session) error - RefreshSession(session *model.Session) error - UpdateSession(session *model.Session) error - DeleteSession(sessionID string) error - CleanUpSessions(expireTime int64) error - - UpsertSharing(sharing model.Sharing) error - GetSharing(rootID string) (*model.Sharing, error) - - UpsertTeamSignupToken(team model.Team) error - UpsertTeamSettings(team model.Team) error - GetTeam(ID string) (*model.Team, error) - GetTeamsForUser(userID string) ([]*model.Team, error) - GetAllTeams() ([]*model.Team, error) - GetTeamCount() (int64, error) - - InsertBoard(board *model.Board, userID string) (*model.Board, error) - // @withTransaction - InsertBoardWithAdmin(board *model.Board, userID string) (*model.Board, *model.BoardMember, error) - // @withTransaction - PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) - GetBoard(id string) (*model.Board, error) - GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) - GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error) - // @withTransaction - DeleteBoard(boardID, userID string) error - - SaveMember(bm *model.BoardMember) (*model.BoardMember, error) - DeleteMember(boardID, userID string) error - GetMemberForBoard(boardID, userID string) (*model.BoardMember, error) - GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) - GetMembersForBoard(boardID string) ([]*model.BoardMember, error) - GetMembersForUser(userID string) ([]*model.BoardMember, error) - CanSeeUser(seerID string, seenID string) (bool, error) - SearchBoardsForUser(term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) - SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) - - // @withTransaction - CreateBoardsAndBlocksWithAdmin(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) - // @withTransaction - CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) - // @withTransaction - PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) - // @withTransaction - DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error - - GetCategory(id string) (*model.Category, error) - - GetUserCategories(userID, teamID string) ([]model.Category, error) - // @withTransaction - CreateCategory(category model.Category) error - UpdateCategory(category model.Category) error - DeleteCategory(categoryID, userID, teamID string) error - ReorderCategories(userID, teamID string, newCategoryOrder []string) ([]string, error) - - GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) - - GetFileInfo(id string) (*mm_model.FileInfo, error) - SaveFileInfo(fileInfo *mm_model.FileInfo) error - - // @withTransaction - AddUpdateCategoryBoard(userID, categoryID string, boardIDs []string) error - ReorderCategoryBoards(categoryID string, newBoardsOrder []string) ([]string, error) - SetBoardVisibility(userID, categoryID, boardID string, visible bool) error - - CreateSubscription(sub *model.Subscription) (*model.Subscription, error) - DeleteSubscription(blockID string, subscriberID string) error - GetSubscription(blockID string, subscriberID string) (*model.Subscription, error) - GetSubscriptions(subscriberID string) ([]*model.Subscription, error) - GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) - GetSubscribersCountForBlock(blockID string) (int, error) - UpdateSubscribersNotifiedAt(blockID string, notifiedAt int64) error - - UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) - DeleteNotificationHint(blockID string) error - GetNotificationHint(blockID string) (*model.NotificationHint, error) - GetNextNotificationHint(remove bool) (*model.NotificationHint, error) - - RemoveDefaultTemplates(boards []*model.Board) error - GetTemplateBoards(teamID, userID string) ([]*model.Board, error) - - // @withTransaction - RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error) - - GetUsedCardsCount() (int, error) - GetCardLimitTimestamp() (int64, error) - UpdateCardLimitTimestamp(cardLimit int) (int64, error) - - DBType() string - DBVersion() string - - GetLicense() *mm_model.License - GetCloudLimits() (*mm_model.ProductLimits, error) - SearchUserChannels(teamID, userID, query string) ([]*mm_model.Channel, error) - GetChannel(teamID, channelID string) (*mm_model.Channel, error) - PostMessage(message, postType, channelID string) error - SendMessage(message, postType string, receipts []string) error - - // Insights - GetTeamBoardsInsights(teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) - GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) - GetUserTimezone(userID string) (string, error) - - // Compliance - GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) - GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) - GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) - - // For unit testing only - DeleteBoardRecord(boardID, modifiedBy string) error - DeleteBlockRecord(blockID, modifiedBy string) error - DropAllTables() error -} - -type NotSupportedError struct { - msg string -} - -func NewNotSupportedError(msg string) NotSupportedError { - return NotSupportedError{msg: msg} -} - -func (pe NotSupportedError) Error() string { - return pe.msg -} diff --git a/server/boards/services/store/storetests/blocks.go b/server/boards/services/store/storetests/blocks.go deleted file mode 100644 index 2f02371856..0000000000 --- a/server/boards/services/store/storetests/blocks.go +++ /dev/null @@ -1,1255 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "math" - "strconv" - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - testUserID = "user-id" - testTeamID = "team-id" - testBoardID = "board-id" -) - -func StoreTestBlocksStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("InsertBlock", func(t *testing.T) { - runStoreTests(t, testInsertBlock) - }) - t.Run("InsertBlocks", func(t *testing.T) { - runStoreTests(t, testInsertBlocks) - }) - t.Run("PatchBlock", func(t *testing.T) { - runStoreTests(t, testPatchBlock) - }) - t.Run("PatchBlocks", func(t *testing.T) { - runStoreTests(t, testPatchBlocks) - }) - t.Run("DeleteBlock", func(t *testing.T) { - runStoreTests(t, testDeleteBlock) - }) - t.Run("UndeleteBlock", func(t *testing.T) { - runStoreTests(t, testUndeleteBlock) - }) - t.Run("GetSubTree2", func(t *testing.T) { - runStoreTests(t, testGetSubTree2) - }) - t.Run("GetBlocks", func(t *testing.T) { - runStoreTests(t, testGetBlocks) - }) - t.Run("GetBlock", func(t *testing.T) { - runStoreTests(t, testGetBlock) - }) - t.Run("DuplicateBlock", func(t *testing.T) { - runStoreTests(t, testDuplicateBlock) - }) - t.Run("GetBlockMetadata", func(t *testing.T) { - runStoreTests(t, testGetBlockMetadata) - }) - t.Run("UndeleteBlockChildren", func(t *testing.T) { - runStoreTests(t, testUndeleteBlockChildren) - }) - t.Run("GetBlockHistoryNewestChildren", func(t *testing.T) { - runStoreTests(t, testGetBlockHistoryNewestChildren) - }) -} - -func testInsertBlock(t *testing.T, store store.Store) { - userID := testUserID - boardID := testBoardID - - blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, errBlocks) - initialCount := len(blocks) - - t.Run("valid block", func(t *testing.T) { - fields := map[string]any{"Field": "Value"} - block := &model.Block{ - ID: "id-test", - BoardID: boardID, - ModifiedBy: userID, - Fields: fields, - } - - err := store.InsertBlock(block, "user-id-1") - require.NoError(t, err) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+1) - - insertedBlock, err := store.GetBlock("id-test") - require.Equal(t, block.BoardID, insertedBlock.BoardID) - require.Equal(t, fields, insertedBlock.Fields) - require.NoError(t, err) - }) - - t.Run("invalid rootid", func(t *testing.T) { - block := &model.Block{ - ID: "id-test", - BoardID: "", - ModifiedBy: userID, - } - - err := store.InsertBlock(block, "user-id-1") - require.Error(t, err) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+1) - }) - - t.Run("invalid fields data", func(t *testing.T) { - block := &model.Block{ - ID: "id-test", - BoardID: "id-test", - ModifiedBy: userID, - Fields: map[string]interface{}{"no-serialiable-value": t.Run}, - } - - err := store.InsertBlock(block, "user-id-1") - require.Error(t, err) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+1) - }) - - t.Run("insert new block", func(t *testing.T) { - block := &model.Block{ - BoardID: testBoardID, - } - - err := store.InsertBlock(block, "user-id-2") - require.NoError(t, err) - require.Equal(t, "user-id-2", block.CreatedBy) - }) - - t.Run("update existing block", func(t *testing.T) { - block := &model.Block{ - ID: "id-2", - BoardID: "board-id-1", - Title: "Old Title", - } - - // inserting - err := store.InsertBlock(block, "user-id-2") - require.NoError(t, err) - - // created by populated from user id for new blocks - require.Equal(t, "user-id-2", block.CreatedBy) - - // hack to avoid multiple, quick updates to a card - // violating block_history composite primary key constraint - time.Sleep(1 * time.Millisecond) - - // updating - newBlock := &model.Block{ - ID: "id-2", - BoardID: "board-id-1", - CreatedBy: "user-id-3", - Title: "New Title", - } - err = store.InsertBlock(newBlock, "user-id-4") - require.NoError(t, err) - // created by is not altered for existing blocks - require.Equal(t, "user-id-3", newBlock.CreatedBy) - require.Equal(t, "New Title", newBlock.Title) - }) - - createdAt, err := time.Parse(time.RFC822, "01 Jan 90 01:00 IST") - assert.NoError(t, err) - - updateAt, err := time.Parse(time.RFC822, "02 Jan 90 01:00 IST") - assert.NoError(t, err) - - t.Run("data tamper attempt", func(t *testing.T) { - block := &model.Block{ - ID: "id-10", - BoardID: "board-id-1", - Title: "Old Title", - CreateAt: utils.GetMillisForTime(createdAt), - UpdateAt: utils.GetMillisForTime(updateAt), - CreatedBy: "user-id-5", - ModifiedBy: "user-id-6", - } - - // inserting - err := store.InsertBlock(block, "user-id-1") - require.NoError(t, err) - expectedTime := time.Now() - - retrievedBlock, err := store.GetBlock("id-10") - assert.NoError(t, err) - assert.NotNil(t, retrievedBlock) - assert.Equal(t, "board-id-1", retrievedBlock.BoardID) - assert.Equal(t, "user-id-1", retrievedBlock.CreatedBy) - assert.Equal(t, "user-id-1", retrievedBlock.ModifiedBy) - assert.WithinDurationf(t, expectedTime, utils.GetTimeForMillis(retrievedBlock.CreateAt), 1*time.Second, "create time should be current time") - assert.WithinDurationf(t, expectedTime, utils.GetTimeForMillis(retrievedBlock.UpdateAt), 1*time.Second, "update time should be current time") - }) -} - -func testInsertBlocks(t *testing.T, store store.Store) { - userID := testUserID - - blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: "id-test"}) - require.NoError(t, errBlocks) - initialCount := len(blocks) - - t.Run("invalid block", func(t *testing.T) { - validBlock := &model.Block{ - ID: "id-test", - BoardID: "id-test", - ModifiedBy: userID, - } - - invalidBlock := &model.Block{ - ID: "id-test", - BoardID: "", - ModifiedBy: userID, - } - - newBlocks := []*model.Block{validBlock, invalidBlock} - - time.Sleep(1 * time.Millisecond) - err := store.InsertBlocks(newBlocks, "user-id-1") - require.Error(t, err) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: "id-test"}) - require.NoError(t, err) - // no blocks should have been inserted - require.Len(t, blocks, initialCount) - }) -} - -func testPatchBlock(t *testing.T, store store.Store) { - userID := testUserID - boardID := "board-id-1" - - block := &model.Block{ - ID: "id-test", - BoardID: boardID, - Title: "oldTitle", - ModifiedBy: userID, - Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"}, - } - - err := store.InsertBlock(block, "user-id-1") - require.NoError(t, err) - - blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, errBlocks) - initialCount := len(blocks) - - t.Run("not existing block id", func(t *testing.T) { - err := store.PatchBlock("invalid-block-id", &model.BlockPatch{}, "user-id-1") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.True(t, model.IsErrNotFound(err)) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount) - }) - - t.Run("invalid fields data", func(t *testing.T) { - blockPatch := &model.BlockPatch{ - UpdatedFields: map[string]interface{}{"no-serialiable-value": t.Run}, - } - - err := store.PatchBlock("id-test", blockPatch, "user-id-1") - require.Error(t, err) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount) - }) - - t.Run("update block fields", func(t *testing.T) { - newTitle := "New title" - blockPatch := model.BlockPatch{ - Title: &newTitle, - } - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - - // inserting - err := store.PatchBlock("id-test", &blockPatch, "user-id-2") - require.NoError(t, err) - - retrievedBlock, err := store.GetBlock("id-test") - require.NoError(t, err) - - // created by populated from user id for new blocks - require.Equal(t, "user-id-2", retrievedBlock.ModifiedBy) - require.Equal(t, "New title", retrievedBlock.Title) - }) - - t.Run("update block custom fields", func(t *testing.T) { - blockPatch := &model.BlockPatch{ - UpdatedFields: map[string]interface{}{"test": "new test value", "test3": "new value"}, - } - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - - // inserting - err := store.PatchBlock("id-test", blockPatch, "user-id-2") - require.NoError(t, err) - - retrievedBlock, err := store.GetBlock("id-test") - require.NoError(t, err) - - // created by populated from user id for new blocks - require.Equal(t, "user-id-2", retrievedBlock.ModifiedBy) - require.Equal(t, "new test value", retrievedBlock.Fields["test"]) - require.Equal(t, "test value 2", retrievedBlock.Fields["test2"]) - require.Equal(t, "new value", retrievedBlock.Fields["test3"]) - }) - - t.Run("remove block custom fields", func(t *testing.T) { - blockPatch := &model.BlockPatch{ - DeletedFields: []string{"test", "test3", "test100"}, - } - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - - // inserting - err := store.PatchBlock("id-test", blockPatch, "user-id-2") - require.NoError(t, err) - - retrievedBlock, err := store.GetBlock("id-test") - require.NoError(t, err) - - // created by populated from user id for new blocks - require.Equal(t, "user-id-2", retrievedBlock.ModifiedBy) - require.Equal(t, nil, retrievedBlock.Fields["test"]) - require.Equal(t, "test value 2", retrievedBlock.Fields["test2"]) - require.Equal(t, nil, retrievedBlock.Fields["test3"]) - }) -} - -func testPatchBlocks(t *testing.T, store store.Store) { - block := &model.Block{ - ID: "id-test", - BoardID: "id-test", - Title: "oldTitle", - } - - block2 := &model.Block{ - ID: "id-test2", - BoardID: "id-test2", - Title: "oldTitle2", - } - - insertBlocks := []*model.Block{block, block2} - err := store.InsertBlocks(insertBlocks, "user-id-1") - require.NoError(t, err) - - t.Run("successful updated existing blocks", func(t *testing.T) { - title := "updatedTitle" - blockPatch := model.BlockPatch{ - Title: &title, - } - - blockPatch2 := model.BlockPatch{ - Title: &title, - } - - blockIds := []string{"id-test", "id-test2"} - blockPatches := []model.BlockPatch{blockPatch, blockPatch2} - - time.Sleep(1 * time.Millisecond) - err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") - require.NoError(t, err) - - retrievedBlock, err := store.GetBlock("id-test") - require.NoError(t, err) - require.Equal(t, title, retrievedBlock.Title) - - retrievedBlock2, err := store.GetBlock("id-test2") - require.NoError(t, err) - require.Equal(t, title, retrievedBlock2.Title) - }) - - t.Run("invalid block id, nothing updated existing blocks", func(t *testing.T) { - title := "Another Title" - blockPatch := model.BlockPatch{ - Title: &title, - } - - blockPatch2 := model.BlockPatch{ - Title: &title, - } - - blockIds := []string{"id-test", "invalid id"} - blockPatches := []model.BlockPatch{blockPatch, blockPatch2} - - time.Sleep(1 * time.Millisecond) - err := store.PatchBlocks(&model.BlockPatchBatch{BlockIDs: blockIds, BlockPatches: blockPatches}, "user-id-1") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - - retrievedBlock, err := store.GetBlock("id-test") - require.NoError(t, err) - require.NotEqual(t, title, retrievedBlock.Title) - }) -} - -var ( - subtreeSampleBlocks = []*model.Block{ - { - ID: "parent", - BoardID: testBoardID, - ModifiedBy: testUserID, - }, - { - ID: "child1", - BoardID: testBoardID, - ParentID: "parent", - ModifiedBy: testUserID, - }, - { - ID: "child2", - BoardID: testBoardID, - ParentID: "parent", - ModifiedBy: testUserID, - }, - { - ID: "grandchild1", - BoardID: testBoardID, - ParentID: "child1", - ModifiedBy: testUserID, - }, - { - ID: "grandchild2", - BoardID: testBoardID, - ParentID: "child2", - ModifiedBy: testUserID, - }, - { - ID: "greatgrandchild1", - BoardID: testBoardID, - ParentID: "grandchild1", - ModifiedBy: testUserID, - }, - } -) - -func testGetSubTree2(t *testing.T, store store.Store) { - boardID := testBoardID - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - initialCount := len(blocks) - - InsertBlocks(t, store, subtreeSampleBlocks, "user-id-1") - time.Sleep(1 * time.Millisecond) - defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") - - blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+6) - - t.Run("from root id", func(t *testing.T) { - blocks, err = store.GetSubTree2(boardID, "parent", model.QuerySubtreeOptions{}) - require.NoError(t, err) - require.Len(t, blocks, 3) - require.True(t, ContainsBlockWithID(blocks, "parent")) - require.True(t, ContainsBlockWithID(blocks, "child1")) - require.True(t, ContainsBlockWithID(blocks, "child2")) - }) - - t.Run("from child id", func(t *testing.T) { - blocks, err = store.GetSubTree2(boardID, "child1", model.QuerySubtreeOptions{}) - require.NoError(t, err) - require.Len(t, blocks, 2) - require.True(t, ContainsBlockWithID(blocks, "child1")) - require.True(t, ContainsBlockWithID(blocks, "grandchild1")) - }) - - t.Run("from not existing id", func(t *testing.T) { - blocks, err = store.GetSubTree2(boardID, "not-exists", model.QuerySubtreeOptions{}) - require.NoError(t, err) - require.Empty(t, blocks) - }) -} - -func testDeleteBlock(t *testing.T, store store.Store) { - userID := testUserID - boardID := testBoardID - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - initialCount := len(blocks) - - blocksToInsert := []*model.Block{ - { - ID: "block1", - BoardID: boardID, - ModifiedBy: userID, - }, - { - ID: "block2", - BoardID: boardID, - ModifiedBy: userID, - }, - { - ID: "block3", - BoardID: boardID, - ModifiedBy: userID, - }, - } - InsertBlocks(t, store, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, blocksToInsert, "test") - - blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+3) - - t.Run("existing id", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock("block1", userID) - require.NoError(t, err) - }) - - t.Run("existing id multiple times", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock("block1", userID) - require.NoError(t, err) - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.DeleteBlock("block1", userID) - require.NoError(t, err) - }) - - t.Run("from not existing id", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock("not-exists", userID) - require.NoError(t, err) - }) -} - -func testUndeleteBlock(t *testing.T, store store.Store) { - boardID := testBoardID - userID := testUserID - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - initialCount := len(blocks) - - blocksToInsert := []*model.Block{ - { - ID: "block1", - BoardID: boardID, - ModifiedBy: userID, - }, - { - ID: "block2", - BoardID: boardID, - ModifiedBy: userID, - }, - { - ID: "block3", - BoardID: boardID, - ModifiedBy: userID, - }, - } - InsertBlocks(t, store, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, blocksToInsert, "test") - - blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, initialCount+3) - - t.Run("existing id", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock("block1", userID) - require.NoError(t, err) - - block, err := store.GetBlock("block1") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, block) - - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBlock("block1", userID) - require.NoError(t, err) - - block, err = store.GetBlock("block1") - require.NoError(t, err) - require.NotNil(t, block) - }) - - t.Run("existing id multiple times", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.DeleteBlock("block1", userID) - require.NoError(t, err) - - block, err := store.GetBlock("block1") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, block) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBlock("block1", userID) - require.NoError(t, err) - - block, err = store.GetBlock("block1") - require.NoError(t, err) - require.NotNil(t, block) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBlock("block1", userID) - require.NoError(t, err) - - block, err = store.GetBlock("block1") - require.NoError(t, err) - require.NotNil(t, block) - }) - - t.Run("from not existing id", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.UndeleteBlock("not-exists", userID) - require.NoError(t, err) - - block, err := store.GetBlock("not-exists") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, block) - }) -} - -func testGetBlocks(t *testing.T, store store.Store) { - boardID := testBoardID - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - - blocksToInsert := []*model.Block{ - { - ID: "block1", - BoardID: boardID, - ParentID: "", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block2", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block3", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block4", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test2", - }, - { - ID: "block5", - BoardID: boardID, - ParentID: "block2", - ModifiedBy: testUserID, - Type: "test", - }, - } - InsertBlocks(t, store, blocksToInsert, "user-id-1") - defer DeleteBlocks(t, store, blocksToInsert, "test") - - t.Run("not existing parent with type", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "not-exists", BlockType: model.BlockType("test")} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) - - t.Run("not existing type with parent", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1", BlockType: model.BlockType("not-existing")} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) - - t.Run("valid parent and type", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1", BlockType: model.BlockType("test")} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Len(t, blocks, 2) - }) - - t.Run("not existing parent", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "not-exists"} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) - - t.Run("valid parent", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1"} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Len(t, blocks, 3) - }) - - t.Run("not existing type", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, BlockType: model.BlockType("not-exists")} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) - - t.Run("valid type", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID, BlockType: model.BlockType("test")} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Len(t, blocks, 4) - }) - - t.Run("not existing board", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: "not-exists"} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) - - t.Run("all blocks of the a board", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - opts := model.QueryBlocksOptions{BoardID: boardID} - blocks, err = store.GetBlocks(opts) - require.NoError(t, err) - require.Len(t, blocks, 5) - }) - - t.Run("several blocks by ids", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksByIDs([]string{"block2", "block4"}) - require.NoError(t, err) - require.Len(t, blocks, 2) - }) - - t.Run("blocks by ids where some are not found", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksByIDs([]string{"block2", "blockNonexistent"}) - var naf *model.ErrNotAllFound - require.ErrorAs(t, err, &naf) - require.True(t, model.IsErrNotFound(err)) - require.Len(t, blocks, 1) - }) - - t.Run("blocks by ids where none are found", func(t *testing.T) { - time.Sleep(1 * time.Millisecond) - blocks, err = store.GetBlocksByIDs([]string{"blockNonexistent1", "blockNonexistent2"}) - var naf *model.ErrNotAllFound - require.ErrorAs(t, err, &naf) - require.True(t, model.IsErrNotFound(err)) - require.Empty(t, blocks) - }) -} - -func testGetBlock(t *testing.T, store store.Store) { - t.Run("get a block", func(t *testing.T) { - block := &model.Block{ - ID: "block-id-10", - BoardID: "board-id-1", - ModifiedBy: "user-id-1", - } - - err := store.InsertBlock(block, "user-id-1") - require.NoError(t, err) - - fetchedBlock, err := store.GetBlock("block-id-10") - require.NoError(t, err) - require.NotNil(t, fetchedBlock) - require.Equal(t, "block-id-10", fetchedBlock.ID) - require.Equal(t, "board-id-1", fetchedBlock.BoardID) - require.Equal(t, "user-id-1", fetchedBlock.CreatedBy) - require.Equal(t, "user-id-1", fetchedBlock.ModifiedBy) - assert.WithinDurationf(t, time.Now(), utils.GetTimeForMillis(fetchedBlock.CreateAt), 1*time.Second, "create time should be current time") - assert.WithinDurationf(t, time.Now(), utils.GetTimeForMillis(fetchedBlock.UpdateAt), 1*time.Second, "update time should be current time") - }) - - t.Run("get a non-existing block", func(t *testing.T) { - fetchedBlock, err := store.GetBlock("non-existing-id") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, fetchedBlock) - }) -} - -func testDuplicateBlock(t *testing.T, store store.Store) { - blocksToInsert := subtreeSampleBlocks - blocksToInsert = append(blocksToInsert, - &model.Block{ - ID: "grandchild1a", - BoardID: testBoardID, - ParentID: "child1", - ModifiedBy: testUserID, - Type: model.TypeComment, - }, - &model.Block{ - ID: "grandchild2a", - BoardID: testBoardID, - ParentID: "child2", - ModifiedBy: testUserID, - Type: model.TypeComment, - }, - ) - - InsertBlocks(t, store, blocksToInsert, "user-id-1") - time.Sleep(1 * time.Millisecond) - defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") - - t.Run("duplicate existing block as no template", func(t *testing.T) { - blocks, err := store.DuplicateBlock(testBoardID, "child1", testUserID, false) - require.NoError(t, err) - require.Len(t, blocks, 2) - require.Equal(t, false, blocks[0].Fields["isTemplate"]) - }) - - t.Run("duplicate existing block as template", func(t *testing.T) { - blocks, err := store.DuplicateBlock(testBoardID, "child1", testUserID, true) - require.NoError(t, err) - require.Len(t, blocks, 2) - require.Equal(t, true, blocks[0].Fields["isTemplate"]) - }) - - t.Run("duplicate not existing block", func(t *testing.T) { - blocks, err := store.DuplicateBlock(testBoardID, "not-existing-id", testUserID, false) - require.Error(t, err) - require.Nil(t, blocks) - }) - - t.Run("duplicate not existing board", func(t *testing.T) { - blocks, err := store.DuplicateBlock("not-existing-board", "not-existing-id", testUserID, false) - require.Error(t, err) - require.Nil(t, blocks) - }) - - t.Run("not matching board/block", func(t *testing.T) { - blocks, err := store.DuplicateBlock("other-id", "child1", testUserID, false) - require.Error(t, err) - require.Nil(t, blocks) - }) -} - -func testGetBlockMetadata(t *testing.T, store store.Store) { - boardID := testBoardID - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - - blocksToInsert := []*model.Block{ - { - ID: "block1", - BoardID: boardID, - ParentID: "", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block2", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block3", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test", - }, - { - ID: "block4", - BoardID: boardID, - ParentID: "block1", - ModifiedBy: testUserID, - Type: "test2", - }, - { - ID: "block5", - BoardID: boardID, - ParentID: "block2", - ModifiedBy: testUserID, - Type: "test", - }, - } - - for _, v := range blocksToInsert { - time.Sleep(20 * time.Millisecond) - subBlocks := []*model.Block{v} - InsertBlocks(t, store, subBlocks, testUserID) - } - defer DeleteBlocks(t, store, blocksToInsert, "test") - - t.Run("get full block history", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{ - Descending: false, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 5) - expectedBlock := blocksToInsert[0] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get full block history descending", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{ - Descending: true, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 5) - expectedBlock := blocksToInsert[len(blocksToInsert)-1] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get limited block history", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{ - Limit: 3, - Descending: false, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 3) - }) - - t.Run("get first block history", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: false, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 1) - expectedBlock := blocksToInsert[0] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get last block history", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{ - Limit: 1, - Descending: true, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 1) - expectedBlock := blocksToInsert[len(blocksToInsert)-1] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get block history after updateAt", func(t *testing.T) { - rBlock, err2 := store.GetBlock("block3") - require.NoError(t, err2) - require.NotZero(t, rBlock.UpdateAt) - - opts := model.QueryBlockHistoryOptions{ - AfterUpdateAt: rBlock.UpdateAt, - Descending: false, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 2) - expectedBlock := blocksToInsert[3] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get block history before updateAt", func(t *testing.T) { - rBlock, err2 := store.GetBlock("block3") - require.NoError(t, err2) - require.NotZero(t, rBlock.UpdateAt) - - opts := model.QueryBlockHistoryOptions{ - BeforeUpdateAt: rBlock.UpdateAt, - Descending: true, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - require.Len(t, blocks, 2) - expectedBlock := blocksToInsert[1] - block := blocks[0] - - require.Equal(t, expectedBlock.ID, block.ID) - }) - - t.Run("get full block history after delete", func(t *testing.T) { - time.Sleep(20 * time.Millisecond) - // this will delete `block1` and any other blocks with `block1` as parent. - err = store.DeleteBlock(blocksToInsert[0].ID, testUserID) - require.NoError(t, err) - - opts := model.QueryBlockHistoryOptions{ - Descending: true, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - // all 5 blocks get a history record for insert, then `block1` gets a record for delete, - // and all 3 `block1` children get a record for delete. Thus total is 9. - require.Len(t, blocks, 9) - }) - - t.Run("get full block history after undelete", func(t *testing.T) { - time.Sleep(20 * time.Millisecond) - // this will undelete `block1` and its children - err = store.UndeleteBlock(blocksToInsert[0].ID, testUserID) - require.NoError(t, err) - - opts := model.QueryBlockHistoryOptions{ - Descending: true, - } - blocks, err = store.GetBlockHistoryDescendants(boardID, opts) - require.NoError(t, err) - // previous test put 9 records in history table. In this test 1 record was added for undeleting - // `block1` and another 3 for undeleting the children for a total of 13. - require.Len(t, blocks, 13) - }) - - t.Run("get block history of a board with no history", func(t *testing.T) { - opts := model.QueryBlockHistoryOptions{} - - blocks, err = store.GetBlockHistoryDescendants("nonexistent-board-id", opts) - require.NoError(t, err) - require.Empty(t, blocks) - }) -} - -func testUndeleteBlockChildren(t *testing.T, store store.Store) { - boards := createTestBoards(t, store, testTeamID, testUserID, 2) - boardDelete := boards[0] - boardKeep := boards[1] - userID := testUserID - - // create some blocks to be deleted - cardsDelete := createTestCards(t, store, userID, boardDelete.ID, 3) - blocksDelete := createTestBlocksForCard(t, store, cardsDelete[0].ID, 5) - require.Len(t, blocksDelete, 5) - - // create some blocks to keep - cardsKeep := createTestCards(t, store, userID, boardKeep.ID, 3) - blocksKeep := createTestBlocksForCard(t, store, cardsKeep[0].ID, 4) - require.Len(t, blocksKeep, 4) - - t.Run("undelete block children for card", func(t *testing.T) { - cardDelete := cardsDelete[0] - cardKeep := cardsKeep[0] - - // delete a card - err := store.DeleteBlock(cardDelete.ID, testUserID) - require.NoError(t, err) - - // ensure the card was deleted - block, err := store.GetBlock(cardDelete.ID) - require.Error(t, err) - require.Nil(t, block) - - // ensure the card children were deleted - blocks, err := store.GetBlocks(model.QueryBlocksOptions{ - BoardID: cardDelete.BoardID, - ParentID: cardDelete.ID, - BlockType: model.TypeText}, - ) - require.NoError(t, err) - assert.Empty(t, blocks) - - // ensure the other card children remain. - blocks, err = store.GetBlocks(model.QueryBlocksOptions{ - BoardID: cardKeep.BoardID, - ParentID: cardKeep.ID, - BlockType: model.TypeText}, - ) - require.NoError(t, err) - assert.Len(t, blocks, len(blocksKeep)) - - // undelete the card - err = store.UndeleteBlock(cardDelete.ID, testUserID) - require.NoError(t, err) - - // ensure the card was restored - block, err = store.GetBlock(cardDelete.ID) - require.NoError(t, err) - require.NotNil(t, block) - - // ensure the card children were restored - blocks, err = store.GetBlocks(model.QueryBlocksOptions{ - BoardID: cardDelete.BoardID, - ParentID: cardDelete.ID, - BlockType: model.TypeText}, - ) - require.NoError(t, err) - assert.Len(t, blocks, len(blocksDelete)) - }) - - t.Run("undelete block children for board", func(t *testing.T) { - // delete the board - err := store.DeleteBoard(boardDelete.ID, testUserID) - require.NoError(t, err) - - // ensure the board was deleted - board, err := store.GetBoard(boardDelete.ID) - require.Error(t, err) - require.Nil(t, board) - - // ensure all cards and blocks for the board were deleted - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardDelete.ID}) - require.NoError(t, err) - assert.Empty(t, blocks) - - // ensure the other board's cards and blocks remain. - blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardKeep.ID}) - require.NoError(t, err) - assert.Len(t, blocks, len(blocksKeep)+len(cardsKeep)) - - // undelete the board - err = store.UndeleteBoard(boardDelete.ID, testUserID) - require.NoError(t, err) - - // ensure the board was restored - board, err = store.GetBoard(boardDelete.ID) - require.NoError(t, err) - require.NotNil(t, board) - - // ensure the board's cards and blocks were restored. - blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardDelete.ID}) - require.NoError(t, err) - assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete)) - }) -} - -func testGetBlockHistoryNewestChildren(t *testing.T, store store.Store) { - boards := createTestBoards(t, store, testTeamID, testUserID, 2) - board := boards[0] - - const cardCount = 10 - const patchCount = 5 - - // create a card and some content blocks - cards := createTestCards(t, store, testUserID, board.ID, 1) - card := cards[0] - content := createTestBlocksForCard(t, store, card.ID, cardCount) - - // patch the content blocks to create some history records - for i := 1; i <= patchCount; i++ { - for _, block := range content { - title := strconv.FormatInt(int64(i), 10) - patch := &model.BlockPatch{ - Title: &title, - } - err := store.PatchBlock(block.ID, patch, testUserID) - require.NoError(t, err, "error patching content blocks") - } - } - - // delete some of the content blocks - err := store.DeleteBlock(content[0].ID, testUserID) - require.NoError(t, err, "error deleting content block") - err = store.DeleteBlock(content[3].ID, testUserID) - require.NoError(t, err, "error deleting content block") - err = store.DeleteBlock(content[7].ID, testUserID) - require.NoError(t, err, "error deleting content block") - - t.Run("invalid card", func(t *testing.T) { - opts := model.QueryBlockHistoryChildOptions{} - blocks, hasMore, err := store.GetBlockHistoryNewestChildren(utils.NewID(utils.IDTypeCard), opts) - require.NoError(t, err) - require.False(t, hasMore) - require.Empty(t, blocks) - }) - - t.Run("valid card with no children", func(t *testing.T) { - opts := model.QueryBlockHistoryChildOptions{} - emptyCard := createTestCards(t, store, testUserID, board.ID, 1)[0] - blocks, hasMore, err := store.GetBlockHistoryNewestChildren(emptyCard.ID, opts) - require.NoError(t, err) - require.False(t, hasMore) - require.Empty(t, blocks) - }) - - t.Run("valid card with children", func(t *testing.T) { - opts := model.QueryBlockHistoryChildOptions{} - blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts) - require.NoError(t, err) - require.False(t, hasMore) - require.Len(t, blocks, cardCount) - require.ElementsMatch(t, extractIDs(t, blocks), extractIDs(t, content)) - - expected := strconv.FormatInt(patchCount, 10) - for _, b := range blocks { - require.Equal(t, expected, b.Title) - } - }) - - t.Run("pagination", func(t *testing.T) { - opts := model.QueryBlockHistoryChildOptions{ - PerPage: 3, - } - - collected := make([]*model.Block, 0) - reps := 0 - for { - reps++ - blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts) - require.NoError(t, err) - collected = append(collected, blocks...) - if !hasMore { - break - } - opts.Page++ - } - - assert.Len(t, collected, cardCount) - assert.Equal(t, math.Floor(float64(cardCount/opts.PerPage)+1), float64(reps)) - - expected := strconv.FormatInt(patchCount, 10) - for _, b := range collected { - require.Equal(t, expected, b.Title) - } - }) -} diff --git a/server/boards/services/store/storetests/board_insights.go b/server/boards/services/store/storetests/board_insights.go deleted file mode 100644 index 5c399ec687..0000000000 --- a/server/boards/services/store/storetests/board_insights.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -const ( - testInsightsUserID1 = "user-id-1" -) - -func StoreTestBoardsInsightsStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetBoardsInsights", func(t *testing.T) { - runStoreTests(t, getBoardsInsightsTest) - }) -} - -func getBoardsInsightsTest(t *testing.T, store store.Store) { - // creating sample data - teamID := testTeamID - userID := testUserID - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", TeamID: teamID, Type: model.BoardTypeOpen, Icon: "💬"}, - {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-3", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: model.TypeCard}, - {ID: "block-id-3", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-4", BoardID: "board-id-2", Type: model.TypeCard}, - {ID: "block-id-5", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-6", BoardID: "board-id-2", Type: model.TypeCard}, - {ID: "block-id-7", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard}, - {ID: "block-id-9", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-10", BoardID: "board-id-3", Type: model.TypeCard}, - {ID: "block-id-11", BoardID: "board-id-3", Type: model.TypeCard}, - {ID: "block-id-12", BoardID: "board-id-3", Type: model.TypeCard}, - }, - } - - bab, err := store.CreateBoardsAndBlocks(newBab, userID) - require.NoError(t, err) - require.NotNil(t, bab) - - newBab = &model.BoardsAndBlocks{ - Blocks: []*model.Block{ - {ID: "block-id-13", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-14", BoardID: "board-id-1", Type: model.TypeCard}, - }, - } - bab, err = store.CreateBoardsAndBlocks(newBab, testInsightsUserID1) - require.NoError(t, err) - require.NotNil(t, bab) - bm := &model.BoardMember{ - UserID: userID, - BoardID: "board-id-2", - SchemeAdmin: true, - } - - _, _ = store.SaveMember(bm) - - boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID, true) - boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID, true) - t.Run("team insights", func(t *testing.T) { - boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID} - topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID, - 0, 0, 10, boardIDs) - require.NoError(t, err) - require.Len(t, topTeamBoards.Items, 3) - // validate board insight content - require.Equal(t, topTeamBoards.Items[0].ActivityCount, strconv.Itoa(8)) - require.Equal(t, topTeamBoards.Items[0].Icon, "💬") - require.Equal(t, topTeamBoards.Items[1].ActivityCount, strconv.Itoa(5)) - require.Equal(t, topTeamBoards.Items[2].ActivityCount, strconv.Itoa(4)) - }) - - t.Run("user insights", func(t *testing.T) { - boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID} - topUser1Boards, err := store.GetUserBoardsInsights(testTeamID, testUserID, 0, 0, 10, boardIDs) - require.NoError(t, err) - require.Len(t, topUser1Boards.Items, 3) - require.Equal(t, topUser1Boards.Items[0].Icon, "💬") - require.Equal(t, topUser1Boards.Items[0].BoardID, "board-id-1") - boardIDs = []string{boardsUser2[0].ID, boardsUser2[1].ID} - topUser2Boards, err := store.GetUserBoardsInsights(testTeamID, testInsightsUserID1, 0, 0, 10, boardIDs) - require.NoError(t, err) - require.Len(t, topUser2Boards.Items, 1) - require.Equal(t, topUser2Boards.Items[0].BoardID, "board-id-1") - }) -} diff --git a/server/boards/services/store/storetests/boards.go b/server/boards/services/store/storetests/boards.go deleted file mode 100644 index 23ae620885..0000000000 --- a/server/boards/services/store/storetests/boards.go +++ /dev/null @@ -1,1167 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func StoreTestBoardStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetBoard", func(t *testing.T) { - runStoreTests(t, testGetBoard) - }) - t.Run("GetBoardsForUserAndTeam", func(t *testing.T) { - runStoreTests(t, testGetBoardsForUserAndTeam) - }) - t.Run("GetBoardsInTeamByIds", func(t *testing.T) { - runStoreTests(t, testGetBoardsInTeamByIds) - }) - t.Run("InsertBoard", func(t *testing.T) { - runStoreTests(t, testInsertBoard) - }) - t.Run("PatchBoard", func(t *testing.T) { - runStoreTests(t, testPatchBoard) - }) - t.Run("DeleteBoard", func(t *testing.T) { - runStoreTests(t, testDeleteBoard) - }) - t.Run("UndeleteBoard", func(t *testing.T) { - runStoreTests(t, testUndeleteBoard) - }) - t.Run("InsertBoardWithAdmin", func(t *testing.T) { - runStoreTests(t, testInsertBoardWithAdmin) - }) - t.Run("SaveMember", func(t *testing.T) { - runStoreTests(t, testSaveMember) - }) - t.Run("GetMemberForBoard", func(t *testing.T) { - runStoreTests(t, testGetMemberForBoard) - }) - t.Run("GetMembersForBoard", func(t *testing.T) { - runStoreTests(t, testGetMembersForBoard) - }) - t.Run("GetMembersForUser", func(t *testing.T) { - runStoreTests(t, testGetMembersForUser) - }) - t.Run("DeleteMember", func(t *testing.T) { - runStoreTests(t, testDeleteMember) - }) - t.Run("SearchBoardsForUser", func(t *testing.T) { - runStoreTests(t, testSearchBoardsForUser) - }) - t.Run("SearchBoardsForUserInTeam", func(t *testing.T) { - runStoreTests(t, testSearchBoardsForUserInTeam) - }) - t.Run("GetBoardHistory", func(t *testing.T) { - runStoreTests(t, testGetBoardHistory) - }) - t.Run("GetBoardCount", func(t *testing.T) { - runStoreTests(t, testGetBoardCount) - }) -} - -func testGetBoard(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("existing board", func(t *testing.T) { - board := &model.Board{ - ID: "id-1", - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - _, err := store.InsertBoard(board, userID) - require.NoError(t, err) - - rBoard, err := store.GetBoard(board.ID) - require.NoError(t, err) - require.Equal(t, board.ID, rBoard.ID) - require.Equal(t, board.TeamID, rBoard.TeamID) - require.Equal(t, userID, rBoard.CreatedBy) - require.Equal(t, userID, rBoard.ModifiedBy) - require.Equal(t, board.Type, rBoard.Type) - require.NotZero(t, rBoard.CreateAt) - require.NotZero(t, rBoard.UpdateAt) - }) - - t.Run("nonexisting board", func(t *testing.T) { - rBoard, err := store.GetBoard("nonexistent-id") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, rBoard) - }) -} - -func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) { - userID := "user-id-1" - - t.Run("should return empty list if no results are found", func(t *testing.T) { - boards, err := store.GetBoardsForUserAndTeam(testUserID, testTeamID, true) - require.NoError(t, err) - require.Empty(t, boards) - }) - - t.Run("should return only the boards of the team that the user is a member of", func(t *testing.T) { - teamID1 := "team-id-1" - teamID2 := "team-id-2" - - // team 1 boards - board1 := &model.Board{ - ID: "board-id-1", - TeamID: teamID1, - Type: model.BoardTypeOpen, - } - rBoard1, _, err := store.InsertBoardWithAdmin(board1, userID) - require.NoError(t, err) - - board2 := &model.Board{ - ID: "board-id-2", - TeamID: teamID1, - Type: model.BoardTypePrivate, - } - rBoard2, _, err := store.InsertBoardWithAdmin(board2, userID) - require.NoError(t, err) - - board3 := &model.Board{ - ID: "board-id-3", - TeamID: teamID1, - Type: model.BoardTypeOpen, - } - rBoard3, err := store.InsertBoard(board3, "other-user") - require.NoError(t, err) - - board4 := &model.Board{ - ID: "board-id-4", - TeamID: teamID1, - Type: model.BoardTypePrivate, - } - _, err = store.InsertBoard(board4, "other-user") - require.NoError(t, err) - - // team 2 boards - board5 := &model.Board{ - ID: "board-id-5", - TeamID: teamID2, - Type: model.BoardTypeOpen, - } - _, _, err = store.InsertBoardWithAdmin(board5, userID) - require.NoError(t, err) - - board6 := &model.Board{ - ID: "board-id-6", - TeamID: teamID1, - Type: model.BoardTypePrivate, - } - _, err = store.InsertBoard(board6, "other-user") - require.NoError(t, err) - - t.Run("should only find the two boards that the user is a member of for team 1 plus the one open board", func(t *testing.T) { - boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, true) - require.NoError(t, err) - require.ElementsMatch(t, []*model.Board{ - rBoard1, - rBoard2, - rBoard3, - }, boards) - }) - - t.Run("should only find the two boards that the user is a member of for team 1", func(t *testing.T) { - boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, false) - require.NoError(t, err) - require.ElementsMatch(t, []*model.Board{ - rBoard1, - rBoard2, - }, boards) - }) - - t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) { - boards, err := store.GetBoardsForUserAndTeam(userID, teamID2, true) - require.NoError(t, err) - require.Len(t, boards, 1) - require.Equal(t, board5.ID, boards[0].ID) - }) - }) -} - -func testGetBoardsInTeamByIds(t *testing.T, store store.Store) { - t.Run("should return err not all found if one or more of the ids are not found", func(t *testing.T) { - for _, boardID := range []string{"board-id-1", "board-id-2"} { - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - rBoard, _, err := store.InsertBoardWithAdmin(board, testUserID) - require.NoError(t, err) - require.NotNil(t, rBoard) - } - - testCases := []struct { - Name string - BoardIDs []string - ExpectedError bool - ExpectedLen int - }{ - { - Name: "if none of the IDs are found", - BoardIDs: []string{"nonexistent-1", "nonexistent-2"}, - ExpectedError: true, - ExpectedLen: 0, - }, - { - Name: "if not all of the IDs are found", - BoardIDs: []string{"nonexistent-1", "board-id-1"}, - ExpectedError: true, - ExpectedLen: 1, - }, - { - Name: "if all of the IDs are found", - BoardIDs: []string{"board-id-1", "board-id-2"}, - ExpectedError: false, - ExpectedLen: 2, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - boards, err := store.GetBoardsInTeamByIds(tc.BoardIDs, testTeamID) - if tc.ExpectedError { - var naf *model.ErrNotAllFound - require.ErrorAs(t, err, &naf) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - } else { - require.NoError(t, err) - } - require.Len(t, boards, tc.ExpectedLen) - }) - } - }) -} - -func testInsertBoard(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("valid public board", func(t *testing.T) { - board := &model.Board{ - ID: "id-test-public", - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.Equal(t, board.ID, newBoard.ID) - require.Equal(t, newBoard.Type, model.BoardTypeOpen) - require.NotZero(t, newBoard.CreateAt) - require.NotZero(t, newBoard.UpdateAt) - require.Zero(t, newBoard.DeleteAt) - require.Equal(t, userID, newBoard.CreatedBy) - require.Equal(t, newBoard.CreatedBy, newBoard.ModifiedBy) - }) - - t.Run("valid private board", func(t *testing.T) { - board := &model.Board{ - ID: "id-test-private", - TeamID: testTeamID, - Type: model.BoardTypePrivate, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.Equal(t, board.ID, newBoard.ID) - require.Equal(t, newBoard.Type, model.BoardTypePrivate) - require.NotZero(t, newBoard.CreateAt) - require.NotZero(t, newBoard.UpdateAt) - require.Zero(t, newBoard.DeleteAt) - require.Equal(t, userID, newBoard.CreatedBy) - require.Equal(t, newBoard.CreatedBy, newBoard.ModifiedBy) - }) - - t.Run("invalid properties field board", func(t *testing.T) { - board := &model.Board{ - ID: "id-test-props", - TeamID: testTeamID, - Properties: map[string]interface{}{"no-serializable-value": t.Run}, - } - - _, err := store.InsertBoard(board, userID) - require.Error(t, err) - - rBoard, err := store.GetBoard(board.ID) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, rBoard) - }) - - t.Run("update board", func(t *testing.T) { - board := &model.Board{ - ID: "id-test-public", - TeamID: testTeamID, - Title: "New title", - } - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newBoard, err := store.InsertBoard(board, "user2") - require.NoError(t, err) - require.Equal(t, "New title", newBoard.Title) - require.Equal(t, "user2", newBoard.ModifiedBy) - }) - - t.Run("test update board type", func(t *testing.T) { - board := &model.Board{ - ID: "id-test-type-board", - Title: "Public board", - Type: model.BoardTypeOpen, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.Equal(t, model.BoardTypeOpen, newBoard.Type) - - boardUpdate := &model.Board{ - ID: "id-test-type-board", - Type: model.BoardTypePrivate, - } - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - modifiedBoard, err := store.InsertBoard(boardUpdate, userID) - require.NoError(t, err) - require.Equal(t, model.BoardTypePrivate, modifiedBoard.Type) - }) -} - -func testPatchBoard(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("should return error if the board doesn't exist", func(t *testing.T) { - newTitle := "A new title" - patch := &model.BoardPatch{Title: &newTitle} - - board, err := store.PatchBoard("nonexistent-board-id", patch, userID) - require.Error(t, err) - require.Nil(t, board) - }) - - t.Run("should correctly apply a simple patch", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - userID2 := "user-id-2" - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - Title: "A simple title", - Description: "A simple description", - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, userID, newBoard.CreatedBy) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newTitle := "A new title" - newDescription := "A new description" - patch := &model.BoardPatch{Title: &newTitle, Description: &newDescription} - patchedBoard, err := store.PatchBoard(boardID, patch, userID2) - require.NoError(t, err) - require.Equal(t, newTitle, patchedBoard.Title) - require.Equal(t, newDescription, patchedBoard.Description) - require.Equal(t, userID, patchedBoard.CreatedBy) - require.Equal(t, userID2, patchedBoard.ModifiedBy) - }) - - t.Run("should correctly update the board properties", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - Properties: map[string]interface{}{ - "one": "1", - "two": "2", - }, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, "1", newBoard.Properties["one"].(string)) - require.Equal(t, "2", newBoard.Properties["two"].(string)) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - patch := &model.BoardPatch{ - UpdatedProperties: map[string]interface{}{"three": "3"}, - DeletedProperties: []string{"one"}, - } - patchedBoard, err := store.PatchBoard(boardID, patch, userID) - require.NoError(t, err) - require.NotContains(t, patchedBoard.Properties, "one") - require.Equal(t, "2", patchedBoard.Properties["two"].(string)) - require.Equal(t, "3", patchedBoard.Properties["three"].(string)) - }) - - t.Run("should correctly modify the board's type", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, newBoard.Type, model.BoardTypeOpen) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newType := model.BoardTypePrivate - patch := &model.BoardPatch{Type: &newType} - patchedBoard, err := store.PatchBoard(boardID, patch, userID) - require.NoError(t, err) - require.Equal(t, model.BoardTypePrivate, patchedBoard.Type) - }) - - t.Run("a patch that doesn't include any of the properties should not modify them", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - properties := map[string]interface{}{"prop1": "val1"} - cardProperties := []map[string]interface{}{{"prop2": "val2"}} - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - Properties: properties, - CardProperties: cardProperties, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, newBoard.Type, model.BoardTypeOpen) - require.Equal(t, properties, newBoard.Properties) - require.Equal(t, cardProperties, newBoard.CardProperties) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newType := model.BoardTypePrivate - patch := &model.BoardPatch{Type: &newType} - patchedBoard, err := store.PatchBoard(boardID, patch, userID) - require.NoError(t, err) - require.Equal(t, model.BoardTypePrivate, patchedBoard.Type) - require.Equal(t, properties, patchedBoard.Properties) - require.Equal(t, cardProperties, patchedBoard.CardProperties) - }) - - t.Run("a patch that removes a card property and updates another should work correctly", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - prop1 := map[string]interface{}{"id": "prop1", "value": "val1"} - prop2 := map[string]interface{}{"id": "prop2", "value": "val2"} - prop3 := map[string]interface{}{"id": "prop3", "value": "val3"} - cardProperties := []map[string]interface{}{prop1, prop2, prop3} - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - CardProperties: cardProperties, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, newBoard.Type, model.BoardTypeOpen) - require.Equal(t, cardProperties, newBoard.CardProperties) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newProp1 := map[string]interface{}{"id": "prop1", "value": "newval1"} - expectedCardProperties := []map[string]interface{}{newProp1, prop3} - patch := &model.BoardPatch{ - UpdatedCardProperties: []map[string]interface{}{newProp1}, - DeletedCardProperties: []string{"prop2"}, - } - patchedBoard, err := store.PatchBoard(boardID, patch, userID) - require.NoError(t, err) - require.ElementsMatch(t, expectedCardProperties, patchedBoard.CardProperties) - }) -} - -func testDeleteBoard(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("should return an error if the board doesn't exist", func(t *testing.T) { - require.Error(t, store.DeleteBoard("nonexistent-board-id", userID)) - }) - - t.Run("should correctly delete the board", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - - rBoard, err := store.GetBoard(boardID) - require.NoError(t, err) - require.NotNil(t, rBoard) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - require.NoError(t, store.DeleteBoard(boardID, userID)) - - r2Board, err := store.GetBoard(boardID) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, r2Board) - }) -} - -func testInsertBoardWithAdmin(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("should correctly create a board and the admin membership with the creator", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - newBoard, newMember, err := store.InsertBoardWithAdmin(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - require.Equal(t, userID, newBoard.CreatedBy) - require.Equal(t, userID, newBoard.ModifiedBy) - require.NotNil(t, newMember) - require.Equal(t, userID, newMember.UserID) - require.Equal(t, boardID, newMember.BoardID) - require.True(t, newMember.SchemeAdmin) - require.True(t, newMember.SchemeEditor) - }) -} - -func testSaveMember(t *testing.T, store store.Store) { - userID := testUserID - boardID := testBoardID - - t.Run("should correctly create a member", func(t *testing.T) { - bm := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: true, - } - - memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - initialMemberHistory := len(memberHistory) - - nbm, err := store.SaveMember(bm) - require.NoError(t, err) - require.Equal(t, userID, nbm.UserID) - require.Equal(t, boardID, nbm.BoardID) - - require.True(t, nbm.SchemeAdmin) - - memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - require.Len(t, memberHistory, initialMemberHistory+1) - }) - - t.Run("should correctly update a member", func(t *testing.T) { - bm := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeEditor: true, - SchemeViewer: true, - } - - memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - initialMemberHistory := len(memberHistory) - - nbm, err := store.SaveMember(bm) - require.NoError(t, err) - require.Equal(t, userID, nbm.UserID) - require.Equal(t, boardID, nbm.BoardID) - - require.False(t, nbm.SchemeAdmin) - require.True(t, nbm.SchemeEditor) - require.True(t, nbm.SchemeViewer) - - memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - require.Len(t, memberHistory, initialMemberHistory) - }) - - t.Run("should return empty list if no results are found", func(t *testing.T) { - memberHistory, err := store.GetBoardMemberHistory(boardID, "nonexistent-user", 0) - require.NoError(t, err) - require.Empty(t, memberHistory) - }) -} - -func testGetMemberForBoard(t *testing.T, store store.Store) { - userID := testUserID - boardID := testBoardID - - t.Run("should return an error not found for nonexisting membership", func(t *testing.T) { - bm, err := store.GetMemberForBoard(boardID, userID) - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, bm) - }) - - t.Run("should return the membership if exists", func(t *testing.T) { - bm := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: true, - } - - nbm, err := store.SaveMember(bm) - require.NoError(t, err) - require.NotNil(t, nbm) - - rbm, err := store.GetMemberForBoard(boardID, userID) - require.NoError(t, err) - require.NotNil(t, rbm) - require.Equal(t, userID, rbm.UserID) - require.Equal(t, boardID, rbm.BoardID) - require.True(t, rbm.SchemeAdmin) - }) -} - -func testGetMembersForBoard(t *testing.T, store store.Store) { - t.Run("should return empty list if there are no members on a board", func(t *testing.T) { - members, err := store.GetMembersForBoard(testBoardID) - require.NoError(t, err) - require.Empty(t, members) - }) - - t.Run("should return the members of the board", func(t *testing.T) { - boardID1 := "board-id-1" - boardID2 := "board-id-2" - - userID1 := "user-id-11" - userID2 := "user-id-12" - userID3 := "user-id-13" - - bm1 := &model.BoardMember{BoardID: boardID1, UserID: userID1, SchemeAdmin: true} - _, err1 := store.SaveMember(bm1) - require.NoError(t, err1) - - bm2 := &model.BoardMember{BoardID: boardID1, UserID: userID2, SchemeEditor: true} - _, err2 := store.SaveMember(bm2) - require.NoError(t, err2) - - bm3 := &model.BoardMember{BoardID: boardID2, UserID: userID3, SchemeAdmin: true} - _, err3 := store.SaveMember(bm3) - require.NoError(t, err3) - - getMemberIDs := func(members []*model.BoardMember) []string { - ids := make([]string, len(members)) - for i, member := range members { - ids[i] = member.UserID - } - return ids - } - - board1Members, err := store.GetMembersForBoard(boardID1) - require.NoError(t, err) - require.Len(t, board1Members, 2) - require.ElementsMatch(t, []string{userID1, userID2}, getMemberIDs(board1Members)) - - board2Members, err := store.GetMembersForBoard(boardID2) - require.NoError(t, err) - require.Len(t, board2Members, 1) - require.ElementsMatch(t, []string{userID3}, getMemberIDs(board2Members)) - }) -} - -func testGetMembersForUser(t *testing.T, store store.Store) { - t.Run("should return empty list if there are no memberships for a user", func(t *testing.T) { - members, err := store.GetMembersForUser(testUserID) - require.NoError(t, err) - require.Empty(t, members) - }) -} - -func testDeleteMember(t *testing.T, store store.Store) { - userID := testUserID - boardID := testBoardID - - t.Run("should return nil if deleting a nonexistent member", func(t *testing.T) { - memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - initialMemberHistory := len(memberHistory) - - require.NoError(t, store.DeleteMember(boardID, userID)) - - memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - require.Len(t, memberHistory, initialMemberHistory) - }) - - t.Run("should correctly delete a member", func(t *testing.T) { - bm := &model.BoardMember{ - UserID: userID, - BoardID: boardID, - SchemeAdmin: true, - } - - nbm, err := store.SaveMember(bm) - require.NoError(t, err) - require.NotNil(t, nbm) - - memberHistory, err := store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - initialMemberHistory := len(memberHistory) - - require.NoError(t, store.DeleteMember(boardID, userID)) - - rbm, err := store.GetMemberForBoard(boardID, userID) - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, rbm) - - memberHistory, err = store.GetBoardMemberHistory(boardID, userID, 0) - require.NoError(t, err) - require.Len(t, memberHistory, initialMemberHistory+1) - }) -} - -func testSearchBoardsForUser(t *testing.T, store store.Store) { - teamID1 := "team-id-1" - teamID2 := "team-id-2" - userID := "user-id-1" - - t.Run("should return empty if user is not a member of any board and there are no public boards on the team", func(t *testing.T) { - boards, err := store.SearchBoardsForUser("", model.BoardSearchFieldTitle, userID, true) - require.NoError(t, err) - require.Empty(t, boards) - }) - - board1 := &model.Board{ - ID: "board-id-1", - TeamID: teamID1, - Type: model.BoardTypeOpen, - Title: "Public Board with admin", - Properties: map[string]any{"foo": "bar1"}, - } - _, _, err := store.InsertBoardWithAdmin(board1, userID) - require.NoError(t, err) - - board2 := &model.Board{ - ID: "board-id-2", - TeamID: teamID1, - Type: model.BoardTypeOpen, - Title: "Public Board", - Properties: map[string]any{"foo": "bar2"}, - } - _, err = store.InsertBoard(board2, userID) - require.NoError(t, err) - - board3 := &model.Board{ - ID: "board-id-3", - TeamID: teamID1, - Type: model.BoardTypePrivate, - Title: "Private Board with admin", - } - _, _, err = store.InsertBoardWithAdmin(board3, userID) - require.NoError(t, err) - - board4 := &model.Board{ - ID: "board-id-4", - TeamID: teamID1, - Type: model.BoardTypePrivate, - Title: "Private Board", - } - _, err = store.InsertBoard(board4, userID) - require.NoError(t, err) - - board5 := &model.Board{ - ID: "board-id-5", - TeamID: teamID2, - Type: model.BoardTypeOpen, - Title: "Public Board with admin in team 2", - } - _, _, err = store.InsertBoardWithAdmin(board5, userID) - require.NoError(t, err) - - testCases := []struct { - Name string - TeamID string - UserID string - Term string - SearchField model.BoardSearchField - IncludePublic bool - ExpectedBoardIDs []string - }{ - { - Name: "should find all private boards that the user is a member of and public boards with an empty term", - TeamID: teamID1, - UserID: userID, - Term: "", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: true, - ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID}, - }, - { - Name: "should find all with term board", - TeamID: teamID1, - UserID: userID, - Term: "board", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: true, - ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID}, - }, - { - Name: "should find all with term board where the user is member of", - TeamID: teamID1, - UserID: userID, - Term: "board", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: false, - ExpectedBoardIDs: []string{board1.ID, board3.ID, board5.ID}, - }, - { - Name: "should find only public as per the term, wether user is a member or not", - TeamID: teamID1, - UserID: userID, - Term: "public", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: true, - ExpectedBoardIDs: []string{board1.ID, board2.ID, board5.ID}, - }, - { - Name: "should find only private as per the term, wether user is a member or not", - TeamID: teamID1, - UserID: userID, - Term: "priv", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: true, - ExpectedBoardIDs: []string{board3.ID}, - }, - { - Name: "should find no board in team 2 with a non matching term", - TeamID: teamID2, - UserID: userID, - Term: "non-matching-term", - SearchField: model.BoardSearchFieldTitle, - IncludePublic: true, - ExpectedBoardIDs: []string{}, - }, - { - Name: "should find all boards with a named property", - TeamID: teamID1, - UserID: userID, - Term: "foo", - SearchField: model.BoardSearchFieldPropertyName, - IncludePublic: true, - ExpectedBoardIDs: []string{board1.ID, board2.ID}, - }, - { - Name: "should find no boards with a non-existing named property", - TeamID: teamID1, - UserID: userID, - Term: "bogus", - SearchField: model.BoardSearchFieldPropertyName, - IncludePublic: true, - ExpectedBoardIDs: []string{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - boards, err := store.SearchBoardsForUser(tc.Term, tc.SearchField, tc.UserID, tc.IncludePublic) - require.NoError(t, err) - - boardIDs := []string{} - for _, board := range boards { - boardIDs = append(boardIDs, board.ID) - } - require.ElementsMatch(t, tc.ExpectedBoardIDs, boardIDs) - }) - } -} - -func testSearchBoardsForUserInTeam(t *testing.T, store store.Store) { - t.Run("should return empty list if there are no resutls", func(t *testing.T) { - boards, err := store.SearchBoardsForUserInTeam("nonexistent-team-id", "", testUserID) - require.NoError(t, err) - require.Empty(t, boards) - }) -} - -func testUndeleteBoard(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("existing id", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - Title: "Dunder Mifflin Scranton", - MinimumRole: model.BoardRoleCommenter, - Description: "Bears, beets, Battlestar Gallectica", - Icon: "🐻", - ShowDescription: true, - IsTemplate: false, - Properties: map[string]interface{}{ - "prop_1": "value_1", - }, - CardProperties: []map[string]interface{}{ - { - "prop_1": "value_1", - }, - }, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.DeleteBoard(boardID, userID) - require.NoError(t, err) - - board, err = store.GetBoard(boardID) - require.Error(t, err) - require.Nil(t, board) - - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBoard(boardID, userID) - require.NoError(t, err) - - board, err = store.GetBoard(boardID) - require.NoError(t, err) - require.NotNil(t, board) - - // verifying the data after un-delete - require.Equal(t, "Dunder Mifflin Scranton", board.Title) - require.Equal(t, "user-id", board.CreatedBy) - require.Equal(t, "user-id", board.ModifiedBy) - require.Equal(t, model.BoardRoleCommenter, board.MinimumRole) - require.Equal(t, "Bears, beets, Battlestar Gallectica", board.Description) - require.Equal(t, "🐻", board.Icon) - require.True(t, board.ShowDescription) - require.False(t, board.IsTemplate) - require.Equal(t, board.Properties["prop_1"].(string), "value_1") - require.Equal(t, 1, len(board.CardProperties)) - require.Equal(t, board.CardProperties[0]["prop_1"], "value_1") - require.Equal(t, board.CardProperties[0]["prop_1"], "value_1") - }) - - t.Run("existing id multiple times", func(t *testing.T) { - boardID := utils.NewID(utils.IDTypeBoard) - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - newBoard, err := store.InsertBoard(board, userID) - require.NoError(t, err) - require.NotNil(t, newBoard) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.DeleteBoard(boardID, userID) - require.NoError(t, err) - - board, err = store.GetBoard(boardID) - require.Error(t, err) - require.Nil(t, board) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBoard(boardID, userID) - require.NoError(t, err) - - board, err = store.GetBoard(boardID) - require.NoError(t, err) - require.NotNil(t, board) - - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err = store.UndeleteBoard(boardID, userID) - require.NoError(t, err) - - board, err = store.GetBoard(boardID) - require.NoError(t, err) - require.NotNil(t, board) - }) - - t.Run("from not existing id", func(t *testing.T) { - // Wait for not colliding the ID+insert_at key - time.Sleep(1 * time.Millisecond) - err := store.UndeleteBoard("not-exists", userID) - require.NoError(t, err) - - block, err := store.GetBoard("not-exists") - require.Error(t, err) - require.Nil(t, block) - }) -} - -func testGetBoardHistory(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("testGetBoardHistory: create board", func(t *testing.T) { - originalTitle := "Board: original title" - boardID := utils.NewID(utils.IDTypeBoard) - board := &model.Board{ - ID: boardID, - Title: originalTitle, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - rBoard1, err := store.InsertBoard(board, userID) - require.NoError(t, err) - - opts := model.QueryBoardHistoryOptions{ - Limit: 0, - Descending: false, - } - - boards, err := store.GetBoardHistory(board.ID, opts) - require.NoError(t, err) - require.Len(t, boards, 1) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - userID2 := "user-id-2" - newTitle := "Board: A new title" - newDescription := "A new description" - patch := &model.BoardPatch{Title: &newTitle, Description: &newDescription} - patchedBoard, err := store.PatchBoard(boardID, patch, userID2) - require.NoError(t, err) - - // Updated history - boards, err = store.GetBoardHistory(board.ID, opts) - require.NoError(t, err) - require.Len(t, boards, 2) - require.Equal(t, boards[0].Title, originalTitle) - require.Equal(t, boards[1].Title, newTitle) - require.Equal(t, boards[1].Description, newDescription) - - // Check history against latest board - rBoard2, err := store.GetBoard(board.ID) - require.NoError(t, err) - require.Equal(t, rBoard2.Title, newTitle) - require.Equal(t, rBoard2.Title, boards[1].Title) - require.NotZero(t, rBoard2.UpdateAt) - require.Equal(t, rBoard1.UpdateAt, boards[0].UpdateAt) - require.Equal(t, rBoard2.UpdateAt, patchedBoard.UpdateAt) - require.Equal(t, rBoard2.UpdateAt, boards[1].UpdateAt) - require.Equal(t, rBoard1, boards[0]) - require.Equal(t, rBoard2, boards[1]) - - // wait to avoid hitting pk uniqueness constraint in history - time.Sleep(10 * time.Millisecond) - - newTitle2 := "Board: A new title 2" - patch2 := &model.BoardPatch{Title: &newTitle2} - patchBoard2, err := store.PatchBoard(boardID, patch2, userID2) - require.NoError(t, err) - - // Updated history - opts = model.QueryBoardHistoryOptions{ - Limit: 1, - Descending: true, - } - boards, err = store.GetBoardHistory(board.ID, opts) - require.NoError(t, err) - require.Len(t, boards, 1) - require.Equal(t, boards[0].Title, newTitle2) - require.Equal(t, boards[0], patchBoard2) - - // Delete board - time.Sleep(10 * time.Millisecond) - err = store.DeleteBoard(boardID, userID) - require.NoError(t, err) - - // Updated history after delete - opts = model.QueryBoardHistoryOptions{ - Limit: 0, - Descending: true, - } - boards, err = store.GetBoardHistory(board.ID, opts) - require.NoError(t, err) - require.Len(t, boards, 4) - require.NotZero(t, boards[0].UpdateAt) - require.Greater(t, boards[0].UpdateAt, patchBoard2.UpdateAt) - require.NotZero(t, boards[0].DeleteAt) - require.Greater(t, boards[0].DeleteAt, patchBoard2.UpdateAt) - }) - - t.Run("testGetBoardHistory: nonexisting board", func(t *testing.T) { - opts := model.QueryBoardHistoryOptions{ - Limit: 0, - Descending: false, - } - boards, err := store.GetBoardHistory("nonexistent-id", opts) - require.NoError(t, err) - require.Len(t, boards, 0) - }) -} - -func testGetBoardCount(t *testing.T, store store.Store) { - userID := testUserID - - t.Run("test GetBoardCount", func(t *testing.T) { - originalCount, err := store.GetBoardCount() - require.NoError(t, err) - - title := "Board: original title" - boardID := utils.NewID(utils.IDTypeBoard) - board := &model.Board{ - ID: boardID, - Title: title, - TeamID: testTeamID, - Type: model.BoardTypeOpen, - } - - _, err = store.InsertBoard(board, userID) - require.NoError(t, err) - - newCount, err := store.GetBoardCount() - require.NoError(t, err) - require.Equal(t, originalCount+1, newCount) - }) -} diff --git a/server/boards/services/store/storetests/boards_and_blocks.go b/server/boards/services/store/storetests/boards_and_blocks.go deleted file mode 100644 index 5bb4580e54..0000000000 --- a/server/boards/services/store/storetests/boards_and_blocks.go +++ /dev/null @@ -1,606 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func StoreTestBoardsAndBlocksStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("createBoardsAndBlocks", func(t *testing.T) { - runStoreTests(t, testCreateBoardsAndBlocks) - }) - t.Run("patchBoardsAndBlocks", func(t *testing.T) { - runStoreTests(t, testPatchBoardsAndBlocks) - }) - t.Run("deleteBoardsAndBlocks", func(t *testing.T) { - runStoreTests(t, testDeleteBoardsAndBlocks) - }) - t.Run("duplicateBoard", func(t *testing.T) { - runStoreTests(t, testDuplicateBoard) - }) -} - -func testCreateBoardsAndBlocks(t *testing.T, store store.Store) { - teamID := testTeamID - userID := testUserID - - boards, err := store.GetBoardsForUserAndTeam(userID, teamID, true) - require.NoError(t, err) - require.Empty(t, boards) - - t.Run("create boards and blocks", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-3", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-2", BoardID: "board-id-2", Type: model.TypeCard}, - }, - } - - bab, err := store.CreateBoardsAndBlocks(newBab, userID) - require.NoError(t, err) - require.NotNil(t, bab) - require.Len(t, bab.Boards, 3) - require.Len(t, bab.Blocks, 2) - - boardIDs := []string{} - for _, board := range bab.Boards { - boardIDs = append(boardIDs, board.ID) - } - - blockIDs := []string{} - for _, block := range bab.Blocks { - blockIDs = append(blockIDs, block.ID) - } - - require.ElementsMatch(t, []string{"board-id-1", "board-id-2", "board-id-3"}, boardIDs) - require.ElementsMatch(t, []string{"block-id-1", "block-id-2"}, blockIDs) - }) - - t.Run("create boards and blocks with admin", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-4", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-5", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-6", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-3", BoardID: "board-id-4", Type: model.TypeCard}, - {ID: "block-id-4", BoardID: "board-id-5", Type: model.TypeCard}, - }, - } - - bab, members, err := store.CreateBoardsAndBlocksWithAdmin(newBab, userID) - require.NoError(t, err) - require.NotNil(t, bab) - require.Len(t, bab.Boards, 3) - require.Len(t, bab.Blocks, 2) - require.Len(t, members, 3) - - boardIDs := []string{} - for _, board := range bab.Boards { - boardIDs = append(boardIDs, board.ID) - } - - blockIDs := []string{} - for _, block := range bab.Blocks { - blockIDs = append(blockIDs, block.ID) - } - - require.ElementsMatch(t, []string{"board-id-4", "board-id-5", "board-id-6"}, boardIDs) - require.ElementsMatch(t, []string{"block-id-3", "block-id-4"}, blockIDs) - - memberBoardIDs := []string{} - for _, member := range members { - require.Equal(t, userID, member.UserID) - memberBoardIDs = append(memberBoardIDs, member.BoardID) - } - require.ElementsMatch(t, []string{"board-id-4", "board-id-5", "board-id-6"}, memberBoardIDs) - }) - - t.Run("on failure, nothing should be saved", func(t *testing.T) { - // one of the blocks is invalid as it doesn't have BoardID - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-7", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-8", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-9", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-5", BoardID: "board-id-7", Type: model.TypeCard}, - {ID: "block-id-6", BoardID: "", Type: model.TypeCard}, - }, - } - - bab, err := store.CreateBoardsAndBlocks(newBab, userID) - require.Error(t, err) - require.Nil(t, bab) - - bab, members, err := store.CreateBoardsAndBlocksWithAdmin(newBab, userID) - require.Error(t, err) - require.Empty(t, bab) - require.Empty(t, members) - }) -} - -func testPatchBoardsAndBlocks(t *testing.T, store store.Store) { - teamID := testTeamID - userID := testUserID - - t.Run("on failure, nothing should be saved", func(t *testing.T) { - initialTitle := "initial title" - newTitle := "new title" - - board := &model.Board{ - ID: "board-id-1", - Title: initialTitle, - TeamID: teamID, - Type: model.BoardTypeOpen, - } - _, err := store.InsertBoard(board, userID) - require.NoError(t, err) - - block := &model.Block{ - ID: "block-id-1", - BoardID: "board-id-1", - Title: initialTitle, - } - require.NoError(t, store.InsertBlock(block, userID)) - - // apply the patches - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{"board-id-1"}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle}, - }, - BlockIDs: []string{"block-id-1", "block-id-2"}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Title: &newTitle}, - }, - } - - time.Sleep(10 * time.Millisecond) - - bab, err := store.PatchBoardsAndBlocks(pbab, userID) - require.Error(t, err) - require.Nil(t, bab) - - // check that things have changed - rBoard, err := store.GetBoard("board-id-1") - require.NoError(t, err) - require.Equal(t, initialTitle, rBoard.Title) - - rBlock, err := store.GetBlock("block-id-1") - require.NoError(t, err) - require.Equal(t, initialTitle, rBlock.Title) - }) - - t.Run("patch boards and blocks", func(t *testing.T) { - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", Description: "initial description", TeamID: teamID, Type: model.BoardTypeOpen}, - {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-3", Title: "initial title", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", Title: "initial title", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-2", Schema: 1, BoardID: "board-id-2", Type: model.TypeCard}, - }, - } - - rBab, err := store.CreateBoardsAndBlocks(newBab, userID) - require.NoError(t, err) - require.NotNil(t, rBab) - require.Len(t, rBab.Boards, 3) - require.Len(t, rBab.Blocks, 2) - - // apply the patches - newTitle := "new title" - newDescription := "new description" - var newSchema int64 = 2 - - pbab := &model.PatchBoardsAndBlocks{ - BoardIDs: []string{"board-id-3", "board-id-1"}, - BoardPatches: []*model.BoardPatch{ - {Title: &newTitle, Description: &newDescription}, - {Description: &newDescription}, - }, - BlockIDs: []string{"block-id-1", "block-id-2"}, - BlockPatches: []*model.BlockPatch{ - {Title: &newTitle}, - {Schema: &newSchema}, - }, - } - - time.Sleep(10 * time.Millisecond) - - bab, err := store.PatchBoardsAndBlocks(pbab, userID) - require.NoError(t, err) - require.NotNil(t, bab) - require.Len(t, bab.Boards, 2) - require.Len(t, bab.Blocks, 2) - - // check that things have changed - board1, err := store.GetBoard("board-id-1") - require.NoError(t, err) - require.Equal(t, newDescription, board1.Description) - - board3, err := store.GetBoard("board-id-3") - require.NoError(t, err) - require.Equal(t, newTitle, board3.Title) - require.Equal(t, newDescription, board3.Description) - - block1, err := store.GetBlock("block-id-1") - require.NoError(t, err) - require.Equal(t, newTitle, block1.Title) - - block2, err := store.GetBlock("block-id-2") - require.NoError(t, err) - require.Equal(t, newSchema, block2.Schema) - }) -} - -func testDeleteBoardsAndBlocks(t *testing.T, store store.Store) { - teamID := testTeamID - userID := testUserID - - t.Run("should not delete anything if a block doesn't belong to any of the boards", func(t *testing.T) { - newBoard1 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := store.InsertBoard(newBoard1, userID) - require.NoError(t, err) - - block1 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block1, userID)) - - block2 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block2, userID)) - - newBoard2 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := store.InsertBoard(newBoard2, userID) - require.NoError(t, err) - - block3 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block3, userID)) - - block4 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: "different-board-id", - } - require.NoError(t, store.InsertBlock(block4, userID)) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board1.ID, board2.ID}, - Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, - } - - time.Sleep(10 * time.Millisecond) - - expectedErrorMsg := fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", block4.ID) - require.EqualError(t, store.DeleteBoardsAndBlocks(dbab, userID), expectedErrorMsg) - - // all the entities should still exist - rBoard1, err := store.GetBoard(board1.ID) - require.NoError(t, err) - require.NotNil(t, rBoard1) - rBlock1, err := store.GetBlock(block1.ID) - require.NoError(t, err) - require.NotNil(t, rBlock1) - rBlock2, err := store.GetBlock(block2.ID) - require.NoError(t, err) - require.NotNil(t, rBlock2) - - rBoard2, err := store.GetBoard(board2.ID) - require.NoError(t, err) - require.NotNil(t, rBoard2) - rBlock3, err := store.GetBlock(block3.ID) - require.NoError(t, err) - require.NotNil(t, rBlock3) - rBlock4, err := store.GetBlock(block4.ID) - require.NoError(t, err) - require.NotNil(t, rBlock4) - }) - - t.Run("should not delete anything if a board doesn't exist", func(t *testing.T) { - newBoard1 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := store.InsertBoard(newBoard1, userID) - require.NoError(t, err) - - block1 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block1, userID)) - - block2 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block2, userID)) - - newBoard2 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := store.InsertBoard(newBoard2, userID) - require.NoError(t, err) - - block3 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block3, userID)) - - block4 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block4, userID)) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board1.ID, board2.ID, "a nonexistent board ID"}, - Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, - } - - time.Sleep(10 * time.Millisecond) - - require.True(t, model.IsErrNotFound(store.DeleteBoardsAndBlocks(dbab, userID))) - - // all the entities should still exist - rBoard1, err := store.GetBoard(board1.ID) - require.NoError(t, err) - require.NotNil(t, rBoard1) - rBlock1, err := store.GetBlock(block1.ID) - require.NoError(t, err) - require.NotNil(t, rBlock1) - rBlock2, err := store.GetBlock(block2.ID) - require.NoError(t, err) - require.NotNil(t, rBlock2) - - rBoard2, err := store.GetBoard(board2.ID) - require.NoError(t, err) - require.NotNil(t, rBoard2) - rBlock3, err := store.GetBlock(block3.ID) - require.NoError(t, err) - require.NotNil(t, rBlock3) - rBlock4, err := store.GetBlock(block4.ID) - require.NoError(t, err) - require.NotNil(t, rBlock4) - }) - - t.Run("should not delete anything if a block doesn't exist", func(t *testing.T) { - newBoard1 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := store.InsertBoard(newBoard1, userID) - require.NoError(t, err) - - block1 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block1, userID)) - - block2 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block2, userID)) - - newBoard2 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := store.InsertBoard(newBoard2, userID) - require.NoError(t, err) - - block3 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block3, userID)) - - block4 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block4, userID)) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board1.ID, board2.ID}, - Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID, "a nonexistent block ID"}, - } - - time.Sleep(10 * time.Millisecond) - - require.True(t, model.IsErrNotFound(store.DeleteBoardsAndBlocks(dbab, userID))) - - // all the entities should still exist - rBoard1, err := store.GetBoard(board1.ID) - require.NoError(t, err) - require.NotNil(t, rBoard1) - rBlock1, err := store.GetBlock(block1.ID) - require.NoError(t, err) - require.NotNil(t, rBlock1) - rBlock2, err := store.GetBlock(block2.ID) - require.NoError(t, err) - require.NotNil(t, rBlock2) - - rBoard2, err := store.GetBoard(board2.ID) - require.NoError(t, err) - require.NotNil(t, rBoard2) - rBlock3, err := store.GetBlock(block3.ID) - require.NoError(t, err) - require.NotNil(t, rBlock3) - rBlock4, err := store.GetBlock(block4.ID) - require.NoError(t, err) - require.NotNil(t, rBlock4) - }) - - t.Run("should work properly if all the entities are related", func(t *testing.T) { - newBoard1 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board1, err := store.InsertBoard(newBoard1, userID) - require.NoError(t, err) - - block1 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block1, userID)) - - block2 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board1.ID, - } - require.NoError(t, store.InsertBlock(block2, userID)) - - newBoard2 := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: model.BoardTypeOpen, - } - board2, err := store.InsertBoard(newBoard2, userID) - require.NoError(t, err) - - block3 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block3, userID)) - - block4 := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: board2.ID, - } - require.NoError(t, store.InsertBlock(block4, userID)) - - dbab := &model.DeleteBoardsAndBlocks{ - Boards: []string{board1.ID, board2.ID}, - Blocks: []string{block1.ID, block2.ID, block3.ID, block4.ID}, - } - - time.Sleep(10 * time.Millisecond) - - require.NoError(t, store.DeleteBoardsAndBlocks(dbab, userID)) - - rBoard1, err := store.GetBoard(board1.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBoard1) - rBlock1, err := store.GetBlock(block1.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBlock1) - rBlock2, err := store.GetBlock(block2.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBlock2) - - rBoard2, err := store.GetBoard(board2.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBoard2) - rBlock3, err := store.GetBlock(block3.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBlock3) - rBlock4, err := store.GetBlock(block4.ID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, rBlock4) - }) -} - -func testDuplicateBoard(t *testing.T, store store.Store) { - teamID := testTeamID - userID := testUserID - - newBab := &model.BoardsAndBlocks{ - Boards: []*model.Board{ - {ID: "board-id-1", TeamID: teamID, Type: model.BoardTypeOpen, ChannelID: "test-channel"}, - {ID: "board-id-2", TeamID: teamID, Type: model.BoardTypePrivate}, - {ID: "board-id-3", TeamID: teamID, Type: model.BoardTypeOpen}, - }, - Blocks: []*model.Block{ - {ID: "block-id-1", BoardID: "board-id-1", Type: model.TypeCard}, - {ID: "block-id-1a", BoardID: "board-id-1", Type: model.TypeComment}, - {ID: "block-id-2", BoardID: "board-id-2", Type: model.TypeCard}, - }, - } - - bab, err := store.CreateBoardsAndBlocks(newBab, userID) - require.NoError(t, err) - require.NotNil(t, bab) - require.Len(t, bab.Boards, 3) - require.Len(t, bab.Blocks, 3) - - t.Run("duplicate existing board as no template", func(t *testing.T) { - bab, members, err := store.DuplicateBoard("board-id-1", userID, teamID, false) - require.NoError(t, err) - require.Len(t, members, 1) - require.Len(t, bab.Boards, 1) - require.Len(t, bab.Blocks, 1) - require.Equal(t, bab.Boards[0].IsTemplate, false) - require.Equal(t, "", bab.Boards[0].ChannelID) - }) - - t.Run("duplicate existing board as template", func(t *testing.T) { - bab, members, err := store.DuplicateBoard("board-id-1", userID, teamID, true) - require.NoError(t, err) - require.Len(t, members, 1) - require.Len(t, bab.Boards, 1) - require.Len(t, bab.Blocks, 1) - require.Equal(t, bab.Boards[0].IsTemplate, true) - require.Equal(t, "", bab.Boards[0].ChannelID) - }) - - t.Run("duplicate not existing board", func(t *testing.T) { - bab, members, err := store.DuplicateBoard("not-existing-id", userID, teamID, false) - require.Error(t, err) - require.Nil(t, members) - require.Nil(t, bab) - }) -} diff --git a/server/boards/services/store/storetests/category.go b/server/boards/services/store/storetests/category.go deleted file mode 100644 index a9224e5932..0000000000 --- a/server/boards/services/store/storetests/category.go +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -type testFunc func(t *testing.T, store store.Store) - -func StoreTestCategoryStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - tests := map[string]testFunc{ - "CreateCategory": testGetCreateCategory, - "UpdateCategory": testUpdateCategory, - "DeleteCategory": testDeleteCategory, - "GetUserCategories": testGetUserCategories, - "ReorderCategories": testReorderCategories, - "ReorderCategoriesBoards": testReorderCategoryBoards, - } - - for name, f := range tests { - t.Run(name, func(t *testing.T) { - runStoreTests(t, f) - }) - } -} - -func testGetCreateCategory(t *testing.T, store store.Store) { - t.Run("save uncollapsed category", func(t *testing.T) { - now := utils.GetMillis() - category := model.Category{ - ID: "category_id_1", - Name: "Category", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - - err := store.CreateCategory(category) - assert.NoError(t, err) - - createdCategory, err := store.GetCategory("category_id_1") - assert.NoError(t, err) - assert.Equal(t, "Category", createdCategory.Name) - assert.Equal(t, "user_id_1", createdCategory.UserID) - assert.Equal(t, "team_id_1", createdCategory.TeamID) - assert.Equal(t, false, createdCategory.Collapsed) - }) - - t.Run("save collapsed category", func(t *testing.T) { - now := utils.GetMillis() - category := model.Category{ - ID: "category_id_2", - Name: "Category", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: true, - } - - err := store.CreateCategory(category) - assert.NoError(t, err) - - createdCategory, err := store.GetCategory("category_id_2") - assert.NoError(t, err) - assert.Equal(t, "Category", createdCategory.Name) - assert.Equal(t, "user_id_1", createdCategory.UserID) - assert.Equal(t, "team_id_1", createdCategory.TeamID) - assert.Equal(t, true, createdCategory.Collapsed) - }) - - t.Run("get nonexistent category", func(t *testing.T) { - category, err := store.GetCategory("nonexistent") - assert.Error(t, err) - var nf *model.ErrNotFound - assert.ErrorAs(t, err, &nf) - assert.Nil(t, category) - }) -} - -func testUpdateCategory(t *testing.T, store store.Store) { - now := utils.GetMillis() - category := model.Category{ - ID: "category_id_1", - Name: "Category 1", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - - err := store.CreateCategory(category) - assert.NoError(t, err) - - updateNow := utils.GetMillis() - updatedCategory := model.Category{ - ID: "category_id_1", - Name: "Category 1 New", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: updateNow, - DeleteAt: 0, - Collapsed: true, - } - - err = store.UpdateCategory(updatedCategory) - assert.NoError(t, err) - - fetchedCategory, err := store.GetCategory("category_id_1") - assert.NoError(t, err) - assert.Equal(t, "category_id_1", fetchedCategory.ID) - assert.Equal(t, "Category 1 New", fetchedCategory.Name) - assert.Equal(t, true, fetchedCategory.Collapsed) - - // now lets try to un-collapse the same category - updatedCategory.Collapsed = false - err = store.UpdateCategory(updatedCategory) - assert.NoError(t, err) - - fetchedCategory, err = store.GetCategory("category_id_1") - assert.NoError(t, err) - assert.Equal(t, "category_id_1", fetchedCategory.ID) - assert.Equal(t, "Category 1 New", fetchedCategory.Name) - assert.Equal(t, false, fetchedCategory.Collapsed) -} - -func testDeleteCategory(t *testing.T, store store.Store) { - now := utils.GetMillis() - category := model.Category{ - ID: "category_id_1", - Name: "Category 1", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - - err := store.CreateCategory(category) - assert.NoError(t, err) - - err = store.DeleteCategory("category_id_1", "user_id_1", "team_id_1") - assert.NoError(t, err) - - deletedCategory, err := store.GetCategory("category_id_1") - assert.NoError(t, err) - assert.Equal(t, "category_id_1", deletedCategory.ID) - assert.Equal(t, "Category 1", deletedCategory.Name) - assert.Equal(t, false, deletedCategory.Collapsed) - assert.Greater(t, deletedCategory.DeleteAt, int64(0)) -} - -func testGetUserCategories(t *testing.T, store store.Store) { - now := utils.GetMillis() - category1 := model.Category{ - ID: "category_id_1", - Name: "Category 1", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err := store.CreateCategory(category1) - assert.NoError(t, err) - - category2 := model.Category{ - ID: "category_id_2", - Name: "Category 2", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err = store.CreateCategory(category2) - assert.NoError(t, err) - - category3 := model.Category{ - ID: "category_id_3", - Name: "Category 2", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err = store.CreateCategory(category3) - assert.NoError(t, err) - - userCategories, err := store.GetUserCategoryBoards("user_id_1", "team_id_1") - assert.NoError(t, err) - assert.Equal(t, 3, len(userCategories)) -} - -func testReorderCategories(t *testing.T, store store.Store) { - // setup - err := store.CreateCategory(model.Category{ - ID: "category_id_1", - Name: "Category 1", - Type: "custom", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - err = store.CreateCategory(model.Category{ - ID: "category_id_2", - Name: "Category 2", - Type: "custom", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - err = store.CreateCategory(model.Category{ - ID: "category_id_3", - Name: "Category 3", - Type: "custom", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - // verify the current order - categories, err := store.GetUserCategories("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 3, len(categories)) - - // the categories should show up in reverse insertion order (latest one first) - assert.Equal(t, "category_id_3", categories[0].ID) - assert.Equal(t, "category_id_2", categories[1].ID) - assert.Equal(t, "category_id_1", categories[2].ID) - - // re-ordering categories normally - _, err = store.ReorderCategories("user_id", "team_id", []string{ - "category_id_2", - "category_id_3", - "category_id_1", - }) - assert.NoError(t, err) - - // verify the board order - categories, err = store.GetUserCategories("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 3, len(categories)) - assert.Equal(t, "category_id_2", categories[0].ID) - assert.Equal(t, "category_id_3", categories[1].ID) - assert.Equal(t, "category_id_1", categories[2].ID) - - // lets try specifying a non existing category ID. - // It shouldn't cause any problem - _, err = store.ReorderCategories("user_id", "team_id", []string{ - "category_id_1", - "category_id_2", - "category_id_3", - "non-existing-category-id", - }) - assert.NoError(t, err) - - categories, err = store.GetUserCategories("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 3, len(categories)) - assert.Equal(t, "category_id_1", categories[0].ID) - assert.Equal(t, "category_id_2", categories[1].ID) - assert.Equal(t, "category_id_3", categories[2].ID) -} - -func testReorderCategoryBoards(t *testing.T, store store.Store) { - // setup - err := store.CreateCategory(model.Category{ - ID: "category_id_1", - Name: "Category 1", - Type: "custom", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - err = store.AddUpdateCategoryBoard("user_id", "category_id_1", []string{ - "board_id_1", - "board_id_2", - "board_id_3", - "board_id_4", - }) - assert.NoError(t, err) - - // verify current order - categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_4", Hidden: false}) - - // reordering - newOrder, err := store.ReorderCategoryBoards("category_id_1", []string{ - "board_id_3", - "board_id_1", - "board_id_2", - "board_id_4", - }) - assert.NoError(t, err) - assert.Equal(t, "board_id_3", newOrder[0]) - assert.Equal(t, "board_id_1", newOrder[1]) - assert.Equal(t, "board_id_2", newOrder[2]) - assert.Equal(t, "board_id_4", newOrder[3]) - - // verify new order - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata)) - assert.Equal(t, "board_id_3", categoryBoards[0].BoardMetadata[0].BoardID) - assert.Equal(t, "board_id_1", categoryBoards[0].BoardMetadata[1].BoardID) - assert.Equal(t, "board_id_2", categoryBoards[0].BoardMetadata[2].BoardID) - assert.Equal(t, "board_id_4", categoryBoards[0].BoardMetadata[3].BoardID) -} diff --git a/server/boards/services/store/storetests/categoryBoards.go b/server/boards/services/store/storetests/categoryBoards.go deleted file mode 100644 index c8ad46e4ac..0000000000 --- a/server/boards/services/store/storetests/categoryBoards.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func StoreTestCategoryBoardsStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetUserCategoryBoards", func(t *testing.T) { - runStoreTests(t, testGetUserCategoryBoards) - }) - t.Run("AddUpdateCategoryBoard", func(t *testing.T) { - runStoreTests(t, testAddUpdateCategoryBoard) - }) - t.Run("SetBoardVisibility", func(t *testing.T) { - runStoreTests(t, testSetBoardVisibility) - }) -} - -func testGetUserCategoryBoards(t *testing.T, store store.Store) { - now := utils.GetMillis() - category1 := model.Category{ - ID: "category_id_1", - Name: "Category 1", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err := store.CreateCategory(category1) - assert.NoError(t, err) - - category2 := model.Category{ - ID: "category_id_2", - Name: "Category 2", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err = store.CreateCategory(category2) - assert.NoError(t, err) - - category3 := model.Category{ - ID: "category_id_3", - Name: "Category 3", - UserID: "user_id_1", - TeamID: "team_id_1", - CreateAt: now, - UpdateAt: now, - DeleteAt: 0, - Collapsed: false, - } - err = store.CreateCategory(category3) - assert.NoError(t, err) - - // Adding Board 1 and Board 2 to Category 1 - // The boards don't need to exists in DB for this test - err = store.AddUpdateCategoryBoard("user_id_1", "category_id_1", []string{"board_1"}) - assert.NoError(t, err) - - err = store.AddUpdateCategoryBoard("user_id_1", "category_id_1", []string{"board_2"}) - assert.NoError(t, err) - - // Adding Board 3 to Category 2 - err = store.AddUpdateCategoryBoard("user_id_1", "category_id_2", []string{"board_3"}) - assert.NoError(t, err) - - // we'll leave category 3 empty - - userCategoryBoards, err := store.GetUserCategoryBoards("user_id_1", "team_id_1") - assert.NoError(t, err) - - // we created 3 categories for the user - assert.Equal(t, 3, len(userCategoryBoards)) - - var category1BoardCategory model.CategoryBoards - var category2BoardCategory model.CategoryBoards - var category3BoardCategory model.CategoryBoards - - for i := range userCategoryBoards { - switch userCategoryBoards[i].ID { - case "category_id_1": - category1BoardCategory = userCategoryBoards[i] - case "category_id_2": - category2BoardCategory = userCategoryBoards[i] - case "category_id_3": - category3BoardCategory = userCategoryBoards[i] - } - } - - assert.NotEmpty(t, category1BoardCategory) - assert.Equal(t, 2, len(category1BoardCategory.BoardMetadata)) - - assert.NotEmpty(t, category1BoardCategory) - assert.Equal(t, 1, len(category2BoardCategory.BoardMetadata)) - - assert.NotEmpty(t, category1BoardCategory) - assert.Equal(t, 0, len(category3BoardCategory.BoardMetadata)) - - t.Run("get empty category boards", func(t *testing.T) { - userCategoryBoards, err := store.GetUserCategoryBoards("nonexistent-user-id", "nonexistent-team-id") - assert.NoError(t, err) - assert.Empty(t, userCategoryBoards) - }) -} - -func testAddUpdateCategoryBoard(t *testing.T, store store.Store) { - // creating few boards and categories to later associoate with the category - _, _, err := store.CreateBoardsAndBlocksWithAdmin(&model.BoardsAndBlocks{ - Boards: []*model.Board{ - { - ID: "board_id_1", - TeamID: "team_id", - }, - { - ID: "board_id_2", - TeamID: "team_id", - }, - }, - }, "user_id") - assert.NoError(t, err) - - err = store.CreateCategory(model.Category{ - ID: "category_id", - Name: "Category", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - // adding a few boards to the category - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_2"}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 2, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_1", Hidden: false}) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_2", Hidden: false}) - - // adding new boards to the same category - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_3"}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_3", Hidden: false}) - - // passing empty array - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 3, len(categoryBoards[0].BoardMetadata)) - - // passing duplicate data in input - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_4", "board_id_4"}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_4", Hidden: false}) - - // adding already added board - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_2"}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 4, len(categoryBoards[0].BoardMetadata)) - - // passing already added board along with a new board - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1", "board_id_5"}) - assert.NoError(t, err) - - // verify inserted data - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 5, len(categoryBoards[0].BoardMetadata)) - assert.Contains(t, categoryBoards[0].BoardMetadata, model.CategoryBoardMetadata{BoardID: "board_id_5", Hidden: false}) -} - -func testSetBoardVisibility(t *testing.T, store store.Store) { - _, _, err := store.CreateBoardsAndBlocksWithAdmin(&model.BoardsAndBlocks{ - Boards: []*model.Board{ - { - ID: "board_id_1", - TeamID: "team_id", - }, - }, - }, "user_id") - assert.NoError(t, err) - - err = store.CreateCategory(model.Category{ - ID: "category_id", - Name: "Category", - UserID: "user_id", - TeamID: "team_id", - }) - assert.NoError(t, err) - - // adding a few boards to the category - err = store.AddUpdateCategoryBoard("user_id", "category_id", []string{"board_id_1"}) - assert.NoError(t, err) - - err = store.SetBoardVisibility("user_id", "category_id", "board_id_1", true) - assert.NoError(t, err) - - // verify set visibility - categoryBoards, err := store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.Equal(t, 1, len(categoryBoards)) - assert.Equal(t, "category_id", categoryBoards[0].ID) - assert.Equal(t, 1, len(categoryBoards[0].BoardMetadata)) - assert.False(t, categoryBoards[0].BoardMetadata[0].Hidden) - - err = store.SetBoardVisibility("user_id", "category_id", "board_id_1", false) - assert.NoError(t, err) - - // verify set visibility - categoryBoards, err = store.GetUserCategoryBoards("user_id", "team_id") - assert.NoError(t, err) - assert.True(t, categoryBoards[0].BoardMetadata[0].Hidden) -} diff --git a/server/boards/services/store/storetests/cloud.go b/server/boards/services/store/storetests/cloud.go deleted file mode 100644 index 148cef3992..0000000000 --- a/server/boards/services/store/storetests/cloud.go +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - storeservice "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func StoreTestCloudStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetUsedCardsCount", func(t *testing.T) { - runStoreTests(t, testGetUsedCardsCount) - }) - t.Run("TestGetCardLimitTimestamp", func(t *testing.T) { - runStoreTests(t, testGetCardLimitTimestamp) - }) - t.Run("TestUpdateCardLimitTimestamp", func(t *testing.T) { - runStoreTests(t, testUpdateCardLimitTimestamp) - }) -} - -func testGetUsedCardsCount(t *testing.T, store storeservice.Store) { - userID := "user-id" - - t.Run("should return zero when no cards have been created", func(t *testing.T) { - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Zero(t, count) - }) - - t.Run("should correctly return the cards of all boards", func(t *testing.T) { - // two boards - for _, boardID := range []string{"board1", "board2"} { - boardType := model.BoardTypeOpen - if boardID == "board2" { - boardType = model.BoardTypePrivate - } - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: boardType, - } - - _, err := store.InsertBoard(board, userID) - require.NoError(t, err) - } - - // board 1 has three cards - for _, cardID := range []string{"card1", "card2", "card3"} { - card := &model.Block{ - ID: cardID, - ParentID: "board1", - BoardID: "board1", - Type: model.TypeCard, - } - require.NoError(t, store.InsertBlock(card, userID)) - } - - // board 2 has two cards - for _, cardID := range []string{"card4", "card5"} { - card := &model.Block{ - ID: cardID, - ParentID: "board2", - BoardID: "board2", - Type: model.TypeCard, - } - require.NoError(t, store.InsertBlock(card, userID)) - } - - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Equal(t, 5, count) - }) - - t.Run("should not take into account content blocks", func(t *testing.T) { - // we add a couple of content blocks - text := &model.Block{ - ID: "text-id", - ParentID: "card1", - BoardID: "board1", - Type: model.TypeText, - } - require.NoError(t, store.InsertBlock(text, userID)) - - view := &model.Block{ - ID: "view-id", - ParentID: "board1", - BoardID: "board1", - Type: model.TypeView, - } - require.NoError(t, store.InsertBlock(view, userID)) - - // and count should not change - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Equal(t, 5, count) - }) - - t.Run("should not take into account cards belonging to templates", func(t *testing.T) { - // we add a template with cards - templateID := "template-id" - boardTemplate := &model.Block{ - ID: templateID, - BoardID: templateID, - Type: model.TypeBoard, - Fields: map[string]interface{}{ - "isTemplate": true, - }, - } - require.NoError(t, store.InsertBlock(boardTemplate, userID)) - - for _, cardID := range []string{"card6", "card7", "card8"} { - card := &model.Block{ - ID: cardID, - ParentID: templateID, - BoardID: templateID, - Type: model.TypeCard, - } - require.NoError(t, store.InsertBlock(card, userID)) - } - - // and count should still be the same - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Equal(t, 5, count) - }) - - t.Run("should not take into account deleted cards", func(t *testing.T) { - // we create a ninth card on the first board - card9 := &model.Block{ - ID: "card9", - ParentID: "board1", - BoardID: "board1", - Type: model.TypeCard, - DeleteAt: utils.GetMillis(), - } - require.NoError(t, store.InsertBlock(card9, userID)) - - // and count should still be the same - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Equal(t, 5, count) - }) - - t.Run("should not take into account cards from deleted boards", func(t *testing.T) { - require.NoError(t, store.DeleteBoard("board2", "user-id")) - - count, err := store.GetUsedCardsCount() - require.NoError(t, err) - require.Equal(t, 3, count) - }) -} - -func testGetCardLimitTimestamp(t *testing.T, store storeservice.Store) { - t.Run("should return 0 if there is no entry in the database", func(t *testing.T) { - rawValue, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey) - require.NoError(t, err) - require.Equal(t, "", rawValue) - - cardLimitTimestamp, err := store.GetCardLimitTimestamp() - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - }) - - t.Run("should return an int64 representation of the value", func(t *testing.T) { - require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "1234")) - - cardLimitTimestamp, err := store.GetCardLimitTimestamp() - require.NoError(t, err) - require.Equal(t, int64(1234), cardLimitTimestamp) - }) - - t.Run("should return an invalid value error if the value is not a number", func(t *testing.T) { - require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "abc")) - - cardLimitTimestamp, err := store.GetCardLimitTimestamp() - require.ErrorContains(t, err, "card limit value is invalid") - require.Zero(t, cardLimitTimestamp) - }) -} - -func testUpdateCardLimitTimestamp(t *testing.T, store storeservice.Store) { - userID := "user-id" - - // two boards - for _, boardID := range []string{"board1", "board2"} { - boardType := model.BoardTypeOpen - if boardID == "board2" { - boardType = model.BoardTypePrivate - } - - board := &model.Board{ - ID: boardID, - TeamID: testTeamID, - Type: boardType, - } - - _, err := store.InsertBoard(board, userID) - require.NoError(t, err) - } - - // board 1 has five cards - for _, cardID := range []string{"card1", "card2", "card3", "card4", "card5"} { - card := &model.Block{ - ID: cardID, - ParentID: "board1", - BoardID: "board1", - Type: model.TypeCard, - } - require.NoError(t, store.InsertBlock(card, userID)) - time.Sleep(10 * time.Millisecond) - } - - // board 2 has five cards - for _, cardID := range []string{"card6", "card7", "card8", "card9", "card10"} { - card := &model.Block{ - ID: cardID, - ParentID: "board2", - BoardID: "board2", - Type: model.TypeCard, - } - require.NoError(t, store.InsertBlock(card, userID)) - time.Sleep(10 * time.Millisecond) - } - - t.Run("should set the timestamp to zero if the card limit is zero", func(t *testing.T) { - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0) - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - - cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey) - require.NoError(t, err) - require.Equal(t, "0", cardLimitTimestampStr) - }) - - t.Run("should correctly modify the limit several times in a row", func(t *testing.T) { - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0) - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - - cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(10) - require.NoError(t, err) - require.NotZero(t, cardLimitTimestamp) - - cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey) - require.NoError(t, err) - require.NotEqual(t, "0", cardLimitTimestampStr) - - cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(0) - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - - cardLimitTimestampStr, err = store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey) - require.NoError(t, err) - require.Equal(t, "0", cardLimitTimestampStr) - }) - - t.Run("should set the correct timestamp", func(t *testing.T) { - t.Run("limit 10", func(t *testing.T) { - // we fetch the first block - card1, err := store.GetBlock("card1") - require.NoError(t, err) - - // and assert that if the limit is 10, the stored - // timestamp corresponds to the card's update_at - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10) - require.NoError(t, err) - require.Equal(t, card1.UpdateAt, cardLimitTimestamp) - }) - - t.Run("limit 5", func(t *testing.T) { - // if the limit is 5, the timestamp should be the one from - // the sixth card (the first five are older and out of the - card6, err := store.GetBlock("card6") - require.NoError(t, err) - - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(5) - require.NoError(t, err) - require.Equal(t, card6.UpdateAt, cardLimitTimestamp) - }) - - t.Run("limit should be zero if we have less cards than the limit", func(t *testing.T) { - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(100) - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - }) - - t.Run("we update the first inserted card and assert that with limit 1 that's the limit that is set", func(t *testing.T) { - time.Sleep(10 * time.Millisecond) - card1, err := store.GetBlock("card1") - require.NoError(t, err) - - card1.Title = "New title" - require.NoError(t, store.InsertBlock(card1, userID)) - - newCard1, err := store.GetBlock("card1") - require.NoError(t, err) - - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(1) - require.NoError(t, err) - require.Equal(t, newCard1.UpdateAt, cardLimitTimestamp) - }) - - t.Run("limit should stop applying if we remove the last card", func(t *testing.T) { - initialCardLimitTimestamp, err := store.GetCardLimitTimestamp() - require.NoError(t, err) - require.NotZero(t, initialCardLimitTimestamp) - - time.Sleep(10 * time.Millisecond) - require.NoError(t, store.DeleteBlock("card1", userID)) - - cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10) - require.NoError(t, err) - require.Zero(t, cardLimitTimestamp) - }) - }) -} diff --git a/server/boards/services/store/storetests/compliance.go b/server/boards/services/store/storetests/compliance.go deleted file mode 100644 index 284111e683..0000000000 --- a/server/boards/services/store/storetests/compliance.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "math" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func StoreTestComplianceHistoryStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetBoardsForCompliance", func(t *testing.T) { - runStoreTests(t, testGetBoardsForCompliance) - }) - t.Run("GetBoardsComplianceHistory", func(t *testing.T) { - runStoreTests(t, testGetBoardsComplianceHistory) - }) - t.Run("GetBlocksComplianceHistory", func(t *testing.T) { - runStoreTests(t, testGetBlocksComplianceHistory) - }) -} - -func testGetBoardsForCompliance(t *testing.T, store store.Store) { - team1 := testTeamID - team2 := utils.NewID(utils.IDTypeTeam) - - boardsAdded1 := createTestBoards(t, store, team1, testUserID, 10) - boardsAdded2 := createTestBoards(t, store, team2, testUserID, 7) - - deleteTestBoard(t, store, boardsAdded1[0].ID, testUserID) - deleteTestBoard(t, store, boardsAdded1[1].ID, testUserID) - boardsAdded1 = boardsAdded1[2:] - - t.Run("Invalid teamID", func(t *testing.T) { - opts := model.QueryBoardsForComplianceOptions{ - TeamID: utils.NewID(utils.IDTypeTeam), - } - - boards, hasMore, err := store.GetBoardsForCompliance(opts) - - assert.Empty(t, boards) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("All teams", func(t *testing.T) { - opts := model.QueryBoardsForComplianceOptions{} - - boards, hasMore, err := store.GetBoardsForCompliance(opts) - - assert.ElementsMatch(t, extractIDs(t, boards), extractIDs(t, boardsAdded1, boardsAdded2)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Specific team", func(t *testing.T) { - opts := model.QueryBoardsForComplianceOptions{ - TeamID: team1, - } - - boards, hasMore, err := store.GetBoardsForCompliance(opts) - - assert.ElementsMatch(t, extractIDs(t, boards), extractIDs(t, boardsAdded1)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Pagination", func(t *testing.T) { - opts := model.QueryBoardsForComplianceOptions{ - Page: 0, - PerPage: 3, - } - - reps := 0 - allBoards := make([]*model.Board, 0, 20) - - for { - boards, hasMore, err := store.GetBoardsForCompliance(opts) - require.NoError(t, err) - require.NotEmpty(t, boards) - allBoards = append(allBoards, boards...) - - if !hasMore { - break - } - opts.Page++ - reps++ - } - - assert.ElementsMatch(t, extractIDs(t, allBoards), extractIDs(t, boardsAdded1, boardsAdded2)) - }) -} - -func testGetBoardsComplianceHistory(t *testing.T, store store.Store) { - team1 := testTeamID - team2 := utils.NewID(utils.IDTypeTeam) - - boardsTeam1 := createTestBoards(t, store, team1, testUserID, 11) - boardsTeam2 := createTestBoards(t, store, team2, testUserID, 7) - boardsAdded := make([]*model.Board, 0) - boardsAdded = append(boardsAdded, boardsTeam1...) - boardsAdded = append(boardsAdded, boardsTeam2...) - - deleteTestBoard(t, store, boardsTeam1[0].ID, testUserID) - deleteTestBoard(t, store, boardsTeam1[1].ID, testUserID) - boardsDeleted := boardsTeam1[0:2] - boardsTeam1 = boardsTeam1[2:] - - t.Log("boardsTeam1: ", extractIDs(t, boardsTeam1)) - t.Log("boardsTeam2: ", extractIDs(t, boardsTeam2)) - t.Log("boardsAdded: ", extractIDs(t, boardsAdded)) - t.Log("boardsDeleted: ", extractIDs(t, boardsDeleted)) - - t.Run("Invalid teamID", func(t *testing.T) { - opts := model.QueryBoardsComplianceHistoryOptions{ - TeamID: utils.NewID(utils.IDTypeTeam), - } - - boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts) - - assert.Empty(t, boardHistories) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("All teams, include deleted", func(t *testing.T) { - opts := model.QueryBoardsComplianceHistoryOptions{ - IncludeDeleted: true, - } - - boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts) - - // boardHistories should contain a record for each board added, plus a record for the 2 deleted. - assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsAdded, boardsDeleted)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("All teams, exclude deleted", func(t *testing.T) { - opts := model.QueryBoardsComplianceHistoryOptions{ - IncludeDeleted: false, - } - - boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts) - - // boardHistories should contain a record for each board added, minus the two deleted. - assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsTeam1, boardsTeam2)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Specific team", func(t *testing.T) { - opts := model.QueryBoardsComplianceHistoryOptions{ - TeamID: team1, - } - - boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts) - - assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsTeam1)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Pagination", func(t *testing.T) { - opts := model.QueryBoardsComplianceHistoryOptions{ - Page: 0, - PerPage: 3, - } - - reps := 0 - allHistories := make([]*model.BoardHistory, 0) - - for { - reps++ - boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts) - require.NoError(t, err) - require.NotEmpty(t, boardHistories) - allHistories = append(allHistories, boardHistories...) - - if !hasMore { - break - } - opts.Page++ - } - - assert.ElementsMatch(t, extractIDs(t, allHistories), extractIDs(t, boardsTeam1, boardsTeam2)) - expectedCount := len(boardsTeam1) + len(boardsTeam2) - assert.Equal(t, math.Floor(float64(expectedCount/opts.PerPage)+1), float64(reps)) - }) -} - -func testGetBlocksComplianceHistory(t *testing.T, store store.Store) { - team1 := testTeamID - team2 := utils.NewID(utils.IDTypeTeam) - - boardsTeam1 := createTestBoards(t, store, team1, testUserID, 3) - boardsTeam2 := createTestBoards(t, store, team2, testUserID, 1) - - // add cards (13 in total) - cards1Team1 := createTestCards(t, store, testUserID, boardsTeam1[0].ID, 3) - cards2Team1 := createTestCards(t, store, testUserID, boardsTeam1[1].ID, 5) - cards3Team1 := createTestCards(t, store, testUserID, boardsTeam1[2].ID, 2) - cards1Team2 := createTestCards(t, store, testUserID, boardsTeam2[0].ID, 3) - - deleteTestBoard(t, store, boardsTeam1[0].ID, testUserID) - cardsDeleted := cards1Team1 - - t.Run("Invalid teamID", func(t *testing.T) { - opts := model.QueryBlocksComplianceHistoryOptions{ - TeamID: utils.NewID(utils.IDTypeTeam), - } - - boards, hasMore, err := store.GetBlocksComplianceHistory(opts) - - assert.Empty(t, boards) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("All teams, include deleted", func(t *testing.T) { - opts := model.QueryBlocksComplianceHistoryOptions{ - IncludeDeleted: true, - } - - blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts) - - // blockHistories should have records for all cards added, plus all cards deleted - assert.ElementsMatch(t, extractIDs(t, blockHistories, nil), - extractIDs(t, cards1Team1, cards2Team1, cards3Team1, cards1Team2, cardsDeleted)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("All teams, exclude deleted", func(t *testing.T) { - opts := model.QueryBlocksComplianceHistoryOptions{} - - blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts) - - // blockHistories should have records for all cards added that have not been deleted - assert.ElementsMatch(t, extractIDs(t, blockHistories, nil), - extractIDs(t, cards2Team1, cards3Team1, cards1Team2)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Specific team", func(t *testing.T) { - opts := model.QueryBlocksComplianceHistoryOptions{ - TeamID: team1, - } - - blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts) - - assert.ElementsMatch(t, extractIDs(t, blockHistories), extractIDs(t, cards2Team1, cards3Team1)) - assert.False(t, hasMore) - assert.NoError(t, err) - }) - - t.Run("Pagination", func(t *testing.T) { - opts := model.QueryBlocksComplianceHistoryOptions{ - Page: 0, - PerPage: 3, - } - - reps := 0 - allHistories := make([]*model.BlockHistory, 0) - - for { - reps++ - blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts) - require.NoError(t, err) - require.NotEmpty(t, blockHistories) - allHistories = append(allHistories, blockHistories...) - - if !hasMore { - break - } - opts.Page++ - } - - assert.ElementsMatch(t, extractIDs(t, allHistories), extractIDs(t, cards2Team1, cards3Team1, cards1Team2)) - - expectedCount := len(cards2Team1) + len(cards3Team1) + len(cards1Team2) - assert.Equal(t, math.Floor(float64(expectedCount/opts.PerPage)+1), float64(reps)) - }) -} diff --git a/server/boards/services/store/storetests/data_retention.go b/server/boards/services/store/storetests/data_retention.go deleted file mode 100644 index 05f4686f1a..0000000000 --- a/server/boards/services/store/storetests/data_retention.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -package storetests - -import ( - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -const ( - boardID = "board-id-test" - categoryID = "category-id-test" -) - -func StoreTestDataRetention(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("RunDataRetention", func(t *testing.T) { - runStoreTests(t, func(t *testing.T, store store.Store) { - category := model.Category{ - ID: categoryID, - Name: "TestCategory", - UserID: testUserID, - TeamID: testTeamID, - } - err := store.CreateCategory(category) - require.NoError(t, err) - - testRunDataRetention(t, store, 0) - testRunDataRetention(t, store, 2) - testRunDataRetention(t, store, 10) - }) - }) -} - -func LoadData(t *testing.T, store store.Store) { - validBoard := model.Board{ - ID: boardID, - IsTemplate: false, - ModifiedBy: testUserID, - TeamID: testTeamID, - } - board, err := store.InsertBoard(&validBoard, testUserID) - require.NoError(t, err) - - validBlock := &model.Block{ - ID: "id-test", - BoardID: board.ID, - ModifiedBy: testUserID, - } - - validBlock2 := &model.Block{ - ID: "id-test2", - BoardID: board.ID, - ModifiedBy: testUserID, - } - validBlock3 := &model.Block{ - ID: "id-test3", - BoardID: board.ID, - ModifiedBy: testUserID, - } - - validBlock4 := &model.Block{ - ID: "id-test4", - BoardID: board.ID, - ModifiedBy: testUserID, - } - - newBlocks := []*model.Block{validBlock, validBlock2, validBlock3, validBlock4} - - err = store.InsertBlocks(newBlocks, testUserID) - require.NoError(t, err) - - member := &model.BoardMember{ - UserID: testUserID, - BoardID: boardID, - SchemeAdmin: true, - } - _, err = store.SaveMember(member) - require.NoError(t, err) - - sharing := model.Sharing{ - ID: boardID, - Enabled: true, - Token: "testToken", - } - err = store.UpsertSharing(sharing) - require.NoError(t, err) - - err = store.AddUpdateCategoryBoard(testUserID, categoryID, []string{boardID}) - require.NoError(t, err) -} - -func testRunDataRetention(t *testing.T, store store.Store, batchSize int) { - LoadData(t, store) - - blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, err) - require.Len(t, blocks, 4) - initialCount := len(blocks) - - t.Run("test no deletions", func(t *testing.T) { - deletions, err := store.RunDataRetention(utils.GetMillisForTime(time.Now().Add(-time.Hour*1)), int64(batchSize)) - require.NoError(t, err) - require.Equal(t, int64(0), deletions) - }) - - t.Run("test all deletions", func(t *testing.T) { - deletions, err := store.RunDataRetention(utils.GetMillisForTime(time.Now().Add(time.Hour*1)), int64(batchSize)) - require.NoError(t, err) - require.True(t, deletions > int64(initialCount)) - - // expect all blocks to be deleted. - blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID}) - require.NoError(t, errBlocks) - require.Equal(t, 0, len(blocks)) - - // GetMemberForBoard throws error on now rows found - member, err := store.GetMemberForBoard(boardID, testUserID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err), err) - require.Nil(t, member) - - // GetSharing throws error on now rows found - sharing, err := store.GetSharing(boardID) - require.Error(t, err) - require.True(t, model.IsErrNotFound(err), err) - require.Nil(t, sharing) - - category, err := store.GetUserCategoryBoards(boardID, testTeamID) - require.NoError(t, err) - require.Empty(t, category) - }) -} diff --git a/server/boards/services/store/storetests/files.go b/server/boards/services/store/storetests/files.go deleted file mode 100644 index 67e9cf882c..0000000000 --- a/server/boards/services/store/storetests/files.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/stretchr/testify/require" -) - -func StoreTestFileStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - runStoreTests(t, func(t *testing.T, store store.Store) { - t.Run("should save and retrieve fileinfo", func(t *testing.T) { - fileInfo := &mm_model.FileInfo{ - Id: "file_info_1", - CreateAt: utils.GetMillis(), - Name: "Dunder Mifflin Sales Report 2022", - Extension: ".sales", - Size: 112233, - DeleteAt: 0, - } - - err := store.SaveFileInfo(fileInfo) - require.NoError(t, err) - - retrievedFileInfo, err := store.GetFileInfo("file_info_1") - require.NoError(t, err) - require.Equal(t, "file_info_1", retrievedFileInfo.Id) - require.Equal(t, "Dunder Mifflin Sales Report 2022", retrievedFileInfo.Name) - require.Equal(t, ".sales", retrievedFileInfo.Extension) - require.Equal(t, int64(112233), retrievedFileInfo.Size) - require.Equal(t, int64(0), retrievedFileInfo.DeleteAt) - require.False(t, retrievedFileInfo.Archived) - }) - - t.Run("should return an error on not found", func(t *testing.T) { - fileInfo, err := store.GetFileInfo("nonexistent") - require.Error(t, err) - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, fileInfo) - }) - }) -} diff --git a/server/boards/services/store/storetests/helpers.go b/server/boards/services/store/storetests/helpers.go deleted file mode 100644 index 7b5f796b4e..0000000000 --- a/server/boards/services/store/storetests/helpers.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -func InsertBlocks(t *testing.T, s store.Store, blocks []*model.Block, userID string) { - for i := range blocks { - err := s.InsertBlock(blocks[i], userID) - require.NoError(t, err) - } -} - -func DeleteBlocks(t *testing.T, s store.Store, blocks []*model.Block, modifiedBy string) { - for _, block := range blocks { - err := s.DeleteBlock(block.ID, modifiedBy) - require.NoError(t, err) - } -} - -func ContainsBlockWithID(blocks []*model.Block, blockID string) bool { - for _, block := range blocks { - if block.ID == blockID { - return true - } - } - - return false -} diff --git a/server/boards/services/store/storetests/notificationhints.go b/server/boards/services/store/storetests/notificationhints.go deleted file mode 100644 index 8354984a9c..0000000000 --- a/server/boards/services/store/storetests/notificationhints.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func StoreTestNotificationHintsStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("UpsertNotificationHint", func(t *testing.T) { - runStoreTests(t, testUpsertNotificationHint) - }) - t.Run("DeleteNotificationHint", func(t *testing.T) { - runStoreTests(t, testDeleteNotificationHint) - }) - t.Run("GetNotificationHint", func(t *testing.T) { - runStoreTests(t, testGetNotificationHint) - }) - t.Run("GetNextNotificationHint", func(t *testing.T) { - runStoreTests(t, testGetNextNotificationHint) - }) -} - -func testUpsertNotificationHint(t *testing.T, store store.Store) { - t.Run("create notification hint", func(t *testing.T) { - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: utils.NewID(utils.IDTypeUser), - } - - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - require.NoError(t, err, "upsert notification hint should not error") - assert.Equal(t, hint.BlockID, hintNew.BlockID) - assert.NoError(t, hintNew.IsValid()) - }) - - t.Run("duplicate notification hint", func(t *testing.T) { - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: utils.NewID(utils.IDTypeUser), - } - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - require.NoError(t, err, "upsert notification hint should not error") - - // sleep a short time so the notify_at timestamps won't collide - time.Sleep(time.Millisecond * 20) - - hint = &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: hintNew.BlockID, - ModifiedByID: hintNew.ModifiedByID, - } - hintDup, err := store.UpsertNotificationHint(hint, time.Second*15) - - require.NoError(t, err, "upsert notification hint should not error") - // notify_at should be updated - assert.Greater(t, hintDup.NotifyAt, hintNew.NotifyAt) - }) - - t.Run("invalid notification hint", func(t *testing.T) { - hint := &model.NotificationHint{} - - _, err := store.UpsertNotificationHint(hint, time.Second*15) - assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - - hint.BlockType = "board" - _, err = store.UpsertNotificationHint(hint, time.Second*15) - assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - - _, err = store.UpsertNotificationHint(hint, time.Second*15) - assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - - hint.ModifiedByID = utils.NewID(utils.IDTypeUser) - _, err = store.UpsertNotificationHint(hint, time.Second*15) - assert.ErrorAs(t, err, &model.ErrInvalidNotificationHint{}, "invalid notification hint should error") - - hint.BlockID = utils.NewID(utils.IDTypeBlock) - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - assert.NoError(t, err, "valid notification hint should not error") - assert.NoError(t, hintNew.IsValid(), "created notification hint should be valid") - }) -} - -func testDeleteNotificationHint(t *testing.T, store store.Store) { - t.Run("delete notification hint", func(t *testing.T) { - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: utils.NewID(utils.IDTypeUser), - } - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - require.NoError(t, err, "create notification hint should not error") - - // check the notification hint exists - hint, err = store.GetNotificationHint(hintNew.BlockID) - require.NoError(t, err, "get notification hint should not error") - assert.Equal(t, hintNew.BlockID, hint.BlockID) - assert.Equal(t, hintNew.CreateAt, hint.CreateAt) - - err = store.DeleteNotificationHint(hintNew.BlockID) - require.NoError(t, err, "delete notification hint should not error") - - // check the notification hint was deleted - hint, err = store.GetNotificationHint(hintNew.BlockID) - require.True(t, model.IsErrNotFound(err), "error should be of type store.ErrNotFound") - assert.Nil(t, hint) - }) - - t.Run("delete non-existent notification hint", func(t *testing.T) { - err := store.DeleteNotificationHint("bogus") - require.True(t, model.IsErrNotFound(err), "error should be of type store.ErrNotFound") - }) -} - -func testGetNotificationHint(t *testing.T, store store.Store) { - t.Run("get notification hint", func(t *testing.T) { - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: utils.NewID(utils.IDTypeUser), - } - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - require.NoError(t, err, "create notification hint should not error") - - // make sure notification hint can be fetched - hint, err = store.GetNotificationHint(hintNew.BlockID) - require.NoError(t, err, "get notification hint should not error") - assert.Equal(t, hintNew, hint) - }) - - t.Run("get non-existent notification hint", func(t *testing.T) { - hint, err := store.GetNotificationHint("bogus") - require.True(t, model.IsErrNotFound(err), "error should be of type store.ErrNotFound") - assert.Nil(t, hint, "hint should be nil") - }) -} - -func testGetNextNotificationHint(t *testing.T, store store.Store) { - t.Run("get next notification hint", func(t *testing.T) { - const loops = 5 - ids := [5]string{} - modifiedBy := utils.NewID(utils.IDTypeUser) - - // create some hints with unique notifyAt - for i := 0; i < loops; i++ { - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: modifiedBy, - } - hintNew, err := store.UpsertNotificationHint(hint, time.Second*15) - require.NoError(t, err, "create notification hint should not error") - - ids[i] = hintNew.BlockID - time.Sleep(time.Millisecond * 20) // ensure next timestamp is unique - } - - // check the hints come back in the right order - notifyAt := utils.GetMillisForTime(time.Now().Add(time.Millisecond * 50)) - for i := 0; i < loops; i++ { - hint, err := store.GetNextNotificationHint(false) - require.NoError(t, err, "get next notification hint should not error") - require.NotNil(t, hint, "get next notification hint should not return nil") - assert.Equal(t, ids[i], hint.BlockID) - assert.Less(t, notifyAt, hint.NotifyAt) - notifyAt = hint.NotifyAt - - err = store.DeleteNotificationHint(hint.BlockID) - require.NoError(t, err, "delete notification hint should not error") - } - }) - - t.Run("get next notification hint from empty table", func(t *testing.T) { - // empty the table - err := emptyNotificationHintTable(store) - require.NoError(t, err, "emptying notification hint table should not error") - - for { - hint, err2 := store.GetNextNotificationHint(false) - if model.IsErrNotFound(err2) { - break - } - require.NoError(t, err2, "get next notification hint should not error") - - err2 = store.DeleteNotificationHint(hint.BlockID) - require.NoError(t, err2, "delete notification hint should not error") - } - - _, err = store.GetNextNotificationHint(false) - require.True(t, model.IsErrNotFound(err), "error should be of type store.ErrNotFound") - }) - - t.Run("get next notification hint and remove", func(t *testing.T) { - // empty the table - err := emptyNotificationHintTable(store) - require.NoError(t, err, "emptying notification hint table should not error") - - hint := &model.NotificationHint{ - BlockType: model.TypeCard, - BlockID: utils.NewID(utils.IDTypeBlock), - ModifiedByID: utils.NewID(utils.IDTypeUser), - } - hintNew, err := store.UpsertNotificationHint(hint, time.Second*1) - require.NoError(t, err, "create notification hint should not error") - - hintDeleted, err := store.GetNextNotificationHint(true) - require.NoError(t, err, "get next notification hint should not error") - require.NotNil(t, hintDeleted, "get next notification hint should not return nil") - assert.Equal(t, hintNew.BlockID, hintDeleted.BlockID) - - // should be no hint left - _, err = store.GetNextNotificationHint(false) - require.True(t, model.IsErrNotFound(err), "error should be of type store.ErrNotFound") - }) -} - -func emptyNotificationHintTable(store store.Store) error { - for { - hint, err := store.GetNextNotificationHint(false) - if model.IsErrNotFound(err) { - break - } - - if err != nil { - return err - } - - err = store.DeleteNotificationHint(hint.BlockID) - if err != nil { - return err - } - } - return nil -} diff --git a/server/boards/services/store/storetests/session.go b/server/boards/services/store/storetests/session.go deleted file mode 100644 index a12b983309..0000000000 --- a/server/boards/services/store/storetests/session.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -func StoreTestSessionStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("CreateAndGetAndDeleteSession", func(t *testing.T) { - runStoreTests(t, testCreateAndGetAndDeleteSession) - }) - t.Run("GetActiveUserCount", func(t *testing.T) { - runStoreTests(t, testGetActiveUserCount) - }) - t.Run("UpdateSession", func(t *testing.T) { - runStoreTests(t, testUpdateSession) - }) -} - -func testCreateAndGetAndDeleteSession(t *testing.T, store store.Store) { - session := &model.Session{ - ID: "session-id", - Token: "token", - } - - t.Run("CreateAndGetSession", func(t *testing.T) { - err := store.CreateSession(session) - require.NoError(t, err) - - got, err := store.GetSession(session.Token, 60*60) - require.NoError(t, err) - require.Equal(t, session, got) - }) - - t.Run("Get nonexistent session", func(t *testing.T) { - got, err := store.GetSession("nonexistent-token", 60*60) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, got) - }) - - t.Run("DeleteAndGetSession", func(t *testing.T) { - err := store.DeleteSession(session.ID) - require.NoError(t, err) - - _, err = store.GetSession(session.Token, 60*60) - require.Error(t, err) - }) -} - -func testGetActiveUserCount(t *testing.T, store store.Store) { - t.Run("no active user", func(t *testing.T) { - count, err := store.GetActiveUserCount(60) - require.NoError(t, err) - require.Equal(t, 0, count) - }) - - t.Run("active user", func(t *testing.T) { - // gen random count active user session - count := int(time.Now().Unix() % 10) - for i := 0; i < count; i++ { - session := &model.Session{ - ID: fmt.Sprintf("id-%d", i), - UserID: fmt.Sprintf("user-id-%d", i), - Token: fmt.Sprintf("token-%d", i), - } - err := store.CreateSession(session) - require.NoError(t, err) - } - - got, err := store.GetActiveUserCount(60) - require.NoError(t, err) - require.Equal(t, count, got) - }) -} - -func testUpdateSession(t *testing.T, store store.Store) { - session := &model.Session{ - ID: "session-id", - Token: "token", - Props: map[string]interface{}{"field1": "A"}, - } - - err := store.CreateSession(session) - require.NoError(t, err) - - // update session - session.Props["field1"] = "B" - err = store.UpdateSession(session) - require.NoError(t, err) - - got, err := store.GetSession(session.Token, 60) - require.NoError(t, err) - require.Equal(t, session, got) -} diff --git a/server/boards/services/store/storetests/sharing.go b/server/boards/services/store/storetests/sharing.go deleted file mode 100644 index ace2018006..0000000000 --- a/server/boards/services/store/storetests/sharing.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -func StoreTestSharingStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("UpsertSharingAndGetSharing", func(t *testing.T) { - runStoreTests(t, testUpsertSharingAndGetSharing) - }) -} - -func testUpsertSharingAndGetSharing(t *testing.T, store store.Store) { - t.Run("Insert first sharing and get it", func(t *testing.T) { - sharing := model.Sharing{ - ID: "sharing-id", - Enabled: true, - Token: "token", - ModifiedBy: testUserID, - } - - err := store.UpsertSharing(sharing) - require.NoError(t, err) - newSharing, err := store.GetSharing("sharing-id") - require.NoError(t, err) - newSharing.UpdateAt = 0 - require.Equal(t, sharing, *newSharing) - }) - t.Run("Upsert the inserted sharing and get it", func(t *testing.T) { - sharing := model.Sharing{ - ID: "sharing-id", - Enabled: true, - Token: "token2", - ModifiedBy: "user-id2", - } - - newSharing, err := store.GetSharing("sharing-id") - require.NoError(t, err) - newSharing.UpdateAt = 0 - require.NotEqual(t, sharing, *newSharing) - - err = store.UpsertSharing(sharing) - require.NoError(t, err) - newSharing, err = store.GetSharing("sharing-id") - require.NoError(t, err) - newSharing.UpdateAt = 0 - require.Equal(t, sharing, *newSharing) - }) - t.Run("Get not existing sharing", func(t *testing.T) { - _, err := store.GetSharing("not-existing") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - }) -} diff --git a/server/boards/services/store/storetests/subscriptions.go b/server/boards/services/store/storetests/subscriptions.go deleted file mode 100644 index 9368f3e3dd..0000000000 --- a/server/boards/services/store/storetests/subscriptions.go +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -//nolint:dupl -func StoreTestSubscriptionsStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("CreateSubscription", func(t *testing.T) { - runStoreTests(t, testCreateSubscription) - }) - t.Run("DeleteSubscription", func(t *testing.T) { - runStoreTests(t, testDeleteSubscription) - }) - t.Run("UndeleteSubscription", func(t *testing.T) { - runStoreTests(t, testUndeleteSubscription) - }) - t.Run("GetSubscription", func(t *testing.T) { - runStoreTests(t, testGetSubscription) - }) - t.Run("GetSubscriptions", func(t *testing.T) { - runStoreTests(t, testGetSubscriptions) - }) - t.Run("GetSubscribersForBlock", func(t *testing.T) { - runStoreTests(t, testGetSubscribersForBlock) - }) -} - -func testCreateSubscription(t *testing.T, store store.Store) { - t.Run("create subscriptions", func(t *testing.T) { - users := createTestUsers(t, store, 10) - blocks := createTestBlocks(t, store, users[0].ID, 50) - - for i, user := range users { - for j := 0; j < i; j++ { - sub := &model.Subscription{ - BlockType: blocks[j].Type, - BlockID: blocks[j].ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - subNew, err := store.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - assert.NotZero(t, subNew.NotifiedAt) - assert.NotZero(t, subNew.CreateAt) - assert.Zero(t, subNew.DeleteAt) - } - } - - // ensure each user has the right number of subscriptions - for i, user := range users { - subs, err := store.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Len(t, subs, i) - } - }) - - t.Run("duplicate subscription", func(t *testing.T) { - admin := createTestUsers(t, store, 1)[0] - user := createTestUsers(t, store, 1)[0] - block := createTestBlocks(t, store, admin.ID, 1)[0] - - sub := &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - subNew, err := store.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - sub = &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - - subDup, err := store.CreateSubscription(sub) - require.NoError(t, err, "create duplicate subscription should not error") - - assert.Equal(t, subNew.BlockID, subDup.BlockID) - assert.Equal(t, subNew.SubscriberID, subDup.SubscriberID) - }) - - t.Run("invalid subscription", func(t *testing.T) { - admin := createTestUsers(t, store, 1)[0] - user := createTestUsers(t, store, 1)[0] - block := createTestBlocks(t, store, admin.ID, 1)[0] - - sub := &model.Subscription{} - - _, err := store.CreateSubscription(sub) - assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") - - sub.BlockType = block.Type - _, err = store.CreateSubscription(sub) - assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") - - sub.BlockID = block.ID - _, err = store.CreateSubscription(sub) - assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") - - sub.SubscriberType = "user" - _, err = store.CreateSubscription(sub) - assert.ErrorAs(t, err, &model.ErrInvalidSubscription{}, "invalid subscription should error") - - sub.SubscriberID = user.ID - subNew, err := store.CreateSubscription(sub) - assert.NoError(t, err, "valid subscription should not error") - - assert.NoError(t, subNew.IsValid(), "created subscription should be valid") - }) -} - -func testDeleteSubscription(t *testing.T, s store.Store) { - t.Run("delete subscription", func(t *testing.T) { - user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, user.ID, 1)[0] - - sub := &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - subNew, err := s.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - // check the subscription exists - subs, err := s.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Len(t, subs, 1) - assert.Equal(t, subNew.BlockID, subs[0].BlockID) - assert.Equal(t, subNew.SubscriberID, subs[0].SubscriberID) - - err = s.DeleteSubscription(block.ID, user.ID) - require.NoError(t, err, "delete subscription should not error") - - // check the subscription was deleted - subs, err = s.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Empty(t, subs) - }) - - t.Run("delete non-existent subscription", func(t *testing.T) { - err := s.DeleteSubscription("bogus", "bogus") - require.Error(t, err, "delete non-existent subscription should error") - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - }) -} - -func testUndeleteSubscription(t *testing.T, s store.Store) { - t.Run("undelete subscription", func(t *testing.T) { - user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, user.ID, 1)[0] - - sub := &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - subNew, err := s.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - // check the subscription exists - subs, err := s.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Len(t, subs, 1) - assert.Equal(t, subNew.BlockID, subs[0].BlockID) - assert.Equal(t, subNew.SubscriberID, subs[0].SubscriberID) - - err = s.DeleteSubscription(block.ID, user.ID) - require.NoError(t, err, "delete subscription should not error") - - // check the subscription was deleted - subs, err = s.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Empty(t, subs) - - // re-create the subscription - subUndeleted, err := s.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - // check the undeleted subscription exists - subs, err = s.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Len(t, subs, 1) - assert.Equal(t, subUndeleted.BlockID, subs[0].BlockID) - assert.Equal(t, subUndeleted.SubscriberID, subs[0].SubscriberID) - }) -} - -func testGetSubscription(t *testing.T, s store.Store) { - t.Run("get subscription", func(t *testing.T) { - user := createTestUsers(t, s, 1)[0] - block := createTestBlocks(t, s, user.ID, 1)[0] - - sub := &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - subNew, err := s.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - - // make sure subscription can be fetched - sub, err = s.GetSubscription(block.ID, user.ID) - require.NoError(t, err, "get subscription should not error") - assert.Equal(t, subNew, sub) - }) - - t.Run("get non-existent subscription", func(t *testing.T) { - sub, err := s.GetSubscription("bogus", "bogus") - require.Error(t, err, "get non-existent subscription should error") - require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error") - require.Nil(t, sub, "get subscription should return nil") - }) -} - -func testGetSubscriptions(t *testing.T, store store.Store) { - t.Run("get subscriptions", func(t *testing.T) { - author := createTestUsers(t, store, 1)[0] - user := createTestUsers(t, store, 1)[0] - blocks := createTestBlocks(t, store, author.ID, 50) - - for _, block := range blocks { - sub := &model.Subscription{ - BlockType: block.Type, - BlockID: block.ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - _, err := store.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - } - - // ensure user has the right number of subscriptions - subs, err := store.GetSubscriptions(user.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Len(t, subs, len(blocks)) - - // ensure author has no subscriptions - subs, err = store.GetSubscriptions(author.ID) - require.NoError(t, err, "get subscriptions should not error") - assert.Empty(t, subs) - }) - - t.Run("get subscriptions for invalid user", func(t *testing.T) { - subs, err := store.GetSubscriptions("bogus") - require.NoError(t, err, "get subscriptions should not error") - assert.Empty(t, subs) - }) -} - -func testGetSubscribersForBlock(t *testing.T, store store.Store) { - t.Run("get subscribers for block", func(t *testing.T) { - users := createTestUsers(t, store, 50) - blocks := createTestBlocks(t, store, users[0].ID, 2) - - for _, user := range users { - sub := &model.Subscription{ - BlockType: blocks[1].Type, - BlockID: blocks[1].ID, - SubscriberType: "user", - SubscriberID: user.ID, - } - _, err := store.CreateSubscription(sub) - require.NoError(t, err, "create subscription should not error") - } - - // make sure block[1] has the right number of users subscribed - subs, err := store.GetSubscribersForBlock(blocks[1].ID) - require.NoError(t, err, "get subscribers for block should not error") - assert.Len(t, subs, 50) - - count, err := store.GetSubscribersCountForBlock(blocks[1].ID) - require.NoError(t, err, "get subscribers for block should not error") - assert.Equal(t, 50, count) - - // make sure block[0] has zero users subscribed - subs, err = store.GetSubscribersForBlock(blocks[0].ID) - require.NoError(t, err, "get subscribers for block should not error") - assert.Empty(t, subs) - - count, err = store.GetSubscribersCountForBlock(blocks[0].ID) - require.NoError(t, err, "get subscribers for block should not error") - assert.Zero(t, count) - }) - - t.Run("get subscribers for invalid block", func(t *testing.T) { - subs, err := store.GetSubscribersForBlock("bogus") - require.NoError(t, err, "get subscribers for block should not error") - assert.Empty(t, subs) - }) -} diff --git a/server/boards/services/store/storetests/system.go b/server/boards/services/store/storetests/system.go deleted file mode 100644 index ce5654c45b..0000000000 --- a/server/boards/services/store/storetests/system.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -func StoreTestSystemStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("SetGetSystemSettings", func(t *testing.T) { - runStoreTests(t, testSetGetSystemSettings) - }) -} - -func testSetGetSystemSettings(t *testing.T, store store.Store) { - t.Run("Get empty settings", func(t *testing.T) { - settings, err := store.GetSystemSettings() - require.NoError(t, err) - // although data migrations would usually write some settings - // to the table on store initialization, we're cleaning all - // tables before store tests, so the initial result should - // come back empty - require.Empty(t, settings) - }) - - t.Run("Set, update and get multiple settings", func(t *testing.T) { - err := store.SetSystemSetting("test-1", "test-value-1") - require.NoError(t, err) - err = store.SetSystemSetting("test-2", "test-value-2") - require.NoError(t, err) - settings, err := store.GetSystemSettings() - require.NoError(t, err) - require.Equal(t, map[string]string{"test-1": "test-value-1", "test-2": "test-value-2"}, settings) - - err = store.SetSystemSetting("test-2", "test-value-updated-2") - require.NoError(t, err) - settings, err = store.GetSystemSettings() - require.NoError(t, err) - require.Equal(t, map[string]string{"test-1": "test-value-1", "test-2": "test-value-updated-2"}, settings) - }) - - t.Run("Get a single setting", func(t *testing.T) { - value, err := store.GetSystemSetting("test-1") - require.NoError(t, err) - require.Equal(t, "test-value-1", value) - }) -} diff --git a/server/boards/services/store/storetests/teams.go b/server/boards/services/store/storetests/teams.go deleted file mode 100644 index e25f30fdd6..0000000000 --- a/server/boards/services/store/storetests/teams.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/services/store" -) - -func StoreTestTeamStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetTeam", func(t *testing.T) { - runStoreTests(t, testGetTeam) - }) - t.Run("UpsertTeamSignupToken", func(t *testing.T) { - runStoreTests(t, testUpsertTeamSignupToken) - }) - t.Run("UpsertTeamSettings", func(t *testing.T) { - runStoreTests(t, testUpsertTeamSettings) - }) - t.Run("GetAllTeams", func(t *testing.T) { - runStoreTests(t, testGetAllTeams) - }) -} - -func testGetTeam(t *testing.T, store store.Store) { - t.Run("Nonexistent team", func(t *testing.T) { - got, err := store.GetTeam("nonexistent-id") - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - require.Nil(t, got) - }) - - t.Run("Valid team", func(t *testing.T) { - teamID := "0" - team := &model.Team{ - ID: teamID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - - err := store.UpsertTeamSignupToken(*team) - require.NoError(t, err) - - got, err := store.GetTeam(teamID) - require.NoError(t, err) - require.Equal(t, teamID, got.ID) - }) -} - -func testUpsertTeamSignupToken(t *testing.T, store store.Store) { - t.Run("Insert and update team with signup token", func(t *testing.T) { - teamID := "0" - team := &model.Team{ - ID: teamID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - - // insert - err := store.UpsertTeamSignupToken(*team) - require.NoError(t, err) - - got, err := store.GetTeam(teamID) - require.NoError(t, err) - require.Equal(t, team.ID, got.ID) - require.Equal(t, team.SignupToken, got.SignupToken) - - // update signup token - team.SignupToken = utils.NewID(utils.IDTypeToken) - err = store.UpsertTeamSignupToken(*team) - require.NoError(t, err) - - got, err = store.GetTeam(teamID) - require.NoError(t, err) - require.Equal(t, team.ID, got.ID) - require.Equal(t, team.SignupToken, got.SignupToken) - }) -} - -func testUpsertTeamSettings(t *testing.T, store store.Store) { - t.Run("Insert and update team with settings", func(t *testing.T) { - teamID := "0" - team := &model.Team{ - ID: teamID, - Settings: map[string]interface{}{ - "field1": "A", - }, - } - - // insert - err := store.UpsertTeamSettings(*team) - require.NoError(t, err) - - got, err := store.GetTeam(teamID) - require.NoError(t, err) - require.Equal(t, team.ID, got.ID) - require.Equal(t, team.Settings, got.Settings) - - // update settings - team.Settings = map[string]interface{}{ - "field1": "B", - } - err = store.UpsertTeamSettings(*team) - require.NoError(t, err) - - got2, err := store.GetTeam(teamID) - require.NoError(t, err) - require.Equal(t, team.ID, got2.ID) - require.Equal(t, team.Settings, got2.Settings) - require.Equal(t, got.SignupToken, got2.SignupToken) - }) -} - -func testGetAllTeams(t *testing.T, store store.Store) { - t.Run("No teams response", func(t *testing.T) { - got, err := store.GetAllTeams() - require.NoError(t, err) - require.Empty(t, got) - }) - - t.Run("Insert multiple team and get all teams", func(t *testing.T) { - // insert - teamCount := 10 - for i := 0; i < teamCount; i++ { - teamID := fmt.Sprintf("%d", i) - team := &model.Team{ - ID: teamID, - SignupToken: utils.NewID(utils.IDTypeToken), - } - - err := store.UpsertTeamSignupToken(*team) - require.NoError(t, err) - } - - got, err := store.GetAllTeams() - require.NoError(t, err) - require.Len(t, got, teamCount) - }) -} diff --git a/server/boards/services/store/storetests/users.go b/server/boards/services/store/storetests/users.go deleted file mode 100644 index bfb41648ef..0000000000 --- a/server/boards/services/store/storetests/users.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -//nolint:dupl -func StoreTestUserStore(t *testing.T, runStoreTests func(*testing.T, func(*testing.T, store.Store))) { - t.Run("GetUsersByTeam", func(t *testing.T) { - runStoreTests(t, testGetUsersByTeam) - }) - t.Run("CreateAndGetUser", func(t *testing.T) { - runStoreTests(t, testCreateAndGetUser) - }) - t.Run("GetUsersList", func(t *testing.T) { - runStoreTests(t, testGetUsersList) - }) - t.Run("CreateAndUpdateUser", func(t *testing.T) { - runStoreTests(t, testCreateAndUpdateUser) - }) - t.Run("CreateAndGetRegisteredUserCount", func(t *testing.T) { - runStoreTests(t, testCreateAndGetRegisteredUserCount) - }) - t.Run("TestPatchUserProps", func(t *testing.T) { - runStoreTests(t, testPatchUserProps) - }) -} - -func testGetUsersByTeam(t *testing.T, store store.Store) { - t.Run("GetTeamUsers", func(t *testing.T) { - users, err := store.GetUsersByTeam("team_1", "", false, false) - require.Equal(t, 0, len(users)) - require.NoError(t, err) - - userID := utils.NewID(utils.IDTypeUser) - - user, err := store.CreateUser(&model.User{ - ID: userID, - Username: "darth.vader", - }) - require.NoError(t, err) - require.NotNil(t, user) - require.Equal(t, userID, user.ID) - require.Equal(t, "darth.vader", user.Username) - - defer func() { - _, _ = store.UpdateUser(&model.User{ - ID: userID, - DeleteAt: utils.GetMillis(), - }) - }() - - users, err = store.GetUsersByTeam("team_1", "", false, false) - require.Equal(t, 1, len(users)) - require.Equal(t, "darth.vader", users[0].Username) - require.NoError(t, err) - }) -} - -func testCreateAndGetUser(t *testing.T, store store.Store) { - user := &model.User{ - ID: utils.NewID(utils.IDTypeUser), - Username: "damao", - Email: "mock@email.com", - } - - t.Run("CreateUser", func(t *testing.T) { - newUser, err := store.CreateUser(user) - require.NoError(t, err) - require.NotNil(t, newUser) - }) - - t.Run("GetUserByID", func(t *testing.T) { - got, err := store.GetUserByID(user.ID) - require.NoError(t, err) - require.Equal(t, user.ID, got.ID) - require.Equal(t, user.Username, got.Username) - require.Equal(t, user.Email, got.Email) - }) - - t.Run("GetUserByID nonexistent", func(t *testing.T) { - got, err := store.GetUserByID("nonexistent-id") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, got) - }) - - t.Run("GetUserByUsername", func(t *testing.T) { - got, err := store.GetUserByUsername(user.Username) - require.NoError(t, err) - require.Equal(t, user.ID, got.ID) - require.Equal(t, user.Username, got.Username) - require.Equal(t, user.Email, got.Email) - }) - - t.Run("GetUserByUsername nonexistent", func(t *testing.T) { - got, err := store.GetUserByID("nonexistent-username") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, got) - }) - - t.Run("GetUserByEmail", func(t *testing.T) { - got, err := store.GetUserByEmail(user.Email) - require.NoError(t, err) - require.Equal(t, user.ID, got.ID) - require.Equal(t, user.Username, got.Username) - require.Equal(t, user.Email, got.Email) - }) - - t.Run("GetUserByEmail nonexistent", func(t *testing.T) { - got, err := store.GetUserByID("nonexistent-email") - var nf *model.ErrNotFound - require.ErrorAs(t, err, &nf) - require.Nil(t, got) - }) -} - -func testGetUsersList(t *testing.T, store store.Store) { - for _, id := range []string{"user1", "user2"} { - user := &model.User{ - ID: id, - Username: fmt.Sprintf("%s-username", id), - Email: fmt.Sprintf("%s@sample.com", id), - } - newUser, err := store.CreateUser(user) - require.NoError(t, err) - require.NotNil(t, newUser) - } - - testCases := []struct { - Name string - UserIDs []string - ExpectedError bool - ExpectedIDs []string - }{ - { - Name: "all of the IDs are found", - UserIDs: []string{"user1", "user2"}, - ExpectedError: false, - ExpectedIDs: []string{"user1", "user2"}, - }, - { - Name: "some of the IDs are found", - UserIDs: []string{"user2", "non-existent"}, - ExpectedError: true, - ExpectedIDs: []string{"user2"}, - }, - { - Name: "none of the IDs are found", - UserIDs: []string{"non-existent-1", "non-existent-2"}, - ExpectedError: true, - ExpectedIDs: []string{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - users, err := store.GetUsersList(tc.UserIDs, false, false) - if tc.ExpectedError { - require.Error(t, err) - require.True(t, model.IsErrNotFound(err)) - } else { - require.NoError(t, err) - } - - userIDs := []string{} - for _, user := range users { - userIDs = append(userIDs, user.ID) - } - require.ElementsMatch(t, tc.ExpectedIDs, userIDs) - }) - } -} - -func testCreateAndUpdateUser(t *testing.T, store store.Store) { - user := &model.User{ - ID: utils.NewID(utils.IDTypeUser), - } - newUser, err := store.CreateUser(user) - require.NoError(t, err) - require.NotNil(t, newUser) - - t.Run("UpdateUser", func(t *testing.T) { - user.Username = "damao" - user.Email = "mock@email.com" - uUser, err := store.UpdateUser(user) - require.NoError(t, err) - require.NotNil(t, uUser) - require.Equal(t, user.Username, uUser.Username) - require.Equal(t, user.Email, uUser.Email) - - got, err := store.GetUserByID(user.ID) - require.NoError(t, err) - require.Equal(t, user.ID, got.ID) - require.Equal(t, user.Username, got.Username) - require.Equal(t, user.Email, got.Email) - }) - - t.Run("UpdateUserPassword", func(t *testing.T) { - newPassword := utils.NewID(utils.IDTypeNone) - err := store.UpdateUserPassword(user.Username, newPassword) - require.NoError(t, err) - - got, err := store.GetUserByUsername(user.Username) - require.NoError(t, err) - require.Equal(t, user.Username, got.Username) - require.Equal(t, newPassword, got.Password) - }) - - t.Run("UpdateUserPasswordByID", func(t *testing.T) { - newPassword := utils.NewID(utils.IDTypeNone) - err := store.UpdateUserPasswordByID(user.ID, newPassword) - require.NoError(t, err) - - got, err := store.GetUserByID(user.ID) - require.NoError(t, err) - require.Equal(t, user.ID, got.ID) - require.Equal(t, newPassword, got.Password) - }) -} - -func testCreateAndGetRegisteredUserCount(t *testing.T, store store.Store) { - randomN := int(time.Now().Unix() % 10) - for i := 0; i < randomN; i++ { - user, err := store.CreateUser(&model.User{ - ID: utils.NewID(utils.IDTypeUser), - }) - require.NoError(t, err) - require.NotNil(t, user) - } - - got, err := store.GetRegisteredUserCount() - require.NoError(t, err) - require.Equal(t, randomN, got) -} - -func testPatchUserProps(t *testing.T, store store.Store) { - user := &model.User{ - ID: utils.NewID(utils.IDTypeUser), - } - newUser, err := store.CreateUser(user) - require.NoError(t, err) - require.NotNil(t, newUser) - - key1 := "new_key_1" - key2 := "new_key_2" - key3 := "new_key_3" - - // Only update props - patch := model.UserPreferencesPatch{ - UpdatedFields: map[string]string{ - key1: "new_value_1", - key2: "new_value_2", - key3: "new_value_3", - }, - } - - userPreferences, err := store.PatchUserPreferences(user.ID, patch) - require.NoError(t, err) - require.Equal(t, 3, len(userPreferences)) - - for _, preference := range userPreferences { - switch preference.Name { - case key1: - require.Equal(t, "new_value_1", preference.Value) - case key2: - require.Equal(t, "new_value_2", preference.Value) - case key3: - require.Equal(t, "new_value_3", preference.Value) - } - } - - // Delete a prop - patch = model.UserPreferencesPatch{ - DeletedFields: []string{ - key1, - }, - } - - userPreferences, err = store.PatchUserPreferences(user.ID, patch) - require.NoError(t, err) - - for _, preference := range userPreferences { - switch preference.Name { - case key1: - t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it") - case key2: - require.Equal(t, "new_value_2", preference.Value) - case key3: - require.Equal(t, "new_value_3", preference.Value) - } - } - - // update and delete together - patch = model.UserPreferencesPatch{ - UpdatedFields: map[string]string{ - key3: "new_value_3_new_again", - }, - DeletedFields: []string{ - key2, - }, - } - userPreferences, err = store.PatchUserPreferences(user.ID, patch) - require.NoError(t, err) - - for _, preference := range userPreferences { - switch preference.Name { - case key1: - t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it") - case key2: - t.Errorf("new_key_2 shouldn't exist in user preference as we just deleted it") - case key3: - require.Equal(t, "new_value_3_new_again", preference.Value) - } - } -} diff --git a/server/boards/services/store/storetests/util.go b/server/boards/services/store/storetests/util.go deleted file mode 100644 index 562f0bd4ff..0000000000 --- a/server/boards/services/store/storetests/util.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetests - -import ( - "fmt" - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/store" - "github.com/mattermost/mattermost/server/v8/boards/utils" -) - -func createTestUsers(t *testing.T, store store.Store, num int) []*model.User { - var users []*model.User - for i := 0; i < num; i++ { - user := &model.User{ - ID: utils.NewID(utils.IDTypeUser), - Username: fmt.Sprintf("mooncake.%d", i), - Email: fmt.Sprintf("mooncake.%d@example.com", i), - } - newUser, err := store.CreateUser(user) - require.NoError(t, err) - require.NotNil(t, newUser) - - users = append(users, user) - } - return users -} - -func createTestBlocks(t *testing.T, store store.Store, userID string, num int) []*model.Block { - var blocks []*model.Block - for i := 0; i < num; i++ { - block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: utils.NewID(utils.IDTypeBoard), - Type: model.TypeCard, - CreatedBy: userID, - } - err := store.InsertBlock(block, userID) - require.NoError(t, err) - - blocks = append(blocks, block) - } - return blocks -} - -func createTestBlocksForCard(t *testing.T, store store.Store, cardID string, num int) []*model.Block { - card, err := store.GetBlock(cardID) - require.NoError(t, err) - assert.EqualValues(t, model.TypeCard, card.Type) - - var blocks []*model.Block - for i := 0; i < num; i++ { - block := &model.Block{ - ID: utils.NewID(utils.IDTypeBlock), - BoardID: card.BoardID, - Type: model.TypeText, - CreatedBy: card.CreatedBy, - ParentID: card.ID, - Title: fmt.Sprintf("text %d", i), - } - err := store.InsertBlock(block, card.CreatedBy) - require.NoError(t, err) - - blocks = append(blocks, block) - } - return blocks -} - -//nolint:unparam -func createTestCards(t *testing.T, store store.Store, userID string, boardID string, num int) []*model.Block { - var blocks []*model.Block - for i := 0; i < num; i++ { - block := &model.Block{ - ID: utils.NewID(utils.IDTypeCard), - BoardID: boardID, - ParentID: boardID, - Type: model.TypeCard, - CreatedBy: userID, - Title: fmt.Sprintf("card %d", i), - } - err := store.InsertBlock(block, userID) - require.NoError(t, err) - - blocks = append(blocks, block) - } - return blocks -} - -//nolint:unparam -func createTestBoards(t *testing.T, store store.Store, teamID string, userID string, num int) []*model.Board { - var boards []*model.Board - for i := 0; i < num; i++ { - board := &model.Board{ - ID: utils.NewID(utils.IDTypeBoard), - TeamID: teamID, - Type: "O", - CreatedBy: userID, - Title: fmt.Sprintf("board %d", i), - } - boardNew, err := store.InsertBoard(board, userID) - require.NoError(t, err) - - boards = append(boards, boardNew) - } - return boards -} - -//nolint:unparam -func deleteTestBoard(t *testing.T, store store.Store, boardID string, userID string) { - err := store.DeleteBoard(boardID, userID) - require.NoError(t, err) -} - -// extractIDs is a test helper that extracts a sorted slice of IDs from slices of various struct types. -// Might have used generics here except that would require implementing a `GetID` method on each type. -func extractIDs(t *testing.T, arr ...any) []string { - ids := make([]string, 0) - - for _, item := range arr { - if item == nil { - continue - } - - switch tarr := item.(type) { - case []*model.Board: - for _, b := range tarr { - if b != nil { - ids = append(ids, b.ID) - } - } - case []*model.BoardHistory: - for _, bh := range tarr { - ids = append(ids, bh.ID) - } - case []*model.Block: - for _, b := range tarr { - if b != nil { - ids = append(ids, b.ID) - } - } - case []*model.BlockHistory: - for _, bh := range tarr { - ids = append(ids, bh.ID) - } - default: - t.Errorf("unsupported type %T extracting board ID", item) - } - } - - // sort the ids to make it easier to compare lists of ids visually. - sort.Strings(ids) - return ids -} diff --git a/server/boards/services/telemetry/mocks/ServerIface.go b/server/boards/services/telemetry/mocks/ServerIface.go deleted file mode 100644 index ec0ea350e5..0000000000 --- a/server/boards/services/telemetry/mocks/ServerIface.go +++ /dev/null @@ -1,147 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -// Regenerate this file using `make telemetry-mocks`. - -package mocks - -import ( - httpservice "github.com/mattermost/mattermost/server/v8/platform/services/httpservice" - mock "github.com/stretchr/testify/mock" - - model "github.com/mattermost/mattermost/server/public/model" - - plugin "github.com/mattermost/mattermost/server/public/plugin" -) - -// ServerIface is an autogenerated mock type for the ServerIface type -type ServerIface struct { - mock.Mock -} - -// Config provides a mock function with given fields: -func (_m *ServerIface) Config() *model.Config { - ret := _m.Called() - - var r0 *model.Config - if rf, ok := ret.Get(0).(func() *model.Config); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Config) - } - } - - return r0 -} - -// GetPluginsEnvironment provides a mock function with given fields: -func (_m *ServerIface) GetPluginsEnvironment() *plugin.Environment { - ret := _m.Called() - - var r0 *plugin.Environment - if rf, ok := ret.Get(0).(func() *plugin.Environment); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.Environment) - } - } - - return r0 -} - -// GetRoleByName provides a mock function with given fields: _a0 -func (_m *ServerIface) GetRoleByName(_a0 string) (*model.Role, *model.AppError) { - ret := _m.Called(_a0) - - var r0 *model.Role - if rf, ok := ret.Get(0).(func(string) *model.Role); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Role) - } - } - - var r1 *model.AppError - if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*model.AppError) - } - } - - return r0, r1 -} - -// GetSchemes provides a mock function with given fields: _a0, _a1, _a2 -func (_m *ServerIface) GetSchemes(_a0 string, _a1 int, _a2 int) ([]*model.Scheme, *model.AppError) { - ret := _m.Called(_a0, _a1, _a2) - - var r0 []*model.Scheme - if rf, ok := ret.Get(0).(func(string, int, int) []*model.Scheme); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Scheme) - } - } - - var r1 *model.AppError - if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok { - r1 = rf(_a0, _a1, _a2) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*model.AppError) - } - } - - return r0, r1 -} - -// HttpService provides a mock function with given fields: -func (_m *ServerIface) HttpService() httpservice.HTTPService { - ret := _m.Called() - - var r0 httpservice.HTTPService - if rf, ok := ret.Get(0).(func() httpservice.HTTPService); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(httpservice.HTTPService) - } - } - - return r0 -} - -// IsLeader provides a mock function with given fields: -func (_m *ServerIface) IsLeader() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// License provides a mock function with given fields: -func (_m *ServerIface) License() *model.License { - ret := _m.Called() - - var r0 *model.License - if rf, ok := ret.Get(0).(func() *model.License); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.License) - } - } - - return r0 -} diff --git a/server/boards/services/telemetry/telemetry.go b/server/boards/services/telemetry/telemetry.go deleted file mode 100644 index 6dd6bce1b9..0000000000 --- a/server/boards/services/telemetry/telemetry.go +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package telemetry - -import ( - "os" - "time" - - rudder "github.com/rudderlabs/analytics-go" - - "github.com/mattermost/mattermost/server/v8/boards/services/scheduler" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/utils" -) - -const ( - rudderDataplaneURL = "https://pdat.matterlytics.com" - rudderKeyProd = "1myWcDbTkIThnpPYyms7DKlmQWl" - rudderKeyTest = "1myWYwHRDFdLDTpznQ7qFlOPQaa" - - // These are placeholders to allow the existing release pipelines to run without failing to - // insert the values that are now hard-coded above. Remove this once we converge on the - // unified delivery pipeline in GitHub. - _ = "placeholder_rudder_dataplane_url" - _ = "placeholder_boards_rudder_key" - - timeBetweenTelemetryChecks = 10 * time.Minute -) - -type TrackerFunc func() (Tracker, error) - -type Tracker map[string]interface{} - -type Service struct { - trackers map[string]TrackerFunc - logger mlog.LoggerIFace - rudderClient rudder.Client - telemetryID string - timestampLastTelemetrySent time.Time -} - -type RudderConfig struct { - RudderKey string - DataplaneURL string -} - -func New(telemetryID string, logger mlog.LoggerIFace) *Service { - service := &Service{ - logger: logger, - telemetryID: telemetryID, - trackers: map[string]TrackerFunc{}, - } - - return service -} - -func (ts *Service) RegisterTracker(name string, f TrackerFunc) { - ts.trackers[name] = f -} - -func (ts *Service) getRudderConfig() RudderConfig { - // Support unit testing - if os.Getenv("RUDDER_KEY") != "" && os.Getenv("RUDDER_DATAPLANE_URL") != "" { - return RudderConfig{os.Getenv("RUDDER_KEY"), os.Getenv("RUDDER_DATAPLANE_URL")} - } - - rudderKey := "" - switch model.GetServiceEnvironment() { - case model.ServiceEnvironmentProduction: - rudderKey = rudderKeyProd - case model.ServiceEnvironmentTest: - rudderKey = rudderKeyTest - case model.ServiceEnvironmentDev: - } - - return RudderConfig{rudderKey, rudderDataplaneURL} -} - -func (ts *Service) sendDailyTelemetry(override bool) { - config := ts.getRudderConfig() - if (config.DataplaneURL != "" && config.RudderKey != "") || override { - ts.initRudder(config.DataplaneURL, config.RudderKey) - - for name, tracker := range ts.trackers { - m, err := tracker() - if err != nil { - ts.logger.Error("Error fetching telemetry data", mlog.String("name", name), mlog.Err(err)) - continue - } - ts.sendTelemetry(name, m) - } - } -} - -func (ts *Service) sendTelemetry(event string, properties map[string]interface{}) { - if ts.rudderClient != nil { - var context *rudder.Context - _ = ts.rudderClient.Enqueue(rudder.Track{ - Event: event, - UserId: ts.telemetryID, - Properties: properties, - Context: context, - }) - } -} - -func (ts *Service) initRudder(endpoint, rudderKey string) { - if ts.rudderClient == nil { - config := rudder.Config{} - config.Logger = rudder.StdLogger(ts.logger.StdLogger(mlog.LvlFBTelemetry)) - config.Endpoint = endpoint - // For testing - if endpoint != rudderDataplaneURL { - config.Verbose = true - config.BatchSize = 1 - } - client, err := rudder.NewWithConfig(rudderKey, endpoint, config) - if err != nil { - ts.logger.Fatal("Failed to create Rudder instance") - return - } - _ = client.Enqueue(rudder.Identify{ - UserId: ts.telemetryID, - }) - - ts.rudderClient = client - } -} - -func (ts *Service) doTelemetryIfNeeded(firstRun time.Time) { - hoursSinceFirstServerRun := time.Since(firstRun).Hours() - - // Send once every 10 minutes for the first hour - if hoursSinceFirstServerRun < 1 { - ts.doTelemetry() - return - } - - // Send once every hour thereafter for the first 12 hours - if hoursSinceFirstServerRun <= 12 && time.Since(ts.timestampLastTelemetrySent) >= time.Hour { - ts.doTelemetry() - return - } - - // Send at the 24 hour mark and every 24 hours after - if hoursSinceFirstServerRun > 12 && time.Since(ts.timestampLastTelemetrySent) >= 24*time.Hour { - ts.doTelemetry() - return - } -} - -func (ts *Service) RunTelemetryJob(firstRunMillis int64) { - // Send on boot - ts.doTelemetry() - scheduler.CreateRecurringTask("Telemetry", func() { - ts.doTelemetryIfNeeded(utils.TimeFromMillis(firstRunMillis)) - }, timeBetweenTelemetryChecks) -} - -func (ts *Service) doTelemetry() { - ts.timestampLastTelemetrySent = time.Now() - ts.sendDailyTelemetry(false) -} - -// Shutdown closes the telemetry client. -func (ts *Service) Shutdown() error { - if ts.rudderClient != nil { - return ts.rudderClient.Close() - } - - return nil -} diff --git a/server/boards/services/telemetry/telemetry_test.go b/server/boards/services/telemetry/telemetry_test.go deleted file mode 100644 index de40cdf730..0000000000 --- a/server/boards/services/telemetry/telemetry_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package telemetry - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func mockServer() (chan []byte, *httptest.Server) { - done := make(chan []byte, 1) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, r.Body); err != nil { - panic(err) - } - - var v interface{} - err := json.Unmarshal(buf.Bytes(), &v) - if err != nil { - panic(err) - } - - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - panic(err) - } - - // filter the identify message - if strings.Contains(string(b), `"type": "identify"`) { - return - } - - done <- b - })) - - return done, server -} - -func TestTelemetry(t *testing.T) { - receiveChan, server := mockServer() - - os.Setenv("RUDDER_KEY", "mock-test-rudder-key") - os.Setenv("RUDDER_DATAPLANE_URL", server.URL) - - checkMockRudderServer := func(t *testing.T) { - // check mock rudder server got - got := string(<-receiveChan) - require.Contains(t, got, "mockTrackerKey") - require.Contains(t, got, "mockTrackerValue") - } - - t.Run("Register tracker and run telemetry job", func(t *testing.T) { - service := New("mockTelemetryID", mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)) - service.RegisterTracker("mockTracker", func() (Tracker, error) { - return map[string]interface{}{ - "mockTrackerKey": "mockTrackerValue", - }, nil - }) - - service.RunTelemetryJob(time.Now().UnixNano() / int64(time.Millisecond)) - checkMockRudderServer(t) - }) - - t.Run("do telemetry if needed", func(t *testing.T) { - service := New("mockTelemetryID", mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)) - service.RegisterTracker("mockTracker", func() (Tracker, error) { - return map[string]interface{}{ - "mockTrackerKey": "mockTrackerValue", - }, nil - }) - - firstRun := time.Now() - t.Run("Send once every 10 minutes for the first hour", func(t *testing.T) { - service.doTelemetryIfNeeded(firstRun.Add(-30 * time.Minute)) - checkMockRudderServer(t) - }) - - t.Run("Send once every hour thereafter for the first 12 hours", func(t *testing.T) { - // firstRun is 2 hours ago and timestampLastTelemetrySent is hour ago - // need to do telemetry - service.timestampLastTelemetrySent = time.Now().Add(-time.Hour) - service.doTelemetryIfNeeded(firstRun.Add(-2 * time.Hour)) - checkMockRudderServer(t) - - // firstRun is 2 hours ago and timestampLastTelemetrySent is just now - // no need to do telemetry - service.doTelemetryIfNeeded(firstRun.Add(-2 * time.Hour)) - require.Equal(t, 0, len(receiveChan)) - }) - t.Run("Send at the 24 hour mark and every 24 hours after", func(t *testing.T) { - // firstRun is 24 hours ago and timestampLastTelemetrySent is 24 hours ago - // need to do telemetry - service.timestampLastTelemetrySent = time.Now().Add(-24 * time.Hour) - service.doTelemetryIfNeeded(firstRun.Add(-24 * time.Hour)) - checkMockRudderServer(t) - - // firstRun is 24 hours ago and timestampLastTelemetrySent is just now - // no need to do telemetry - service.doTelemetryIfNeeded(firstRun.Add(-24 * time.Hour)) - require.Equal(t, 0, len(receiveChan)) - }) - }) -} diff --git a/server/boards/services/webhook/webhook.go b/server/boards/services/webhook/webhook.go deleted file mode 100644 index 5c0856b0b1..0000000000 --- a/server/boards/services/webhook/webhook.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package webhook - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// NotifyUpdate calls webhooks. -func (wh *Client) NotifyUpdate(block *model.Block) { - if len(wh.config.WebhookUpdate) < 1 { - return - } - - json, err := json.Marshal(block) - if err != nil { - wh.logger.Fatal("NotifyUpdate: json.Marshal", mlog.Err(err)) - } - for _, url := range wh.config.WebhookUpdate { - resp, _ := http.Post(url, "application/json", bytes.NewBuffer(json)) //nolint:gosec - _, _ = io.ReadAll(resp.Body) - resp.Body.Close() - - wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url)) - } -} - -// Client is a webhook client. -type Client struct { - config *config.Configuration - logger mlog.LoggerIFace -} - -// NewClient creates a new Client. -func NewClient(config *config.Configuration, logger mlog.LoggerIFace) *Client { - return &Client{ - config: config, - logger: logger, - } -} diff --git a/server/boards/services/webhook/webhook_test.go b/server/boards/services/webhook/webhook_test.go deleted file mode 100644 index 466b542d69..0000000000 --- a/server/boards/services/webhook/webhook_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package webhook - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/services/config" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func TestClientUpdateNotify(t *testing.T) { - var isNotified bool - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - isNotified = true - })) - defer ts.Close() - - cfg := &config.Configuration{ - WebhookUpdate: []string{ts.URL}, - } - - logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) - defer func() { - err := logger.Shutdown() - assert.NoError(t, err) - }() - - client := NewClient(cfg, logger) - - client.NotifyUpdate(&model.Block{}) - - if !isNotified { - t.Error("webhook url not be notified") - } -} diff --git a/server/boards/swagger/README.md b/server/boards/swagger/README.md deleted file mode 100644 index 889dc52ba8..0000000000 --- a/server/boards/swagger/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Swagger / OpenAPI 2.0 auto-generated files - -⚠️ **Warning:** The API is currently considered Beta and major changes are planned. Please [see this note](https://github.com/mattermost/focalboard/discussions/2139) for more details. - -This folder is generated by the `make swagger` command from comments in the server code. - -Prerequisites: -1. [go-swagger](https://goswagger.io/install.html) -2. [openapi-generator](https://github.com/OpenAPITools/openapi-generator) - -These can be installed via Homebrew: -``` -brew tap go-swagger/go-swagger -brew install go-swagger -brew install openapi-generator -``` - -# Server API documentation - -See the generated [server API documentation here](https://htmlpreview.github.io/?https://github.com/mattermost/focalboard/blob/main/server/swagger/docs/html/index.html). - -# How to authenticate - -To auth against Personal Server, first call login with your credentials to get a token, e.g. -``` -curl -X POST \ - -H "Accept: application/json" \ - -H "X-Requested-With: XMLHttpRequest" \ - -H "Content-Type: application/json" \ - "http://localhost:8000/api/v2/login" \ - -d '{ - "type" : "normal", - "username" : "testuser", - "password" : "testpass" -}' -``` - -This should return a token in the form: -``` -{"token":"abcdefghijklmnopqrstuvwxyz1"} -``` - -Pass this as the bearer auth to subsequent calls, e.g. -``` -curl -X GET \ - -H "Accept: application/json" \ - -H "Authorization: Bearer abcdefghijklmnopqrstuvwxyz1" \ - -H "X-Requested-With: XMLHttpRequest" \ - -H "Content-Type: application/json" \ - "http://localhost:8000/api/v2/teams/0/boards" -``` - -# Differences for Mattermost Boards - -The auto-generated Swagger API documentation is for Focalboard Personal Server. If you are calling the API on Mattermost Boards, the additional changes are: - -### API URLs endpoint - -The API endpoint is at `https://SERVERNAME/plugins/focalboard/api/`, e.g. `https://community.mattermost.com/plugins/focalboard/api/`. - -### Use the Mattermost auth token - -Refer to the [Mattermost API documentation here](https://api.mattermost.com/#tag/authentication) on how to obtain the auth token. - -Pass this token as a bearer token to the Boards APIs, e.g. - -``` -curl -i -H "X-Requested-With: XMLHttpRequest" -H 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz' https://community.mattermost.com/plugins/focalboard/api/v2/workspaces -``` - -Note that the `X-Requested-With: XMLHttpRequest` header is required to pass the CSRF check. - -# We want to hear from you! - -If you are planning on using the Boards API, we would love to hear about what you'd like to do, and how we can improve the APIs in the future. [See here](https://github.com/mattermost/focalboard/discussions/2139) for more details on how to connect. diff --git a/server/boards/swagger/docs/html/.openapi-generator-ignore b/server/boards/swagger/docs/html/.openapi-generator-ignore deleted file mode 100644 index 7484ee590a..0000000000 --- a/server/boards/swagger/docs/html/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/server/boards/swagger/docs/html/.openapi-generator/VERSION b/server/boards/swagger/docs/html/.openapi-generator/VERSION deleted file mode 100644 index 0df17dd0f6..0000000000 --- a/server/boards/swagger/docs/html/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -6.2.1 \ No newline at end of file diff --git a/server/boards/swagger/docs/html/index.html b/server/boards/swagger/docs/html/index.html deleted file mode 100644 index e3a8b66193..0000000000 --- a/server/boards/swagger/docs/html/index.html +++ /dev/null @@ -1,35262 +0,0 @@ - - - - - Focalboard Server - - - - - - - - - - - - - - - - - -
-
- -
-
-
-

Focalboard Server

-
-
-
- -
-
-

Default

-
-
-
-

addMember

-

-
-
-
-

-

Adds a new member to a board

-

-
-
/boards/{boardID}/members
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/members" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.addMember(boardID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#addMember");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.addMember(boardID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#addMember");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-Object *body = Object; // 
-
-[apiInstance addMemberWith:boardID
-    body:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.addMember(boardID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class addMemberExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.addMember(boardID, body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.addMember: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->addMember($boardID, $body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->addMember: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->addMember(boardID => $boardID, body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->addMember: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-body = Object # Object | 
-
-try:
-    api_response = api_instance.add_member(boardID, body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->addMember: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.addMember(boardID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

membership to replace the current one with

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

archiveExportBoard

-

Exports an archive of all blocks for one boards.

-
-
-
-

-

-

-
-
/boards/{boardID}/archive/export
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/archive/export"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Id of board to export
-
-        try {
-            apiInstance.archiveExportBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveExportBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Id of board to export
-
-        try {
-            apiInstance.archiveExportBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveExportBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Id of board to export (default to null)
-
-// Exports an archive of all blocks for one boards.
-[apiInstance archiveExportBoardWith:boardID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Id of board to export
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.archiveExportBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class archiveExportBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Id of board to export (default to null)
-
-            try {
-                // Exports an archive of all blocks for one boards.
-                apiInstance.archiveExportBoard(boardID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.archiveExportBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Id of board to export
-
-try {
-    $api_instance->archiveExportBoard($boardID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->archiveExportBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Id of board to export
-
-eval {
-    $api_instance->archiveExportBoard(boardID => $boardID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->archiveExportBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Id of board to export (default to null)
-
-try:
-    # Exports an archive of all blocks for one boards.
-    api_instance.archive_export_board(boardID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->archiveExportBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.archiveExportBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Id of board to export -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

archiveExportTeam

-

Exports an archive of all blocks for all the boards in a team.

-
-
-
-

-

-

-
-
/teams/{teamID}/archive/export
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/archive/export"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Id of team
-
-        try {
-            apiInstance.archiveExportTeam(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveExportTeam");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Id of team
-
-        try {
-            apiInstance.archiveExportTeam(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveExportTeam");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Id of team (default to null)
-
-// Exports an archive of all blocks for all the boards in a team.
-[apiInstance archiveExportTeamWith:teamID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Id of team
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.archiveExportTeam(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class archiveExportTeamExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Id of team (default to null)
-
-            try {
-                // Exports an archive of all blocks for all the boards in a team.
-                apiInstance.archiveExportTeam(teamID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.archiveExportTeam: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Id of team
-
-try {
-    $api_instance->archiveExportTeam($teamID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->archiveExportTeam: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Id of team
-
-eval {
-    $api_instance->archiveExportTeam(teamID => $teamID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->archiveExportTeam: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Id of team (default to null)
-
-try:
-    # Exports an archive of all blocks for all the boards in a team.
-    api_instance.archive_export_team(teamID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->archiveExportTeam: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.archiveExportTeam(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Id of team -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

archiveImport

-

Import an archive of boards.

-
-
-
-

-

-

-
-
/teams/{teamID}/archive/import
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: multipart/form-data" \
- "http://localhost/api/v2/teams/{teamID}/archive/import"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        File file = BINARY_DATA_HERE; // File | archive file to import
-
-        try {
-            apiInstance.archiveImport(teamID, file);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveImport");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        File file = BINARY_DATA_HERE; // File | archive file to import
-
-        try {
-            apiInstance.archiveImport(teamID, file);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#archiveImport");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-File *file = BINARY_DATA_HERE; // archive file to import (default to null)
-
-// Import an archive of boards.
-[apiInstance archiveImportWith:teamID
-    file:file
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var file = BINARY_DATA_HERE; // {File} archive file to import
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.archiveImport(teamID, file, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class archiveImportExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var file = BINARY_DATA_HERE;  // File | archive file to import (default to null)
-
-            try {
-                // Import an archive of boards.
-                apiInstance.archiveImport(teamID, file);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.archiveImport: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$file = BINARY_DATA_HERE; // File | archive file to import
-
-try {
-    $api_instance->archiveImport($teamID, $file);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->archiveImport: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $file = BINARY_DATA_HERE; # File | archive file to import
-
-eval {
-    $api_instance->archiveImport(teamID => $teamID, file => $file);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->archiveImport: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-file = BINARY_DATA_HERE # File | archive file to import (default to null)
-
-try:
-    # Import an archive of boards.
-    api_instance.archive_import(teamID, file)
-except ApiException as e:
-    print("Exception when calling DefaultApi->archiveImport: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let file = BINARY_DATA_HERE; // File
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.archiveImport(teamID, file, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - -
Form parameters
- - - - - - - - - -
NameDescription
file* - - -
-
-
- - File - - - (binary) - - -
-archive file to import -
-
-
- Required -
-
-
-
- - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

changePassword

-

-
-
-
-

-

Change a user's password

-

-
-
/users/{userID}/changepassword
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/users/{userID}/changepassword" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.changePassword(userID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#changePassword");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.changePassword(userID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#changePassword");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *userID = userID_example; // User ID (default to null)
-Object *body = Object; // 
-
-[apiInstance changePasswordWith:userID
-    body:body
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var userID = userID_example; // {String} User ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.changePassword(userID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class changePasswordExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var userID = userID_example;  // String | User ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                apiInstance.changePassword(userID, body);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.changePassword: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$userID = userID_example; // String | User ID
-$body = Object; // Object | 
-
-try {
-    $api_instance->changePassword($userID, $body);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->changePassword: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $userID = userID_example; # String | User ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    $api_instance->changePassword(userID => $userID, body => $body);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->changePassword: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-userID = userID_example # String | User ID (default to null)
-body = Object # Object | 
-
-try:
-    api_instance.change_password(userID, body)
-except ApiException as e:
-    print("Exception when calling DefaultApi->changePassword: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let userID = userID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.changePassword(userID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

Change password request

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

cloudLimits

-

Fetches the cloud limits of the server.

-
-
-
-

-

-

-
-
/limits
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/limits"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.cloudLimits();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#cloudLimits");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.cloudLimits();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#cloudLimits");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-// Fetches the cloud limits of the server.
-[apiInstance cloudLimitsWithCompletionHandler: 
-              ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.cloudLimits(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class cloudLimitsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                // Fetches the cloud limits of the server.
-                Object result = apiInstance.cloudLimits();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.cloudLimits: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->cloudLimits();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->cloudLimits: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->cloudLimits();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->cloudLimits: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    # Fetches the cloud limits of the server.
-    api_response = api_instance.cloud_limits()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->cloudLimits: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.cloudLimits(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

createBoard

-

-
-
-
-

-

Creates a new board

-

-
-
/boards
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createBoard(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createBoard(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance createBoardWith:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.createBoard(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class createBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.createBoard(body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.createBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->createBoard($body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->createBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->createBoard(body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->createBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_response = api_instance.create_board(body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->createBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.createBoard(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the board to create

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

createCard

-

Creates a new card for the specified board.

-
-
-
-

-

-

-
-
/boards/{boardID}/cards
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/cards?disable_notify=" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data inserting)
-
-        try {
-            Object result = apiInstance.createCard(boardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createCard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data inserting)
-
-        try {
-            Object result = apiInstance.createCard(boardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createCard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-Object *body = Object; // 
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk data inserting) (optional) (default to null)
-
-// Creates a new card for the specified board.
-[apiInstance createCardWith:boardID
-    body:body
-    disableNotify:disableNotify
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var body = Object; // {Object} 
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk data inserting)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.createCard(boardID, body, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class createCardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var body = Object;  // Object | 
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk data inserting) (optional)  (default to null)
-
-            try {
-                // Creates a new card for the specified board.
-                Object result = apiInstance.createCard(boardID, body, disableNotify);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.createCard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$body = Object; // Object | 
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data inserting)
-
-try {
-    $result = $api_instance->createCard($boardID, $body, $disableNotify);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->createCard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk data inserting)
-
-eval {
-    my $result = $api_instance->createCard(boardID => $boardID, body => $body, disableNotify => $disableNotify);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->createCard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-body = Object # Object | 
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk data inserting) (optional) (default to null)
-
-try:
-    # Creates a new card for the specified board.
-    api_response = api_instance.create_card(boardID, body, disableNotify=disableNotify)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->createCard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = Object; // Object
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.createCard(boardID, body, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the card to create

- -
-
- - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk data inserting) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

createCategory

-

-
-
-
-

-

Create a category for boards

-

-
-
/teams/{teamID}/categories
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/teams/{teamID}/categories" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createCategory(teamID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createCategory(teamID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-Object *body = Object; // 
-
-[apiInstance createCategoryWith:teamID
-    body:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.createCategory(teamID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class createCategoryExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.createCategory(teamID, body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.createCategory: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->createCategory($teamID, $body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->createCategory: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->createCategory(teamID => $teamID, body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->createCategory: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-body = Object # Object | 
-
-try:
-    api_response = api_instance.create_category(teamID, body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->createCategory: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.createCategory(teamID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

category to create

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

createSubscription

-

Creates a subscription to a block for a user. The user will receive change notifications for the block.

-
-
-
-

-

-

-
-
/subscriptions
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/subscriptions" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createSubscription(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createSubscription");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.createSubscription(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#createSubscription");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-// Creates a subscription to a block for a user. The user will receive change notifications for the block.
-[apiInstance createSubscriptionWith:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.createSubscription(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class createSubscriptionExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                // Creates a subscription to a block for a user. The user will receive change notifications for the block.
-                Object result = apiInstance.createSubscription(body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.createSubscription: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->createSubscription($body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->createSubscription: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->createSubscription(body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->createSubscription: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    # Creates a subscription to a block for a user. The user will receive change notifications for the block.
-    api_response = api_instance.create_subscription(body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->createSubscription: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.createSubscription(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

subscription definition

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteBlock

-

-
-
-
-

-

Deletes a block

-

-
-
/boards/{boardID}/blocks/{blockID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks/{blockID}?disable_notify="
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to delete
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk deletion)
-
-        try {
-            apiInstance.deleteBlock(boardID, blockID, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to delete
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk deletion)
-
-        try {
-            apiInstance.deleteBlock(boardID, blockID, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *blockID = blockID_example; // ID of block to delete (default to null)
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk deletion) (optional) (default to null)
-
-[apiInstance deleteBlockWith:boardID
-    blockID:blockID
-    disableNotify:disableNotify
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var blockID = blockID_example; // {String} ID of block to delete
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk deletion)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteBlock(boardID, blockID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteBlockExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var blockID = blockID_example;  // String | ID of block to delete (default to null)
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk deletion) (optional)  (default to null)
-
-            try {
-                apiInstance.deleteBlock(boardID, blockID, disableNotify);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteBlock: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$blockID = blockID_example; // String | ID of block to delete
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk deletion)
-
-try {
-    $api_instance->deleteBlock($boardID, $blockID, $disableNotify);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteBlock: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $blockID = blockID_example; # String | ID of block to delete
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk deletion)
-
-eval {
-    $api_instance->deleteBlock(boardID => $boardID, blockID => $blockID, disableNotify => $disableNotify);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteBlock: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-blockID = blockID_example # String | ID of block to delete (default to null)
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk deletion) (optional) (default to null)
-
-try:
-    api_instance.delete_block(boardID, blockID, disableNotify=disableNotify)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteBlock: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let blockID = blockID_example; // String
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteBlock(boardID, blockID, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
blockID* - - -
-
-
- - String - - -
-ID of block to delete -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk deletion) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteBoard

-

-
-
-
-

-

Removes a board

-

-
-
/boards/{boardID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.deleteBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.deleteBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance deleteBoardWith:boardID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                apiInstance.deleteBoard(boardID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $api_instance->deleteBoard($boardID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    $api_instance->deleteBoard(boardID => $boardID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_instance.delete_board(boardID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteBoardsAndBlocks

-

-
-
-
-

-

Deletes boards and blocks

-

-
-
/boards-and-blocks
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards-and-blocks" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.deleteBoardsAndBlocks(body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.deleteBoardsAndBlocks(body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance deleteBoardsAndBlocksWith:body
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteBoardsAndBlocks(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteBoardsAndBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                apiInstance.deleteBoardsAndBlocks(body);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteBoardsAndBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $api_instance->deleteBoardsAndBlocks($body);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteBoardsAndBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    $api_instance->deleteBoardsAndBlocks(body => $body);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteBoardsAndBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_instance.delete_boards_and_blocks(body)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteBoardsAndBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteBoardsAndBlocks(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the boards and blocks to delete

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteCategory

-

-
-
-
-

-

Delete a category

-

-
-
/teams/{teamID}/categories/{categoryID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/categories/{categoryID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-
-        try {
-            apiInstance.deleteCategory(teamID, categoryID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-
-        try {
-            apiInstance.deleteCategory(teamID, categoryID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *categoryID = categoryID_example; // Category ID (default to null)
-
-[apiInstance deleteCategoryWith:teamID
-    categoryID:categoryID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var categoryID = categoryID_example; // {String} Category ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteCategory(teamID, categoryID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteCategoryExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var categoryID = categoryID_example;  // String | Category ID (default to null)
-
-            try {
-                apiInstance.deleteCategory(teamID, categoryID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteCategory: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$categoryID = categoryID_example; // String | Category ID
-
-try {
-    $api_instance->deleteCategory($teamID, $categoryID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteCategory: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $categoryID = categoryID_example; # String | Category ID
-
-eval {
-    $api_instance->deleteCategory(teamID => $teamID, categoryID => $categoryID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteCategory: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-categoryID = categoryID_example # String | Category ID (default to null)
-
-try:
-    api_instance.delete_category(teamID, categoryID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteCategory: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let categoryID = categoryID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteCategory(teamID, categoryID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
categoryID* - - -
-
-
- - String - - -
-Category ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteMember

-

-
-
-
-

-

Deletes a member from a board

-

-
-
/boards/{boardID}/members/{userID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/members/{userID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String userID = userID_example; // String | User ID
-
-        try {
-            apiInstance.deleteMember(boardID, userID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteMember");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String userID = userID_example; // String | User ID
-
-        try {
-            apiInstance.deleteMember(boardID, userID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteMember");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *userID = userID_example; // User ID (default to null)
-
-[apiInstance deleteMemberWith:boardID
-    userID:userID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var userID = userID_example; // {String} User ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteMember(boardID, userID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteMemberExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var userID = userID_example;  // String | User ID (default to null)
-
-            try {
-                apiInstance.deleteMember(boardID, userID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteMember: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$userID = userID_example; // String | User ID
-
-try {
-    $api_instance->deleteMember($boardID, $userID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteMember: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $userID = userID_example; # String | User ID
-
-eval {
-    $api_instance->deleteMember(boardID => $boardID, userID => $userID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteMember: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-userID = userID_example # String | User ID (default to null)
-
-try:
-    api_instance.delete_member(boardID, userID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteMember: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let userID = userID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteMember(boardID, userID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

deleteSubscription

-

Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.

-
-
-
-

-

-

-
-
/subscriptions/{blockID}/{subscriberID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X DELETE \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/subscriptions/{blockID}/{subscriberID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String blockID = blockID_example; // String | Block ID
-        String subscriberID = subscriberID_example; // String | Subscriber ID
-
-        try {
-            apiInstance.deleteSubscription(blockID, subscriberID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteSubscription");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String blockID = blockID_example; // String | Block ID
-        String subscriberID = subscriberID_example; // String | Subscriber ID
-
-        try {
-            apiInstance.deleteSubscription(blockID, subscriberID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#deleteSubscription");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *blockID = blockID_example; // Block ID (default to null)
-String *subscriberID = subscriberID_example; // Subscriber ID (default to null)
-
-// Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
-[apiInstance deleteSubscriptionWith:blockID
-    subscriberID:subscriberID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var blockID = blockID_example; // {String} Block ID
-var subscriberID = subscriberID_example; // {String} Subscriber ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.deleteSubscription(blockID, subscriberID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class deleteSubscriptionExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var blockID = blockID_example;  // String | Block ID (default to null)
-            var subscriberID = subscriberID_example;  // String | Subscriber ID (default to null)
-
-            try {
-                // Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
-                apiInstance.deleteSubscription(blockID, subscriberID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.deleteSubscription: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$blockID = blockID_example; // String | Block ID
-$subscriberID = subscriberID_example; // String | Subscriber ID
-
-try {
-    $api_instance->deleteSubscription($blockID, $subscriberID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->deleteSubscription: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $blockID = blockID_example; # String | Block ID
-my $subscriberID = subscriberID_example; # String | Subscriber ID
-
-eval {
-    $api_instance->deleteSubscription(blockID => $blockID, subscriberID => $subscriberID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->deleteSubscription: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-blockID = blockID_example # String | Block ID (default to null)
-subscriberID = subscriberID_example # String | Subscriber ID (default to null)
-
-try:
-    # Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
-    api_instance.delete_subscription(blockID, subscriberID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->deleteSubscription: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let blockID = blockID_example; // String
-    let subscriberID = subscriberID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.deleteSubscription(blockID, subscriberID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
blockID* - - -
-
-
- - String - - -
-Block ID -
-
-
- Required -
-
-
-
subscriberID* - - -
-
-
- - String - - -
-Subscriber ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

duplicateBlock

-

-
-
-
-

-

Returns the new created blocks

-

-
-
/boards/{boardID}/blocks/{blockID}/duplicate
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks/{blockID}/duplicate"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | Block ID
-
-        try {
-            array[Object] result = apiInstance.duplicateBlock(boardID, blockID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#duplicateBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | Block ID
-
-        try {
-            array[Object] result = apiInstance.duplicateBlock(boardID, blockID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#duplicateBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *blockID = blockID_example; // Block ID (default to null)
-
-[apiInstance duplicateBlockWith:boardID
-    blockID:blockID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var blockID = blockID_example; // {String} Block ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.duplicateBlock(boardID, blockID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class duplicateBlockExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var blockID = blockID_example;  // String | Block ID (default to null)
-
-            try {
-                array[Object] result = apiInstance.duplicateBlock(boardID, blockID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.duplicateBlock: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$blockID = blockID_example; // String | Block ID
-
-try {
-    $result = $api_instance->duplicateBlock($boardID, $blockID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->duplicateBlock: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $blockID = blockID_example; # String | Block ID
-
-eval {
-    my $result = $api_instance->duplicateBlock(boardID => $boardID, blockID => $blockID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->duplicateBlock: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-blockID = blockID_example # String | Block ID (default to null)
-
-try:
-    api_response = api_instance.duplicate_block(boardID, blockID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->duplicateBlock: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let blockID = blockID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.duplicateBlock(boardID, blockID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
blockID* - - -
-
-
- - String - - -
-Block ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

duplicateBoard

-

-
-
-
-

-

Returns the new created board and all the blocks

-

-
-
/boards/{boardID}/duplicate
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/duplicate"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.duplicateBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#duplicateBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.duplicateBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#duplicateBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance duplicateBoardWith:boardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.duplicateBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class duplicateBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                Object result = apiInstance.duplicateBoard(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.duplicateBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->duplicateBoard($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->duplicateBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->duplicateBoard(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->duplicateBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.duplicate_board(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->duplicateBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.duplicateBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getBlocks

-

-
-
-
-

-

Returns blocks

-

-
-
/boards/{boardID}/blocks
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks?parent_id=parentId_example&type=type_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String parentId = parentId_example; // String | ID of parent block, omit to specify all blocks
-        String type = type_example; // String | Type of blocks to return, omit to specify all types
-
-        try {
-            array[Object] result = apiInstance.getBlocks(boardID, parentId, type);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String parentId = parentId_example; // String | ID of parent block, omit to specify all blocks
-        String type = type_example; // String | Type of blocks to return, omit to specify all types
-
-        try {
-            array[Object] result = apiInstance.getBlocks(boardID, parentId, type);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *parentId = parentId_example; // ID of parent block, omit to specify all blocks (optional) (default to null)
-String *type = type_example; // Type of blocks to return, omit to specify all types (optional) (default to null)
-
-[apiInstance getBlocksWith:boardID
-    parentId:parentId
-    type:type
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var opts = {
-  'parentId': parentId_example, // {String} ID of parent block, omit to specify all blocks
-  'type': type_example // {String} Type of blocks to return, omit to specify all types
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getBlocks(boardID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var parentId = parentId_example;  // String | ID of parent block, omit to specify all blocks (optional)  (default to null)
-            var type = type_example;  // String | Type of blocks to return, omit to specify all types (optional)  (default to null)
-
-            try {
-                array[Object] result = apiInstance.getBlocks(boardID, parentId, type);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$parentId = parentId_example; // String | ID of parent block, omit to specify all blocks
-$type = type_example; // String | Type of blocks to return, omit to specify all types
-
-try {
-    $result = $api_instance->getBlocks($boardID, $parentId, $type);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $parentId = parentId_example; # String | ID of parent block, omit to specify all blocks
-my $type = type_example; # String | Type of blocks to return, omit to specify all types
-
-eval {
-    my $result = $api_instance->getBlocks(boardID => $boardID, parentId => $parentId, type => $type);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-parentId = parentId_example # String | ID of parent block, omit to specify all blocks (optional) (default to null)
-type = type_example # String | Type of blocks to return, omit to specify all types (optional) (default to null)
-
-try:
-    api_response = api_instance.get_blocks(boardID, parentId=parentId, type=type)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let parentId = parentId_example; // String
-    let type = type_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getBlocks(boardID, parentId, type, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - - - - - -
NameDescription
parent_id - - -
-
-
- - String - - -
-ID of parent block, omit to specify all blocks -
-
-
-
-
type - - -
-
-
- - String - - -
-Type of blocks to return, omit to specify all types -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getBoard

-

-
-
-
-

-

Returns a board

-

-
-
/boards/{boardID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance getBoardWith:boardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                Object result = apiInstance.getBoard(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->getBoard($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->getBoard(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.get_board(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getBoardMetadata

-

-
-
-
-

-

Returns a board's metadata

-

-
-
/boards/{boardID}/metadata
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/metadata"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getBoardMetadata(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoardMetadata");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getBoardMetadata(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoardMetadata");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance getBoardMetadataWith:boardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getBoardMetadata(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getBoardMetadataExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                Object result = apiInstance.getBoardMetadata(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getBoardMetadata: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->getBoardMetadata($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getBoardMetadata: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->getBoardMetadata(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getBoardMetadata: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.get_board_metadata(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getBoardMetadata: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getBoardMetadata(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getBoards

-

-
-
-
-

-

Returns team boards

-

-
-
/teams/{teamID}/boards
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/boards"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getBoards(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getBoards(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-[apiInstance getBoardsWith:teamID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getBoards(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getBoardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                array[Object] result = apiInstance.getBoards(teamID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getBoards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $result = $api_instance->getBoards($teamID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getBoards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    my $result = $api_instance->getBoards(teamID => $teamID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getBoards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    api_response = api_instance.get_boards(teamID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getBoards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getBoards(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getCard

-

Fetches the specified card.

-
-
-
-

-

-

-
-
/cards/{cardID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/cards/{cardID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String cardID = cardID_example; // String | Card ID
-
-        try {
-            Object result = apiInstance.getCard(cardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getCard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String cardID = cardID_example; // String | Card ID
-
-        try {
-            Object result = apiInstance.getCard(cardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getCard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *cardID = cardID_example; // Card ID (default to null)
-
-// Fetches the specified card.
-[apiInstance getCardWith:cardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var cardID = cardID_example; // {String} Card ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getCard(cardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getCardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var cardID = cardID_example;  // String | Card ID (default to null)
-
-            try {
-                // Fetches the specified card.
-                Object result = apiInstance.getCard(cardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getCard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$cardID = cardID_example; // String | Card ID
-
-try {
-    $result = $api_instance->getCard($cardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getCard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $cardID = cardID_example; # String | Card ID
-
-eval {
-    my $result = $api_instance->getCard(cardID => $cardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getCard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-cardID = cardID_example # String | Card ID (default to null)
-
-try:
-    # Fetches the specified card.
-    api_response = api_instance.get_card(cardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getCard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let cardID = cardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getCard(cardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
cardID* - - -
-
-
- - String - - -
-Card ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getCards

-

Fetches cards for the specified board.

-
-
-
-

-

-

-
-
/boards/{boardID}/cards
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/cards?page=56&per_page=56"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Integer page = 56; // Integer | The page to select (default=0)
-        Integer perPage = 56; // Integer | Number of cards to return per page(default=100)
-
-        try {
-            array[Object] result = apiInstance.getCards(boardID, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getCards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Integer page = 56; // Integer | The page to select (default=0)
-        Integer perPage = 56; // Integer | Number of cards to return per page(default=100)
-
-        try {
-            array[Object] result = apiInstance.getCards(boardID, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getCards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-Integer *page = 56; // The page to select (default=0) (optional) (default to null)
-Integer *perPage = 56; // Number of cards to return per page(default=100) (optional) (default to null)
-
-// Fetches cards for the specified board.
-[apiInstance getCardsWith:boardID
-    page:page
-    perPage:perPage
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var opts = {
-  'page': 56, // {Integer} The page to select (default=0)
-  'perPage': 56 // {Integer} Number of cards to return per page(default=100)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getCards(boardID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getCardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var page = 56;  // Integer | The page to select (default=0) (optional)  (default to null)
-            var perPage = 56;  // Integer | Number of cards to return per page(default=100) (optional)  (default to null)
-
-            try {
-                // Fetches cards for the specified board.
-                array[Object] result = apiInstance.getCards(boardID, page, perPage);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getCards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$page = 56; // Integer | The page to select (default=0)
-$perPage = 56; // Integer | Number of cards to return per page(default=100)
-
-try {
-    $result = $api_instance->getCards($boardID, $page, $perPage);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getCards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $page = 56; # Integer | The page to select (default=0)
-my $perPage = 56; # Integer | Number of cards to return per page(default=100)
-
-eval {
-    my $result = $api_instance->getCards(boardID => $boardID, page => $page, perPage => $perPage);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getCards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-page = 56 # Integer | The page to select (default=0) (optional) (default to null)
-perPage = 56 # Integer | Number of cards to return per page(default=100) (optional) (default to null)
-
-try:
-    # Fetches cards for the specified board.
-    api_response = api_instance.get_cards(boardID, page=page, perPage=perPage)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getCards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let page = 56; // Integer
-    let perPage = 56; // Integer
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getCards(boardID, page, perPage, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - - - - - -
NameDescription
page - - -
-
-
- - Integer - - -
-The page to select (default=0) -
-
-
-
-
per_page - - -
-
-
- - Integer - - -
-Number of cards to return per page(default=100) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getChannel

-

-
-
-
-

-

Returns the requested channel

-

-
-
/teams/{teamID}/channels/{channelID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/channels/{channelID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String channelID = channelID_example; // String | Channel ID
-
-        try {
-            array[Channel] result = apiInstance.getChannel(teamID, channelID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getChannel");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String channelID = channelID_example; // String | Channel ID
-
-        try {
-            array[Channel] result = apiInstance.getChannel(teamID, channelID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getChannel");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *channelID = channelID_example; // Channel ID (default to null)
-
-[apiInstance getChannelWith:teamID
-    channelID:channelID
-              completionHandler: ^(array[Channel] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var channelID = channelID_example; // {String} Channel ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getChannel(teamID, channelID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getChannelExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var channelID = channelID_example;  // String | Channel ID (default to null)
-
-            try {
-                array[Channel] result = apiInstance.getChannel(teamID, channelID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getChannel: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$channelID = channelID_example; // String | Channel ID
-
-try {
-    $result = $api_instance->getChannel($teamID, $channelID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getChannel: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $channelID = channelID_example; # String | Channel ID
-
-eval {
-    my $result = $api_instance->getChannel(teamID => $teamID, channelID => $channelID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getChannel: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-channelID = channelID_example # String | Channel ID (default to null)
-
-try:
-    api_response = api_instance.get_channel(teamID, channelID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getChannel: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let channelID = channelID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getChannel(teamID, channelID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
channelID* - - -
-
-
- - String - - -
-Channel ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getClientConfig

-

-
-
-
-

-

Returns the client configuration

-

-
-
/clientConfig
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
- -H "Accept: application/json" \
- "http://localhost/api/v2/clientConfig"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.getClientConfig();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getClientConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.getClientConfig();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getClientConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance getClientConfigWithCompletionHandler: 
-              ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getClientConfig(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getClientConfigExample
-    {
-        public void main()
-        {
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                Object result = apiInstance.getClientConfig();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getClientConfig: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->getClientConfig();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getClientConfig: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->getClientConfig();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getClientConfig: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_response = api_instance.get_client_config()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getClientConfig: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getClientConfig(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getFile

-

-
-
-
-

-

Returns the contents of an uploaded file

-

-
-
/files/teams/{teamID}/{boardID}/{filename}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json,image/jpg,image/png,image/gif" \
- "http://localhost/api/v2/files/teams/{teamID}/{boardID}/{filename}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String boardID = boardID_example; // String | Board ID
-        String filename = filename_example; // String | name of the file
-
-        try {
-            apiInstance.getFile(teamID, boardID, filename);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getFile");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String boardID = boardID_example; // String | Board ID
-        String filename = filename_example; // String | name of the file
-
-        try {
-            apiInstance.getFile(teamID, boardID, filename);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getFile");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *boardID = boardID_example; // Board ID (default to null)
-String *filename = filename_example; // name of the file (default to null)
-
-[apiInstance getFileWith:teamID
-    boardID:boardID
-    filename:filename
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var boardID = boardID_example; // {String} Board ID
-var filename = filename_example; // {String} name of the file
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.getFile(teamID, boardID, filename, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getFileExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var filename = filename_example;  // String | name of the file (default to null)
-
-            try {
-                apiInstance.getFile(teamID, boardID, filename);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getFile: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$boardID = boardID_example; // String | Board ID
-$filename = filename_example; // String | name of the file
-
-try {
-    $api_instance->getFile($teamID, $boardID, $filename);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getFile: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $boardID = boardID_example; # String | Board ID
-my $filename = filename_example; # String | name of the file
-
-eval {
-    $api_instance->getFile(teamID => $teamID, boardID => $boardID, filename => $filename);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getFile: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-boardID = boardID_example # String | Board ID (default to null)
-filename = filename_example # String | name of the file (default to null)
-
-try:
-    api_instance.get_file(teamID, boardID, filename)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getFile: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let boardID = boardID_example; // String
-    let filename = filename_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getFile(teamID, boardID, filename, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
filename* - - -
-
-
- - String - - -
-name of the file -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getMe

-

-
-
-
-

-

Returns the currently logged-in user

-

-
-
/users/me
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users/me"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.getMe();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMe");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Object result = apiInstance.getMe();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMe");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance getMeWithCompletionHandler: 
-              ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getMe(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getMeExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                Object result = apiInstance.getMe();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getMe: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->getMe();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getMe: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->getMe();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getMe: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_response = api_instance.get_me()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getMe: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getMe(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getMembersForBoard

-

-
-
-
-

-

Returns the members of the board

-

-
-
/boards/{boardID}/members
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/members"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            array[Object] result = apiInstance.getMembersForBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMembersForBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            array[Object] result = apiInstance.getMembersForBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMembersForBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance getMembersForBoardWith:boardID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getMembersForBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getMembersForBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                array[Object] result = apiInstance.getMembersForBoard(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getMembersForBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->getMembersForBoard($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getMembersForBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->getMembersForBoard(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getMembersForBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.get_members_for_board(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getMembersForBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getMembersForBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getMyMemberships

-

-
-
-
-

-

Returns the currently users board memberships

-

-
-
/users/me/memberships
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users/me/memberships"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            array[Object] result = apiInstance.getMyMemberships();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMyMemberships");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            array[Object] result = apiInstance.getMyMemberships();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getMyMemberships");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance getMyMembershipsWithCompletionHandler: 
-              ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getMyMemberships(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getMyMembershipsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                array[Object] result = apiInstance.getMyMemberships();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getMyMemberships: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->getMyMemberships();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getMyMemberships: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->getMyMemberships();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getMyMemberships: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_response = api_instance.get_my_memberships()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getMyMemberships: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getMyMemberships(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getSharing

-

-
-
-
-

-

Returns sharing information for a board

-

-
-
/boards/{boardID}/sharing
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/sharing"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getSharing(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getSharing");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.getSharing(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getSharing");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance getSharingWith:boardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getSharing(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getSharingExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                Object result = apiInstance.getSharing(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getSharing: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->getSharing($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getSharing: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->getSharing(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getSharing: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.get_sharing(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getSharing: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getSharing(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getSubscriptions

-

Gets subscriptions for a user.

-
-
-
-

-

-

-
-
/subscriptions/{subscriberID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/subscriptions/{subscriberID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String subscriberID = subscriberID_example; // String | Subscriber ID
-
-        try {
-            array[Object] result = apiInstance.getSubscriptions(subscriberID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getSubscriptions");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String subscriberID = subscriberID_example; // String | Subscriber ID
-
-        try {
-            array[Object] result = apiInstance.getSubscriptions(subscriberID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getSubscriptions");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *subscriberID = subscriberID_example; // Subscriber ID (default to null)
-
-// Gets subscriptions for a user.
-[apiInstance getSubscriptionsWith:subscriberID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var subscriberID = subscriberID_example; // {String} Subscriber ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getSubscriptions(subscriberID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getSubscriptionsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var subscriberID = subscriberID_example;  // String | Subscriber ID (default to null)
-
-            try {
-                // Gets subscriptions for a user.
-                array[Object] result = apiInstance.getSubscriptions(subscriberID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getSubscriptions: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$subscriberID = subscriberID_example; // String | Subscriber ID
-
-try {
-    $result = $api_instance->getSubscriptions($subscriberID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getSubscriptions: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $subscriberID = subscriberID_example; # String | Subscriber ID
-
-eval {
-    my $result = $api_instance->getSubscriptions(subscriberID => $subscriberID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getSubscriptions: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-subscriberID = subscriberID_example # String | Subscriber ID (default to null)
-
-try:
-    # Gets subscriptions for a user.
-    api_response = api_instance.get_subscriptions(subscriberID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getSubscriptions: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let subscriberID = subscriberID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getSubscriptions(subscriberID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
subscriberID* - - -
-
-
- - String - - -
-Subscriber ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getTeam

-

-
-
-
-

-

Returns information of the root team

-

-
-
/teams/{teamID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            Object result = apiInstance.getTeam(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeam");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            Object result = apiInstance.getTeam(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeam");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-[apiInstance getTeamWith:teamID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getTeam(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getTeamExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                Object result = apiInstance.getTeam(teamID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getTeam: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $result = $api_instance->getTeam($teamID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getTeam: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    my $result = $api_instance->getTeam(teamID => $teamID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getTeam: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    api_response = api_instance.get_team(teamID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getTeam: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getTeam(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getTeamUsers

-

-
-
-
-

-

Returns team users

-

-
-
/teams/{teamID}/users
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/users?search=search_example&exclude_bots=true"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String search = search_example; // String | string to filter users list
-        Boolean excludeBots = true; // Boolean | exclude bot users
-
-        try {
-            array[Object] result = apiInstance.getTeamUsers(teamID, search, excludeBots);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeamUsers");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String search = search_example; // String | string to filter users list
-        Boolean excludeBots = true; // Boolean | exclude bot users
-
-        try {
-            array[Object] result = apiInstance.getTeamUsers(teamID, search, excludeBots);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeamUsers");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *search = search_example; // string to filter users list (optional) (default to null)
-Boolean *excludeBots = true; // exclude bot users (optional) (default to null)
-
-[apiInstance getTeamUsersWith:teamID
-    search:search
-    excludeBots:excludeBots
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var opts = {
-  'search': search_example, // {String} string to filter users list
-  'excludeBots': true // {Boolean} exclude bot users
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getTeamUsers(teamID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getTeamUsersExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var search = search_example;  // String | string to filter users list (optional)  (default to null)
-            var excludeBots = true;  // Boolean | exclude bot users (optional)  (default to null)
-
-            try {
-                array[Object] result = apiInstance.getTeamUsers(teamID, search, excludeBots);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getTeamUsers: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$search = search_example; // String | string to filter users list
-$excludeBots = true; // Boolean | exclude bot users
-
-try {
-    $result = $api_instance->getTeamUsers($teamID, $search, $excludeBots);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getTeamUsers: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $search = search_example; # String | string to filter users list
-my $excludeBots = true; # Boolean | exclude bot users
-
-eval {
-    my $result = $api_instance->getTeamUsers(teamID => $teamID, search => $search, excludeBots => $excludeBots);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getTeamUsers: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-search = search_example # String | string to filter users list (optional) (default to null)
-excludeBots = true # Boolean | exclude bot users (optional) (default to null)
-
-try:
-    api_response = api_instance.get_team_users(teamID, search=search, excludeBots=excludeBots)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getTeamUsers: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let search = search_example; // String
-    let excludeBots = true; // Boolean
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getTeamUsers(teamID, search, excludeBots, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - - - - - -
NameDescription
search - - - -
exclude_bots - - -
-
-
- - Boolean - - -
-exclude bot users -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getTeams

-

-
-
-
-

-

Returns information of all the teams

-

-
-
/teams
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            array[Object] result = apiInstance.getTeams();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeams");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            array[Object] result = apiInstance.getTeams();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTeams");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance getTeamsWithCompletionHandler: 
-              ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getTeams(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getTeamsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                array[Object] result = apiInstance.getTeams();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getTeams: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->getTeams();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getTeams: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->getTeams();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getTeams: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_response = api_instance.get_teams()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getTeams: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getTeams(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getTemplates

-

-
-
-
-

-

Returns team templates

-

-
-
/teams/{teamID}/templates
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/templates"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getTemplates(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTemplates");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getTemplates(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getTemplates");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-[apiInstance getTemplatesWith:teamID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getTemplates(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getTemplatesExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                array[Object] result = apiInstance.getTemplates(teamID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getTemplates: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $result = $api_instance->getTemplates($teamID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getTemplates: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    my $result = $api_instance->getTemplates(teamID => $teamID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getTemplates: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    api_response = api_instance.get_templates(teamID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getTemplates: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getTemplates(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getUser

-

-
-
-
-

-

Returns a user

-

-
-
/users/{userID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users/{userID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-
-        try {
-            Object result = apiInstance.getUser(userID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUser");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-
-        try {
-            Object result = apiInstance.getUser(userID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUser");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *userID = userID_example; // User ID (default to null)
-
-[apiInstance getUserWith:userID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var userID = userID_example; // {String} User ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getUser(userID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getUserExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var userID = userID_example;  // String | User ID (default to null)
-
-            try {
-                Object result = apiInstance.getUser(userID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getUser: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$userID = userID_example; // String | User ID
-
-try {
-    $result = $api_instance->getUser($userID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getUser: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $userID = userID_example; # String | User ID
-
-eval {
-    my $result = $api_instance->getUser(userID => $userID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getUser: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-userID = userID_example # String | User ID (default to null)
-
-try:
-    api_response = api_instance.get_user(userID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getUser: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let userID = userID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getUser(userID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getUserBoardsInsights

-

-
-
-
-

-

Returns user boards insights

-

-
-
/users/me/boards/insights
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users/me/boards/insights?time_range=timeRange_example&page=page_example&per_page=perPage_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String timeRange = timeRange_example; // String | duration of data to calculate insights for
-        String page = page_example; // String | page offset for top boards
-        String perPage = perPage_example; // String | limit for boards in a page.
-
-        try {
-            array[Object] result = apiInstance.getUserBoardsInsights(teamID, timeRange, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserBoardsInsights");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String timeRange = timeRange_example; // String | duration of data to calculate insights for
-        String page = page_example; // String | page offset for top boards
-        String perPage = perPage_example; // String | limit for boards in a page.
-
-        try {
-            array[Object] result = apiInstance.getUserBoardsInsights(teamID, timeRange, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserBoardsInsights");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *timeRange = timeRange_example; // duration of data to calculate insights for (default to null)
-String *page = page_example; // page offset for top boards (default to null)
-String *perPage = perPage_example; // limit for boards in a page. (default to null)
-
-[apiInstance getUserBoardsInsightsWith:teamID
-    timeRange:timeRange
-    page:page
-    perPage:perPage
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var timeRange = timeRange_example; // {String} duration of data to calculate insights for
-var page = page_example; // {String} page offset for top boards
-var perPage = perPage_example; // {String} limit for boards in a page.
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getUserBoardsInsights(teamID, timeRange, page, perPage, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getUserBoardsInsightsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var timeRange = timeRange_example;  // String | duration of data to calculate insights for (default to null)
-            var page = page_example;  // String | page offset for top boards (default to null)
-            var perPage = perPage_example;  // String | limit for boards in a page. (default to null)
-
-            try {
-                array[Object] result = apiInstance.getUserBoardsInsights(teamID, timeRange, page, perPage);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getUserBoardsInsights: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$timeRange = timeRange_example; // String | duration of data to calculate insights for
-$page = page_example; // String | page offset for top boards
-$perPage = perPage_example; // String | limit for boards in a page.
-
-try {
-    $result = $api_instance->getUserBoardsInsights($teamID, $timeRange, $page, $perPage);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getUserBoardsInsights: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $timeRange = timeRange_example; # String | duration of data to calculate insights for
-my $page = page_example; # String | page offset for top boards
-my $perPage = perPage_example; # String | limit for boards in a page.
-
-eval {
-    my $result = $api_instance->getUserBoardsInsights(teamID => $teamID, timeRange => $timeRange, page => $page, perPage => $perPage);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getUserBoardsInsights: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-timeRange = timeRange_example # String | duration of data to calculate insights for (default to null)
-page = page_example # String | page offset for top boards (default to null)
-perPage = perPage_example # String | limit for boards in a page. (default to null)
-
-try:
-    api_response = api_instance.get_user_boards_insights(teamID, timeRange, page, perPage)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getUserBoardsInsights: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let timeRange = timeRange_example; // String
-    let page = page_example; // String
-    let perPage = perPage_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getUserBoardsInsights(teamID, timeRange, page, perPage, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - - - - - - - - - -
NameDescription
time_range* - - -
-
-
- - String - - -
-duration of data to calculate insights for -
-
-
- Required -
-
-
-
page* - - -
-
-
- - String - - -
-page offset for top boards -
-
-
- Required -
-
-
-
per_page* - - -
-
-
- - String - - -
-limit for boards in a page. -
-
-
- Required -
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getUserCategoryBoards

-

-
-
-
-

-

Gets the user's board categories

-

-
-
/teams/{teamID}/categories
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/categories"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getUserCategoryBoards(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserCategoryBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            array[Object] result = apiInstance.getUserCategoryBoards(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserCategoryBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-[apiInstance getUserCategoryBoardsWith:teamID
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getUserCategoryBoards(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getUserCategoryBoardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                array[Object] result = apiInstance.getUserCategoryBoards(teamID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getUserCategoryBoards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $result = $api_instance->getUserCategoryBoards($teamID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getUserCategoryBoards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    my $result = $api_instance->getUserCategoryBoards(teamID => $teamID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getUserCategoryBoards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    api_response = api_instance.get_user_category_boards(teamID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getUserCategoryBoards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getUserCategoryBoards(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getUserConfig

-

-
-
-
-

-

Returns an array of user preferences

-

-
-
/users/me/config
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users/me/config"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Preferences result = apiInstance.getUserConfig();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            Preferences result = apiInstance.getUserConfig();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUserConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance getUserConfigWithCompletionHandler: 
-              ^(Preferences output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getUserConfig(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getUserConfigExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                Preferences result = apiInstance.getUserConfig();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getUserConfig: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->getUserConfig();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getUserConfig: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->getUserConfig();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getUserConfig: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_response = api_instance.get_user_config()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getUserConfig: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getUserConfig(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

getUsersList

-

-
-
-
-

-

Returns a user[]

-

-
-
/users
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/users"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-
-        try {
-            Object result = apiInstance.getUsersList(userID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUsersList");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-
-        try {
-            Object result = apiInstance.getUsersList(userID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#getUsersList");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *userID = userID_example; // User ID (default to null)
-
-[apiInstance getUsersListWith:userID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var userID = userID_example; // {String} User ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.getUsersList(userID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class getUsersListExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var userID = userID_example;  // String | User ID (default to null)
-
-            try {
-                Object result = apiInstance.getUsersList(userID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.getUsersList: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$userID = userID_example; // String | User ID
-
-try {
-    $result = $api_instance->getUsersList($userID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->getUsersList: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $userID = userID_example; # String | User ID
-
-eval {
-    my $result = $api_instance->getUsersList(userID => $userID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->getUsersList: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-userID = userID_example # String | User ID (default to null)
-
-try:
-    api_response = api_instance.get_users_list(userID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->getUsersList: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let userID = userID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.getUsersList(userID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

handleNotifyAdminUpgrade

-

Notifies admins for upgrade request.

-
-
-
-

-

-

-
-
/api/v2/teams/{teamID}/notifyadminupgrade
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/api/v2/teams/{teamID}/notifyadminupgrade"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            apiInstance.handleNotifyAdminUpgrade(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleNotifyAdminUpgrade");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            apiInstance.handleNotifyAdminUpgrade(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleNotifyAdminUpgrade");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-// Notifies admins for upgrade request.
-[apiInstance handleNotifyAdminUpgradeWith:teamID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.handleNotifyAdminUpgrade(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class handleNotifyAdminUpgradeExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                // Notifies admins for upgrade request.
-                apiInstance.handleNotifyAdminUpgrade(teamID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.handleNotifyAdminUpgrade: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $api_instance->handleNotifyAdminUpgrade($teamID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->handleNotifyAdminUpgrade: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    $api_instance->handleNotifyAdminUpgrade(teamID => $teamID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->handleNotifyAdminUpgrade: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    # Notifies admins for upgrade request.
-    api_instance.handle_notify_admin_upgrade(teamID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->handleNotifyAdminUpgrade: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.handleNotifyAdminUpgrade(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

handleStatistics

-

Fetches the statistic of the server.

-
-
-
-

-

-

-
-
/statistics
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/statistics"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            BoardStatistics result = apiInstance.handleStatistics();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleStatistics");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            BoardStatistics result = apiInstance.handleStatistics();
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleStatistics");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-// Fetches the statistic  of the server.
-[apiInstance handleStatisticsWithCompletionHandler: 
-              ^(BoardStatistics output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.handleStatistics(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class handleStatisticsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                // Fetches the statistic  of the server.
-                BoardStatistics result = apiInstance.handleStatistics();
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.handleStatistics: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $result = $api_instance->handleStatistics();
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->handleStatistics: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    my $result = $api_instance->handleStatistics();
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->handleStatistics: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    # Fetches the statistic  of the server.
-    api_response = api_instance.handle_statistics()
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->handleStatistics: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.handleStatistics(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

handleTeamBoardsInsights

-

-
-
-
-

-

Returns team boards insights

-

-
-
/teams/{teamID}/boards/insights
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/boards/insights?time_range=timeRange_example&page=page_example&per_page=perPage_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String timeRange = timeRange_example; // String | duration of data to calculate insights for
-        String page = page_example; // String | page offset for top boards
-        String perPage = perPage_example; // String | limit for boards in a page.
-
-        try {
-            array[Object] result = apiInstance.handleTeamBoardsInsights(teamID, timeRange, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleTeamBoardsInsights");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String timeRange = timeRange_example; // String | duration of data to calculate insights for
-        String page = page_example; // String | page offset for top boards
-        String perPage = perPage_example; // String | limit for boards in a page.
-
-        try {
-            array[Object] result = apiInstance.handleTeamBoardsInsights(teamID, timeRange, page, perPage);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#handleTeamBoardsInsights");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *timeRange = timeRange_example; // duration of data to calculate insights for (default to null)
-String *page = page_example; // page offset for top boards (default to null)
-String *perPage = perPage_example; // limit for boards in a page. (default to null)
-
-[apiInstance handleTeamBoardsInsightsWith:teamID
-    timeRange:timeRange
-    page:page
-    perPage:perPage
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var timeRange = timeRange_example; // {String} duration of data to calculate insights for
-var page = page_example; // {String} page offset for top boards
-var perPage = perPage_example; // {String} limit for boards in a page.
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.handleTeamBoardsInsights(teamID, timeRange, page, perPage, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class handleTeamBoardsInsightsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var timeRange = timeRange_example;  // String | duration of data to calculate insights for (default to null)
-            var page = page_example;  // String | page offset for top boards (default to null)
-            var perPage = perPage_example;  // String | limit for boards in a page. (default to null)
-
-            try {
-                array[Object] result = apiInstance.handleTeamBoardsInsights(teamID, timeRange, page, perPage);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.handleTeamBoardsInsights: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$timeRange = timeRange_example; // String | duration of data to calculate insights for
-$page = page_example; // String | page offset for top boards
-$perPage = perPage_example; // String | limit for boards in a page.
-
-try {
-    $result = $api_instance->handleTeamBoardsInsights($teamID, $timeRange, $page, $perPage);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->handleTeamBoardsInsights: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $timeRange = timeRange_example; # String | duration of data to calculate insights for
-my $page = page_example; # String | page offset for top boards
-my $perPage = perPage_example; # String | limit for boards in a page.
-
-eval {
-    my $result = $api_instance->handleTeamBoardsInsights(teamID => $teamID, timeRange => $timeRange, page => $page, perPage => $perPage);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->handleTeamBoardsInsights: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-timeRange = timeRange_example # String | duration of data to calculate insights for (default to null)
-page = page_example # String | page offset for top boards (default to null)
-perPage = perPage_example # String | limit for boards in a page. (default to null)
-
-try:
-    api_response = api_instance.handle_team_boards_insights(teamID, timeRange, page, perPage)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->handleTeamBoardsInsights: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let timeRange = timeRange_example; // String
-    let page = page_example; // String
-    let perPage = perPage_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.handleTeamBoardsInsights(teamID, timeRange, page, perPage, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - - - - - - - - - -
NameDescription
time_range* - - -
-
-
- - String - - -
-duration of data to calculate insights for -
-
-
- Required -
-
-
-
page* - - -
-
-
- - String - - -
-page offset for top boards -
-
-
- Required -
-
-
-
per_page* - - -
-
-
- - String - - -
-limit for boards in a page. -
-
-
- Required -
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

hello

-

Responds with `Hello` if the web service is running.

-
-
-
-

-

-

-
-
/hello
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
- "http://localhost/api/v2/hello"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.hello();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#hello");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.hello();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#hello");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-// Responds with `Hello` if the web service is running.
-[apiInstance helloWithCompletionHandler: 
-              ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.hello(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class helloExample
-    {
-        public void main()
-        {
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                // Responds with `Hello` if the web service is running.
-                apiInstance.hello();
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.hello: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $api_instance->hello();
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->hello: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    $api_instance->hello();
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->hello: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    # Responds with `Hello` if the web service is running.
-    api_instance.hello()
-except ApiException as e:
-    print("Exception when calling DefaultApi->hello: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.hello(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
-
-
-
-
-
-

insertBoardsAndBlocks

-

-
-
-
-

-

Creates new boards and blocks

-

-
-
/boards-and-blocks
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards-and-blocks" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.insertBoardsAndBlocks(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#insertBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.insertBoardsAndBlocks(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#insertBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance insertBoardsAndBlocksWith:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.insertBoardsAndBlocks(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class insertBoardsAndBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.insertBoardsAndBlocks(body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.insertBoardsAndBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->insertBoardsAndBlocks($body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->insertBoardsAndBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->insertBoardsAndBlocks(body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->insertBoardsAndBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_response = api_instance.insert_boards_and_blocks(body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->insertBoardsAndBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.insertBoardsAndBlocks(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the boards and blocks to create

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

joinBoard

-

-
-
-
-

-

Become a member of a board

-

-
-
/boards/{boardID}/join
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/join"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.joinBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#joinBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            Object result = apiInstance.joinBoard(boardID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#joinBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance joinBoardWith:boardID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.joinBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class joinBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                Object result = apiInstance.joinBoard(boardID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.joinBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $result = $api_instance->joinBoard($boardID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->joinBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    my $result = $api_instance->joinBoard(boardID => $boardID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->joinBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_response = api_instance.join_board(boardID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->joinBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.joinBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

leaveBoard

-

-
-
-
-

-

Remove your own membership from a board

-

-
-
/boards/{boardID}/leave
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/leave"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.leaveBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#leaveBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.leaveBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#leaveBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance leaveBoardWith:boardID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.leaveBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class leaveBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                apiInstance.leaveBoard(boardID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.leaveBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $api_instance->leaveBoard($boardID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->leaveBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    $api_instance->leaveBoard(boardID => $boardID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->leaveBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_instance.leave_board(boardID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->leaveBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.leaveBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

login

-

-
-
-
-

-

Login user

-

-
-
/login
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/login" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.login(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#login");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.login(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#login");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance loginWith:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.login(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class loginExample
-    {
-        public void main()
-        {
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.login(body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.login: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->login($body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->login: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->login(body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->login: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_response = api_instance.login(body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->login: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.login(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

Login request

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

logout

-

-
-
-
-

-

Logout user

-

-
-
/logout
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/logout"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.logout();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#logout");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.logout();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#logout");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-[apiInstance logoutWithCompletionHandler: 
-              ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.logout(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class logoutExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                apiInstance.logout();
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.logout: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $api_instance->logout();
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->logout: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    $api_instance->logout();
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->logout: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    api_instance.logout()
-except ApiException as e:
-    print("Exception when calling DefaultApi->logout: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.logout(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

onboard

-

Onboards a user on Boards.

-
-
-
-

-

-

-
-
/team/{teamID}/onboard
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/team/{teamID}/onboard"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            onboard_200_response result = apiInstance.onboard(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#onboard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            onboard_200_response result = apiInstance.onboard(teamID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#onboard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-// Onboards a user on Boards.
-[apiInstance onboardWith:teamID
-              completionHandler: ^(onboard_200_response output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.onboard(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class onboardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                // Onboards a user on Boards.
-                onboard_200_response result = apiInstance.onboard(teamID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.onboard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $result = $api_instance->onboard($teamID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->onboard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    my $result = $api_instance->onboard(teamID => $teamID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->onboard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    # Onboards a user on Boards.
-    api_response = api_instance.onboard(teamID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->onboard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.onboard(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

patchBlock

-

-
-
-
-

-

Partially updates a block

-

-
-
/boards/{boardID}/blocks/{blockID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks/{blockID}?disable_notify=" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to patch
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-        try {
-            apiInstance.patchBlock(boardID, blockID, body, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to patch
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-        try {
-            apiInstance.patchBlock(boardID, blockID, body, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *blockID = blockID_example; // ID of block to patch (default to null)
-Object *body = Object; // 
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk patching) (optional) (default to null)
-
-[apiInstance patchBlockWith:boardID
-    blockID:blockID
-    body:body
-    disableNotify:disableNotify
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var blockID = blockID_example; // {String} ID of block to patch
-var body = Object; // {Object} 
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk patching)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.patchBlock(boardID, blockID, body, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class patchBlockExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var blockID = blockID_example;  // String | ID of block to patch (default to null)
-            var body = Object;  // Object | 
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk patching) (optional)  (default to null)
-
-            try {
-                apiInstance.patchBlock(boardID, blockID, body, disableNotify);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.patchBlock: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$blockID = blockID_example; // String | ID of block to patch
-$body = Object; // Object | 
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-try {
-    $api_instance->patchBlock($boardID, $blockID, $body, $disableNotify);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->patchBlock: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $blockID = blockID_example; # String | ID of block to patch
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-eval {
-    $api_instance->patchBlock(boardID => $boardID, blockID => $blockID, body => $body, disableNotify => $disableNotify);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->patchBlock: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-blockID = blockID_example # String | ID of block to patch (default to null)
-body = Object # Object | 
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk patching) (optional) (default to null)
-
-try:
-    api_instance.patch_block(boardID, blockID, body, disableNotify=disableNotify)
-except ApiException as e:
-    print("Exception when calling DefaultApi->patchBlock: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let blockID = blockID_example; // String
-    let body = Object; // Object
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.patchBlock(boardID, blockID, body, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
blockID* - - -
-
-
- - String - - -
-ID of block to patch -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

block patch to apply

- -
-
- - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk patching) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

patchBlocks

-

-
-
-
-

-

Partially updates batch of blocks

-

-
-
/boards/{boardID}/blocks/
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks/?disable_notify=" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Workspace ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-        try {
-            apiInstance.patchBlocks(boardID, body, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Workspace ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-        try {
-            apiInstance.patchBlocks(boardID, body, disableNotify);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Workspace ID (default to null)
-Object *body = Object; // 
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk patching) (optional) (default to null)
-
-[apiInstance patchBlocksWith:boardID
-    body:body
-    disableNotify:disableNotify
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Workspace ID
-var body = Object; // {Object} 
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk patching)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.patchBlocks(boardID, body, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class patchBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Workspace ID (default to null)
-            var body = Object;  // Object | 
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk patching) (optional)  (default to null)
-
-            try {
-                apiInstance.patchBlocks(boardID, body, disableNotify);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.patchBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Workspace ID
-$body = Object; // Object | 
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-try {
-    $api_instance->patchBlocks($boardID, $body, $disableNotify);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->patchBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Workspace ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk patching)
-
-eval {
-    $api_instance->patchBlocks(boardID => $boardID, body => $body, disableNotify => $disableNotify);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->patchBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Workspace ID (default to null)
-body = Object # Object | 
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk patching) (optional) (default to null)
-
-try:
-    api_instance.patch_blocks(boardID, body, disableNotify=disableNotify)
-except ApiException as e:
-    print("Exception when calling DefaultApi->patchBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = Object; // Object
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.patchBlocks(boardID, body, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Workspace ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

block Ids and block patches to apply

- -
-
- - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk patching) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

patchBoard

-

-
-
-
-

-

Partially updates a board

-

-
-
/boards/{boardID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.patchBoard(boardID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.patchBoard(boardID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-Object *body = Object; // 
-
-[apiInstance patchBoardWith:boardID
-    body:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.patchBoard(boardID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class patchBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.patchBoard(boardID, body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.patchBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->patchBoard($boardID, $body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->patchBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->patchBoard(boardID => $boardID, body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->patchBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-body = Object # Object | 
-
-try:
-    api_response = api_instance.patch_board(boardID, body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->patchBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.patchBoard(boardID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

board patch to apply

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

patchBoardsAndBlocks

-

-
-
-
-

-

Patches a set of related boards and blocks

-

-
-
/boards-and-blocks
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards-and-blocks" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.patchBoardsAndBlocks(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.patchBoardsAndBlocks(body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchBoardsAndBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance patchBoardsAndBlocksWith:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.patchBoardsAndBlocks(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class patchBoardsAndBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.patchBoardsAndBlocks(body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.patchBoardsAndBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->patchBoardsAndBlocks($body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->patchBoardsAndBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->patchBoardsAndBlocks(body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->patchBoardsAndBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_response = api_instance.patch_boards_and_blocks(body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->patchBoardsAndBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.patchBoardsAndBlocks(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the patches for the boards and blocks

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

patchCard

-

Patches the specified card.

-
-
-
-

-

-

-
-
/cards/{cardID}/cards
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/cards/{cardID}/cards?disable_notify=" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String cardID = cardID_example; // String | Card ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data patching)
-
-        try {
-            Object result = apiInstance.patchCard(cardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchCard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String cardID = cardID_example; // String | Card ID
-        Object body = Object; // Object | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data patching)
-
-        try {
-            Object result = apiInstance.patchCard(cardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#patchCard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *cardID = cardID_example; // Card ID (default to null)
-Object *body = Object; // 
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk data patching) (optional) (default to null)
-
-// Patches the specified card.
-[apiInstance patchCardWith:cardID
-    body:body
-    disableNotify:disableNotify
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var cardID = cardID_example; // {String} Card ID
-var body = Object; // {Object} 
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk data patching)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.patchCard(cardID, body, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class patchCardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var cardID = cardID_example;  // String | Card ID (default to null)
-            var body = Object;  // Object | 
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk data patching) (optional)  (default to null)
-
-            try {
-                // Patches the specified card.
-                Object result = apiInstance.patchCard(cardID, body, disableNotify);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.patchCard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$cardID = cardID_example; // String | Card ID
-$body = Object; // Object | 
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk data patching)
-
-try {
-    $result = $api_instance->patchCard($cardID, $body, $disableNotify);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->patchCard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $cardID = cardID_example; # String | Card ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk data patching)
-
-eval {
-    my $result = $api_instance->patchCard(cardID => $cardID, body => $body, disableNotify => $disableNotify);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->patchCard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-cardID = cardID_example # String | Card ID (default to null)
-body = Object # Object | 
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk data patching) (optional) (default to null)
-
-try:
-    # Patches the specified card.
-    api_response = api_instance.patch_card(cardID, body, disableNotify=disableNotify)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->patchCard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let cardID = cardID_example; // String
-    let body = Object; // Object
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.patchCard(cardID, body, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
cardID* - - -
-
-
- - String - - -
-Card ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

the card patch

- -
-
- - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk data patching) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

ping

-

Responds with server metadata if the web service is running.

-
-
-
-

-

-

-
-
/ping
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
- "http://localhost/api/v2/ping"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.ping();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#ping");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-
-        try {
-            apiInstance.ping();
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#ping");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-
-// Responds with server metadata if the web service is running.
-[apiInstance pingWithCompletionHandler: 
-              ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.ping(callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class pingExample
-    {
-        public void main()
-        {
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-
-            try {
-                // Responds with server metadata if the web service is running.
-                apiInstance.ping();
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.ping: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-
-try {
-    $api_instance->ping();
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->ping: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-
-eval {
-    $api_instance->ping();
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->ping: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-
-try:
-    # Responds with server metadata if the web service is running.
-    api_instance.ping()
-except ApiException as e:
-    print("Exception when calling DefaultApi->ping: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.ping(&context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - - -

Responses

-

-

- - - - - - -
-
-
-
-
-
-
-
-

postSharing

-

-
-
-
-

-

Sets sharing information for a board

-

-
-
/boards/{boardID}/sharing
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/sharing" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.postSharing(boardID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#postSharing");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.postSharing(boardID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#postSharing");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-Object *body = Object; // 
-
-[apiInstance postSharingWith:boardID
-    body:body
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.postSharing(boardID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class postSharingExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                apiInstance.postSharing(boardID, body);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.postSharing: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$body = Object; // Object | 
-
-try {
-    $api_instance->postSharing($boardID, $body);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->postSharing: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    $api_instance->postSharing(boardID => $boardID, body => $body);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->postSharing: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-body = Object # Object | 
-
-try:
-    api_instance.post_sharing(boardID, body)
-except ApiException as e:
-    print("Exception when calling DefaultApi->postSharing: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.postSharing(boardID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

sharing information for a root block

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

regenerateSignupToken

-

-
-
-
-

-

Regenerates the signup token for the root team

-

-
-
/teams/{teamID}/regenerate_signup_token
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/regenerate_signup_token"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            apiInstance.regenerateSignupToken(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#regenerateSignupToken");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-
-        try {
-            apiInstance.regenerateSignupToken(teamID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#regenerateSignupToken");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-
-[apiInstance regenerateSignupTokenWith:teamID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.regenerateSignupToken(teamID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class regenerateSignupTokenExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-
-            try {
-                apiInstance.regenerateSignupToken(teamID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.regenerateSignupToken: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-
-try {
-    $api_instance->regenerateSignupToken($teamID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->regenerateSignupToken: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-
-eval {
-    $api_instance->regenerateSignupToken(teamID => $teamID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->regenerateSignupToken: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-
-try:
-    api_instance.regenerate_signup_token(teamID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->regenerateSignupToken: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.regenerateSignupToken(teamID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

register

-

-
-
-
-

-

Register new user

-

-
-
/register
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/register" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.register(body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#register");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.register(body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#register");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-

-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-Object *body = Object; // 
-
-[apiInstance registerWith:body
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.register(body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class registerExample
-    {
-        public void main()
-        {
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var body = Object;  // Object | 
-
-            try {
-                apiInstance.register(body);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.register: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$body = Object; // Object | 
-
-try {
-    $api_instance->register($body);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->register: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    $api_instance->register(body => $body);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->register: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-body = Object # Object | 
-
-try:
-    api_instance.register(body)
-except ApiException as e:
-    print("Exception when calling DefaultApi->register: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.register(body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

Register request

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

searchAllBoards

-

-
-
-
-

-

Returns the boards that match with a search term

-

-
-
/boards/search
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/search?q=q_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchAllBoards(q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchAllBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchAllBoards(q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchAllBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *q = q_example; // The search term. Must have at least one character (default to null)
-
-[apiInstance searchAllBoardsWith:q
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var q = q_example; // {String} The search term. Must have at least one character
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.searchAllBoards(q, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class searchAllBoardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var q = q_example;  // String | The search term. Must have at least one character (default to null)
-
-            try {
-                array[Object] result = apiInstance.searchAllBoards(q);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.searchAllBoards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$q = q_example; // String | The search term. Must have at least one character
-
-try {
-    $result = $api_instance->searchAllBoards($q);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->searchAllBoards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $q = q_example; # String | The search term. Must have at least one character
-
-eval {
-    my $result = $api_instance->searchAllBoards(q => $q);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->searchAllBoards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-q = q_example # String | The search term. Must have at least one character (default to null)
-
-try:
-    api_response = api_instance.search_all_boards(q)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->searchAllBoards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let q = q_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.searchAllBoards(q, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- - - - - -
Query parameters
- - - - - - - - - -
NameDescription
q* - - -
-
-
- - String - - -
-The search term. Must have at least one character -
-
-
- Required -
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

searchBoards

-

-
-
-
-

-

Returns the boards that match with a search term in the team

-

-
-
/teams/{teamID}/boards/search
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/boards/search?q=q_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchBoards(teamID, q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchBoards(teamID, q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *q = q_example; // The search term. Must have at least one character (default to null)
-
-[apiInstance searchBoardsWith:teamID
-    q:q
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var q = q_example; // {String} The search term. Must have at least one character
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.searchBoards(teamID, q, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class searchBoardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var q = q_example;  // String | The search term. Must have at least one character (default to null)
-
-            try {
-                array[Object] result = apiInstance.searchBoards(teamID, q);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.searchBoards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$q = q_example; // String | The search term. Must have at least one character
-
-try {
-    $result = $api_instance->searchBoards($teamID, $q);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->searchBoards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $q = q_example; # String | The search term. Must have at least one character
-
-eval {
-    my $result = $api_instance->searchBoards(teamID => $teamID, q => $q);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->searchBoards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-q = q_example # String | The search term. Must have at least one character (default to null)
-
-try:
-    api_response = api_instance.search_boards(teamID, q)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->searchBoards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let q = q_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.searchBoards(teamID, q, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - -
NameDescription
q* - - -
-
-
- - String - - -
-The search term. Must have at least one character -
-
-
- Required -
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

searchLinkableBoards

-

-
-
-
-

-

Returns the boards that match with a search term in the team and the -user has permission to manage members

-

-
-
/teams/{teamID}/boards/search/linkable
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/boards/search/linkable?q=q_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchLinkableBoards(teamID, q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchLinkableBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String q = q_example; // String | The search term. Must have at least one character
-
-        try {
-            array[Object] result = apiInstance.searchLinkableBoards(teamID, q);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchLinkableBoards");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *q = q_example; // The search term. Must have at least one character (default to null)
-
-[apiInstance searchLinkableBoardsWith:teamID
-    q:q
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var q = q_example; // {String} The search term. Must have at least one character
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.searchLinkableBoards(teamID, q, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class searchLinkableBoardsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var q = q_example;  // String | The search term. Must have at least one character (default to null)
-
-            try {
-                array[Object] result = apiInstance.searchLinkableBoards(teamID, q);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.searchLinkableBoards: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$q = q_example; // String | The search term. Must have at least one character
-
-try {
-    $result = $api_instance->searchLinkableBoards($teamID, $q);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->searchLinkableBoards: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $q = q_example; # String | The search term. Must have at least one character
-
-eval {
-    my $result = $api_instance->searchLinkableBoards(teamID => $teamID, q => $q);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->searchLinkableBoards: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-q = q_example # String | The search term. Must have at least one character (default to null)
-
-try:
-    api_response = api_instance.search_linkable_boards(teamID, q)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->searchLinkableBoards: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let q = q_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.searchLinkableBoards(teamID, q, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - -
NameDescription
q* - - -
-
-
- - String - - -
-The search term. Must have at least one character -
-
-
- Required -
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

searchMyChannels

-

-
-
-
-

-

Returns the user available channels

-

-
-
/teams/{teamID}/channels
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X GET \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/channels?search=search_example"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String search = search_example; // String | string to filter channels list
-
-        try {
-            array[Channel] result = apiInstance.searchMyChannels(teamID, search);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchMyChannels");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String search = search_example; // String | string to filter channels list
-
-        try {
-            array[Channel] result = apiInstance.searchMyChannels(teamID, search);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#searchMyChannels");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *search = search_example; // string to filter channels list (optional) (default to null)
-
-[apiInstance searchMyChannelsWith:teamID
-    search:search
-              completionHandler: ^(array[Channel] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var opts = {
-  'search': search_example // {String} string to filter channels list
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.searchMyChannels(teamID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class searchMyChannelsExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var search = search_example;  // String | string to filter channels list (optional)  (default to null)
-
-            try {
-                array[Channel] result = apiInstance.searchMyChannels(teamID, search);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.searchMyChannels: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$search = search_example; // String | string to filter channels list
-
-try {
-    $result = $api_instance->searchMyChannels($teamID, $search);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->searchMyChannels: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $search = search_example; # String | string to filter channels list
-
-eval {
-    my $result = $api_instance->searchMyChannels(teamID => $teamID, search => $search);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->searchMyChannels: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-search = search_example # String | string to filter channels list (optional) (default to null)
-
-try:
-    api_response = api_instance.search_my_channels(teamID, search=search)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->searchMyChannels: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let search = search_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.searchMyChannels(teamID, search, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
- - - - -
Query parameters
- - - - - - - - - -
NameDescription
search - - - -
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

undeleteBlock

-

-
-
-
-

-

Undeletes a block

-

-
-
/boards/{boardID}/blocks/{blockID}/undelete
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks/{blockID}/undelete"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to undelete
-
-        try {
-            Object result = apiInstance.undeleteBlock(boardID, blockID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#undeleteBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String blockID = blockID_example; // String | ID of block to undelete
-
-        try {
-            Object result = apiInstance.undeleteBlock(boardID, blockID);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#undeleteBlock");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *blockID = blockID_example; // ID of block to undelete (default to null)
-
-[apiInstance undeleteBlockWith:boardID
-    blockID:blockID
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var blockID = blockID_example; // {String} ID of block to undelete
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.undeleteBlock(boardID, blockID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class undeleteBlockExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var blockID = blockID_example;  // String | ID of block to undelete (default to null)
-
-            try {
-                Object result = apiInstance.undeleteBlock(boardID, blockID);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.undeleteBlock: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$blockID = blockID_example; // String | ID of block to undelete
-
-try {
-    $result = $api_instance->undeleteBlock($boardID, $blockID);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->undeleteBlock: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $blockID = blockID_example; # String | ID of block to undelete
-
-eval {
-    my $result = $api_instance->undeleteBlock(boardID => $boardID, blockID => $blockID);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->undeleteBlock: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-blockID = blockID_example # String | ID of block to undelete (default to null)
-
-try:
-    api_response = api_instance.undelete_block(boardID, blockID)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->undeleteBlock: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let blockID = blockID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.undeleteBlock(boardID, blockID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
blockID* - - -
-
-
- - String - - -
-ID of block to undelete -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

undeleteBoard

-

-
-
-
-

-

Undeletes a board

-

-
-
/boards/{boardID}/undelete
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/boards/{boardID}/undelete"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | ID of board to undelete
-
-        try {
-            apiInstance.undeleteBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#undeleteBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | ID of board to undelete
-
-        try {
-            apiInstance.undeleteBoard(boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#undeleteBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // ID of board to undelete (default to null)
-
-[apiInstance undeleteBoardWith:boardID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} ID of board to undelete
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.undeleteBoard(boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class undeleteBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | ID of board to undelete (default to null)
-
-            try {
-                apiInstance.undeleteBoard(boardID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.undeleteBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | ID of board to undelete
-
-try {
-    $api_instance->undeleteBoard($boardID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->undeleteBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | ID of board to undelete
-
-eval {
-    $api_instance->undeleteBoard(boardID => $boardID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->undeleteBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | ID of board to undelete (default to null)
-
-try:
-    api_instance.undelete_board(boardID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->undeleteBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.undeleteBoard(boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-ID of board to undelete -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

updateBlocks

-

-
-
-
-

-

Insert blocks. The specified IDs will only be used to link -blocks with existing ones, the rest will be replaced by server -generated IDs

-

-
-
/boards/{boardID}/blocks
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/blocks?disable_notify=" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        array[Object] body = ; // array[Object] | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk inserting)
-
-        try {
-            array[Object] result = apiInstance.updateBlocks(boardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        array[Object] body = ; // array[Object] | 
-        oas_any_type_not_mapped disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk inserting)
-
-        try {
-            array[Object] result = apiInstance.updateBlocks(boardID, body, disableNotify);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateBlocks");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-array[Object] *body = ; // 
-oas_any_type_not_mapped *disableNotify = ; // Disables notifications (for bulk inserting) (optional) (default to null)
-
-[apiInstance updateBlocksWith:boardID
-    body:body
-    disableNotify:disableNotify
-              completionHandler: ^(array[Object] output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var body = ; // {array[Object]} 
-var opts = {
-  'disableNotify':  // {oas_any_type_not_mapped} Disables notifications (for bulk inserting)
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.updateBlocks(boardID, body, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class updateBlocksExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var body = new array[Object](); // array[Object] | 
-            var disableNotify = new oas_any_type_not_mapped(); // oas_any_type_not_mapped | Disables notifications (for bulk inserting) (optional)  (default to null)
-
-            try {
-                array[Object] result = apiInstance.updateBlocks(boardID, body, disableNotify);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.updateBlocks: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$body = ; // array[Object] | 
-$disableNotify = ; // oas_any_type_not_mapped | Disables notifications (for bulk inserting)
-
-try {
-    $result = $api_instance->updateBlocks($boardID, $body, $disableNotify);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->updateBlocks: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $body = [WWW::OPenAPIClient::Object::array[Object]->new()]; # array[Object] | 
-my $disableNotify = ; # oas_any_type_not_mapped | Disables notifications (for bulk inserting)
-
-eval {
-    my $result = $api_instance->updateBlocks(boardID => $boardID, body => $body, disableNotify => $disableNotify);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->updateBlocks: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-body =  # array[Object] | 
-disableNotify =  # oas_any_type_not_mapped | Disables notifications (for bulk inserting) (optional) (default to null)
-
-try:
-    api_response = api_instance.update_blocks(boardID, body, disableNotify=disableNotify)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->updateBlocks: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let body = ; // array[Object]
-    let disableNotify = ; // oas_any_type_not_mapped
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.updateBlocks(boardID, body, disableNotify, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

array of blocks to insert or update

- -
-
- - -
Query parameters
- - - - - - - - - -
NameDescription
disable_notify - - -
-
-
- - oas_any_type_not_mapped - - -
-Disables notifications (for bulk inserting) -
-
-
-
-
- -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

updateCategory

-

-
-
-
-

-

Create a category for boards

-

-
-
/teams/{teamID}/categories/{categoryID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PUT \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/teams/{teamID}/categories/{categoryID}" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.updateCategory(teamID, categoryID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.updateCategory(teamID, categoryID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateCategory");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *categoryID = categoryID_example; // Category ID (default to null)
-Object *body = Object; // 
-
-[apiInstance updateCategoryWith:teamID
-    categoryID:categoryID
-    body:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var categoryID = categoryID_example; // {String} Category ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.updateCategory(teamID, categoryID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class updateCategoryExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var categoryID = categoryID_example;  // String | Category ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.updateCategory(teamID, categoryID, body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.updateCategory: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$categoryID = categoryID_example; // String | Category ID
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->updateCategory($teamID, $categoryID, $body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->updateCategory: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $categoryID = categoryID_example; # String | Category ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->updateCategory(teamID => $teamID, categoryID => $categoryID, body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->updateCategory: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-categoryID = categoryID_example # String | Category ID (default to null)
-body = Object # Object | 
-
-try:
-    api_response = api_instance.update_category(teamID, categoryID, body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->updateCategory: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let categoryID = categoryID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.updateCategory(teamID, categoryID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
categoryID* - - -
-
-
- - String - - -
-Category ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

category to update

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

updateCategoryBoard

-

-
-
-
-

-

Set the category of a board

-

-
-
/teams/{teamID}/categories/{categoryID}/boards/{boardID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- "http://localhost/api/v2/teams/{teamID}/categories/{categoryID}/boards/{boardID}"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.updateCategoryBoard(teamID, categoryID, boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateCategoryBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | Team ID
-        String categoryID = categoryID_example; // String | Category ID
-        String boardID = boardID_example; // String | Board ID
-
-        try {
-            apiInstance.updateCategoryBoard(teamID, categoryID, boardID);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateCategoryBoard");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // Team ID (default to null)
-String *categoryID = categoryID_example; // Category ID (default to null)
-String *boardID = boardID_example; // Board ID (default to null)
-
-[apiInstance updateCategoryBoardWith:teamID
-    categoryID:categoryID
-    boardID:boardID
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} Team ID
-var categoryID = categoryID_example; // {String} Category ID
-var boardID = boardID_example; // {String} Board ID
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.updateCategoryBoard(teamID, categoryID, boardID, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class updateCategoryBoardExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | Team ID (default to null)
-            var categoryID = categoryID_example;  // String | Category ID (default to null)
-            var boardID = boardID_example;  // String | Board ID (default to null)
-
-            try {
-                apiInstance.updateCategoryBoard(teamID, categoryID, boardID);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.updateCategoryBoard: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | Team ID
-$categoryID = categoryID_example; // String | Category ID
-$boardID = boardID_example; // String | Board ID
-
-try {
-    $api_instance->updateCategoryBoard($teamID, $categoryID, $boardID);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->updateCategoryBoard: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | Team ID
-my $categoryID = categoryID_example; # String | Category ID
-my $boardID = boardID_example; # String | Board ID
-
-eval {
-    $api_instance->updateCategoryBoard(teamID => $teamID, categoryID => $categoryID, boardID => $boardID);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->updateCategoryBoard: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | Team ID (default to null)
-categoryID = categoryID_example # String | Category ID (default to null)
-boardID = boardID_example # String | Board ID (default to null)
-
-try:
-    api_instance.update_category_board(teamID, categoryID, boardID)
-except ApiException as e:
-    print("Exception when calling DefaultApi->updateCategoryBoard: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let categoryID = categoryID_example; // String
-    let boardID = boardID_example; // String
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.updateCategoryBoard(teamID, categoryID, boardID, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-Team ID -
-
-
- Required -
-
-
-
categoryID* - - -
-
-
- - String - - -
-Category ID -
-
-
- Required -
-
-
-
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

updateMember

-

-
-
-
-

-

Updates a board member

-

-
-
/boards/{boardID}/members/{userID}
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PUT \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/boards/{boardID}/members/{userID}" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.updateMember(boardID, userID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateMember");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String boardID = boardID_example; // String | Board ID
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            Object result = apiInstance.updateMember(boardID, userID, body);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateMember");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *boardID = boardID_example; // Board ID (default to null)
-String *userID = userID_example; // User ID (default to null)
-Object *body = Object; // 
-
-[apiInstance updateMemberWith:boardID
-    userID:userID
-    body:body
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var boardID = boardID_example; // {String} Board ID
-var userID = userID_example; // {String} User ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.updateMember(boardID, userID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class updateMemberExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var userID = userID_example;  // String | User ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                Object result = apiInstance.updateMember(boardID, userID, body);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.updateMember: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$boardID = boardID_example; // String | Board ID
-$userID = userID_example; // String | User ID
-$body = Object; // Object | 
-
-try {
-    $result = $api_instance->updateMember($boardID, $userID, $body);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->updateMember: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $boardID = boardID_example; # String | Board ID
-my $userID = userID_example; # String | User ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    my $result = $api_instance->updateMember(boardID => $boardID, userID => $userID, body => $body);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->updateMember: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-boardID = boardID_example # String | Board ID (default to null)
-userID = userID_example # String | User ID (default to null)
-body = Object # Object | 
-
-try:
-    api_response = api_instance.update_member(boardID, userID, body)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->updateMember: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let boardID = boardID_example; // String
-    let userID = userID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.updateMember(boardID, userID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

membership to replace the current one with

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

updateUserConfig

-

-
-
-
-

-

Updates user config

-

-
-
/users/{userID}/config
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X PATCH \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- "http://localhost/api/v2/users/{userID}/config" \
- -d ''
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.updateUserConfig(userID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateUserConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String userID = userID_example; // String | User ID
-        Object body = Object; // Object | 
-
-        try {
-            apiInstance.updateUserConfig(userID, body);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#updateUserConfig");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *userID = userID_example; // User ID (default to null)
-Object *body = Object; // 
-
-[apiInstance updateUserConfigWith:userID
-    body:body
-              completionHandler: ^(NSError* error) {
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var userID = userID_example; // {String} User ID
-var body = Object; // {Object} 
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully.');
-  }
-};
-api.updateUserConfig(userID, body, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class updateUserConfigExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var userID = userID_example;  // String | User ID (default to null)
-            var body = Object;  // Object | 
-
-            try {
-                apiInstance.updateUserConfig(userID, body);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.updateUserConfig: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$userID = userID_example; // String | User ID
-$body = Object; // Object | 
-
-try {
-    $api_instance->updateUserConfig($userID, $body);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->updateUserConfig: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $userID = userID_example; # String | User ID
-my $body = WWW::OPenAPIClient::Object::Object->new(); # Object | 
-
-eval {
-    $api_instance->updateUserConfig(userID => $userID, body => $body);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->updateUserConfig: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-userID = userID_example # String | User ID (default to null)
-body = Object # Object | 
-
-try:
-    api_instance.update_user_config(userID, body)
-except ApiException as e:
-    print("Exception when calling DefaultApi->updateUserConfig: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let userID = userID_example; // String
-    let body = Object; // Object
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.updateUserConfig(userID, body, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - -
NameDescription
userID* - - -
-
-
- - String - - -
-User ID -
-
-
- Required -
-
-
-
- - -
Body parameters
- - - - - - - - - -
NameDescription
body * -

User config patch to apply

- -
-
- - - -

Responses

-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
-
-

uploadFile

-

-
-
-
-

-

Upload a binary file, attached to a root block

-

-
-
/teams/{teamID}/boards/{boardID}/files
-

-

Usage and SDK Samples

-

- - -
-
-
curl -X POST \
--H "Authorization: [[apiKey]]" \
- -H "Accept: application/json" \
- -H "Content-Type: multipart/form-data" \
- "http://localhost/api/v2/teams/{teamID}/boards/{boardID}/files"
-
-
-
-
import org.openapitools.client.*;
-import org.openapitools.client.auth.*;
-import org.openapitools.client.model.*;
-import org.openapitools.client.api.DefaultApi;
-
-import java.io.File;
-import java.util.*;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        ApiClient defaultClient = Configuration.getDefaultApiClient();
-        
-        // Configure API key authorization: BearerAuth
-        ApiKeyAuth BearerAuth = (ApiKeyAuth) defaultClient.getAuthentication("BearerAuth");
-        BearerAuth.setApiKey("YOUR API KEY");
-        // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-        //BearerAuth.setApiKeyPrefix("Token");
-
-        // Create an instance of the API class
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | ID of the team
-        String boardID = boardID_example; // String | Board ID
-        File uploaded file = BINARY_DATA_HERE; // File | The file to upload
-
-        try {
-            Object result = apiInstance.uploadFile(teamID, boardID, uploaded file);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#uploadFile");
-            e.printStackTrace();
-        }
-    }
-}
-
-
- -
-
import org.openapitools.client.api.DefaultApi;
-
-public class DefaultApiExample {
-    public static void main(String[] args) {
-        DefaultApi apiInstance = new DefaultApi();
-        String teamID = teamID_example; // String | ID of the team
-        String boardID = boardID_example; // String | Board ID
-        File uploaded file = BINARY_DATA_HERE; // File | The file to upload
-
-        try {
-            Object result = apiInstance.uploadFile(teamID, boardID, uploaded file);
-            System.out.println(result);
-        } catch (ApiException e) {
-            System.err.println("Exception when calling DefaultApi#uploadFile");
-            e.printStackTrace();
-        }
-    }
-}
-
- -
-
Configuration *apiConfig = [Configuration sharedConfig];
-
-// Configure API key authorization: (authentication scheme: BearerAuth)
-[apiConfig setApiKey:@"YOUR_API_KEY" forApiKeyIdentifier:@"Authorization"];
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//[apiConfig setApiKeyPrefix:@"Bearer" forApiKeyIdentifier:@"Authorization"];
-
-
-// Create an instance of the API class
-DefaultApi *apiInstance = [[DefaultApi alloc] init];
-String *teamID = teamID_example; // ID of the team (default to null)
-String *boardID = boardID_example; // Board ID (default to null)
-File *uploaded file = BINARY_DATA_HERE; // The file to upload (optional) (default to null)
-
-[apiInstance uploadFileWith:teamID
-    boardID:boardID
-    uploaded file:uploaded file
-              completionHandler: ^(Object output, NSError* error) {
-    if (output) {
-        NSLog(@"%@", output);
-    }
-    if (error) {
-        NSLog(@"Error: %@", error);
-    }
-}];
-
-
- -
-
var FocalboardServer = require('focalboard_server');
-var defaultClient = FocalboardServer.ApiClient.instance;
-
-// Configure API key authorization: BearerAuth
-var BearerAuth = defaultClient.authentications['BearerAuth'];
-BearerAuth.apiKey = "YOUR API KEY";
-// Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null)
-//BearerAuth.apiKeyPrefix['Authorization'] = "Token";
-
-// Create an instance of the API class
-var api = new FocalboardServer.DefaultApi()
-var teamID = teamID_example; // {String} ID of the team
-var boardID = boardID_example; // {String} Board ID
-var opts = {
-  'uploaded file': BINARY_DATA_HERE // {File} The file to upload
-};
-
-var callback = function(error, data, response) {
-  if (error) {
-    console.error(error);
-  } else {
-    console.log('API called successfully. Returned data: ' + data);
-  }
-};
-api.uploadFile(teamID, boardID, opts, callback);
-
-
- - -
-
using System;
-using System.Diagnostics;
-using Org.OpenAPITools.Api;
-using Org.OpenAPITools.Client;
-using Org.OpenAPITools.Model;
-
-namespace Example
-{
-    public class uploadFileExample
-    {
-        public void main()
-        {
-            // Configure API key authorization: BearerAuth
-            Configuration.Default.ApiKey.Add("Authorization", "YOUR_API_KEY");
-            // Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-            // Configuration.Default.ApiKeyPrefix.Add("Authorization", "Bearer");
-
-            // Create an instance of the API class
-            var apiInstance = new DefaultApi();
-            var teamID = teamID_example;  // String | ID of the team (default to null)
-            var boardID = boardID_example;  // String | Board ID (default to null)
-            var uploaded file = BINARY_DATA_HERE;  // File | The file to upload (optional)  (default to null)
-
-            try {
-                Object result = apiInstance.uploadFile(teamID, boardID, uploaded file);
-                Debug.WriteLine(result);
-            } catch (Exception e) {
-                Debug.Print("Exception when calling DefaultApi.uploadFile: " + e.Message );
-            }
-        }
-    }
-}
-
-
- -
-
<?php
-require_once(__DIR__ . '/vendor/autoload.php');
-
-// Configure API key authorization: BearerAuth
-OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authorization', 'YOUR_API_KEY');
-// Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-// OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKeyPrefix('Authorization', 'Bearer');
-
-// Create an instance of the API class
-$api_instance = new OpenAPITools\Client\Api\DefaultApi();
-$teamID = teamID_example; // String | ID of the team
-$boardID = boardID_example; // String | Board ID
-$uploaded file = BINARY_DATA_HERE; // File | The file to upload
-
-try {
-    $result = $api_instance->uploadFile($teamID, $boardID, $uploaded file);
-    print_r($result);
-} catch (Exception $e) {
-    echo 'Exception when calling DefaultApi->uploadFile: ', $e->getMessage(), PHP_EOL;
-}
-?>
-
- -
-
use Data::Dumper;
-use WWW::OPenAPIClient::Configuration;
-use WWW::OPenAPIClient::DefaultApi;
-
-# Configure API key authorization: BearerAuth
-$WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
-# uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-#$WWW::OPenAPIClient::Configuration::api_key_prefix->{'Authorization'} = "Bearer";
-
-# Create an instance of the API class
-my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
-my $teamID = teamID_example; # String | ID of the team
-my $boardID = boardID_example; # String | Board ID
-my $uploaded file = BINARY_DATA_HERE; # File | The file to upload
-
-eval {
-    my $result = $api_instance->uploadFile(teamID => $teamID, boardID => $boardID, uploaded file => $uploaded file);
-    print Dumper($result);
-};
-if ($@) {
-    warn "Exception when calling DefaultApi->uploadFile: $@\n";
-}
-
- -
-
from __future__ import print_statement
-import time
-import openapi_client
-from openapi_client.rest import ApiException
-from pprint import pprint
-
-# Configure API key authorization: BearerAuth
-openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
-# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-# openapi_client.configuration.api_key_prefix['Authorization'] = 'Bearer'
-
-# Create an instance of the API class
-api_instance = openapi_client.DefaultApi()
-teamID = teamID_example # String | ID of the team (default to null)
-boardID = boardID_example # String | Board ID (default to null)
-uploaded file = BINARY_DATA_HERE # File | The file to upload (optional) (default to null)
-
-try:
-    api_response = api_instance.upload_file(teamID, boardID, uploaded file=uploaded file)
-    pprint(api_response)
-except ApiException as e:
-    print("Exception when calling DefaultApi->uploadFile: %s\n" % e)
-
- -
-
extern crate DefaultApi;
-
-pub fn main() {
-    let teamID = teamID_example; // String
-    let boardID = boardID_example; // String
-    let uploaded file = BINARY_DATA_HERE; // File
-
-    let mut context = DefaultApi::Context::default();
-    let result = client.uploadFile(teamID, boardID, uploaded file, &context).wait();
-
-    println!("{:?}", result);
-}
-
-
-
- -

Scopes

- - -
- -

Parameters

- -
Path parameters
- - - - - - - - - - - - - -
NameDescription
teamID* - - -
-
-
- - String - - -
-ID of the team -
-
-
- Required -
-
-
-
boardID* - - -
-
-
- - String - - -
-Board ID -
-
-
- Required -
-
-
-
- - - -
Form parameters
- - - - - - - - - -
NameDescription
uploaded file - - -
-
-
- - File - - - (binary) - - -
-The file to upload -
-
-
-
-
- - -

Responses

-

-

- - - - - - -
-
-
- -
- -
-
-

-

- - - - - - -
-
-

-

- - - - - - -
-
-
- -
- -
-
-
-
-
-
-
- -
-
-
- - - - - - - - - - - - - - diff --git a/server/boards/swagger/swagger.yml b/server/boards/swagger/swagger.yml deleted file mode 100644 index 38800c1eec..0000000000 --- a/server/boards/swagger/swagger.yml +++ /dev/null @@ -1,2023 +0,0 @@ -basePath: /api/v2 -consumes: - - application/json -definitions: - Block: - description: Block is the basic data unit - x-go-package: github.com/mattermost/focalboard/server/model - BlockPatch: - description: BlockPatch is a patch for modify blocks - x-go-package: github.com/mattermost/focalboard/server/model - BlockPatchBatch: - description: BlockPatchBatch is a batch of IDs and patches for modify blocks - x-go-package: github.com/mattermost/focalboard/server/model - Board: - description: Board groups a set of blocks and its layout - x-go-package: github.com/mattermost/focalboard/server/model - BoardInsight: - description: BoardInsight gives insight into activities in a Board - x-go-package: github.com/mattermost/focalboard/server/model - BoardMember: - description: BoardMember stores the information of the membership of a user on a board - x-go-package: github.com/mattermost/focalboard/server/model - BoardMemberHistoryEntry: - description: BoardMemberHistoryEntry stores the information of the membership of a user on a board - x-go-package: github.com/mattermost/focalboard/server/model - BoardMetadata: - description: BoardMetadata contains metadata for a Board - x-go-package: github.com/mattermost/focalboard/server/model - BoardPatch: - description: BoardPatch is a patch for modify boards - x-go-package: github.com/mattermost/focalboard/server/model - BoardsAndBlocks: - description: |- - BoardsAndBlocks is used to operate over boards and blocks at the - same time - x-go-package: github.com/mattermost/focalboard/server/model - BoardsCloudLimits: - description: |- - BoardsCloudLimits is the representation of the limits for the - Boards server - x-go-package: github.com/mattermost/focalboard/server/model - BoardsStatistics: - description: BoardsStatistics is the representation of the statistics for the Boards server - x-go-package: github.com/mattermost/focalboard/server/model - Card: - title: Card represents a group of content blocks and properties. - x-go-package: github.com/mattermost/focalboard/server/model - CardPatch: - description: CardPatch is a patch for modifying cards - x-go-package: github.com/mattermost/focalboard/server/model - Category: - description: Category is a board category - x-go-package: github.com/mattermost/focalboard/server/model - CategoryBoards: - description: CategoryBoards is a board category and associated boards - x-go-package: github.com/mattermost/focalboard/server/model - ChangePasswordRequest: - description: ChangePasswordRequest is a user password change request - x-go-package: github.com/mattermost/focalboard/server/model - ClientConfig: - description: ClientConfig is the client configuration - x-go-package: github.com/mattermost/focalboard/server/model - DeleteBoardsAndBlocks: - description: |- - DeleteBoardsAndBlocks is used to list the boards and blocks to - delete on a request - x-go-package: github.com/mattermost/focalboard/server/model - ErrorResponse: - description: ErrorResponse is an error response - x-go-package: github.com/mattermost/focalboard/server/model - FileUploadResponse: - description: FileUploadResponse is the response to a file upload - x-go-package: github.com/mattermost/focalboard/server/api - LoginRequest: - description: LoginRequest is a login request - x-go-package: github.com/mattermost/focalboard/server/model - LoginResponse: - description: LoginResponse is a login response - x-go-package: github.com/mattermost/focalboard/server/model - NotificationHint: - description: |- - NotificationHint provides a hint that a block has been modified and has subscribers that - should be notified. - x-go-package: github.com/mattermost/focalboard/server/model - PatchBoardsAndBlocks: - description: |- - PatchBoardsAndBlocks is used to patch multiple boards and blocks on - a single request - x-go-package: github.com/mattermost/focalboard/server/model - RegisterRequest: - description: RegisterRequest is a user registration request - x-go-package: github.com/mattermost/focalboard/server/model - Sharing: - description: Sharing is sharing information for a root block - x-go-package: github.com/mattermost/focalboard/server/model - Subscriber: - description: Subscriber is an entity (e.g. user, channel) that can subscribe to events from boards, cards, etc - x-go-package: github.com/mattermost/focalboard/server/model - Subscription: - title: Subscription is a subscription to a board, card, etc, for a user or channel. - x-go-package: github.com/mattermost/focalboard/server/model - Team: - description: Team is information global to a team - x-go-package: github.com/mattermost/focalboard/server/model - User: - description: User is a user - x-go-package: github.com/mattermost/focalboard/server/model - UserPreferencesPatch: - description: UserPreferencesPatch is a user property patch - x-go-package: github.com/mattermost/focalboard/server/model -host: localhost -info: - contact: - email: api@focalboard.com - name: Focalboard - url: https://www.focalboard.com - description: Focalboard Server - license: - name: Custom - url: https://github.com/mattermost/focalboard/blob/main/LICENSE.txt - title: Focalboard Server - version: 2.0.0 -paths: - /api/v2/teams/{teamID}/notifyadminupgrade: - get: - operationId: handleNotifyAdminUpgrade - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Notifies admins for upgrade request. - /boards: - post: - description: Creates a new board - operationId: createBoard - parameters: - - description: the board to create - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Board' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Board' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards-and-blocks: - delete: - description: Deletes boards and blocks - operationId: deleteBoardsAndBlocks - parameters: - - description: the boards and blocks to delete - in: body - name: Body - required: true - schema: - $ref: '#/definitions/DeleteBoardsAndBlocks' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - patch: - description: Patches a set of related boards and blocks - operationId: patchBoardsAndBlocks - parameters: - - description: the patches for the boards and blocks - in: body - name: Body - required: true - schema: - $ref: '#/definitions/PatchBoardsAndBlocks' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardsAndBlocks' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: Creates new boards and blocks - operationId: insertBoardsAndBlocks - parameters: - - description: the boards and blocks to create - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BoardsAndBlocks' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardsAndBlocks' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}: - delete: - description: Removes a board - operationId: deleteBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - get: - description: Returns a board - operationId: getBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Board' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - patch: - description: Partially updates a board - operationId: patchBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: board patch to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BoardPatch' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Board' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/archive/export: - get: - operationId: archiveExportBoard - parameters: - - description: Id of board to export - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Exports an archive of all blocks for one boards. - /boards/{boardID}/blocks: - get: - description: Returns blocks - operationId: getBlocks - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: ID of parent block, omit to specify all blocks - in: query - name: parent_id - type: string - - description: Type of blocks to return, omit to specify all types - in: query - name: type - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: |- - Insert blocks. The specified IDs will only be used to link - blocks with existing ones, the rest will be replaced by server - generated IDs - operationId: updateBlocks - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: Disables notifications (for bulk inserting) - in: query - name: disable_notify - type: bool - - description: array of blocks to insert or update - in: body - name: Body - required: true - schema: - items: - $ref: '#/definitions/Block' - type: array - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/blocks/: - patch: - description: Partially updates batch of blocks - operationId: patchBlocks - parameters: - - description: Workspace ID - in: path - name: boardID - required: true - type: string - - description: Disables notifications (for bulk patching) - in: query - name: disable_notify - type: bool - - description: block Ids and block patches to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BlockPatchBatch' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/blocks/{blockID}: - delete: - description: Deletes a block - operationId: deleteBlock - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: ID of block to delete - in: path - name: blockID - required: true - type: string - - description: Disables notifications (for bulk deletion) - in: query - name: disable_notify - type: bool - produces: - - application/json - responses: - "200": - description: success - "404": - description: block not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - patch: - description: Partially updates a block - operationId: patchBlock - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: ID of block to patch - in: path - name: blockID - required: true - type: string - - description: Disables notifications (for bulk patching) - in: query - name: disable_notify - type: bool - - description: block patch to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BlockPatch' - produces: - - application/json - responses: - "200": - description: success - "404": - description: block not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/blocks/{blockID}/duplicate: - post: - description: Returns the new created blocks - operationId: duplicateBlock - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: Block ID - in: path - name: blockID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Block' - type: array - "404": - description: board or block not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /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: [] - /boards/{boardID}/cards: - get: - operationId: getCards - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: The page to select (default=0) - in: query - name: page - type: integer - - description: Number of cards to return per page(default=100) - in: query - name: per_page - type: integer - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Card' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Fetches cards for the specified board. - post: - operationId: createCard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: the card to create - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Card' - - description: Disables notifications (for bulk data inserting) - in: query - name: disable_notify - type: bool - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Card' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Creates a new card for the specified board. - /boards/{boardID}/duplicate: - post: - description: Returns the new created board and all the blocks - operationId: duplicateBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardsAndBlocks' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/join: - post: - description: Become a member of a board - operationId: joinBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardMember' - "403": - description: access denied - "404": - description: board not found - 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 - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/members: - get: - description: Returns the members of the board - operationId: getMembersForBoard - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/BoardMember' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: Adds a new member to a board - operationId: addMember - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: membership to replace the current one with - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BoardMember' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardMember' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/members/{userID}: - delete: - description: Deletes a member from a board - operationId: deleteMember - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: User ID - in: path - name: userID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - put: - description: Updates a board member - operationId: updateMember - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: User ID - in: path - name: userID - required: true - type: string - - description: membership to replace the current one with - in: body - name: Body - required: true - schema: - $ref: '#/definitions/BoardMember' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardMember' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/metadata: - get: - description: Returns a board's metadata - operationId: getBoardMetadata - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardMetadata' - "404": - description: board not found - "501": - description: required license not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/sharing: - get: - description: Returns sharing information for a board - operationId: getSharing - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Sharing' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: Sets sharing information for a board - operationId: postSharing - parameters: - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: sharing information for a root block - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Sharing' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/{boardID}/undelete: - post: - description: Undeletes a board - operationId: undeleteBoard - parameters: - - description: ID of board to undelete - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /boards/search: - get: - description: Returns the boards that match with a search term - operationId: searchAllBoards - parameters: - - description: The search term. Must have at least one character - in: query - name: q - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Board' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /cards/{cardID}: - get: - operationId: getCard - parameters: - - description: Card ID - in: path - name: cardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Card' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Fetches the specified card. - /cards/{cardID}/cards: - patch: - operationId: patchCard - parameters: - - description: Card ID - in: path - name: cardID - required: true - type: string - - description: the card patch - in: body - name: Body - required: true - schema: - $ref: '#/definitions/CardPatch' - - description: Disables notifications (for bulk data patching) - in: query - name: disable_notify - type: bool - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Card' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Patches the specified card. - /clientConfig: - get: - description: Returns the client configuration - operationId: getClientConfig - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/ClientConfig' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - /files/teams/{teamID}/{boardID}/{filename}: - get: - description: Returns the contents of an uploaded file - operationId: getFile - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: name of the file - in: path - name: filename - required: true - type: string - produces: - - application/json - - image/jpg - - image/png - - image/gif - responses: - "200": - description: success - "404": - description: file not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /hello: - get: - operationId: hello - produces: - - text/plain - responses: - "200": - description: success - summary: Responds with `Hello` if the web service is running. - /limits: - get: - operationId: cloudLimits - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardsCloudLimits' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Fetches the cloud limits of the server. - /login: - post: - description: Login user - operationId: login - parameters: - - description: Login request - in: body - name: body - required: true - schema: - $ref: '#/definitions/LoginRequest' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/LoginResponse' - "401": - description: invalid login - schema: - $ref: '#/definitions/ErrorResponse' - "500": - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - /logout: - post: - description: Logout user - operationId: logout - produces: - - application/json - responses: - "200": - description: success - "500": - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /ping: - get: - operationId: ping - produces: - - application/json - responses: - "200": - description: success - summary: Responds with server metadata if the web service is running. - /register: - post: - description: Register new user - operationId: register - parameters: - - description: Register request - in: body - name: body - required: true - schema: - $ref: '#/definitions/RegisterRequest' - produces: - - application/json - responses: - "200": - description: success - "401": - description: invalid registration token - "500": - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - /statistics: - get: - operationId: handleStatistics - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/BoardStatistics' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Fetches the statistic of the server. - /subscriptions: - post: - operationId: createSubscription - parameters: - - description: subscription definition - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Subscription' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/User' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Creates a subscription to a block for a user. The user will receive change notifications for the block. - /subscriptions/{blockID}/{subscriberID}: - delete: - operationId: deleteSubscription - parameters: - - description: Block ID - in: path - name: blockID - required: true - type: string - - description: Subscriber ID - in: path - name: subscriberID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block. - /subscriptions/{subscriberID}: - get: - operationId: getSubscriptions - parameters: - - description: Subscriber ID - in: path - name: subscriberID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/User' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Gets subscriptions for a user. - /team/{teamID}/onboard: - post: - operationId: onboard - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - properties: - boardID: - description: Board ID - type: string - teamID: - description: Team ID - type: string - type: object - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Onboards a user on Boards. - /teams: - get: - description: Returns information of all the teams - operationId: getTeams - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Team' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}: - get: - description: Returns information of the root team - operationId: getTeam - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Team' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/archive/export: - get: - operationId: archiveExportTeam - parameters: - - description: Id of team - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - summary: Exports an archive of all blocks for all the boards in a team. - /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. - /teams/{teamID}/boards: - get: - description: Returns team boards - operationId: getBoards - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Board' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/boards/{boardID}/files: - post: - consumes: - - multipart/form-data - description: Upload a binary file, attached to a root block - operationId: uploadFile - parameters: - - description: ID of the team - in: path - name: teamID - required: true - type: string - - description: Board ID - in: path - name: boardID - required: true - type: string - - description: The file to upload - in: formData - name: uploaded file - type: file - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/FileUploadResponse' - "404": - description: board not found - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/boards/insights: - get: - description: Returns team boards insights - operationId: handleTeamBoardsInsights - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: duration of data to calculate insights for - in: query - name: time_range - required: true - type: string - - description: page offset for top boards - in: query - name: page - required: true - type: string - - description: limit for boards in a page. - in: query - name: per_page - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/BoardInsight' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/boards/search: - get: - description: Returns the boards that match with a search term in the team - operationId: searchBoards - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: The search term. Must have at least one character - in: query - name: q - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Board' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/boards/search/linkable: - get: - description: |- - Returns the boards that match with a search term in the team and the - user has permission to manage members - operationId: searchLinkableBoards - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: The search term. Must have at least one character - in: query - name: q - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Board' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/categories: - get: - description: Gets the user's board categories - operationId: getUserCategoryBoards - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/CategoryBoards' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - post: - description: Create a category for boards - operationId: createCategory - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: category to create - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Category' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Category' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/categories/{categoryID}: - delete: - description: Delete a category - operationId: deleteCategory - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: Category ID - in: path - name: categoryID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - put: - description: Create a category for boards - operationId: updateCategory - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: Category ID - in: path - name: categoryID - required: true - type: string - - description: category to update - in: body - name: Body - required: true - schema: - $ref: '#/definitions/Category' - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Category' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/categories/{categoryID}/boards/{boardID}: - post: - description: Set the category of a board - operationId: updateCategoryBoard - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: Category ID - in: path - name: categoryID - required: true - type: string - - description: Board ID - in: path - name: boardID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/channels: - get: - description: Returns the user available channels - operationId: searchMyChannels - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: string to filter channels list - in: query - name: search - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Channel' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/channels/{channelID}: - get: - description: Returns the requested channel - operationId: getChannel - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: Channel ID - in: path - name: channelID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Channel' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/regenerate_signup_token: - post: - description: Regenerates the signup token for the root team - operationId: regenerateSignupToken - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/templates: - get: - description: Returns team templates - operationId: getTemplates - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/Board' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /teams/{teamID}/users: - get: - description: Returns team users - operationId: getTeamUsers - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: string to filter users list - in: query - name: search - type: string - - description: exclude bot users - in: query - name: exclude_bots - type: boolean - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/User' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users: - post: - description: Returns a user[] - operationId: getUsersList - parameters: - - description: User ID - in: path - name: userID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/User' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/{userID}: - get: - description: Returns a user - operationId: getUser - parameters: - - description: User ID - in: path - name: userID - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/User' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/{userID}/changepassword: - post: - description: Change a user's password - operationId: changePassword - parameters: - - description: User ID - in: path - name: userID - required: true - type: string - - description: Change password request - in: body - name: body - required: true - schema: - $ref: '#/definitions/ChangePasswordRequest' - produces: - - application/json - responses: - "200": - description: success - "400": - description: invalid request - schema: - $ref: '#/definitions/ErrorResponse' - "500": - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/{userID}/config: - patch: - description: Updates user config - operationId: updateUserConfig - parameters: - - description: User ID - in: path - name: userID - required: true - type: string - - description: User config patch to apply - in: body - name: Body - required: true - schema: - $ref: '#/definitions/UserPreferencesPatch' - produces: - - application/json - responses: - "200": - description: success - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/me: - get: - description: Returns the currently logged-in user - operationId: getMe - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/User' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/me/boards/insights: - get: - description: Returns user boards insights - operationId: getUserBoardsInsights - parameters: - - description: Team ID - in: path - name: teamID - required: true - type: string - - description: duration of data to calculate insights for - in: query - name: time_range - required: true - type: string - - description: page offset for top boards - in: query - name: page - required: true - type: string - - description: limit for boards in a page. - in: query - name: per_page - required: true - type: string - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/BoardInsight' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/me/config: - get: - description: Returns an array of user preferences - operationId: getUserConfig - produces: - - application/json - responses: - "200": - description: success - schema: - $ref: '#/definitions/Preferences' - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] - /users/me/memberships: - get: - description: Returns the currently users board memberships - operationId: getMyMemberships - produces: - - application/json - responses: - "200": - description: success - schema: - items: - $ref: '#/definitions/BoardMember' - type: array - default: - description: internal error - schema: - $ref: '#/definitions/ErrorResponse' - security: - - BearerAuth: [] -produces: - - application/json -schemes: - - http - - https -securityDefinitions: - BearerAuth: - description: 'Pass session token using Bearer authentication, e.g. set header "Authorization: Bearer "' - in: header - name: Authorization - type: apiKey -swagger: "2.0" diff --git a/server/boards/utils/callbackqueue.go b/server/boards/utils/callbackqueue.go deleted file mode 100644 index 89583c0362..0000000000 --- a/server/boards/utils/callbackqueue.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "context" - "runtime/debug" - "sync/atomic" - "time" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// CallbackFunc is a func that can enqueued in the callback queue and will be -// called when dequeued. -type CallbackFunc func() error - -// CallbackQueue provides a simple thread pool for processing callbacks. Callbacks will -// be executed in the order in which they are enqueued, but no guarantees are provided -// regarding the order in which they finish (unless poolSize == 1). -type CallbackQueue struct { - name string - poolSize int - - queue chan CallbackFunc - done chan struct{} - alive chan int - - idone uint32 - - logger mlog.LoggerIFace -} - -// NewCallbackQueue creates a new CallbackQueue and starts a thread pool to service it. -func NewCallbackQueue(name string, queueSize int, poolSize int, logger mlog.LoggerIFace) *CallbackQueue { - cn := &CallbackQueue{ - name: name, - poolSize: poolSize, - queue: make(chan CallbackFunc, queueSize), - done: make(chan struct{}), - alive: make(chan int, poolSize), - logger: logger, - } - - for i := 0; i < poolSize; i++ { - go cn.loop(i) - } - - return cn -} - -// Shutdown stops accepting enqueues and exits all pool threads. This method waits -// as long as the context allows for the threads to exit. -// Returns true if the pool exited, false on timeout. -func (cn *CallbackQueue) Shutdown(context context.Context) bool { - if !atomic.CompareAndSwapUint32(&cn.idone, 0, 1) { - // already shutdown - return true - } - - // signal threads to exit - close(cn.done) - - // wait for the threads to exit or timeout - count := 0 - for count < cn.poolSize { - select { - case <-cn.alive: - count++ - case <-context.Done(): - return false - } - } - - // try to drain any remaining callbacks - for { - select { - case f := <-cn.queue: - cn.exec(f) - case <-context.Done(): - return false - default: - return true - } - } -} - -// Enqueue adds a callback to the queue. -func (cn *CallbackQueue) Enqueue(f CallbackFunc) { - if atomic.LoadUint32(&cn.idone) != 0 { - cn.logger.Debug("CallbackQueue skipping enqueue, notifier is shutdown", mlog.String("name", cn.name)) - return - } - - select { - case cn.queue <- f: - default: - start := time.Now() - cn.queue <- f - dur := time.Since(start) - cn.logger.Warn("CallbackQueue queue backlog", mlog.String("name", cn.name), mlog.Duration("wait_time", dur)) - } -} - -func (cn *CallbackQueue) loop(id int) { - defer func() { - cn.logger.Trace("CallbackQueue thread exited", mlog.String("name", cn.name), mlog.Int("id", id)) - cn.alive <- id - }() - - for { - select { - case f := <-cn.queue: - cn.exec(f) - case <-cn.done: - return - } - } -} - -func (cn *CallbackQueue) exec(f CallbackFunc) { - // don't let a panic in the callback exit the thread. - defer func() { - if r := recover(); r != nil { - stack := debug.Stack() - cn.logger.Error("CallbackQueue callback panic", - mlog.String("name", cn.name), - mlog.Any("panic", r), - mlog.String("stack", string(stack)), - ) - } - }() - - if err := f(); err != nil { - cn.logger.Error("CallbackQueue callback error", mlog.String("name", cn.name), mlog.Err(err)) - } -} diff --git a/server/boards/utils/callbackqueue_test.go b/server/boards/utils/callbackqueue_test.go deleted file mode 100644 index fce8a8b58d..0000000000 --- a/server/boards/utils/callbackqueue_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "context" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func Test_newChangeNotifier(t *testing.T) { - logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) - - t.Run("startup, shutdown", func(t *testing.T) { - cn := NewCallbackQueue("test1", 100, 5, logger) - - var callbackCount int32 - callback := func() error { - atomic.AddInt32(&callbackCount, 1) - return nil - } - - const loops = 500 - for i := 0; i < loops; i++ { - cn.Enqueue(callback) - // don't peg the cpu - if i%20 == 0 { - time.Sleep(time.Millisecond * 1) - } - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - ok := cn.Shutdown(ctx) - assert.True(t, ok, "shutdown should return true (no timeout)") - - assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount)) - }) - - t.Run("handle panic", func(t *testing.T) { - cn := NewCallbackQueue("test2", 100, 5, logger) - - var callbackCount int32 - callback := func() error { - atomic.AddInt32(&callbackCount, 1) - panic("oh no!") - } - - const loops = 5 - for i := 0; i < loops; i++ { - cn.Enqueue(callback) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - ok := cn.Shutdown(ctx) - assert.True(t, ok, "shutdown should return true (no timeout)") - - assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount)) - }) -} diff --git a/server/boards/utils/debug.go b/server/boards/utils/debug.go deleted file mode 100644 index 82e869b769..0000000000 --- a/server/boards/utils/debug.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "os" - "strings" -) - -// IsRunningUnitTests returns true if this instance of FocalBoard is running unit or integration tests. -func IsRunningUnitTests() bool { - testing := os.Getenv("FOCALBOARD_UNIT_TESTING") - if testing == "" { - return false - } - - switch strings.ToLower(testing) { - case "1", "t", "y", "true", "yes": - return true - } - return false -} diff --git a/server/boards/utils/links.go b/server/boards/utils/links.go deleted file mode 100644 index 06c8b6c4fa..0000000000 --- a/server/boards/utils/links.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import "fmt" - -// MakeCardLink creates fully qualified card links based on card id and parents. -func MakeCardLink(serverRoot string, teamID string, boardID string, cardID string) string { - return fmt.Sprintf("%s/team/%s/%s/0/%s", serverRoot, teamID, boardID, cardID) -} - -func MakeBoardLink(serverRoot string, teamID string, board string) string { - return fmt.Sprintf("%s/team/%s/%s", serverRoot, teamID, board) -} diff --git a/server/boards/utils/testUtils.go b/server/boards/utils/testUtils.go deleted file mode 100644 index 91afbba1bb..0000000000 --- a/server/boards/utils/testUtils.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import "github.com/stretchr/testify/mock" - -var Anything = mock.MatchedBy(func(interface{}) bool { return true }) diff --git a/server/boards/utils/utils.go b/server/boards/utils/utils.go deleted file mode 100644 index 89e916f88b..0000000000 --- a/server/boards/utils/utils.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "encoding/json" - "path" - "reflect" - "time" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -type IDType byte - -const ( - IDTypeNone IDType = '7' - IDTypeTeam IDType = 't' - IDTypeBoard IDType = 'b' - IDTypeCard IDType = 'c' - IDTypeView IDType = 'v' - IDTypeSession IDType = 's' - IDTypeUser IDType = 'u' - IDTypeToken IDType = 'k' - IDTypeBlock IDType = 'a' - IDTypeAttachment IDType = 'i' -) - -// NewId is a globally unique identifier. It is a [A-Z0-9] string 27 -// characters long. It is a UUID version 4 Guid that is zbased32 encoded -// with the padding stripped off, and a one character alpha prefix indicating the -// type of entity or a `7` if unknown type. -func NewID(idType IDType) string { - return string(idType) + mm_model.NewId() -} - -// GetMillis is a convenience method to get milliseconds since epoch. -func GetMillis() int64 { - return mm_model.GetMillis() -} - -// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. -func GetMillisForTime(thisTime time.Time) int64 { - return mm_model.GetMillisForTime(thisTime) -} - -// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch. -func GetTimeForMillis(millis int64) time.Time { - return mm_model.GetTimeForMillis(millis) -} - -// SecondsToMillis is a convenience method to convert seconds to milliseconds. -func SecondsToMillis(seconds int64) int64 { - return seconds * 1000 -} - -func StructToMap(v interface{}) (m map[string]interface{}) { - b, _ := json.Marshal(v) - _ = json.Unmarshal(b, &m) - return -} - -func intersection(a []interface{}, b []interface{}) []interface{} { - set := make([]interface{}, 0) - hash := make(map[interface{}]bool) - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - - for i := 0; i < av.Len(); i++ { - el := av.Index(i).Interface() - hash[el] = true - } - - for i := 0; i < bv.Len(); i++ { - el := bv.Index(i).Interface() - if _, found := hash[el]; found { - set = append(set, el) - } - } - - return set -} - -func Intersection(x ...[]interface{}) []interface{} { - if len(x) == 0 { - return nil - } - - if len(x) == 1 { - return x[0] - } - - result := x[0] - i := 1 - for i < len(x) { - result = intersection(result, x[i]) - i++ - } - - return result -} - -func IsCloudLicense(license *mm_model.License) bool { - return license != nil && - license.Features != nil && - license.Features.Cloud != nil && - *license.Features.Cloud -} - -func DedupeStringArr(arr []string) []string { - hashMap := map[string]bool{} - - for _, item := range arr { - hashMap[item] = true - } - - dedupedArr := make([]string, len(hashMap)) - i := 0 - for key := range hashMap { - dedupedArr[i] = key - i++ - } - - return dedupedArr -} - -func GetBaseFilePath() string { - return path.Join("boards", time.Now().Format("20060102")) -} diff --git a/server/boards/web/webserver.go b/server/boards/web/webserver.go deleted file mode 100644 index fe5726cb55..0000000000 --- a/server/boards/web/webserver.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package web - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strings" - "text/template" - - "github.com/gorilla/mux" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -// RoutedService defines the interface that is needed for any service to -// register themself in the web server to provide new endpoints. (see -// AddRoutes). -type RoutedService interface { - RegisterRoutes(*mux.Router) -} - -// Server is the structure responsible for managing our http web server. -type Server struct { - http.Server - - baseURL string - rootPath string - basePrefix string - port int - ssl bool - logger mlog.LoggerIFace -} - -// NewServer creates a new instance of the webserver. -func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool, logger mlog.LoggerIFace) *Server { - r := mux.NewRouter() - - basePrefix := os.Getenv("FOCALBOARD_HTTP_SERVER_BASEPATH") - if basePrefix != "" { - r = r.PathPrefix(basePrefix).Subrouter() - } - - var addr string - if localOnly { - addr = fmt.Sprintf(`localhost:%d`, port) - } else { - addr = fmt.Sprintf(`:%d`, port) - } - - baseURL := "" - url, err := url.Parse(serverRoot) - if err != nil { - logger.Error("Invalid ServerRoot setting", mlog.Err(err)) - } - baseURL = url.Path - - ws := &Server{ - // (TODO: Add ReadHeaderTimeout) - Server: http.Server{ //nolint:gosec - Addr: addr, - Handler: r, - }, - baseURL: baseURL, - rootPath: rootPath, - port: port, - ssl: ssl, - logger: logger, - basePrefix: basePrefix, - } - - return ws -} - -func (ws *Server) Router() *mux.Router { - return ws.Server.Handler.(*mux.Router) -} - -// AddRoutes allows services to register themself in the webserver router and provide new endpoints. -func (ws *Server) AddRoutes(rs RoutedService) { - rs.RegisterRoutes(ws.Router()) -} - -func (ws *Server) registerRoutes() { - ws.Router().PathPrefix("/static").Handler(http.StripPrefix(ws.basePrefix+"/static/", http.FileServer(http.Dir(filepath.Join(ws.rootPath, "static"))))) - ws.Router().PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - indexTemplate, err := template.New("index").ParseFiles(path.Join(ws.rootPath, "index.html")) - if err != nil { - ws.logger.Log(errorOrWarn(), "Unable to serve the index.html file", mlog.Err(err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - err = indexTemplate.ExecuteTemplate(w, "index.html", map[string]string{"BaseURL": ws.baseURL}) - if err != nil { - ws.logger.Log(errorOrWarn(), "Unable to serve the index.html file", mlog.Err(err)) - w.WriteHeader(http.StatusInternalServerError) - return - } - }) -} - -// Start runs the web server and start listening for connections. -func (ws *Server) Start() { - ws.registerRoutes() - if ws.port == -1 { - ws.logger.Debug("server not bind to any port") - return - } - - isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem") - if isSSL { - ws.logger.Info("https server started", mlog.Int("port", ws.port)) - go func() { - if err := ws.ListenAndServeTLS("./cert/cert.pem", "./cert/key.pem"); err != nil { - ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err)) - } - }() - - return - } - - ws.logger.Info("http server started", mlog.Int("port", ws.port)) - go func() { - if err := ws.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err)) - } - ws.logger.Info("http server stopped") - }() -} - -func (ws *Server) Shutdown() error { - return ws.Close() -} - -// fileExists returns true if a file exists at the path. -func fileExists(path string) bool { - _, err := os.Stat(path) - if os.IsNotExist(err) { - return false - } - - return err == nil -} - -// errorOrWarn returns a `warn` level if this server instance is running unit tests, otherwise `error`. -func errorOrWarn() mlog.Level { - unitTesting := strings.ToLower(strings.TrimSpace(os.Getenv("FOCALBOARD_UNIT_TESTING"))) - if unitTesting == "1" || unitTesting == "y" || unitTesting == "t" { - return mlog.LvlWarn - } - return mlog.LvlError -} diff --git a/server/boards/web/webserver_test.go b/server/boards/web/webserver_test.go deleted file mode 100644 index 5f820ce5cd..0000000000 --- a/server/boards/web/webserver_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package web - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func Test_NewServer(t *testing.T) { - tests := []struct { - name string - rootPath string - serverRoot string - ssl bool - port int - localOnly bool - logger mlog.LoggerIFace - expectedBaseURL string - expectedServerAddr string - }{ - { - name: "should return server with given properties", - rootPath: "./test/path/to/root", - serverRoot: "https://some-fake-server.com/fake-url", - ssl: false, - port: 9999, // fake port number - localOnly: false, - logger: &mlog.Logger{}, - expectedBaseURL: "/fake-url", - expectedServerAddr: ":9999", - }, - { - name: "should return local server with given properties", - rootPath: "./test/path/to/root", - serverRoot: "https://some-fake-server.com/fake-url", - ssl: false, - port: 3000, // fake port number - localOnly: true, - logger: &mlog.Logger{}, - expectedBaseURL: "/fake-url", - expectedServerAddr: "localhost:3000", - }, - { - name: "should match Server properties when ssl true", - rootPath: "./test/path/to/root", - serverRoot: "https://some-fake-server.com/fake-url", - ssl: true, - port: 8000, // fake port number - localOnly: false, - logger: &mlog.Logger{}, - expectedBaseURL: "/fake-url", - expectedServerAddr: ":8000", - }, - { - name: "should return local server when ssl true", - rootPath: "./test/path/to/root", - serverRoot: "https://localhost:8080/fake-url", - ssl: true, - port: 9999, // fake port number - localOnly: true, - logger: &mlog.Logger{}, - expectedBaseURL: "/fake-url", - expectedServerAddr: "localhost:9999", - }, - { - name: "should return '/' as base url is not good!", - rootPath: "", - serverRoot: "https://localhost:8080/#!@$@#@", - ssl: true, - port: 9999, // fake port number - localOnly: true, - logger: &mlog.Logger{}, - expectedBaseURL: "/", - expectedServerAddr: "localhost:9999", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ws := NewServer(test.rootPath, test.serverRoot, test.port, test.ssl, test.localOnly, test.logger) - - require.NotNil(t, ws, "The webserver object is nil!") - - require.Equal(t, test.expectedBaseURL, ws.baseURL, "baseURL does not match") - require.Equal(t, test.rootPath, ws.rootPath, "rootPath does not match") - require.Equal(t, test.port, ws.port, "rootPath does not match") - require.Equal(t, test.ssl, ws.ssl, "logger pointer does not match") - require.Equal(t, test.logger, ws.logger, "logger pointer does not match") - - if test.localOnly == true { - require.Equal(t, test.expectedServerAddr, ws.Server.Addr, "localhost address not as matching!") - } else { - require.Equal(t, test.expectedServerAddr, ws.Server.Addr, "server address not matching!") - } - }) - } -} diff --git a/server/boards/ws/adapter.go b/server/boards/ws/adapter.go deleted file mode 100644 index 5cc8217e56..0000000000 --- a/server/boards/ws/adapter.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockstore.go -package mocks . Store -package ws - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -const ( - websocketActionAuth = "AUTH" - websocketActionSubscribeTeam = "SUBSCRIBE_TEAM" - websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM" - websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS" - websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS" - websocketActionUpdateBoard = "UPDATE_BOARD" - websocketActionUpdateMember = "UPDATE_MEMBER" - websocketActionDeleteMember = "DELETE_MEMBER" - websocketActionUpdateBlock = "UPDATE_BLOCK" - websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG" - websocketActionUpdateCategory = "UPDATE_CATEGORY" - websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY" - websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION" - websocketActionUpdateCardLimitTimestamp = "UPDATE_CARD_LIMIT_TIMESTAMP" - websocketActionReorderCategories = "REORDER_CATEGORIES" - websocketActionReorderCategoryBoards = "REORDER_CATEGORY_BOARDS" -) - -type Store interface { - GetBlock(blockID string) (*model.Block, error) - GetMembersForBoard(boardID string) ([]*model.BoardMember, error) -} - -type Adapter interface { - BroadcastBlockChange(teamID string, block *model.Block) - BroadcastBlockDelete(teamID, blockID, boardID string) - BroadcastBoardChange(teamID string, board *model.Board) - BroadcastBoardDelete(teamID, boardID string) - BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) - BroadcastMemberDelete(teamID, boardID, userID string) - BroadcastConfigChange(clientConfig model.ClientConfig) - BroadcastCategoryChange(category model.Category) - BroadcastCategoryBoardChange(teamID, userID string, blockCategory []*model.BoardCategoryWebsocketData) - BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) - BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) - BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) - BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardsOrder []string) -} diff --git a/server/boards/ws/common.go b/server/boards/ws/common.go deleted file mode 100644 index c0283716c2..0000000000 --- a/server/boards/ws/common.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// UpdateCategoryMessage is sent on block updates. -type UpdateCategoryMessage struct { - Action string `json:"action"` - TeamID string `json:"teamId"` - Category *model.Category `json:"category,omitempty"` - BoardCategories []*model.BoardCategoryWebsocketData `json:"blockCategories,omitempty"` -} - -// UpdateBlockMsg is sent on block updates. -type UpdateBlockMsg struct { - Action string `json:"action"` - TeamID string `json:"teamId"` - Block *model.Block `json:"block"` -} - -// UpdateBoardMsg is sent on block updates. -type UpdateBoardMsg struct { - Action string `json:"action"` - TeamID string `json:"teamId"` - Board *model.Board `json:"board"` -} - -// UpdateMemberMsg is sent on membership updates. -type UpdateMemberMsg struct { - Action string `json:"action"` - TeamID string `json:"teamId"` - Member *model.BoardMember `json:"member"` -} - -// UpdateSubscription is sent on subscription updates. -type UpdateSubscription struct { - Action string `json:"action"` - Subscription *model.Subscription `json:"subscription"` -} - -// UpdateClientConfig is sent on block updates. -type UpdateClientConfig struct { - Action string `json:"action"` - ClientConfig model.ClientConfig `json:"clientconfig"` -} - -// UpdateClientConfig is sent on block updates. -type UpdateCardLimitTimestamp struct { - Action string `json:"action"` - Timestamp int64 `json:"timestamp"` -} - -// WebsocketCommand is an incoming command from the client. -type WebsocketCommand struct { - Action string `json:"action"` - TeamID string `json:"teamId"` - Token string `json:"token"` - ReadToken string `json:"readToken"` - BlockIDs []string `json:"blockIds"` -} - -type CategoryReorderMessage struct { - Action string `json:"action"` - CategoryOrder []string `json:"categoryOrder"` - TeamID string `json:"teamId"` -} - -type CategoryBoardReorderMessage struct { - Action string `json:"action"` - CategoryID string `json:"CategoryId"` - BoardOrder []string `json:"BoardOrder"` - TeamID string `json:"teamId"` -} diff --git a/server/boards/ws/helpers_test.go b/server/boards/ws/helpers_test.go deleted file mode 100644 index b2f23b3746..0000000000 --- a/server/boards/ws/helpers_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "testing" - - authMocks "github.com/mattermost/mattermost/server/v8/boards/auth/mocks" - wsMocks "github.com/mattermost/mattermost/server/v8/boards/ws/mocks" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/golang/mock/gomock" -) - -type TestHelper struct { - api *wsMocks.MockAPI - auth *authMocks.MockAuthInterface - store *wsMocks.MockStore - ctrl *gomock.Controller - pa *PluginAdapter -} - -func SetupTestHelper(t *testing.T) *TestHelper { - ctrl := gomock.NewController(t) - mockAPI := wsMocks.NewMockAPI(ctrl) - mockAuth := authMocks.NewMockAuthInterface(ctrl) - mockStore := wsMocks.NewMockStore(ctrl) - - mockAPI.EXPECT().LogDebug(gomock.Any(), gomock.Any()).AnyTimes() - mockAPI.EXPECT().LogInfo(gomock.Any(), gomock.Any()).AnyTimes() - mockAPI.EXPECT().LogError(gomock.Any(), gomock.Any()).AnyTimes() - mockAPI.EXPECT().LogWarn(gomock.Any(), gomock.Any()).AnyTimes() - - return &TestHelper{ - api: mockAPI, - auth: mockAuth, - store: mockStore, - ctrl: ctrl, - pa: NewPluginAdapter(mockAPI, mockAuth, mockStore, mlog.CreateConsoleTestLogger(true, mlog.LvlDebug)), - } -} - -func (th *TestHelper) ReceiveWebSocketMessage(webConnID, userID, action string, data map[string]interface{}) { - req := &mm_model.WebSocketRequest{Action: websocketMessagePrefix + action, Data: data} - - th.pa.WebSocketMessageHasBeenPosted(webConnID, userID, req) -} - -func (th *TestHelper) SubscribeWebConnToTeam(webConnID, userID, teamID string) { - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID, teamID). - Return(true) - - msgData := map[string]interface{}{"teamId": teamID} - th.ReceiveWebSocketMessage(webConnID, userID, websocketActionSubscribeTeam, msgData) -} - -func (th *TestHelper) UnsubscribeWebConnFromTeam(webConnID, userID, teamID string) { - msgData := map[string]interface{}{"teamId": teamID} - th.ReceiveWebSocketMessage(webConnID, userID, websocketActionUnsubscribeTeam, msgData) -} diff --git a/server/boards/ws/mocks/mockpluginapi.go b/server/boards/ws/mocks/mockpluginapi.go deleted file mode 100644 index f3cfc95df8..0000000000 --- a/server/boards/ws/mocks/mockpluginapi.go +++ /dev/null @@ -1,2659 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/public/plugin (interfaces: API) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - io "io" - http "net/http" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/public/model" -) - -// MockAPI is a mock of API interface. -type MockAPI struct { - ctrl *gomock.Controller - recorder *MockAPIMockRecorder -} - -// MockAPIMockRecorder is the mock recorder for MockAPI. -type MockAPIMockRecorder struct { - mock *MockAPI -} - -// NewMockAPI creates a new mock instance. -func NewMockAPI(ctrl *gomock.Controller) *MockAPI { - mock := &MockAPI{ctrl: ctrl} - mock.recorder = &MockAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAPI) EXPECT() *MockAPIMockRecorder { - return m.recorder -} - -// AddChannelMember mocks base method. -func (m *MockAPI) AddChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddChannelMember indicates an expected call of AddChannelMember. -func (mr *MockAPIMockRecorder) AddChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddChannelMember", reflect.TypeOf((*MockAPI)(nil).AddChannelMember), arg0, arg1) -} - -// AddReaction mocks base method. -func (m *MockAPI) AddReaction(arg0 *model.Reaction) (*model.Reaction, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddReaction", arg0) - ret0, _ := ret[0].(*model.Reaction) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddReaction indicates an expected call of AddReaction. -func (mr *MockAPIMockRecorder) AddReaction(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddReaction", reflect.TypeOf((*MockAPI)(nil).AddReaction), arg0) -} - -// AddUserToChannel mocks base method. -func (m *MockAPI) AddUserToChannel(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddUserToChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// AddUserToChannel indicates an expected call of AddUserToChannel. -func (mr *MockAPIMockRecorder) AddUserToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUserToChannel", reflect.TypeOf((*MockAPI)(nil).AddUserToChannel), arg0, arg1, arg2) -} - -// CopyFileInfos mocks base method. -func (m *MockAPI) CopyFileInfos(arg0 string, arg1 []string) ([]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CopyFileInfos", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CopyFileInfos indicates an expected call of CopyFileInfos. -func (mr *MockAPIMockRecorder) CopyFileInfos(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFileInfos", reflect.TypeOf((*MockAPI)(nil).CopyFileInfos), arg0, arg1) -} - -// CreateBot mocks base method. -func (m *MockAPI) CreateBot(arg0 *model.Bot) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBot", arg0) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateBot indicates an expected call of CreateBot. -func (mr *MockAPIMockRecorder) CreateBot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBot", reflect.TypeOf((*MockAPI)(nil).CreateBot), arg0) -} - -// CreateChannel mocks base method. -func (m *MockAPI) CreateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateChannel indicates an expected call of CreateChannel. -func (mr *MockAPIMockRecorder) CreateChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannel", reflect.TypeOf((*MockAPI)(nil).CreateChannel), arg0) -} - -// CreateChannelSidebarCategory mocks base method. -func (m *MockAPI) CreateChannelSidebarCategory(arg0, arg1 string, arg2 *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateChannelSidebarCategory", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.SidebarCategoryWithChannels) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateChannelSidebarCategory indicates an expected call of CreateChannelSidebarCategory. -func (mr *MockAPIMockRecorder) CreateChannelSidebarCategory(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChannelSidebarCategory", reflect.TypeOf((*MockAPI)(nil).CreateChannelSidebarCategory), arg0, arg1, arg2) -} - -// CreateCommand mocks base method. -func (m *MockAPI) CreateCommand(arg0 *model.Command) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCommand", arg0) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateCommand indicates an expected call of CreateCommand. -func (mr *MockAPIMockRecorder) CreateCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCommand", reflect.TypeOf((*MockAPI)(nil).CreateCommand), arg0) -} - -// CreateOAuthApp mocks base method. -func (m *MockAPI) CreateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateOAuthApp indicates an expected call of CreateOAuthApp. -func (mr *MockAPIMockRecorder) CreateOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOAuthApp", reflect.TypeOf((*MockAPI)(nil).CreateOAuthApp), arg0) -} - -// CreatePost mocks base method. -func (m *MockAPI) CreatePost(arg0 *model.Post) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreatePost indicates an expected call of CreatePost. -func (mr *MockAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockAPI)(nil).CreatePost), arg0) -} - -// CreateSession mocks base method. -func (m *MockAPI) CreateSession(arg0 *model.Session) (*model.Session, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSession", arg0) - ret0, _ := ret[0].(*model.Session) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateSession indicates an expected call of CreateSession. -func (mr *MockAPIMockRecorder) CreateSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockAPI)(nil).CreateSession), arg0) -} - -// CreateTeam mocks base method. -func (m *MockAPI) CreateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeam indicates an expected call of CreateTeam. -func (mr *MockAPIMockRecorder) CreateTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeam", reflect.TypeOf((*MockAPI)(nil).CreateTeam), arg0) -} - -// CreateTeamMember mocks base method. -func (m *MockAPI) CreateTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMember indicates an expected call of CreateTeamMember. -func (mr *MockAPIMockRecorder) CreateTeamMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMember", reflect.TypeOf((*MockAPI)(nil).CreateTeamMember), arg0, arg1) -} - -// CreateTeamMembers mocks base method. -func (m *MockAPI) CreateTeamMembers(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMembers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMembers indicates an expected call of CreateTeamMembers. -func (mr *MockAPIMockRecorder) CreateTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembers", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembers), arg0, arg1, arg2) -} - -// CreateTeamMembersGracefully mocks base method. -func (m *MockAPI) CreateTeamMembersGracefully(arg0 string, arg1 []string, arg2 string) ([]*model.TeamMemberWithError, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTeamMembersGracefully", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMemberWithError) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateTeamMembersGracefully indicates an expected call of CreateTeamMembersGracefully. -func (mr *MockAPIMockRecorder) CreateTeamMembersGracefully(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTeamMembersGracefully", reflect.TypeOf((*MockAPI)(nil).CreateTeamMembersGracefully), arg0, arg1, arg2) -} - -// CreateUploadSession mocks base method. -func (m *MockAPI) CreateUploadSession(arg0 *model.UploadSession) (*model.UploadSession, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUploadSession", arg0) - ret0, _ := ret[0].(*model.UploadSession) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateUploadSession indicates an expected call of CreateUploadSession. -func (mr *MockAPIMockRecorder) CreateUploadSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUploadSession", reflect.TypeOf((*MockAPI)(nil).CreateUploadSession), arg0) -} - -// CreateUser mocks base method. -func (m *MockAPI) CreateUser(arg0 *model.User) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateUser indicates an expected call of CreateUser. -func (mr *MockAPIMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAPI)(nil).CreateUser), arg0) -} - -// CreateUserAccessToken mocks base method. -func (m *MockAPI) CreateUserAccessToken(arg0 *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUserAccessToken", arg0) - ret0, _ := ret[0].(*model.UserAccessToken) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// CreateUserAccessToken indicates an expected call of CreateUserAccessToken. -func (mr *MockAPIMockRecorder) CreateUserAccessToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserAccessToken", reflect.TypeOf((*MockAPI)(nil).CreateUserAccessToken), arg0) -} - -// DeleteChannel mocks base method. -func (m *MockAPI) DeleteChannel(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChannel", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteChannel indicates an expected call of DeleteChannel. -func (mr *MockAPIMockRecorder) DeleteChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannel", reflect.TypeOf((*MockAPI)(nil).DeleteChannel), arg0) -} - -// DeleteChannelMember mocks base method. -func (m *MockAPI) DeleteChannelMember(arg0, arg1 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteChannelMember indicates an expected call of DeleteChannelMember. -func (mr *MockAPIMockRecorder) DeleteChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChannelMember", reflect.TypeOf((*MockAPI)(nil).DeleteChannelMember), arg0, arg1) -} - -// DeleteCommand mocks base method. -func (m *MockAPI) DeleteCommand(arg0 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCommand", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteCommand indicates an expected call of DeleteCommand. -func (mr *MockAPIMockRecorder) DeleteCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCommand", reflect.TypeOf((*MockAPI)(nil).DeleteCommand), arg0) -} - -// DeleteEphemeralPost mocks base method. -func (m *MockAPI) DeleteEphemeralPost(arg0, arg1 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteEphemeralPost", arg0, arg1) -} - -// DeleteEphemeralPost indicates an expected call of DeleteEphemeralPost. -func (mr *MockAPIMockRecorder) DeleteEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEphemeralPost", reflect.TypeOf((*MockAPI)(nil).DeleteEphemeralPost), arg0, arg1) -} - -// DeleteOAuthApp mocks base method. -func (m *MockAPI) DeleteOAuthApp(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteOAuthApp", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteOAuthApp indicates an expected call of DeleteOAuthApp. -func (mr *MockAPIMockRecorder) DeleteOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuthApp", reflect.TypeOf((*MockAPI)(nil).DeleteOAuthApp), arg0) -} - -// DeletePost mocks base method. -func (m *MockAPI) DeletePost(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePost", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeletePost indicates an expected call of DeletePost. -func (mr *MockAPIMockRecorder) DeletePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockAPI)(nil).DeletePost), arg0) -} - -// DeletePreferencesForUser mocks base method. -func (m *MockAPI) DeletePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser. -func (mr *MockAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).DeletePreferencesForUser), arg0, arg1) -} - -// DeleteTeam mocks base method. -func (m *MockAPI) DeleteTeam(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTeam", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteTeam indicates an expected call of DeleteTeam. -func (mr *MockAPIMockRecorder) DeleteTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeam", reflect.TypeOf((*MockAPI)(nil).DeleteTeam), arg0) -} - -// DeleteTeamMember mocks base method. -func (m *MockAPI) DeleteTeamMember(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTeamMember", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteTeamMember indicates an expected call of DeleteTeamMember. -func (mr *MockAPIMockRecorder) DeleteTeamMember(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTeamMember", reflect.TypeOf((*MockAPI)(nil).DeleteTeamMember), arg0, arg1, arg2) -} - -// DeleteUser mocks base method. -func (m *MockAPI) DeleteUser(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteUser", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DeleteUser indicates an expected call of DeleteUser. -func (mr *MockAPIMockRecorder) DeleteUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockAPI)(nil).DeleteUser), arg0) -} - -// DisablePlugin mocks base method. -func (m *MockAPI) DisablePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DisablePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// DisablePlugin indicates an expected call of DisablePlugin. -func (mr *MockAPIMockRecorder) DisablePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisablePlugin", reflect.TypeOf((*MockAPI)(nil).DisablePlugin), arg0) -} - -// EnablePlugin mocks base method. -func (m *MockAPI) EnablePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnablePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// EnablePlugin indicates an expected call of EnablePlugin. -func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnablePlugin", reflect.TypeOf((*MockAPI)(nil).EnablePlugin), arg0) -} - -// EnsureBotUser mocks base method. -func (m *MockAPI) EnsureBotUser(arg0 *model.Bot) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnsureBotUser", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// EnsureBotUser indicates an expected call of EnsureBotUser. -func (mr *MockAPIMockRecorder) EnsureBotUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBotUser", reflect.TypeOf((*MockAPI)(nil).EnsureBotUser), arg0) -} - -// ExecuteSlashCommand mocks base method. -func (m *MockAPI) ExecuteSlashCommand(arg0 *model.CommandArgs) (*model.CommandResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExecuteSlashCommand", arg0) - ret0, _ := ret[0].(*model.CommandResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ExecuteSlashCommand indicates an expected call of ExecuteSlashCommand. -func (mr *MockAPIMockRecorder) ExecuteSlashCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSlashCommand", reflect.TypeOf((*MockAPI)(nil).ExecuteSlashCommand), arg0) -} - -// ExtendSessionExpiry mocks base method. -func (m *MockAPI) ExtendSessionExpiry(arg0 string, arg1 int64) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExtendSessionExpiry", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// ExtendSessionExpiry indicates an expected call of ExtendSessionExpiry. -func (mr *MockAPIMockRecorder) ExtendSessionExpiry(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendSessionExpiry", reflect.TypeOf((*MockAPI)(nil).ExtendSessionExpiry), arg0, arg1) -} - -// GetBot mocks base method. -func (m *MockAPI) GetBot(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBot", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetBot indicates an expected call of GetBot. -func (mr *MockAPIMockRecorder) GetBot(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBot", reflect.TypeOf((*MockAPI)(nil).GetBot), arg0, arg1) -} - -// GetBots mocks base method. -func (m *MockAPI) GetBots(arg0 *model.BotGetOptions) ([]*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBots", arg0) - ret0, _ := ret[0].([]*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetBots indicates an expected call of GetBots. -func (mr *MockAPIMockRecorder) GetBots(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBots", reflect.TypeOf((*MockAPI)(nil).GetBots), arg0) -} - -// GetBundlePath mocks base method. -func (m *MockAPI) GetBundlePath() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBundlePath") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBundlePath indicates an expected call of GetBundlePath. -func (mr *MockAPIMockRecorder) GetBundlePath() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBundlePath", reflect.TypeOf((*MockAPI)(nil).GetBundlePath)) -} - -// GetChannel mocks base method. -func (m *MockAPI) GetChannel(arg0 string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannel indicates an expected call of GetChannel. -func (mr *MockAPIMockRecorder) GetChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannel", reflect.TypeOf((*MockAPI)(nil).GetChannel), arg0) -} - -// GetChannelByName mocks base method. -func (m *MockAPI) GetChannelByName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelByName", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelByName indicates an expected call of GetChannelByName. -func (mr *MockAPIMockRecorder) GetChannelByName(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByName", reflect.TypeOf((*MockAPI)(nil).GetChannelByName), arg0, arg1, arg2) -} - -// GetChannelByNameForTeamName mocks base method. -func (m *MockAPI) GetChannelByNameForTeamName(arg0, arg1 string, arg2 bool) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelByNameForTeamName", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelByNameForTeamName indicates an expected call of GetChannelByNameForTeamName. -func (mr *MockAPIMockRecorder) GetChannelByNameForTeamName(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByNameForTeamName", reflect.TypeOf((*MockAPI)(nil).GetChannelByNameForTeamName), arg0, arg1, arg2) -} - -// GetChannelMember mocks base method. -func (m *MockAPI) GetChannelMember(arg0, arg1 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMember", arg0, arg1) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMember indicates an expected call of GetChannelMember. -func (mr *MockAPIMockRecorder) GetChannelMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMember", reflect.TypeOf((*MockAPI)(nil).GetChannelMember), arg0, arg1) -} - -// GetChannelMembers mocks base method. -func (m *MockAPI) GetChannelMembers(arg0 string, arg1, arg2 int) (model.ChannelMembers, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembers", arg0, arg1, arg2) - ret0, _ := ret[0].(model.ChannelMembers) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembers indicates an expected call of GetChannelMembers. -func (mr *MockAPIMockRecorder) GetChannelMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembers", reflect.TypeOf((*MockAPI)(nil).GetChannelMembers), arg0, arg1, arg2) -} - -// GetChannelMembersByIds mocks base method. -func (m *MockAPI) GetChannelMembersByIds(arg0 string, arg1 []string) (model.ChannelMembers, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembersByIds", arg0, arg1) - ret0, _ := ret[0].(model.ChannelMembers) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembersByIds indicates an expected call of GetChannelMembersByIds. -func (mr *MockAPIMockRecorder) GetChannelMembersByIds(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersByIds", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersByIds), arg0, arg1) -} - -// GetChannelMembersForUser mocks base method. -func (m *MockAPI) GetChannelMembersForUser(arg0, arg1 string, arg2, arg3 int) ([]*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelMembersForUser", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelMembersForUser indicates an expected call of GetChannelMembersForUser. -func (mr *MockAPIMockRecorder) GetChannelMembersForUser(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelMembersForUser), arg0, arg1, arg2, arg3) -} - -// GetChannelSidebarCategories mocks base method. -func (m *MockAPI) GetChannelSidebarCategories(arg0, arg1 string) (*model.OrderedSidebarCategories, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelSidebarCategories", arg0, arg1) - ret0, _ := ret[0].(*model.OrderedSidebarCategories) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelSidebarCategories indicates an expected call of GetChannelSidebarCategories. -func (mr *MockAPIMockRecorder) GetChannelSidebarCategories(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).GetChannelSidebarCategories), arg0, arg1) -} - -// GetChannelStats mocks base method. -func (m *MockAPI) GetChannelStats(arg0 string) (*model.ChannelStats, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelStats", arg0) - ret0, _ := ret[0].(*model.ChannelStats) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelStats indicates an expected call of GetChannelStats. -func (mr *MockAPIMockRecorder) GetChannelStats(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelStats", reflect.TypeOf((*MockAPI)(nil).GetChannelStats), arg0) -} - -// GetChannelsForTeamForUser mocks base method. -func (m *MockAPI) GetChannelsForTeamForUser(arg0, arg1 string, arg2 bool) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChannelsForTeamForUser", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetChannelsForTeamForUser indicates an expected call of GetChannelsForTeamForUser. -func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2) -} - -// GetCloudLimits mocks base method. -func (m *MockAPI) GetCloudLimits() (*model.ProductLimits, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCloudLimits") - ret0, _ := ret[0].(*model.ProductLimits) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCloudLimits indicates an expected call of GetCloudLimits. -func (mr *MockAPIMockRecorder) GetCloudLimits() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockAPI)(nil).GetCloudLimits)) -} - -// GetCommand mocks base method. -func (m *MockAPI) GetCommand(arg0 string) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCommand", arg0) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCommand indicates an expected call of GetCommand. -func (mr *MockAPIMockRecorder) GetCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommand", reflect.TypeOf((*MockAPI)(nil).GetCommand), arg0) -} - -// GetConfig mocks base method. -func (m *MockAPI) GetConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetConfig indicates an expected call of GetConfig. -func (mr *MockAPIMockRecorder) GetConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockAPI)(nil).GetConfig)) -} - -// GetDiagnosticId mocks base method. -func (m *MockAPI) GetDiagnosticId() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDiagnosticId") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetDiagnosticId indicates an expected call of GetDiagnosticId. -func (mr *MockAPIMockRecorder) GetDiagnosticId() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDiagnosticId", reflect.TypeOf((*MockAPI)(nil).GetDiagnosticId)) -} - -// GetDirectChannel mocks base method. -func (m *MockAPI) GetDirectChannel(arg0, arg1 string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDirectChannel", arg0, arg1) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetDirectChannel indicates an expected call of GetDirectChannel. -func (mr *MockAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockAPI)(nil).GetDirectChannel), arg0, arg1) -} - -// GetEmoji mocks base method. -func (m *MockAPI) GetEmoji(arg0 string) (*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmoji", arg0) - ret0, _ := ret[0].(*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmoji indicates an expected call of GetEmoji. -func (mr *MockAPIMockRecorder) GetEmoji(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmoji", reflect.TypeOf((*MockAPI)(nil).GetEmoji), arg0) -} - -// GetEmojiByName mocks base method. -func (m *MockAPI) GetEmojiByName(arg0 string) (*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiByName", arg0) - ret0, _ := ret[0].(*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmojiByName indicates an expected call of GetEmojiByName. -func (mr *MockAPIMockRecorder) GetEmojiByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiByName", reflect.TypeOf((*MockAPI)(nil).GetEmojiByName), arg0) -} - -// GetEmojiImage mocks base method. -func (m *MockAPI) GetEmojiImage(arg0 string) ([]byte, string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiImage", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(*model.AppError) - return ret0, ret1, ret2 -} - -// GetEmojiImage indicates an expected call of GetEmojiImage. -func (mr *MockAPIMockRecorder) GetEmojiImage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiImage", reflect.TypeOf((*MockAPI)(nil).GetEmojiImage), arg0) -} - -// GetEmojiList mocks base method. -func (m *MockAPI) GetEmojiList(arg0 string, arg1, arg2 int) ([]*model.Emoji, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEmojiList", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Emoji) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetEmojiList indicates an expected call of GetEmojiList. -func (mr *MockAPIMockRecorder) GetEmojiList(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEmojiList", reflect.TypeOf((*MockAPI)(nil).GetEmojiList), arg0, arg1, arg2) -} - -// GetFile mocks base method. -func (m *MockAPI) GetFile(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFile", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFile indicates an expected call of GetFile. -func (mr *MockAPIMockRecorder) GetFile(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFile", reflect.TypeOf((*MockAPI)(nil).GetFile), arg0) -} - -// GetFileInfo mocks base method. -func (m *MockAPI) GetFileInfo(arg0 string) (*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfo", arg0) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileInfo indicates an expected call of GetFileInfo. -func (mr *MockAPIMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockAPI)(nil).GetFileInfo), arg0) -} - -// GetFileInfos mocks base method. -func (m *MockAPI) GetFileInfos(arg0, arg1 int, arg2 *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileInfos", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileInfos indicates an expected call of GetFileInfos. -func (mr *MockAPIMockRecorder) GetFileInfos(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfos", reflect.TypeOf((*MockAPI)(nil).GetFileInfos), arg0, arg1, arg2) -} - -// GetFileLink mocks base method. -func (m *MockAPI) GetFileLink(arg0 string) (string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileLink", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetFileLink indicates an expected call of GetFileLink. -func (mr *MockAPIMockRecorder) GetFileLink(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileLink", reflect.TypeOf((*MockAPI)(nil).GetFileLink), arg0) -} - -// GetGroup mocks base method. -func (m *MockAPI) GetGroup(arg0 string) (*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroup", arg0) - ret0, _ := ret[0].(*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroup indicates an expected call of GetGroup. -func (mr *MockAPIMockRecorder) GetGroup(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroup", reflect.TypeOf((*MockAPI)(nil).GetGroup), arg0) -} - -// GetGroupByName mocks base method. -func (m *MockAPI) GetGroupByName(arg0 string) (*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupByName", arg0) - ret0, _ := ret[0].(*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupByName indicates an expected call of GetGroupByName. -func (mr *MockAPIMockRecorder) GetGroupByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockAPI)(nil).GetGroupByName), arg0) -} - -// GetGroupChannel mocks base method. -func (m *MockAPI) GetGroupChannel(arg0 []string) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupChannel indicates an expected call of GetGroupChannel. -func (mr *MockAPIMockRecorder) GetGroupChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupChannel", reflect.TypeOf((*MockAPI)(nil).GetGroupChannel), arg0) -} - -// GetGroupMemberUsers mocks base method. -func (m *MockAPI) GetGroupMemberUsers(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupMemberUsers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupMemberUsers indicates an expected call of GetGroupMemberUsers. -func (mr *MockAPIMockRecorder) GetGroupMemberUsers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMemberUsers", reflect.TypeOf((*MockAPI)(nil).GetGroupMemberUsers), arg0, arg1, arg2) -} - -// GetGroupsBySource mocks base method. -func (m *MockAPI) GetGroupsBySource(arg0 model.GroupSource) ([]*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupsBySource", arg0) - ret0, _ := ret[0].([]*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupsBySource indicates an expected call of GetGroupsBySource. -func (mr *MockAPIMockRecorder) GetGroupsBySource(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsBySource", reflect.TypeOf((*MockAPI)(nil).GetGroupsBySource), arg0) -} - -// GetGroupsForUser mocks base method. -func (m *MockAPI) GetGroupsForUser(arg0 string) ([]*model.Group, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupsForUser", arg0) - ret0, _ := ret[0].([]*model.Group) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetGroupsForUser indicates an expected call of GetGroupsForUser. -func (mr *MockAPIMockRecorder) GetGroupsForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsForUser", reflect.TypeOf((*MockAPI)(nil).GetGroupsForUser), arg0) -} - -// GetLDAPUserAttributes mocks base method. -func (m *MockAPI) GetLDAPUserAttributes(arg0 string, arg1 []string) (map[string]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLDAPUserAttributes", arg0, arg1) - ret0, _ := ret[0].(map[string]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetLDAPUserAttributes indicates an expected call of GetLDAPUserAttributes. -func (mr *MockAPIMockRecorder) GetLDAPUserAttributes(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLDAPUserAttributes", reflect.TypeOf((*MockAPI)(nil).GetLDAPUserAttributes), arg0, arg1) -} - -// GetLicense mocks base method. -func (m *MockAPI) GetLicense() *model.License { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLicense") - ret0, _ := ret[0].(*model.License) - return ret0 -} - -// GetLicense indicates an expected call of GetLicense. -func (mr *MockAPIMockRecorder) GetLicense() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockAPI)(nil).GetLicense)) -} - -// GetOAuthApp mocks base method. -func (m *MockAPI) GetOAuthApp(arg0 string) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetOAuthApp indicates an expected call of GetOAuthApp. -func (mr *MockAPIMockRecorder) GetOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuthApp", reflect.TypeOf((*MockAPI)(nil).GetOAuthApp), arg0) -} - -// GetPluginConfig mocks base method. -func (m *MockAPI) GetPluginConfig() map[string]interface{} { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPluginConfig") - ret0, _ := ret[0].(map[string]interface{}) - return ret0 -} - -// GetPluginConfig indicates an expected call of GetPluginConfig. -func (mr *MockAPIMockRecorder) GetPluginConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginConfig", reflect.TypeOf((*MockAPI)(nil).GetPluginConfig)) -} - -// GetPluginStatus mocks base method. -func (m *MockAPI) GetPluginStatus(arg0 string) (*model.PluginStatus, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPluginStatus", arg0) - ret0, _ := ret[0].(*model.PluginStatus) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPluginStatus indicates an expected call of GetPluginStatus. -func (mr *MockAPIMockRecorder) GetPluginStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginStatus", reflect.TypeOf((*MockAPI)(nil).GetPluginStatus), arg0) -} - -// GetPlugins mocks base method. -func (m *MockAPI) GetPlugins() ([]*model.Manifest, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPlugins") - ret0, _ := ret[0].([]*model.Manifest) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPlugins indicates an expected call of GetPlugins. -func (mr *MockAPIMockRecorder) GetPlugins() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlugins", reflect.TypeOf((*MockAPI)(nil).GetPlugins)) -} - -// GetPost mocks base method. -func (m *MockAPI) GetPost(arg0 string) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPost indicates an expected call of GetPost. -func (mr *MockAPIMockRecorder) GetPost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPost", reflect.TypeOf((*MockAPI)(nil).GetPost), arg0) -} - -// GetPostThread mocks base method. -func (m *MockAPI) GetPostThread(arg0 string) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostThread", arg0) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostThread indicates an expected call of GetPostThread. -func (mr *MockAPIMockRecorder) GetPostThread(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostThread", reflect.TypeOf((*MockAPI)(nil).GetPostThread), arg0) -} - -// GetPostsAfter mocks base method. -func (m *MockAPI) GetPostsAfter(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsAfter", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsAfter indicates an expected call of GetPostsAfter. -func (mr *MockAPIMockRecorder) GetPostsAfter(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsAfter", reflect.TypeOf((*MockAPI)(nil).GetPostsAfter), arg0, arg1, arg2, arg3) -} - -// GetPostsBefore mocks base method. -func (m *MockAPI) GetPostsBefore(arg0, arg1 string, arg2, arg3 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsBefore", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsBefore indicates an expected call of GetPostsBefore. -func (mr *MockAPIMockRecorder) GetPostsBefore(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsBefore", reflect.TypeOf((*MockAPI)(nil).GetPostsBefore), arg0, arg1, arg2, arg3) -} - -// GetPostsForChannel mocks base method. -func (m *MockAPI) GetPostsForChannel(arg0 string, arg1, arg2 int) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsForChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsForChannel indicates an expected call of GetPostsForChannel. -func (mr *MockAPIMockRecorder) GetPostsForChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsForChannel", reflect.TypeOf((*MockAPI)(nil).GetPostsForChannel), arg0, arg1, arg2) -} - -// GetPostsSince mocks base method. -func (m *MockAPI) GetPostsSince(arg0 string, arg1 int64) (*model.PostList, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPostsSince", arg0, arg1) - ret0, _ := ret[0].(*model.PostList) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPostsSince indicates an expected call of GetPostsSince. -func (mr *MockAPIMockRecorder) GetPostsSince(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsSince", reflect.TypeOf((*MockAPI)(nil).GetPostsSince), arg0, arg1) -} - -// GetPreferencesForUser mocks base method. -func (m *MockAPI) GetPreferencesForUser(arg0 string) ([]model.Preference, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0) - ret0, _ := ret[0].([]model.Preference) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPreferencesForUser indicates an expected call of GetPreferencesForUser. -func (mr *MockAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockAPI)(nil).GetPreferencesForUser), arg0) -} - -// GetProfileImage mocks base method. -func (m *MockAPI) GetProfileImage(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProfileImage", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetProfileImage indicates an expected call of GetProfileImage. -func (mr *MockAPIMockRecorder) GetProfileImage(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileImage", reflect.TypeOf((*MockAPI)(nil).GetProfileImage), arg0) -} - -// GetPublicChannelsForTeam mocks base method. -func (m *MockAPI) GetPublicChannelsForTeam(arg0 string, arg1, arg2 int) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPublicChannelsForTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetPublicChannelsForTeam indicates an expected call of GetPublicChannelsForTeam. -func (mr *MockAPIMockRecorder) GetPublicChannelsForTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicChannelsForTeam", reflect.TypeOf((*MockAPI)(nil).GetPublicChannelsForTeam), arg0, arg1, arg2) -} - -// GetReactions mocks base method. -func (m *MockAPI) GetReactions(arg0 string) ([]*model.Reaction, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetReactions", arg0) - ret0, _ := ret[0].([]*model.Reaction) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetReactions indicates an expected call of GetReactions. -func (mr *MockAPIMockRecorder) GetReactions(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReactions", reflect.TypeOf((*MockAPI)(nil).GetReactions), arg0) -} - -// GetServerVersion mocks base method. -func (m *MockAPI) GetServerVersion() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServerVersion") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetServerVersion indicates an expected call of GetServerVersion. -func (mr *MockAPIMockRecorder) GetServerVersion() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerVersion", reflect.TypeOf((*MockAPI)(nil).GetServerVersion)) -} - -// GetSession mocks base method. -func (m *MockAPI) GetSession(arg0 string) (*model.Session, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSession", arg0) - ret0, _ := ret[0].(*model.Session) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetSession indicates an expected call of GetSession. -func (mr *MockAPIMockRecorder) GetSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockAPI)(nil).GetSession), arg0) -} - -// GetSystemInstallDate mocks base method. -func (m *MockAPI) GetSystemInstallDate() (int64, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSystemInstallDate") - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetSystemInstallDate indicates an expected call of GetSystemInstallDate. -func (mr *MockAPIMockRecorder) GetSystemInstallDate() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemInstallDate", reflect.TypeOf((*MockAPI)(nil).GetSystemInstallDate)) -} - -// GetTeam mocks base method. -func (m *MockAPI) GetTeam(arg0 string) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeam indicates an expected call of GetTeam. -func (mr *MockAPIMockRecorder) GetTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeam", reflect.TypeOf((*MockAPI)(nil).GetTeam), arg0) -} - -// GetTeamByName mocks base method. -func (m *MockAPI) GetTeamByName(arg0 string) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamByName", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamByName indicates an expected call of GetTeamByName. -func (mr *MockAPIMockRecorder) GetTeamByName(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamByName", reflect.TypeOf((*MockAPI)(nil).GetTeamByName), arg0) -} - -// GetTeamIcon mocks base method. -func (m *MockAPI) GetTeamIcon(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamIcon", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamIcon indicates an expected call of GetTeamIcon. -func (mr *MockAPIMockRecorder) GetTeamIcon(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamIcon", reflect.TypeOf((*MockAPI)(nil).GetTeamIcon), arg0) -} - -// GetTeamMember mocks base method. -func (m *MockAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMember", arg0, arg1) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMember indicates an expected call of GetTeamMember. -func (mr *MockAPIMockRecorder) GetTeamMember(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMember", reflect.TypeOf((*MockAPI)(nil).GetTeamMember), arg0, arg1) -} - -// GetTeamMembers mocks base method. -func (m *MockAPI) GetTeamMembers(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMembers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMembers indicates an expected call of GetTeamMembers. -func (mr *MockAPIMockRecorder) GetTeamMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembers", reflect.TypeOf((*MockAPI)(nil).GetTeamMembers), arg0, arg1, arg2) -} - -// GetTeamMembersForUser mocks base method. -func (m *MockAPI) GetTeamMembersForUser(arg0 string, arg1, arg2 int) ([]*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamMembersForUser", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamMembersForUser indicates an expected call of GetTeamMembersForUser. -func (mr *MockAPIMockRecorder) GetTeamMembersForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembersForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamMembersForUser), arg0, arg1, arg2) -} - -// GetTeamStats mocks base method. -func (m *MockAPI) GetTeamStats(arg0 string) (*model.TeamStats, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamStats", arg0) - ret0, _ := ret[0].(*model.TeamStats) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamStats indicates an expected call of GetTeamStats. -func (mr *MockAPIMockRecorder) GetTeamStats(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamStats", reflect.TypeOf((*MockAPI)(nil).GetTeamStats), arg0) -} - -// GetTeams mocks base method. -func (m *MockAPI) GetTeams() ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeams") - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeams indicates an expected call of GetTeams. -func (mr *MockAPIMockRecorder) GetTeams() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeams", reflect.TypeOf((*MockAPI)(nil).GetTeams)) -} - -// GetTeamsForUser mocks base method. -func (m *MockAPI) GetTeamsForUser(arg0 string) ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamsForUser", arg0) - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamsForUser indicates an expected call of GetTeamsForUser. -func (mr *MockAPIMockRecorder) GetTeamsForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsForUser), arg0) -} - -// GetTeamsUnreadForUser mocks base method. -func (m *MockAPI) GetTeamsUnreadForUser(arg0 string) ([]*model.TeamUnread, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTeamsUnreadForUser", arg0) - ret0, _ := ret[0].([]*model.TeamUnread) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetTeamsUnreadForUser indicates an expected call of GetTeamsUnreadForUser. -func (mr *MockAPIMockRecorder) GetTeamsUnreadForUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamsUnreadForUser", reflect.TypeOf((*MockAPI)(nil).GetTeamsUnreadForUser), arg0) -} - -// GetTelemetryId mocks base method. -func (m *MockAPI) GetTelemetryId() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTelemetryId") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetTelemetryId indicates an expected call of GetTelemetryId. -func (mr *MockAPIMockRecorder) GetTelemetryId() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryId", reflect.TypeOf((*MockAPI)(nil).GetTelemetryId)) -} - -// GetUnsanitizedConfig mocks base method. -func (m *MockAPI) GetUnsanitizedConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUnsanitizedConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetUnsanitizedConfig indicates an expected call of GetUnsanitizedConfig. -func (mr *MockAPIMockRecorder) GetUnsanitizedConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnsanitizedConfig", reflect.TypeOf((*MockAPI)(nil).GetUnsanitizedConfig)) -} - -// GetUploadSession mocks base method. -func (m *MockAPI) GetUploadSession(arg0 string) (*model.UploadSession, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUploadSession", arg0) - ret0, _ := ret[0].(*model.UploadSession) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUploadSession indicates an expected call of GetUploadSession. -func (mr *MockAPIMockRecorder) GetUploadSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUploadSession", reflect.TypeOf((*MockAPI)(nil).GetUploadSession), arg0) -} - -// GetUser mocks base method. -func (m *MockAPI) GetUser(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUser indicates an expected call of GetUser. -func (mr *MockAPIMockRecorder) GetUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockAPI)(nil).GetUser), arg0) -} - -// GetUserByEmail mocks base method. -func (m *MockAPI) GetUserByEmail(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByEmail", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserByEmail indicates an expected call of GetUserByEmail. -func (mr *MockAPIMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockAPI)(nil).GetUserByEmail), arg0) -} - -// GetUserByUsername mocks base method. -func (m *MockAPI) GetUserByUsername(arg0 string) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserByUsername", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserByUsername indicates an expected call of GetUserByUsername. -func (mr *MockAPIMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockAPI)(nil).GetUserByUsername), arg0) -} - -// GetUserStatus mocks base method. -func (m *MockAPI) GetUserStatus(arg0 string) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserStatus", arg0) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserStatus indicates an expected call of GetUserStatus. -func (mr *MockAPIMockRecorder) GetUserStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatus", reflect.TypeOf((*MockAPI)(nil).GetUserStatus), arg0) -} - -// GetUserStatusesByIds mocks base method. -func (m *MockAPI) GetUserStatusesByIds(arg0 []string) ([]*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserStatusesByIds", arg0) - ret0, _ := ret[0].([]*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUserStatusesByIds indicates an expected call of GetUserStatusesByIds. -func (mr *MockAPIMockRecorder) GetUserStatusesByIds(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusesByIds", reflect.TypeOf((*MockAPI)(nil).GetUserStatusesByIds), arg0) -} - -// GetUsers mocks base method. -func (m *MockAPI) GetUsers(arg0 *model.UserGetOptions) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsers", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsers indicates an expected call of GetUsers. -func (mr *MockAPIMockRecorder) GetUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockAPI)(nil).GetUsers), arg0) -} - -// GetUsersByUsernames mocks base method. -func (m *MockAPI) GetUsersByUsernames(arg0 []string) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersByUsernames", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersByUsernames indicates an expected call of GetUsersByUsernames. -func (mr *MockAPIMockRecorder) GetUsersByUsernames(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByUsernames", reflect.TypeOf((*MockAPI)(nil).GetUsersByUsernames), arg0) -} - -// GetUsersInChannel mocks base method. -func (m *MockAPI) GetUsersInChannel(arg0, arg1 string, arg2, arg3 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersInChannel", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersInChannel indicates an expected call of GetUsersInChannel. -func (mr *MockAPIMockRecorder) GetUsersInChannel(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInChannel", reflect.TypeOf((*MockAPI)(nil).GetUsersInChannel), arg0, arg1, arg2, arg3) -} - -// GetUsersInTeam mocks base method. -func (m *MockAPI) GetUsersInTeam(arg0 string, arg1, arg2 int) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUsersInTeam", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// GetUsersInTeam indicates an expected call of GetUsersInTeam. -func (mr *MockAPIMockRecorder) GetUsersInTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersInTeam", reflect.TypeOf((*MockAPI)(nil).GetUsersInTeam), arg0, arg1, arg2) -} - -// HasPermissionTo mocks base method. -func (m *MockAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionTo indicates an expected call of HasPermissionTo. -func (mr *MockAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockAPI)(nil).HasPermissionTo), arg0, arg1) -} - -// HasPermissionToChannel mocks base method. -func (m *MockAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToChannel", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToChannel indicates an expected call of HasPermissionToChannel. -func (mr *MockAPIMockRecorder) HasPermissionToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToChannel", reflect.TypeOf((*MockAPI)(nil).HasPermissionToChannel), arg0, arg1, arg2) -} - -// HasPermissionToTeam mocks base method. -func (m *MockAPI) HasPermissionToTeam(arg0, arg1 string, arg2 *model.Permission) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasPermissionToTeam", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasPermissionToTeam indicates an expected call of HasPermissionToTeam. -func (mr *MockAPIMockRecorder) HasPermissionToTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToTeam", reflect.TypeOf((*MockAPI)(nil).HasPermissionToTeam), arg0, arg1, arg2) -} - -// InstallPlugin mocks base method. -func (m *MockAPI) InstallPlugin(arg0 io.Reader, arg1 bool) (*model.Manifest, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstallPlugin", arg0, arg1) - ret0, _ := ret[0].(*model.Manifest) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// InstallPlugin indicates an expected call of InstallPlugin. -func (mr *MockAPIMockRecorder) InstallPlugin(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockAPI)(nil).InstallPlugin), arg0, arg1) -} - -// IsEnterpriseReady mocks base method. -func (m *MockAPI) IsEnterpriseReady() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsEnterpriseReady") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsEnterpriseReady indicates an expected call of IsEnterpriseReady. -func (mr *MockAPIMockRecorder) IsEnterpriseReady() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnterpriseReady", reflect.TypeOf((*MockAPI)(nil).IsEnterpriseReady)) -} - -// KVCompareAndDelete mocks base method. -func (m *MockAPI) KVCompareAndDelete(arg0 string, arg1 []byte) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVCompareAndDelete", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVCompareAndDelete indicates an expected call of KVCompareAndDelete. -func (mr *MockAPIMockRecorder) KVCompareAndDelete(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndDelete", reflect.TypeOf((*MockAPI)(nil).KVCompareAndDelete), arg0, arg1) -} - -// KVCompareAndSet mocks base method. -func (m *MockAPI) KVCompareAndSet(arg0 string, arg1, arg2 []byte) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVCompareAndSet", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVCompareAndSet indicates an expected call of KVCompareAndSet. -func (mr *MockAPIMockRecorder) KVCompareAndSet(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVCompareAndSet", reflect.TypeOf((*MockAPI)(nil).KVCompareAndSet), arg0, arg1, arg2) -} - -// KVDelete mocks base method. -func (m *MockAPI) KVDelete(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVDelete", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVDelete indicates an expected call of KVDelete. -func (mr *MockAPIMockRecorder) KVDelete(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDelete", reflect.TypeOf((*MockAPI)(nil).KVDelete), arg0) -} - -// KVDeleteAll mocks base method. -func (m *MockAPI) KVDeleteAll() *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVDeleteAll") - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVDeleteAll indicates an expected call of KVDeleteAll. -func (mr *MockAPIMockRecorder) KVDeleteAll() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVDeleteAll", reflect.TypeOf((*MockAPI)(nil).KVDeleteAll)) -} - -// KVGet mocks base method. -func (m *MockAPI) KVGet(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVGet", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVGet indicates an expected call of KVGet. -func (mr *MockAPIMockRecorder) KVGet(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVGet", reflect.TypeOf((*MockAPI)(nil).KVGet), arg0) -} - -// KVList mocks base method. -func (m *MockAPI) KVList(arg0, arg1 int) ([]string, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVList", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVList indicates an expected call of KVList. -func (mr *MockAPIMockRecorder) KVList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVList", reflect.TypeOf((*MockAPI)(nil).KVList), arg0, arg1) -} - -// KVSet mocks base method. -func (m *MockAPI) KVSet(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSet", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVSet indicates an expected call of KVSet. -func (mr *MockAPIMockRecorder) KVSet(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSet", reflect.TypeOf((*MockAPI)(nil).KVSet), arg0, arg1) -} - -// KVSetWithExpiry mocks base method. -func (m *MockAPI) KVSetWithExpiry(arg0 string, arg1 []byte, arg2 int64) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSetWithExpiry", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// KVSetWithExpiry indicates an expected call of KVSetWithExpiry. -func (mr *MockAPIMockRecorder) KVSetWithExpiry(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithExpiry", reflect.TypeOf((*MockAPI)(nil).KVSetWithExpiry), arg0, arg1, arg2) -} - -// KVSetWithOptions mocks base method. -func (m *MockAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// KVSetWithOptions indicates an expected call of KVSetWithOptions. -func (mr *MockAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockAPI)(nil).KVSetWithOptions), arg0, arg1, arg2) -} - -// ListBuiltInCommands mocks base method. -func (m *MockAPI) ListBuiltInCommands() ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListBuiltInCommands") - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListBuiltInCommands indicates an expected call of ListBuiltInCommands. -func (mr *MockAPIMockRecorder) ListBuiltInCommands() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuiltInCommands", reflect.TypeOf((*MockAPI)(nil).ListBuiltInCommands)) -} - -// ListCommands mocks base method. -func (m *MockAPI) ListCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListCommands indicates an expected call of ListCommands. -func (mr *MockAPIMockRecorder) ListCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommands", reflect.TypeOf((*MockAPI)(nil).ListCommands), arg0) -} - -// ListCustomCommands mocks base method. -func (m *MockAPI) ListCustomCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListCustomCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListCustomCommands indicates an expected call of ListCustomCommands. -func (mr *MockAPIMockRecorder) ListCustomCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCustomCommands", reflect.TypeOf((*MockAPI)(nil).ListCustomCommands), arg0) -} - -// ListPluginCommands mocks base method. -func (m *MockAPI) ListPluginCommands(arg0 string) ([]*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPluginCommands", arg0) - ret0, _ := ret[0].([]*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPluginCommands indicates an expected call of ListPluginCommands. -func (mr *MockAPIMockRecorder) ListPluginCommands(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPluginCommands", reflect.TypeOf((*MockAPI)(nil).ListPluginCommands), arg0) -} - -// LoadPluginConfiguration mocks base method. -func (m *MockAPI) LoadPluginConfiguration(arg0 interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadPluginConfiguration", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// LoadPluginConfiguration indicates an expected call of LoadPluginConfiguration. -func (mr *MockAPIMockRecorder) LoadPluginConfiguration(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadPluginConfiguration", reflect.TypeOf((*MockAPI)(nil).LoadPluginConfiguration), arg0) -} - -// LogDebug mocks base method. -func (m *MockAPI) LogDebug(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogDebug", varargs...) -} - -// LogDebug indicates an expected call of LogDebug. -func (mr *MockAPIMockRecorder) LogDebug(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogDebug", reflect.TypeOf((*MockAPI)(nil).LogDebug), varargs...) -} - -// LogError mocks base method. -func (m *MockAPI) LogError(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogError", varargs...) -} - -// LogError indicates an expected call of LogError. -func (mr *MockAPIMockRecorder) LogError(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogError", reflect.TypeOf((*MockAPI)(nil).LogError), varargs...) -} - -// LogInfo mocks base method. -func (m *MockAPI) LogInfo(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogInfo", varargs...) -} - -// LogInfo indicates an expected call of LogInfo. -func (mr *MockAPIMockRecorder) LogInfo(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogInfo", reflect.TypeOf((*MockAPI)(nil).LogInfo), varargs...) -} - -// LogWarn mocks base method. -func (m *MockAPI) LogWarn(arg0 string, arg1 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "LogWarn", varargs...) -} - -// LogWarn indicates an expected call of LogWarn. -func (mr *MockAPIMockRecorder) LogWarn(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogWarn", reflect.TypeOf((*MockAPI)(nil).LogWarn), varargs...) -} - -// OpenInteractiveDialog mocks base method. -func (m *MockAPI) OpenInteractiveDialog(arg0 model.OpenDialogRequest) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OpenInteractiveDialog", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// OpenInteractiveDialog indicates an expected call of OpenInteractiveDialog. -func (mr *MockAPIMockRecorder) OpenInteractiveDialog(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenInteractiveDialog", reflect.TypeOf((*MockAPI)(nil).OpenInteractiveDialog), arg0) -} - -// PatchBot mocks base method. -func (m *MockAPI) PatchBot(arg0 string, arg1 *model.BotPatch) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PatchBot", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// PatchBot indicates an expected call of PatchBot. -func (mr *MockAPIMockRecorder) PatchBot(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchBot", reflect.TypeOf((*MockAPI)(nil).PatchBot), arg0, arg1) -} - -// PermanentDeleteBot mocks base method. -func (m *MockAPI) PermanentDeleteBot(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PermanentDeleteBot", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// PermanentDeleteBot indicates an expected call of PermanentDeleteBot. -func (mr *MockAPIMockRecorder) PermanentDeleteBot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermanentDeleteBot", reflect.TypeOf((*MockAPI)(nil).PermanentDeleteBot), arg0) -} - -// PluginHTTP mocks base method. -func (m *MockAPI) PluginHTTP(arg0 *http.Request) *http.Response { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PluginHTTP", arg0) - ret0, _ := ret[0].(*http.Response) - return ret0 -} - -// PluginHTTP indicates an expected call of PluginHTTP. -func (mr *MockAPIMockRecorder) PluginHTTP(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginHTTP", reflect.TypeOf((*MockAPI)(nil).PluginHTTP), arg0) -} - -// PublishPluginClusterEvent mocks base method. -func (m *MockAPI) PublishPluginClusterEvent(arg0 model.PluginClusterEvent, arg1 model.PluginClusterEventSendOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishPluginClusterEvent", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// PublishPluginClusterEvent indicates an expected call of PublishPluginClusterEvent. -func (mr *MockAPIMockRecorder) PublishPluginClusterEvent(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPluginClusterEvent", reflect.TypeOf((*MockAPI)(nil).PublishPluginClusterEvent), arg0, arg1) -} - -// PublishUserTyping mocks base method. -func (m *MockAPI) PublishUserTyping(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishUserTyping", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// PublishUserTyping indicates an expected call of PublishUserTyping. -func (mr *MockAPIMockRecorder) PublishUserTyping(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishUserTyping", reflect.TypeOf((*MockAPI)(nil).PublishUserTyping), arg0, arg1, arg2) -} - -// PublishWebSocketEvent mocks base method. -func (m *MockAPI) PublishWebSocketEvent(arg0 string, arg1 map[string]interface{}, arg2 *model.WebsocketBroadcast) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebSocketEvent", arg0, arg1, arg2) -} - -// PublishWebSocketEvent indicates an expected call of PublishWebSocketEvent. -func (mr *MockAPIMockRecorder) PublishWebSocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebSocketEvent", reflect.TypeOf((*MockAPI)(nil).PublishWebSocketEvent), arg0, arg1, arg2) -} - -// ReadFile mocks base method. -func (m *MockAPI) ReadFile(arg0 string) ([]byte, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadFile", arg0) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// ReadFile indicates an expected call of ReadFile. -func (mr *MockAPIMockRecorder) ReadFile(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockAPI)(nil).ReadFile), arg0) -} - -// RegisterCollectionAndTopic mocks base method. -func (m *MockAPI) RegisterCollectionAndTopic(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RegisterCollectionAndTopic", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// RegisterCollectionAndTopic indicates an expected call of RegisterCollectionAndTopic. -func (mr *MockAPIMockRecorder) RegisterCollectionAndTopic(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCollectionAndTopic", reflect.TypeOf((*MockAPI)(nil).RegisterCollectionAndTopic), arg0, arg1) -} - -// RegisterCommand mocks base method. -func (m *MockAPI) RegisterCommand(arg0 *model.Command) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RegisterCommand", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// RegisterCommand indicates an expected call of RegisterCommand. -func (mr *MockAPIMockRecorder) RegisterCommand(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCommand", reflect.TypeOf((*MockAPI)(nil).RegisterCommand), arg0) -} - -// RemovePlugin mocks base method. -func (m *MockAPI) RemovePlugin(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemovePlugin", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemovePlugin indicates an expected call of RemovePlugin. -func (mr *MockAPIMockRecorder) RemovePlugin(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePlugin", reflect.TypeOf((*MockAPI)(nil).RemovePlugin), arg0) -} - -// RemoveReaction mocks base method. -func (m *MockAPI) RemoveReaction(arg0 *model.Reaction) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveReaction", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveReaction indicates an expected call of RemoveReaction. -func (mr *MockAPIMockRecorder) RemoveReaction(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveReaction", reflect.TypeOf((*MockAPI)(nil).RemoveReaction), arg0) -} - -// RemoveTeamIcon mocks base method. -func (m *MockAPI) RemoveTeamIcon(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveTeamIcon", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveTeamIcon indicates an expected call of RemoveTeamIcon. -func (mr *MockAPIMockRecorder) RemoveTeamIcon(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTeamIcon", reflect.TypeOf((*MockAPI)(nil).RemoveTeamIcon), arg0) -} - -// RemoveUserCustomStatus mocks base method. -func (m *MockAPI) RemoveUserCustomStatus(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveUserCustomStatus", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RemoveUserCustomStatus indicates an expected call of RemoveUserCustomStatus. -func (mr *MockAPIMockRecorder) RemoveUserCustomStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).RemoveUserCustomStatus), arg0) -} - -// RequestTrialLicense mocks base method. -func (m *MockAPI) RequestTrialLicense(arg0 string, arg1 int, arg2, arg3 bool) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestTrialLicense", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RequestTrialLicense indicates an expected call of RequestTrialLicense. -func (mr *MockAPIMockRecorder) RequestTrialLicense(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestTrialLicense", reflect.TypeOf((*MockAPI)(nil).RequestTrialLicense), arg0, arg1, arg2, arg3) -} - -// RevokeSession mocks base method. -func (m *MockAPI) RevokeSession(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeSession", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RevokeSession indicates an expected call of RevokeSession. -func (mr *MockAPIMockRecorder) RevokeSession(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSession", reflect.TypeOf((*MockAPI)(nil).RevokeSession), arg0) -} - -// RevokeUserAccessToken mocks base method. -func (m *MockAPI) RevokeUserAccessToken(arg0 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeUserAccessToken", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// RevokeUserAccessToken indicates an expected call of RevokeUserAccessToken. -func (mr *MockAPIMockRecorder) RevokeUserAccessToken(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockAPI)(nil).RevokeUserAccessToken), arg0) -} - -// RolesGrantPermission mocks base method. -func (m *MockAPI) RolesGrantPermission(arg0 []string, arg1 string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RolesGrantPermission", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// RolesGrantPermission indicates an expected call of RolesGrantPermission. -func (mr *MockAPIMockRecorder) RolesGrantPermission(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RolesGrantPermission", reflect.TypeOf((*MockAPI)(nil).RolesGrantPermission), arg0, arg1) -} - -// SaveConfig mocks base method. -func (m *MockAPI) SaveConfig(arg0 *model.Config) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveConfig", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SaveConfig indicates an expected call of SaveConfig. -func (mr *MockAPIMockRecorder) SaveConfig(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveConfig", reflect.TypeOf((*MockAPI)(nil).SaveConfig), arg0) -} - -// SavePluginConfig mocks base method. -func (m *MockAPI) SavePluginConfig(arg0 map[string]interface{}) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePluginConfig", arg0) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SavePluginConfig indicates an expected call of SavePluginConfig. -func (mr *MockAPIMockRecorder) SavePluginConfig(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePluginConfig", reflect.TypeOf((*MockAPI)(nil).SavePluginConfig), arg0) -} - -// SearchChannels mocks base method. -func (m *MockAPI) SearchChannels(arg0, arg1 string) ([]*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchChannels", arg0, arg1) - ret0, _ := ret[0].([]*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchChannels indicates an expected call of SearchChannels. -func (mr *MockAPIMockRecorder) SearchChannels(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchChannels", reflect.TypeOf((*MockAPI)(nil).SearchChannels), arg0, arg1) -} - -// SearchPostsInTeam mocks base method. -func (m *MockAPI) SearchPostsInTeam(arg0 string, arg1 []*model.SearchParams) ([]*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchPostsInTeam", arg0, arg1) - ret0, _ := ret[0].([]*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchPostsInTeam indicates an expected call of SearchPostsInTeam. -func (mr *MockAPIMockRecorder) SearchPostsInTeam(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeam", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeam), arg0, arg1) -} - -// SearchPostsInTeamForUser mocks base method. -func (m *MockAPI) SearchPostsInTeamForUser(arg0, arg1 string, arg2 model.SearchParameter) (*model.PostSearchResults, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchPostsInTeamForUser", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.PostSearchResults) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchPostsInTeamForUser indicates an expected call of SearchPostsInTeamForUser. -func (mr *MockAPIMockRecorder) SearchPostsInTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchPostsInTeamForUser", reflect.TypeOf((*MockAPI)(nil).SearchPostsInTeamForUser), arg0, arg1, arg2) -} - -// SearchTeams mocks base method. -func (m *MockAPI) SearchTeams(arg0 string) ([]*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchTeams", arg0) - ret0, _ := ret[0].([]*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchTeams indicates an expected call of SearchTeams. -func (mr *MockAPIMockRecorder) SearchTeams(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchTeams", reflect.TypeOf((*MockAPI)(nil).SearchTeams), arg0) -} - -// SearchUsers mocks base method. -func (m *MockAPI) SearchUsers(arg0 *model.UserSearch) ([]*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchUsers", arg0) - ret0, _ := ret[0].([]*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SearchUsers indicates an expected call of SearchUsers. -func (mr *MockAPIMockRecorder) SearchUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockAPI)(nil).SearchUsers), arg0) -} - -// SendEphemeralPost mocks base method. -func (m *MockAPI) SendEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendEphemeralPost", arg0, arg1) - ret0, _ := ret[0].(*model.Post) - return ret0 -} - -// SendEphemeralPost indicates an expected call of SendEphemeralPost. -func (mr *MockAPIMockRecorder) SendEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEphemeralPost", reflect.TypeOf((*MockAPI)(nil).SendEphemeralPost), arg0, arg1) -} - -// SendMail mocks base method. -func (m *MockAPI) SendMail(arg0, arg1, arg2 string) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMail", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SendMail indicates an expected call of SendMail. -func (mr *MockAPIMockRecorder) SendMail(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMail", reflect.TypeOf((*MockAPI)(nil).SendMail), arg0, arg1, arg2) -} - -// SetProfileImage mocks base method. -func (m *MockAPI) SetProfileImage(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProfileImage", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SetProfileImage indicates an expected call of SetProfileImage. -func (mr *MockAPIMockRecorder) SetProfileImage(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileImage", reflect.TypeOf((*MockAPI)(nil).SetProfileImage), arg0, arg1) -} - -// SetTeamIcon mocks base method. -func (m *MockAPI) SetTeamIcon(arg0 string, arg1 []byte) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetTeamIcon", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// SetTeamIcon indicates an expected call of SetTeamIcon. -func (mr *MockAPIMockRecorder) SetTeamIcon(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTeamIcon", reflect.TypeOf((*MockAPI)(nil).SetTeamIcon), arg0, arg1) -} - -// SetUserStatusTimedDND mocks base method. -func (m *MockAPI) SetUserStatusTimedDND(arg0 string, arg1 int64) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserStatusTimedDND", arg0, arg1) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// SetUserStatusTimedDND indicates an expected call of SetUserStatusTimedDND. -func (mr *MockAPIMockRecorder) SetUserStatusTimedDND(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserStatusTimedDND", reflect.TypeOf((*MockAPI)(nil).SetUserStatusTimedDND), arg0, arg1) -} - -// UnregisterCommand mocks base method. -func (m *MockAPI) UnregisterCommand(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnregisterCommand", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnregisterCommand indicates an expected call of UnregisterCommand. -func (mr *MockAPIMockRecorder) UnregisterCommand(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnregisterCommand", reflect.TypeOf((*MockAPI)(nil).UnregisterCommand), arg0, arg1) -} - -// UpdateBotActive mocks base method. -func (m *MockAPI) UpdateBotActive(arg0 string, arg1 bool) (*model.Bot, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateBotActive", arg0, arg1) - ret0, _ := ret[0].(*model.Bot) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateBotActive indicates an expected call of UpdateBotActive. -func (mr *MockAPIMockRecorder) UpdateBotActive(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBotActive", reflect.TypeOf((*MockAPI)(nil).UpdateBotActive), arg0, arg1) -} - -// UpdateChannel mocks base method. -func (m *MockAPI) UpdateChannel(arg0 *model.Channel) (*model.Channel, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannel", arg0) - ret0, _ := ret[0].(*model.Channel) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannel indicates an expected call of UpdateChannel. -func (mr *MockAPIMockRecorder) UpdateChannel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannel", reflect.TypeOf((*MockAPI)(nil).UpdateChannel), arg0) -} - -// UpdateChannelMemberNotifications mocks base method. -func (m *MockAPI) UpdateChannelMemberNotifications(arg0, arg1 string, arg2 map[string]string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelMemberNotifications", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelMemberNotifications indicates an expected call of UpdateChannelMemberNotifications. -func (mr *MockAPIMockRecorder) UpdateChannelMemberNotifications(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberNotifications", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberNotifications), arg0, arg1, arg2) -} - -// UpdateChannelMemberRoles mocks base method. -func (m *MockAPI) UpdateChannelMemberRoles(arg0, arg1, arg2 string) (*model.ChannelMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelMemberRoles", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.ChannelMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelMemberRoles indicates an expected call of UpdateChannelMemberRoles. -func (mr *MockAPIMockRecorder) UpdateChannelMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateChannelMemberRoles), arg0, arg1, arg2) -} - -// UpdateChannelSidebarCategories mocks base method. -func (m *MockAPI) UpdateChannelSidebarCategories(arg0, arg1 string, arg2 []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChannelSidebarCategories", arg0, arg1, arg2) - ret0, _ := ret[0].([]*model.SidebarCategoryWithChannels) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateChannelSidebarCategories indicates an expected call of UpdateChannelSidebarCategories. -func (mr *MockAPIMockRecorder) UpdateChannelSidebarCategories(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChannelSidebarCategories", reflect.TypeOf((*MockAPI)(nil).UpdateChannelSidebarCategories), arg0, arg1, arg2) -} - -// UpdateCommand mocks base method. -func (m *MockAPI) UpdateCommand(arg0 string, arg1 *model.Command) (*model.Command, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateCommand", arg0, arg1) - ret0, _ := ret[0].(*model.Command) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateCommand indicates an expected call of UpdateCommand. -func (mr *MockAPIMockRecorder) UpdateCommand(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCommand", reflect.TypeOf((*MockAPI)(nil).UpdateCommand), arg0, arg1) -} - -// UpdateEphemeralPost mocks base method. -func (m *MockAPI) UpdateEphemeralPost(arg0 string, arg1 *model.Post) *model.Post { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateEphemeralPost", arg0, arg1) - ret0, _ := ret[0].(*model.Post) - return ret0 -} - -// UpdateEphemeralPost indicates an expected call of UpdateEphemeralPost. -func (mr *MockAPIMockRecorder) UpdateEphemeralPost(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEphemeralPost", reflect.TypeOf((*MockAPI)(nil).UpdateEphemeralPost), arg0, arg1) -} - -// UpdateOAuthApp mocks base method. -func (m *MockAPI) UpdateOAuthApp(arg0 *model.OAuthApp) (*model.OAuthApp, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateOAuthApp", arg0) - ret0, _ := ret[0].(*model.OAuthApp) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateOAuthApp indicates an expected call of UpdateOAuthApp. -func (mr *MockAPIMockRecorder) UpdateOAuthApp(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuthApp", reflect.TypeOf((*MockAPI)(nil).UpdateOAuthApp), arg0) -} - -// UpdatePost mocks base method. -func (m *MockAPI) UpdatePost(arg0 *model.Post) (*model.Post, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePost", arg0) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdatePost indicates an expected call of UpdatePost. -func (mr *MockAPIMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockAPI)(nil).UpdatePost), arg0) -} - -// UpdatePreferencesForUser mocks base method. -func (m *MockAPI) UpdatePreferencesForUser(arg0 string, arg1 []model.Preference) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser. -func (mr *MockAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockAPI)(nil).UpdatePreferencesForUser), arg0, arg1) -} - -// UpdateTeam mocks base method. -func (m *MockAPI) UpdateTeam(arg0 *model.Team) (*model.Team, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTeam", arg0) - ret0, _ := ret[0].(*model.Team) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateTeam indicates an expected call of UpdateTeam. -func (mr *MockAPIMockRecorder) UpdateTeam(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeam", reflect.TypeOf((*MockAPI)(nil).UpdateTeam), arg0) -} - -// UpdateTeamMemberRoles mocks base method. -func (m *MockAPI) UpdateTeamMemberRoles(arg0, arg1, arg2 string) (*model.TeamMember, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTeamMemberRoles", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.TeamMember) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateTeamMemberRoles indicates an expected call of UpdateTeamMemberRoles. -func (mr *MockAPIMockRecorder) UpdateTeamMemberRoles(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTeamMemberRoles", reflect.TypeOf((*MockAPI)(nil).UpdateTeamMemberRoles), arg0, arg1, arg2) -} - -// UpdateUser mocks base method. -func (m *MockAPI) UpdateUser(arg0 *model.User) (*model.User, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) - ret0, _ := ret[0].(*model.User) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateUser indicates an expected call of UpdateUser. -func (mr *MockAPIMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockAPI)(nil).UpdateUser), arg0) -} - -// UpdateUserActive mocks base method. -func (m *MockAPI) UpdateUserActive(arg0 string, arg1 bool) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserActive", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdateUserActive indicates an expected call of UpdateUserActive. -func (mr *MockAPIMockRecorder) UpdateUserActive(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserActive", reflect.TypeOf((*MockAPI)(nil).UpdateUserActive), arg0, arg1) -} - -// UpdateUserCustomStatus mocks base method. -func (m *MockAPI) UpdateUserCustomStatus(arg0 string, arg1 *model.CustomStatus) *model.AppError { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserCustomStatus", arg0, arg1) - ret0, _ := ret[0].(*model.AppError) - return ret0 -} - -// UpdateUserCustomStatus indicates an expected call of UpdateUserCustomStatus. -func (mr *MockAPIMockRecorder) UpdateUserCustomStatus(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserCustomStatus), arg0, arg1) -} - -// UpdateUserStatus mocks base method. -func (m *MockAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserStatus", arg0, arg1) - ret0, _ := ret[0].(*model.Status) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UpdateUserStatus indicates an expected call of UpdateUserStatus. -func (mr *MockAPIMockRecorder) UpdateUserStatus(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserStatus), arg0, arg1) -} - -// UploadData mocks base method. -func (m *MockAPI) UploadData(arg0 *model.UploadSession, arg1 io.Reader) (*model.FileInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UploadData", arg0, arg1) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UploadData indicates an expected call of UploadData. -func (mr *MockAPIMockRecorder) UploadData(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadData", reflect.TypeOf((*MockAPI)(nil).UploadData), arg0, arg1) -} - -// UploadFile mocks base method. -func (m *MockAPI) UploadFile(arg0 []byte, arg1, arg2 string) (*model.FileInfo, *model.AppError) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UploadFile", arg0, arg1, arg2) - ret0, _ := ret[0].(*model.FileInfo) - ret1, _ := ret[1].(*model.AppError) - return ret0, ret1 -} - -// UploadFile indicates an expected call of UploadFile. -func (mr *MockAPIMockRecorder) UploadFile(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadFile", reflect.TypeOf((*MockAPI)(nil).UploadFile), arg0, arg1, arg2) -} diff --git a/server/boards/ws/mocks/mockstore.go b/server/boards/ws/mocks/mockstore.go deleted file mode 100644 index b32e4b636c..0000000000 --- a/server/boards/ws/mocks/mockstore.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/boards/ws (interfaces: Store) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/v8/boards/model" -) - -// MockStore is a mock of Store interface. -type MockStore struct { - ctrl *gomock.Controller - recorder *MockStoreMockRecorder -} - -// MockStoreMockRecorder is the mock recorder for MockStore. -type MockStoreMockRecorder struct { - mock *MockStore -} - -// NewMockStore creates a new mock instance. -func NewMockStore(ctrl *gomock.Controller) *MockStore { - mock := &MockStore{ctrl: ctrl} - mock.recorder = &MockStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStore) EXPECT() *MockStoreMockRecorder { - return m.recorder -} - -// GetBlock mocks base method. -func (m *MockStore) GetBlock(arg0 string) (*model.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBlock", arg0) - ret0, _ := ret[0].(*model.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBlock indicates an expected call of GetBlock. -func (mr *MockStoreMockRecorder) GetBlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0) -} - -// GetMembersForBoard mocks base method. -func (m *MockStore) GetMembersForBoard(arg0 string) ([]*model.BoardMember, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMembersForBoard", arg0) - ret0, _ := ret[0].([]*model.BoardMember) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMembersForBoard indicates an expected call of GetMembersForBoard. -func (mr *MockStoreMockRecorder) GetMembersForBoard(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForBoard", reflect.TypeOf((*MockStore)(nil).GetMembersForBoard), arg0) -} diff --git a/server/boards/ws/plugin_adapter.go b/server/boards/ws/plugin_adapter.go deleted file mode 100644 index d5137ec2ed..0000000000 --- a/server/boards/ws/plugin_adapter.go +++ /dev/null @@ -1,678 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost/server/public/plugin API -package ws - -import ( - "fmt" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -const websocketMessagePrefix = "custom_boards_" - -var errMissingTeamInCommand = fmt.Errorf("command doesn't contain teamId") - -type PluginAdapterInterface interface { - Adapter - OnWebSocketConnect(webConnID, userID string) - OnWebSocketDisconnect(webConnID, userID string) - WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) - BroadcastConfigChange(clientConfig model.ClientConfig) - BroadcastBlockChange(teamID string, block *model.Block) - BroadcastBlockDelete(teamID, blockID, parentID string) - BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) - BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) - HandleClusterEvent(ev mm_model.PluginClusterEvent) -} - -type PluginAdapter struct { - api servicesAPI - auth auth.AuthInterface - staleThreshold time.Duration - store Store - logger mlog.LoggerIFace - - listenersMU sync.RWMutex - listeners map[string]*PluginAdapterClient - listenersByUserID map[string][]*PluginAdapterClient - - subscriptionsMU sync.RWMutex - listenersByTeam map[string][]*PluginAdapterClient - listenersByBlock map[string][]*PluginAdapterClient -} - -// servicesAPI is the interface required by the PluginAdapter to interact with -// the mattermost-server. -type servicesAPI interface { - PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) - PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error -} - -func NewPluginAdapter(api servicesAPI, auth auth.AuthInterface, store Store, logger mlog.LoggerIFace) *PluginAdapter { - return &PluginAdapter{ - api: api, - auth: auth, - store: store, - staleThreshold: 5 * time.Minute, - logger: logger, - listeners: make(map[string]*PluginAdapterClient), - listenersByUserID: make(map[string][]*PluginAdapterClient), - listenersByTeam: make(map[string][]*PluginAdapterClient), - listenersByBlock: make(map[string][]*PluginAdapterClient), - listenersMU: sync.RWMutex{}, - subscriptionsMU: sync.RWMutex{}, - } -} - -func (pa *PluginAdapter) GetListenerByWebConnID(webConnID string) (pac *PluginAdapterClient, ok bool) { - pa.listenersMU.RLock() - defer pa.listenersMU.RUnlock() - - pac, ok = pa.listeners[webConnID] - return -} - -func (pa *PluginAdapter) GetListenersByUserID(userID string) []*PluginAdapterClient { - pa.listenersMU.RLock() - defer pa.listenersMU.RUnlock() - - return pa.listenersByUserID[userID] -} - -func (pa *PluginAdapter) GetListenersByTeam(teamID string) []*PluginAdapterClient { - pa.subscriptionsMU.RLock() - defer pa.subscriptionsMU.RUnlock() - - return pa.listenersByTeam[teamID] -} - -func (pa *PluginAdapter) GetListenersByBlock(blockID string) []*PluginAdapterClient { - pa.subscriptionsMU.RLock() - defer pa.subscriptionsMU.RUnlock() - - return pa.listenersByBlock[blockID] -} - -func (pa *PluginAdapter) addListener(pac *PluginAdapterClient) { - pa.listenersMU.Lock() - defer pa.listenersMU.Unlock() - - pa.listeners[pac.webConnID] = pac - pa.listenersByUserID[pac.userID] = append(pa.listenersByUserID[pac.userID], pac) -} - -func (pa *PluginAdapter) removeListener(pac *PluginAdapterClient) { - pa.listenersMU.Lock() - defer pa.listenersMU.Unlock() - - // team subscriptions - for _, team := range pac.teams { - pa.removeListenerFromTeam(pac, team) - } - - // block subscriptions - for _, block := range pac.blocks { - pa.removeListenerFromBlock(pac, block) - } - - // user ID list - newUserListeners := []*PluginAdapterClient{} - for _, listener := range pa.listenersByUserID[pac.userID] { - if listener.webConnID != pac.webConnID { - newUserListeners = append(newUserListeners, listener) - } - } - pa.listenersByUserID[pac.userID] = newUserListeners - - delete(pa.listeners, pac.webConnID) -} - -func (pa *PluginAdapter) removeExpiredForUserID(userID string) { - for _, pac := range pa.GetListenersByUserID(userID) { - if !pac.isActive() && pac.hasExpired(pa.staleThreshold) { - pa.removeListener(pac) - } - } -} - -func (pa *PluginAdapter) removeListenerFromTeam(pac *PluginAdapterClient, teamID string) { - newTeamListeners := []*PluginAdapterClient{} - for _, listener := range pa.GetListenersByTeam(teamID) { - if listener.webConnID != pac.webConnID { - newTeamListeners = append(newTeamListeners, listener) - } - } - pa.subscriptionsMU.Lock() - pa.listenersByTeam[teamID] = newTeamListeners - pa.subscriptionsMU.Unlock() - - pac.unsubscribeFromTeam(teamID) -} - -func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, blockID string) { - newBlockListeners := []*PluginAdapterClient{} - for _, listener := range pa.GetListenersByBlock(blockID) { - if listener.webConnID != pac.webConnID { - newBlockListeners = append(newBlockListeners, listener) - } - } - pa.subscriptionsMU.Lock() - pa.listenersByBlock[blockID] = newBlockListeners - pa.subscriptionsMU.Unlock() - - pac.unsubscribeFromBlock(blockID) -} - -func (pa *PluginAdapter) subscribeListenerToTeam(pac *PluginAdapterClient, teamID string) { - if pac.isSubscribedToTeam(teamID) { - return - } - - pa.subscriptionsMU.Lock() - pa.listenersByTeam[teamID] = append(pa.listenersByTeam[teamID], pac) - pa.subscriptionsMU.Unlock() - - pac.subscribeToTeam(teamID) -} - -func (pa *PluginAdapter) unsubscribeListenerFromTeam(pac *PluginAdapterClient, teamID string) { - if !pac.isSubscribedToTeam(teamID) { - return - } - - pa.removeListenerFromTeam(pac, teamID) -} - -func (pa *PluginAdapter) getUserIDsForTeam(teamID string) []string { - userMap := map[string]bool{} - for _, pac := range pa.GetListenersByTeam(teamID) { - if pac.isActive() { - userMap[pac.userID] = true - } - } - - userIDs := []string{} - for userID := range userMap { - if pa.auth.DoesUserHaveTeamAccess(userID, teamID) { - userIDs = append(userIDs, userID) - } - } - - return userIDs -} - -func (pa *PluginAdapter) getUserIDsForTeamAndBoard(teamID, boardID string, ensureUserIDs ...string) []string { - userMap := map[string]bool{} - for _, pac := range pa.GetListenersByTeam(teamID) { - if pac.isActive() { - userMap[pac.userID] = true - } - } - - members, err := pa.store.GetMembersForBoard(boardID) - if err != nil { - pa.logger.Error("error getting members for board", - mlog.String("method", "getUserIDsForTeamAndBoard"), - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - ) - return nil - } - - // the list of users would be the intersection between the ones - // that are connected to the team and the board members that need - // to see the updates - userIDs := []string{} - for _, member := range members { - for userID := range userMap { - if userID == member.UserID && pa.auth.DoesUserHaveTeamAccess(userID, teamID) { - userIDs = append(userIDs, userID) - } - } - } - - // if we don't have to make sure that some IDs are included, we - // can return at this point - if len(ensureUserIDs) == 0 { - return userIDs - } - - completeUserMap := map[string]bool{} - for _, id := range userIDs { - completeUserMap[id] = true - } - for _, id := range ensureUserIDs { - completeUserMap[id] = true - } - - completeUserIDs := []string{} - for id := range completeUserMap { - completeUserIDs = append(completeUserIDs, id) - } - - return completeUserIDs -} - -//nolint:unused -func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) { - for _, blockID := range blockIDs { - if pac.isSubscribedToBlock(blockID) { - pa.removeListenerFromBlock(pac, blockID) - } - } -} - -func (pa *PluginAdapter) OnWebSocketConnect(webConnID, userID string) { - if existingPAC, ok := pa.GetListenerByWebConnID(webConnID); ok { - pa.logger.Debug("inactive connection found for webconn, reusing", - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - ) - atomic.StoreInt64(&existingPAC.inactiveAt, 0) - return - } - - newPAC := &PluginAdapterClient{ - inactiveAt: 0, - webConnID: webConnID, - userID: userID, - teams: []string{}, - blocks: []string{}, - } - - pa.addListener(newPAC) - pa.removeExpiredForUserID(userID) -} - -func (pa *PluginAdapter) OnWebSocketDisconnect(webConnID, userID string) { - pac, ok := pa.GetListenerByWebConnID(webConnID) - if !ok { - pa.logger.Debug("received a disconnect for an unregistered webconn", - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - ) - return - } - - atomic.StoreInt64(&pac.inactiveAt, mm_model.GetMillis()) -} - -func commandFromRequest(req *mm_model.WebSocketRequest) (*WebsocketCommand, error) { - c := &WebsocketCommand{Action: strings.TrimPrefix(req.Action, websocketMessagePrefix)} - - if teamID, ok := req.Data["teamId"]; ok { - c.TeamID = teamID.(string) - } else { - return nil, errMissingTeamInCommand - } - - if readToken, ok := req.Data["readToken"]; ok { - c.ReadToken = readToken.(string) - } - - if blockIDs, ok := req.Data["blockIds"]; ok { - c.BlockIDs = blockIDs.([]string) - } - - return c, nil -} - -func (pa *PluginAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) { - pac, ok := pa.GetListenerByWebConnID(webConnID) - if !ok { - pa.logger.Debug("received a message for an unregistered webconn", - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - mlog.String("action", req.Action), - ) - return - } - - // only process messages using the plugin actions - if !strings.HasPrefix(req.Action, websocketMessagePrefix) { - return - } - - command, err := commandFromRequest(req) - if err != nil { - pa.logger.Error("error getting command from request", - mlog.String("action", req.Action), - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - mlog.Err(err), - ) - return - } - - switch command.Action { - // The block-related commands are not implemented in the adapter - // as there is no such thing as unauthenticated websocket - // connections in plugin mode. Only a debug line is logged - case websocketActionSubscribeBlocks, websocketActionUnsubscribeBlocks: - pa.logger.Debug(`Command not implemented in plugin mode`, - mlog.String("command", command.Action), - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - mlog.String("teamID", command.TeamID), - ) - - case websocketActionSubscribeTeam: - pa.logger.Debug(`Command not implemented in plugin mode`, - mlog.String("command", command.Action), - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - mlog.String("teamID", command.TeamID), - ) - - if !pa.auth.DoesUserHaveTeamAccess(userID, command.TeamID) { - return - } - - pa.subscribeListenerToTeam(pac, command.TeamID) - case websocketActionUnsubscribeTeam: - pa.logger.Debug(`Command: UNSUBSCRIBE_WORKSPACE`, - mlog.String("webConnID", webConnID), - mlog.String("userID", userID), - mlog.String("teamID", command.TeamID), - ) - - pa.unsubscribeListenerFromTeam(pac, command.TeamID) - } -} - -// sendMessageToAll will send a websocket message to all clients on all nodes. -func (pa *PluginAdapter) sendMessageToAll(event string, payload map[string]interface{}) { - // Empty &mm_model.WebsocketBroadcast will send to all users - pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{}) -} - -func (pa *PluginAdapter) BroadcastConfigChange(pluginConfig model.ClientConfig) { - pa.sendMessageToAll(websocketActionUpdateConfig, utils.StructToMap(pluginConfig)) -} - -// sendUserMessageSkipCluster sends the message to specific users. -func (pa *PluginAdapter) sendUserMessageSkipCluster(event string, payload map[string]interface{}, userIDs ...string) { - for _, userID := range userIDs { - pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{UserId: userID}) - } -} - -// sendTeamMessageSkipCluster sends a message to all the users -// with a websocket client subscribed to a given team. -func (pa *PluginAdapter) sendTeamMessageSkipCluster(event, teamID string, payload map[string]interface{}) { - userIDs := pa.getUserIDsForTeam(teamID) - pa.sendUserMessageSkipCluster(event, payload, userIDs...) -} - -// sendTeamMessage sends and propagates a message that is aimed -// for all the users that are subscribed to a given team. -func (pa *PluginAdapter) sendTeamMessage(event, teamID string, payload map[string]interface{}, ensureUserIDs ...string) { - go func() { - clusterMessage := &ClusterMessage{ - TeamID: teamID, - Payload: payload, - EnsureUsers: ensureUserIDs, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendTeamMessageSkipCluster(event, teamID, payload) -} - -// sendBoardMessageSkipCluster sends a message to all the users -// subscribed to a given team that belong to one of its boards. -func (pa *PluginAdapter) sendBoardMessageSkipCluster(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) { - userIDs := pa.getUserIDsForTeamAndBoard(teamID, boardID, ensureUserIDs...) - pa.sendUserMessageSkipCluster(websocketActionUpdateBoard, payload, userIDs...) -} - -// sendBoardMessage sends and propagates a message that is aimed for -// all the users that are subscribed to the board's team and are -// members of it too. -func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) { - go func() { - clusterMessage := &ClusterMessage{ - TeamID: teamID, - BoardID: boardID, - Payload: payload, - EnsureUsers: ensureUserIDs, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendBoardMessageSkipCluster(teamID, boardID, payload, ensureUserIDs...) -} - -func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block *model.Block) { - pa.logger.Trace("BroadcastingBlockChange", - mlog.String("teamID", teamID), - mlog.String("boardID", block.BoardID), - mlog.String("blockID", block.ID), - ) - - message := UpdateBlockMsg{ - Action: websocketActionUpdateBlock, - TeamID: teamID, - Block: block, - } - - pa.sendBoardMessage(teamID, block.BoardID, utils.StructToMap(message)) -} - -func (pa *PluginAdapter) BroadcastCategoryChange(category model.Category) { - pa.logger.Debug("BroadcastCategoryChange", - mlog.String("userID", category.UserID), - mlog.String("teamID", category.TeamID), - mlog.String("categoryID", category.ID), - ) - - message := UpdateCategoryMessage{ - Action: websocketActionUpdateCategory, - TeamID: category.TeamID, - Category: &category, - } - - payload := utils.StructToMap(message) - - go func() { - clusterMessage := &ClusterMessage{ - Payload: payload, - UserID: category.UserID, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendUserMessageSkipCluster(websocketActionUpdateCategory, payload, category.UserID) -} - -func (pa *PluginAdapter) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) { - pa.logger.Debug("BroadcastCategoryReorder", - mlog.String("userID", userID), - mlog.String("teamID", teamID), - ) - - message := CategoryReorderMessage{ - Action: websocketActionReorderCategories, - CategoryOrder: categoryOrder, - TeamID: teamID, - } - payload := utils.StructToMap(message) - go func() { - clusterMessage := &ClusterMessage{ - Payload: payload, - UserID: userID, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendUserMessageSkipCluster(message.Action, payload, userID) -} - -func (pa *PluginAdapter) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardsOrder []string) { - pa.logger.Debug("BroadcastCategoryBoardsReorder", - mlog.String("userID", userID), - mlog.String("teamID", teamID), - mlog.String("categoryID", categoryID), - ) - - message := CategoryBoardReorderMessage{ - Action: websocketActionReorderCategoryBoards, - CategoryID: categoryID, - BoardOrder: boardsOrder, - TeamID: teamID, - } - payload := utils.StructToMap(message) - go func() { - clusterMessage := &ClusterMessage{ - Payload: payload, - UserID: userID, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendUserMessageSkipCluster(message.Action, payload, userID) -} - -func (pa *PluginAdapter) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) { - pa.logger.Debug( - "BroadcastCategoryBoardChange", - mlog.String("userID", userID), - mlog.String("teamID", teamID), - mlog.Int("numEntries", len(boardCategories)), - ) - - message := UpdateCategoryMessage{ - Action: websocketActionUpdateCategoryBoard, - TeamID: teamID, - BoardCategories: boardCategories, - } - - payload := utils.StructToMap(message) - - go func() { - clusterMessage := &ClusterMessage{ - Payload: payload, - UserID: userID, - } - - pa.sendMessageToCluster(clusterMessage) - }() - - pa.sendUserMessageSkipCluster(websocketActionUpdateCategoryBoard, utils.StructToMap(message), userID) -} - -func (pa *PluginAdapter) BroadcastBlockDelete(teamID, blockID, boardID string) { - now := utils.GetMillis() - block := &model.Block{} - block.ID = blockID - block.BoardID = boardID - block.UpdateAt = now - block.DeleteAt = now - - pa.BroadcastBlockChange(teamID, block) -} - -func (pa *PluginAdapter) BroadcastBoardChange(teamID string, board *model.Board) { - pa.logger.Debug("BroadcastingBoardChange", - mlog.String("teamID", teamID), - mlog.String("boardID", board.ID), - ) - - message := UpdateBoardMsg{ - Action: websocketActionUpdateBoard, - TeamID: teamID, - Board: board, - } - - pa.sendBoardMessage(teamID, board.ID, utils.StructToMap(message)) -} - -func (pa *PluginAdapter) BroadcastBoardDelete(teamID, boardID string) { - now := utils.GetMillis() - board := &model.Board{} - board.ID = boardID - board.TeamID = teamID - board.UpdateAt = now - board.DeleteAt = now - - pa.BroadcastBoardChange(teamID, board) -} - -func (pa *PluginAdapter) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) { - pa.logger.Debug("BroadcastingMemberChange", - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - mlog.String("userID", member.UserID), - ) - - message := UpdateMemberMsg{ - Action: websocketActionUpdateMember, - TeamID: teamID, - Member: member, - } - - pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), member.UserID) -} - -func (pa *PluginAdapter) BroadcastMemberDelete(teamID, boardID, userID string) { - pa.logger.Debug("BroadcastingMemberDelete", - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - mlog.String("userID", userID), - ) - - message := UpdateMemberMsg{ - Action: websocketActionDeleteMember, - TeamID: teamID, - Member: &model.BoardMember{UserID: userID, BoardID: boardID}, - } - - // when fetching the members of the board that should receive the - // member deletion message, the deleted member will not be one of - // them, so we need to ensure they receive the message - pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), userID) -} - -func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) { - pa.logger.Debug("BroadcastingSubscriptionChange", - mlog.String("TeamID", teamID), - mlog.String("blockID", subscription.BlockID), - mlog.String("subscriberID", subscription.SubscriberID), - ) - - message := UpdateSubscription{ - Action: websocketActionUpdateSubscription, - Subscription: subscription, - } - - pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message)) -} - -func (pa *PluginAdapter) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) { - pa.logger.Debug("BroadcastCardLimitTimestampChange", - mlog.Int64("cardLimitTimestamp", cardLimitTimestamp), - ) - - message := UpdateCardLimitTimestamp{ - Action: websocketActionUpdateCardLimitTimestamp, - Timestamp: cardLimitTimestamp, - } - - pa.sendMessageToAll(websocketActionUpdateCardLimitTimestamp, utils.StructToMap(message)) -} diff --git a/server/boards/ws/plugin_adapter_client.go b/server/boards/ws/plugin_adapter_client.go deleted file mode 100644 index 8ca6ef132c..0000000000 --- a/server/boards/ws/plugin_adapter_client.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "sync" - "sync/atomic" - "time" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -type PluginAdapterClient struct { - inactiveAt int64 - webConnID string - userID string - teams []string - blocks []string - mu sync.RWMutex -} - -func (pac *PluginAdapterClient) isActive() bool { - return atomic.LoadInt64(&pac.inactiveAt) == 0 -} - -func (pac *PluginAdapterClient) hasExpired(threshold time.Duration) bool { - return !mm_model.GetTimeForMillis(atomic.LoadInt64(&pac.inactiveAt)).Add(threshold).After(time.Now()) -} - -func (pac *PluginAdapterClient) subscribeToTeam(teamID string) { - pac.mu.Lock() - defer pac.mu.Unlock() - - pac.teams = append(pac.teams, teamID) -} - -func (pac *PluginAdapterClient) unsubscribeFromTeam(teamID string) { - pac.mu.Lock() - defer pac.mu.Unlock() - - newClientTeams := []string{} - for _, id := range pac.teams { - if id != teamID { - newClientTeams = append(newClientTeams, id) - } - } - pac.teams = newClientTeams -} - -func (pac *PluginAdapterClient) unsubscribeFromBlock(blockID string) { - pac.mu.Lock() - defer pac.mu.Unlock() - - newClientBlocks := []string{} - for _, id := range pac.blocks { - if id != blockID { - newClientBlocks = append(newClientBlocks, id) - } - } - pac.blocks = newClientBlocks -} - -func (pac *PluginAdapterClient) isSubscribedToTeam(teamID string) bool { - pac.mu.RLock() - defer pac.mu.RUnlock() - - for _, id := range pac.teams { - if id == teamID { - return true - } - } - - return false -} - -//nolint:unused -func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool { - pac.mu.RLock() - defer pac.mu.RUnlock() - - for _, id := range pac.blocks { - if id == blockID { - return true - } - } - - return false -} diff --git a/server/boards/ws/plugin_adapter_cluster.go b/server/boards/ws/plugin_adapter_cluster.go deleted file mode 100644 index 9dbd57e337..0000000000 --- a/server/boards/ws/plugin_adapter_cluster.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "encoding/json" - - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -type ClusterMessage struct { - TeamID string - BoardID string - UserID string - Payload map[string]interface{} - EnsureUsers []string -} - -func (pa *PluginAdapter) sendMessageToCluster(clusterMessage *ClusterMessage) { - const id = "websocket_message" - b, err := json.Marshal(clusterMessage) - if err != nil { - pa.logger.Error("couldn't get JSON bytes from cluster message", - mlog.String("id", id), - mlog.Err(err), - ) - return - } - - event := mm_model.PluginClusterEvent{Id: id, Data: b} - opts := mm_model.PluginClusterEventSendOptions{ - SendType: mm_model.PluginClusterEventSendTypeReliable, - } - - if err := pa.api.PublishPluginClusterEvent(event, opts); err != nil { - pa.logger.Error("error publishing cluster event", - mlog.String("id", id), - mlog.Err(err), - ) - } -} - -func (pa *PluginAdapter) HandleClusterEvent(ev mm_model.PluginClusterEvent) { - pa.logger.Debug("received cluster event", mlog.String("id", ev.Id)) - - var clusterMessage ClusterMessage - if err := json.Unmarshal(ev.Data, &clusterMessage); err != nil { - pa.logger.Error("cannot unmarshal cluster message data", - mlog.String("id", ev.Id), - mlog.Err(err), - ) - return - } - - if clusterMessage.BoardID != "" { - pa.sendBoardMessageSkipCluster(clusterMessage.TeamID, clusterMessage.BoardID, clusterMessage.Payload, clusterMessage.EnsureUsers...) - return - } - - var action string - if actionRaw, ok := clusterMessage.Payload["action"]; ok { - if s, ok := actionRaw.(string); ok { - action = s - } - } - if action == "" { - // no action was specified in the event; assume block change and warn. - pa.logger.Warn("cannot determine action from cluster message data", - mlog.String("id", ev.Id), - mlog.Map("payload", clusterMessage.Payload), - ) - return - } - - if clusterMessage.UserID != "" { - pa.sendUserMessageSkipCluster(action, clusterMessage.Payload, clusterMessage.UserID) - return - } - - pa.sendTeamMessageSkipCluster(action, clusterMessage.TeamID, clusterMessage.Payload) -} diff --git a/server/boards/ws/plugin_adapter_test.go b/server/boards/ws/plugin_adapter_test.go deleted file mode 100644 index 030496bb5e..0000000000 --- a/server/boards/ws/plugin_adapter_test.go +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "sync" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/model" - - mm_model "github.com/mattermost/mattermost/server/public/model" - - "github.com/stretchr/testify/require" -) - -func TestPluginAdapterTeamSubscription(t *testing.T) { - th := SetupTestHelper(t) - - webConnID := mm_model.NewId() - userID := mm_model.NewId() - teamID := mm_model.NewId() - - var pac *PluginAdapterClient - t.Run("Should correctly add a connection", func(t *testing.T) { - require.Empty(t, th.pa.listeners) - require.Empty(t, th.pa.listenersByTeam) - th.pa.OnWebSocketConnect(webConnID, userID) - require.Len(t, th.pa.listeners, 1) - - var ok bool - pac, ok = th.pa.listeners[webConnID] - require.True(t, ok) - require.NotNil(t, pac) - require.Equal(t, userID, pac.userID) - require.Empty(t, th.pa.listenersByTeam) - }) - - t.Run("Should correctly subscribe to a team", func(t *testing.T) { - require.False(t, pac.isSubscribedToTeam(teamID)) - - th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) - - require.Len(t, th.pa.listenersByTeam[teamID], 1) - require.Contains(t, th.pa.listenersByTeam[teamID], pac) - require.Len(t, pac.teams, 1) - require.Contains(t, pac.teams, teamID) - - require.True(t, pac.isSubscribedToTeam(teamID)) - }) - - t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) { - require.True(t, pac.isSubscribedToTeam(teamID)) - - th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) - - require.Len(t, th.pa.listenersByTeam[teamID], 1) - require.Contains(t, th.pa.listenersByTeam[teamID], pac) - require.Len(t, pac.teams, 1) - require.Contains(t, pac.teams, teamID) - - require.True(t, pac.isSubscribedToTeam(teamID)) - }) - - t.Run("Should correctly unsubscribe to a team", func(t *testing.T) { - require.True(t, pac.isSubscribedToTeam(teamID)) - - th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID) - - require.Empty(t, th.pa.listenersByTeam[teamID]) - require.Empty(t, pac.teams) - - require.False(t, pac.isSubscribedToTeam(teamID)) - }) - - t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) { - require.False(t, pac.isSubscribedToTeam(teamID)) - - th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID) - - require.Empty(t, th.pa.listenersByTeam[teamID]) - require.Empty(t, pac.teams) - - require.False(t, pac.isSubscribedToTeam(teamID)) - }) - - t.Run("Should correctly be marked as inactive if disconnected", func(t *testing.T) { - require.Len(t, th.pa.listeners, 1) - require.True(t, th.pa.listeners[webConnID].isActive()) - - th.pa.OnWebSocketDisconnect(webConnID, userID) - - require.Len(t, th.pa.listeners, 1) - require.False(t, th.pa.listeners[webConnID].isActive()) - }) - - t.Run("Should be marked back as active if reconnect", func(t *testing.T) { - require.Len(t, th.pa.listeners, 1) - require.False(t, th.pa.listeners[webConnID].isActive()) - - th.pa.OnWebSocketConnect(webConnID, userID) - - require.Len(t, th.pa.listeners, 1) - require.True(t, th.pa.listeners[webConnID].isActive()) - }) -} - -func TestPluginAdapterClientReconnect(t *testing.T) { - th := SetupTestHelper(t) - - webConnID := mm_model.NewId() - userID := mm_model.NewId() - teamID := mm_model.NewId() - - var pac *PluginAdapterClient - t.Run("A user should be able to reconnect within the accepted threshold and keep their subscriptions", func(t *testing.T) { - // create the connection - require.Len(t, th.pa.listeners, 0) - require.Len(t, th.pa.listenersByUserID[userID], 0) - th.pa.OnWebSocketConnect(webConnID, userID) - require.Len(t, th.pa.listeners, 1) - require.Len(t, th.pa.listenersByUserID[userID], 1) - var ok bool - pac, ok = th.pa.listeners[webConnID] - require.True(t, ok) - require.NotNil(t, pac) - - th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID) - require.True(t, pac.isSubscribedToTeam(teamID)) - - // disconnect - th.pa.OnWebSocketDisconnect(webConnID, userID) - require.False(t, pac.isActive()) - require.Len(t, th.pa.listeners, 1) - require.Len(t, th.pa.listenersByUserID[userID], 1) - - // reconnect right away. The connection should still be subscribed - th.pa.OnWebSocketConnect(webConnID, userID) - require.Len(t, th.pa.listeners, 1) - require.Len(t, th.pa.listenersByUserID[userID], 1) - require.True(t, pac.isActive()) - require.True(t, pac.isSubscribedToTeam(teamID)) - }) - - t.Run("Should remove old inactive connection when user connects with a different ID", func(t *testing.T) { - // we set the stale threshold to zero so inactive connections always get deleted - oldStaleThreshold := th.pa.staleThreshold - th.pa.staleThreshold = 0 - defer func() { th.pa.staleThreshold = oldStaleThreshold }() - th.pa.OnWebSocketDisconnect(webConnID, userID) - require.Len(t, th.pa.listeners, 1) - require.Len(t, th.pa.listenersByUserID[userID], 1) - require.Equal(t, webConnID, th.pa.listenersByUserID[userID][0].webConnID) - - newWebConnID := mm_model.NewId() - th.pa.OnWebSocketConnect(newWebConnID, userID) - - require.Len(t, th.pa.listeners, 1) - require.Len(t, th.pa.listenersByUserID[userID], 1) - require.Contains(t, th.pa.listeners, newWebConnID) - require.NotContains(t, th.pa.listeners, webConnID) - require.Equal(t, newWebConnID, th.pa.listenersByUserID[userID][0].webConnID) - - // if the same ID connects again, it should have no subscriptions - th.pa.OnWebSocketConnect(webConnID, userID) - require.Len(t, th.pa.listeners, 2) - require.Len(t, th.pa.listenersByUserID[userID], 2) - reconnectedPAC, ok := th.pa.listeners[webConnID] - require.True(t, ok) - require.False(t, reconnectedPAC.isSubscribedToTeam(teamID)) - }) - - t.Run("Should not remove active connections when user connects with a different ID", func(t *testing.T) { - // we set the stale threshold to zero so inactive connections always get deleted - oldStaleThreshold := th.pa.staleThreshold - th.pa.staleThreshold = 0 - defer func() { th.pa.staleThreshold = oldStaleThreshold }() - - // currently we have two listeners for userID, both active - require.Len(t, th.pa.listeners, 2) - - // a new user connects - th.pa.OnWebSocketConnect(mm_model.NewId(), userID) - - // and we should have three connections, all of them active - require.Len(t, th.pa.listeners, 3) - - for _, listener := range th.pa.listeners { - require.True(t, listener.isActive()) - } - }) -} - -func TestGetUserIDsForTeam(t *testing.T) { - th := SetupTestHelper(t) - - // we have two teams - teamID1 := mm_model.NewId() - teamID2 := mm_model.NewId() - - // user 1 has two connections - userID1 := mm_model.NewId() - webConnID1 := mm_model.NewId() - webConnID2 := mm_model.NewId() - - // user 2 has one connection - userID2 := mm_model.NewId() - webConnID3 := mm_model.NewId() - - wg := new(sync.WaitGroup) - wg.Add(3) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID1, userID1) - th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1) - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID2, userID1) - th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2) - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID3, userID2) - th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2) - wg.Done() - }(wg) - - wg.Wait() - - t.Run("should find that only user1 is connected to team 1", func(t *testing.T) { - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID1). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID1) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) - - t.Run("should find that both users are connected to team 2", func(t *testing.T) { - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID2) - require.ElementsMatch(t, []string{userID1, userID2}, userIDs) - }) - - t.Run("should ignore user1 if webConn 2 inactive when getting team 2 user ids", func(t *testing.T) { - th.pa.OnWebSocketDisconnect(webConnID2, userID1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID2) - require.ElementsMatch(t, []string{userID2}, userIDs) - }) - - t.Run("should still find user 1 in team 1 after the webConn 2 disconnection", func(t *testing.T) { - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID1). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID1) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) - - t.Run("should find again both users if the webConn 2 comes back", func(t *testing.T) { - th.pa.OnWebSocketConnect(webConnID2, userID1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID2) - require.ElementsMatch(t, []string{userID1, userID2}, userIDs) - }) - - t.Run("should only find user 1 if user 2 has an active connection but is not a team member anymore", func(t *testing.T) { - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - - // userID2 does not have team access - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(false). - Times(1) - - userIDs := th.pa.getUserIDsForTeam(teamID2) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) -} - -func TestGetUserIDsForTeamAndBoard(t *testing.T) { - th := SetupTestHelper(t) - - // we have two teams - teamID1 := mm_model.NewId() - boardID1 := mm_model.NewId() - teamID2 := mm_model.NewId() - boardID2 := mm_model.NewId() - - // user 1 has two connections - userID1 := mm_model.NewId() - webConnID1 := mm_model.NewId() - webConnID2 := mm_model.NewId() - - // user 2 has one connection - userID2 := mm_model.NewId() - webConnID3 := mm_model.NewId() - - wg := new(sync.WaitGroup) - wg.Add(3) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID1, userID1) - th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1) - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID2, userID1) - th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2) - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.pa.OnWebSocketConnect(webConnID3, userID2) - th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2) - wg.Done() - }(wg) - - wg.Wait() - - t.Run("should find that only user1 is connected to team 1 and board 1", func(t *testing.T) { - mockedMembers := []*model.BoardMember{{UserID: userID1}} - th.store.EXPECT(). - GetMembersForBoard(boardID1). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID1). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID1, boardID1) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) - - t.Run("should find that both users are connected to team 2 and board 2", func(t *testing.T) { - mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} - th.store.EXPECT(). - GetMembersForBoard(boardID2). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) - require.ElementsMatch(t, []string{userID1, userID2}, userIDs) - }) - - t.Run("should find that only one user is connected to team 2 and board 2 if there is only one membership with both connected", func(t *testing.T) { - mockedMembers := []*model.BoardMember{{UserID: userID1}} - th.store.EXPECT(). - GetMembersForBoard(boardID2). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) - - t.Run("should find only one if the other is inactive", func(t *testing.T) { - th.pa.OnWebSocketDisconnect(webConnID3, userID2) - defer th.pa.OnWebSocketConnect(webConnID3, userID2) - - mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} - th.store.EXPECT(). - GetMembersForBoard(boardID2). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) - - t.Run("should include a user that is not present if it's ensured", func(t *testing.T) { - userID3 := mm_model.NewId() - mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} - th.store.EXPECT(). - GetMembersForBoard(boardID2). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(true). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2, userID3) - require.ElementsMatch(t, []string{userID1, userID2, userID3}, userIDs) - }) - - t.Run("should not include a user that, although present, has no team access anymore", func(t *testing.T) { - mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}} - th.store.EXPECT(). - GetMembersForBoard(boardID2). - Return(mockedMembers, nil). - Times(1) - - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID1, teamID2). - Return(true). - Times(1) - - // userID2 has no team access - th.auth.EXPECT(). - DoesUserHaveTeamAccess(userID2, teamID2). - Return(false). - Times(1) - - userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2) - require.ElementsMatch(t, []string{userID1}, userIDs) - }) -} - -func TestParallelSubscriptionsOnMultipleConnections(t *testing.T) { - th := SetupTestHelper(t) - - teamID1 := mm_model.NewId() - teamID2 := mm_model.NewId() - teamID3 := mm_model.NewId() - teamID4 := mm_model.NewId() - - userID := mm_model.NewId() - webConnID1 := mm_model.NewId() - webConnID2 := mm_model.NewId() - - th.pa.OnWebSocketConnect(webConnID1, userID) - pac1, ok := th.pa.GetListenerByWebConnID(webConnID1) - require.True(t, ok) - - th.pa.OnWebSocketConnect(webConnID2, userID) - pac2, ok := th.pa.GetListenerByWebConnID(webConnID2) - require.True(t, ok) - - wg := new(sync.WaitGroup) - wg.Add(4) - - go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToTeam(webConnID1, userID, teamID1) - require.True(t, pac1.isSubscribedToTeam(teamID1)) - - th.SubscribeWebConnToTeam(webConnID2, userID, teamID1) - require.True(t, pac2.isSubscribedToTeam(teamID1)) - - th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID1) - require.False(t, pac1.isSubscribedToTeam(teamID1)) - - th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID1) - require.False(t, pac2.isSubscribedToTeam(teamID1)) - - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToTeam(webConnID1, userID, teamID2) - require.True(t, pac1.isSubscribedToTeam(teamID2)) - - th.SubscribeWebConnToTeam(webConnID2, userID, teamID2) - require.True(t, pac2.isSubscribedToTeam(teamID2)) - - th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID2) - require.False(t, pac1.isSubscribedToTeam(teamID2)) - - th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID2) - require.False(t, pac2.isSubscribedToTeam(teamID2)) - - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToTeam(webConnID1, userID, teamID3) - require.True(t, pac1.isSubscribedToTeam(teamID3)) - - th.SubscribeWebConnToTeam(webConnID2, userID, teamID3) - require.True(t, pac2.isSubscribedToTeam(teamID3)) - - th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID3) - require.False(t, pac1.isSubscribedToTeam(teamID3)) - - th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID3) - require.False(t, pac2.isSubscribedToTeam(teamID3)) - - wg.Done() - }(wg) - - go func(wg *sync.WaitGroup) { - th.SubscribeWebConnToTeam(webConnID1, userID, teamID4) - require.True(t, pac1.isSubscribedToTeam(teamID4)) - - th.SubscribeWebConnToTeam(webConnID2, userID, teamID4) - require.True(t, pac2.isSubscribedToTeam(teamID4)) - - th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID4) - require.False(t, pac1.isSubscribedToTeam(teamID4)) - - th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID4) - require.False(t, pac2.isSubscribedToTeam(teamID4)) - - wg.Done() - }(wg) - - wg.Wait() -} diff --git a/server/boards/ws/server.go b/server/boards/ws/server.go deleted file mode 100644 index 2391e673f9..0000000000 --- a/server/boards/ws/server.go +++ /dev/null @@ -1,810 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "encoding/json" - "net/http" - "sync" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/model" - "github.com/mattermost/mattermost/server/v8/boards/utils" - - "github.com/mattermost/mattermost/server/public/shared/mlog" -) - -func (wss *websocketSession) WriteJSON(v interface{}) error { - wss.mu.Lock() - defer wss.mu.Unlock() - err := wss.conn.WriteJSON(v) - return err -} - -func (wss *websocketSession) isSubscribedToTeam(teamID string) bool { - for _, id := range wss.teams { - if id == teamID { - return true - } - } - - return false -} - -func (wss *websocketSession) isSubscribedToBlock(blockID string) bool { - for _, id := range wss.blocks { - if id == blockID { - return true - } - } - - return false -} - -// Server is a WebSocket server. -type Server struct { - upgrader websocket.Upgrader - listeners map[*websocketSession]bool - listenersByTeam map[string][]*websocketSession - listenersByBlock map[string][]*websocketSession - mu sync.RWMutex - auth *auth.Auth - singleUserToken string - isMattermostAuth bool - logger mlog.LoggerIFace - store Store -} - -type websocketSession struct { - conn *websocket.Conn - userID string - mu sync.Mutex - teams []string - blocks []string -} - -func (wss *websocketSession) isAuthenticated() bool { - return wss.userID != "" -} - -// NewServer creates a new Server. -func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger mlog.LoggerIFace, store Store) *Server { - return &Server{ - listeners: make(map[*websocketSession]bool), - listenersByTeam: make(map[string][]*websocketSession), - listenersByBlock: make(map[string][]*websocketSession), - upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - }, - auth: auth, - singleUserToken: singleUserToken, - isMattermostAuth: isMattermostAuth, - logger: logger, - store: store, - } -} - -// RegisterRoutes registers routes. -func (ws *Server) RegisterRoutes(r *mux.Router) { - r.HandleFunc("/ws", ws.handleWebSocket) -} - -func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { - // Upgrade initial GET request to a websocket - client, err := ws.upgrader.Upgrade(w, r, nil) - if err != nil { - ws.logger.Error("ERROR upgrading to websocket", mlog.Err(err)) - return - } - - // create an empty session with websocket client - wsSession := &websocketSession{ - conn: client, - userID: "", - mu: sync.Mutex{}, - teams: []string{}, - blocks: []string{}, - } - - if ws.isMattermostAuth { - wsSession.userID = r.Header.Get("Mattermost-User-Id") - } - - ws.addListener(wsSession) - - // Make sure we close the connection when the function returns - defer func() { - ws.logger.Debug("DISCONNECT WebSocket", mlog.Stringer("client", wsSession.conn.RemoteAddr())) - - // Remove session from listeners - ws.removeListener(wsSession) - wsSession.conn.Close() - }() - - // Simple message handling loop - for { - _, p, err := wsSession.conn.ReadMessage() - if err != nil { - ws.logger.Error("ERROR WebSocket", - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - mlog.Err(err), - ) - ws.removeListener(wsSession) - break - } - - var command WebsocketCommand - - err = json.Unmarshal(p, &command) - if err != nil { - // handle this error - ws.logger.Error(`ERROR webSocket parsing command`, mlog.String("json", string(p))) - - continue - } - - if command.Action == websocketActionAuth { - ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", wsSession.conn.RemoteAddr())) - ws.authenticateListener(wsSession, command.Token) - - continue - } - - // if the client wants to subscribe to a set of blocks and it - // is sending a read token, we don't need to check for - // authentication - if command.Action == websocketActionSubscribeBlocks { - ws.logger.Debug(`Command: SUBSCRIBE_BLOCKS`, - mlog.String("teamID", command.TeamID), - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - ) - - if !ws.isCommandReadTokenValid(command) { - ws.logger.Error(`Rejected invalid read token`, - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - mlog.String("action", command.Action), - mlog.String("readToken", command.ReadToken), - ) - - continue - } - - ws.subscribeListenerToBlocks(wsSession, command.BlockIDs) - continue - } - - if command.Action == websocketActionUnsubscribeBlocks { - ws.logger.Debug(`Command: UNSUBSCRIBE_BLOCKS`, - mlog.String("teamID", command.TeamID), - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - ) - - if !ws.isCommandReadTokenValid(command) { - ws.logger.Error(`Rejected invalid read token`, - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - mlog.String("action", command.Action), - mlog.String("readToken", command.ReadToken), - ) - - continue - } - - ws.unsubscribeListenerFromBlocks(wsSession, command.BlockIDs) - continue - } - - // if the command is not authenticated at this point, it will - // not be processed - if !wsSession.isAuthenticated() { - ws.logger.Error(`Rejected unauthenticated message`, - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - mlog.String("action", command.Action), - ) - - continue - } - - switch command.Action { - case websocketActionSubscribeTeam: - ws.logger.Debug(`Command: SUBSCRIBE_TEAM`, - mlog.String("teamID", command.TeamID), - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - ) - - // if single user mode, check that the userID is valid and - // assume that the user has permission if so - if ws.singleUserToken != "" { - if wsSession.userID != model.SingleUser { - continue - } - - // if not in single user mode validate that the session - // has permissions to the team - } else { - ws.logger.Debug("Not single user mode") - if !ws.auth.DoesUserHaveTeamAccess(wsSession.userID, command.TeamID) { - ws.logger.Error("WS user doesn't have team access", mlog.String("teamID", command.TeamID), mlog.String("userID", wsSession.userID)) - continue - } - } - - ws.subscribeListenerToTeam(wsSession, command.TeamID) - case websocketActionUnsubscribeTeam: - ws.logger.Debug(`Command: UNSUBSCRIBE_TEAM`, - mlog.String("teamID", command.TeamID), - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - ) - - ws.unsubscribeListenerFromTeam(wsSession, command.TeamID) - default: - ws.logger.Error(`ERROR webSocket command, invalid action`, mlog.String("action", command.Action)) - } - } -} - -// isCommandReadTokenValid ensures that a command contains a read -// token and a set of block ids that said token is valid for. -func (ws *Server) isCommandReadTokenValid(command WebsocketCommand) bool { - if command.TeamID == "" { - return false - } - - boardID := "" - // all the blocks must be part of the same board - for _, blockID := range command.BlockIDs { - block, err := ws.store.GetBlock(blockID) - if err != nil { - return false - } - - if boardID == "" { - boardID = block.BoardID - continue - } - - if boardID != block.BoardID { - return false - } - } - - // the read token must be valid for the board - isValid, err := ws.auth.IsValidReadToken(boardID, command.ReadToken) - if err != nil { - ws.logger.Error(`ERROR when checking token validity`, - mlog.String("teamID", command.TeamID), - mlog.Err(err), - ) - return false - } - - return isValid -} - -// addListener adds a listener to the websocket server. The listener -// should not receive any update from the server until it subscribes -// itself to some entity changes. Adding a listener to the server -// doesn't mean that it's authenticated in any way. -func (ws *Server) addListener(listener *websocketSession) { - ws.mu.Lock() - defer ws.mu.Unlock() - ws.listeners[listener] = true -} - -// removeListener removes a listener and all its subscriptions, if -// any, from the websockets server. -func (ws *Server) removeListener(listener *websocketSession) { - ws.mu.Lock() - defer ws.mu.Unlock() - - // remove the listener from its subscriptions, if any - - // team subscriptions - for _, team := range listener.teams { - ws.removeListenerFromTeam(listener, team) - } - - // block subscriptions - for _, block := range listener.blocks { - ws.removeListenerFromBlock(listener, block) - } - - delete(ws.listeners, listener) -} - -// subscribeListenerToTeam safely modifies the listener and the -// server to subscribe the listener to a given team updates. -func (ws *Server) subscribeListenerToTeam(listener *websocketSession, teamID string) { - if listener.isSubscribedToTeam(teamID) { - return - } - - ws.mu.Lock() - defer ws.mu.Unlock() - - ws.listenersByTeam[teamID] = append(ws.listenersByTeam[teamID], listener) - listener.teams = append(listener.teams, teamID) -} - -// unsubscribeListenerFromTeam safely modifies the listener and -// the server data structures to remove the link between the listener -// and a given team ID. -func (ws *Server) unsubscribeListenerFromTeam(listener *websocketSession, teamID string) { - if !listener.isSubscribedToTeam(teamID) { - return - } - - ws.mu.Lock() - defer ws.mu.Unlock() - - ws.removeListenerFromTeam(listener, teamID) -} - -// subscribeListenerToBlocks safely modifies the listener and the -// server to subscribe the listener to a given set of block updates. -func (ws *Server) subscribeListenerToBlocks(listener *websocketSession, blockIDs []string) { - ws.mu.Lock() - defer ws.mu.Unlock() - - for _, blockID := range blockIDs { - if listener.isSubscribedToBlock(blockID) { - continue - } - - ws.listenersByBlock[blockID] = append(ws.listenersByBlock[blockID], listener) - listener.blocks = append(listener.blocks, blockID) - } -} - -// unsubscribeListenerFromBlocks safely modifies the listener and the -// server data structures to remove the link between the listener and -// a given set of block IDs. -func (ws *Server) unsubscribeListenerFromBlocks(listener *websocketSession, blockIDs []string) { - ws.mu.Lock() - defer ws.mu.Unlock() - - for _, blockID := range blockIDs { - if listener.isSubscribedToBlock(blockID) { - ws.removeListenerFromBlock(listener, blockID) - } - } -} - -// removeListenerFromTeam removes the listener from both its own -// block subscribed list and the server listeners by team map. -func (ws *Server) removeListenerFromTeam(listener *websocketSession, teamID string) { - // we remove the listener from the team index - newTeamListeners := []*websocketSession{} - for _, l := range ws.listenersByTeam[teamID] { - if l != listener { - newTeamListeners = append(newTeamListeners, l) - } - } - ws.listenersByTeam[teamID] = newTeamListeners - - // we remove the team from the listener subscription list - newListenerTeams := []string{} - for _, id := range listener.teams { - if id != teamID { - newListenerTeams = append(newListenerTeams, id) - } - } - listener.teams = newListenerTeams -} - -// removeListenerFromBlock removes the listener from both its own -// block subscribed list and the server listeners by block map. -func (ws *Server) removeListenerFromBlock(listener *websocketSession, blockID string) { - // we remove the listener from the block index - newBlockListeners := []*websocketSession{} - for _, l := range ws.listenersByBlock[blockID] { - if l != listener { - newBlockListeners = append(newBlockListeners, l) - } - } - ws.listenersByBlock[blockID] = newBlockListeners - - // we remove the block from the listener subscription list - newListenerBlocks := []string{} - for _, id := range listener.blocks { - if id != blockID { - newListenerBlocks = append(newListenerBlocks, id) - } - } - listener.blocks = newListenerBlocks -} - -func (ws *Server) getUserIDForToken(token string) string { - if ws.singleUserToken != "" { - if token == ws.singleUserToken { - return model.SingleUser - } - return "" - } - - session, err := ws.auth.GetSession(token) - if session == nil || err != nil { - return "" - } - - return session.UserID -} - -func (ws *Server) authenticateListener(wsSession *websocketSession, token string) { - ws.logger.Debug("authenticateListener", - mlog.String("token", token), - mlog.String("wsSession.userID", wsSession.userID), - ) - if wsSession.isAuthenticated() { - // Do not allow multiple auth calls (for security) - ws.logger.Debug( - "authenticateListener: Ignoring already authenticated session", - mlog.String("userID", wsSession.userID), - mlog.Stringer("client", wsSession.conn.RemoteAddr()), - ) - return - } - - // Authenticate session - userID := ws.getUserIDForToken(token) - if userID == "" { - wsSession.conn.Close() - return - } - - // Authenticated - wsSession.userID = userID - ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.conn.RemoteAddr())) -} - -// getListenersForBlock returns the listeners subscribed to a -// block changes. -func (ws *Server) getListenersForBlock(blockID string) []*websocketSession { - return ws.listenersByBlock[blockID] -} - -// getListenersForTeam returns the listeners subscribed to a -// team changes. -func (ws *Server) getListenersForTeam(teamID string) []*websocketSession { - return ws.listenersByTeam[teamID] -} - -// getListenersForTeamAndBoard returns the listeners subscribed to a -// team changes and members of a given board. -func (ws *Server) getListenersForTeamAndBoard(teamID, boardID string, ensureUsers ...string) []*websocketSession { - members, err := ws.store.GetMembersForBoard(boardID) - if err != nil { - ws.logger.Error("error getting members for board", - mlog.String("method", "getListenersForTeamAndBoard"), - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - ) - return nil - } - - memberMap := map[string]bool{} - for _, member := range members { - memberMap[member.UserID] = true - } - for _, id := range ensureUsers { - memberMap[id] = true - } - - memberIDs := []string{} - for id := range memberMap { - memberIDs = append(memberIDs, id) - } - - listeners := []*websocketSession{} - for _, memberID := range memberIDs { - for _, listener := range ws.listenersByTeam[teamID] { - if listener.userID == memberID { - listeners = append(listeners, listener) - } - } - } - return listeners -} - -// BroadcastBlockDelete broadcasts delete messages to clients. -func (ws *Server) BroadcastBlockDelete(teamID, blockID, boardID string) { - now := utils.GetMillis() - block := &model.Block{} - block.ID = blockID - block.BoardID = boardID - block.UpdateAt = now - block.DeleteAt = now - - ws.BroadcastBlockChange(teamID, block) -} - -// BroadcastBlockChange broadcasts update messages to clients. -func (ws *Server) BroadcastBlockChange(teamID string, block *model.Block) { - blockIDsToNotify := []string{block.ID, block.ParentID} - - message := UpdateBlockMsg{ - Action: websocketActionUpdateBlock, - TeamID: teamID, - Block: block, - } - - listeners := ws.getListenersForTeamAndBoard(teamID, block.BoardID) - ws.logger.Trace("listener(s) for teamID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.String("boardID", block.BoardID), - ) - - for _, blockID := range blockIDsToNotify { - listeners = append(listeners, ws.getListenersForBlock(blockID)...) - ws.logger.Trace("listener(s) for blockID", - mlog.Int("listener_count", len(listeners)), - mlog.String("blockID", blockID), - ) - } - - for _, listener := range listeners { - ws.logger.Debug("Broadcast block change", - mlog.String("teamID", teamID), - mlog.String("blockID", block.ID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - err := listener.WriteJSON(message) - if err != nil { - ws.logger.Error("broadcast error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastCategoryChange(category model.Category) { - message := UpdateCategoryMessage{ - Action: websocketActionUpdateCategory, - TeamID: category.TeamID, - Category: &category, - } - - listeners := ws.getListenersForTeam(category.TeamID) - ws.logger.Debug("listener(s) for teamID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", category.TeamID), - mlog.String("categoryID", category.ID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast block change", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", category.TeamID), - mlog.String("categoryID", category.ID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - if err := listener.WriteJSON(message); err != nil { - ws.logger.Error("broadcast category change error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) { - message := CategoryReorderMessage{ - Action: websocketActionReorderCategories, - CategoryOrder: categoryOrder, - TeamID: teamID, - } - - listeners := ws.getListenersForTeam(teamID) - ws.logger.Debug("listener(s) for teamID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast category order change", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - if err := listener.WriteJSON(message); err != nil { - ws.logger.Error("broadcast category order change error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardOrder []string) { - message := CategoryBoardReorderMessage{ - Action: websocketActionReorderCategoryBoards, - CategoryID: categoryID, - BoardOrder: boardOrder, - TeamID: teamID, - } - - listeners := ws.getListenersForTeam(teamID) - ws.logger.Debug("listener(s) for teamID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast board category order change", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - if err := listener.WriteJSON(message); err != nil { - ws.logger.Error("broadcast category order change error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) { - message := UpdateCategoryMessage{ - Action: websocketActionUpdateCategoryBoard, - TeamID: teamID, - BoardCategories: boardCategories, - } - - listeners := ws.getListenersForTeam(teamID) - ws.logger.Debug("listener(s) for teamID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.Int("numEntries", len(boardCategories)), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast block change", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.Int("numEntries", len(boardCategories)), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - if err := listener.WriteJSON(message); err != nil { - ws.logger.Error("broadcast category change error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -// BroadcastConfigChange broadcasts update messages to clients. -func (ws *Server) BroadcastConfigChange(clientConfig model.ClientConfig) { - message := UpdateClientConfig{ - Action: websocketActionUpdateConfig, - ClientConfig: clientConfig, - } - - listeners := ws.listeners - ws.logger.Debug("broadcasting config change to listener(s)", - mlog.Int("listener_count", len(listeners)), - ) - - for listener := range listeners { - ws.logger.Debug("Broadcast Config change", - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - err := listener.WriteJSON(message) - if err != nil { - ws.logger.Error("broadcast error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastBoardChange(teamID string, board *model.Board) { - message := UpdateBoardMsg{ - Action: websocketActionUpdateBoard, - TeamID: teamID, - Board: board, - } - - listeners := ws.getListenersForTeamAndBoard(teamID, board.ID) - ws.logger.Trace("listener(s) for teamID and boardID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.String("boardID", board.ID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast board change", - mlog.String("teamID", teamID), - mlog.String("boardID", board.ID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - err := listener.WriteJSON(message) - if err != nil { - ws.logger.Error("broadcast error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastBoardDelete(teamID, boardID string) { - now := utils.GetMillis() - board := &model.Board{} - board.ID = boardID - board.TeamID = teamID - board.UpdateAt = now - board.DeleteAt = now - - ws.BroadcastBoardChange(teamID, board) -} - -func (ws *Server) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) { - message := UpdateMemberMsg{ - Action: websocketActionUpdateMember, - TeamID: teamID, - Member: member, - } - - listeners := ws.getListenersForTeamAndBoard(teamID, boardID) - ws.logger.Trace("listener(s) for teamID and boardID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast member change", - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - err := listener.WriteJSON(message) - if err != nil { - ws.logger.Error("broadcast error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) { - message := UpdateMemberMsg{ - Action: websocketActionDeleteMember, - TeamID: teamID, - Member: &model.BoardMember{UserID: userID, BoardID: boardID}, - } - - // when fetching the members of the board that should receive the - // member deletion message, the deleted member will not be one of - // them, so we need to ensure they receive the message - listeners := ws.getListenersForTeamAndBoard(teamID, boardID, userID) - ws.logger.Trace("listener(s) for teamID and boardID", - mlog.Int("listener_count", len(listeners)), - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - ) - - for _, listener := range listeners { - ws.logger.Debug("Broadcast member removal", - mlog.String("teamID", teamID), - mlog.String("boardID", boardID), - mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()), - ) - - err := listener.WriteJSON(message) - if err != nil { - ws.logger.Error("broadcast error", mlog.Err(err)) - listener.conn.Close() - } - } -} - -func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) { - // not implemented for standalone server. -} - -func (ws *Server) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) { - // not implemented for standalone server. -} diff --git a/server/boards/ws/server_test.go b/server/boards/ws/server_test.go deleted file mode 100644 index acafdac8f6..0000000000 --- a/server/boards/ws/server_test.go +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package ws - -import ( - "sync" - "testing" - - "github.com/mattermost/mattermost/server/v8/boards/auth" - "github.com/mattermost/mattermost/server/v8/boards/model" - - "github.com/mattermost/mattermost/server/public/shared/mlog" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" -) - -func TestTeamSubscription(t *testing.T) { - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) - session := &websocketSession{ - conn: &websocket.Conn{}, - mu: sync.Mutex{}, - teams: []string{}, - blocks: []string{}, - } - teamID := "fake-team-id" - - t.Run("Should correctly add a session", func(t *testing.T) { - server.addListener(session) - require.Len(t, server.listeners, 1) - require.Empty(t, server.listenersByTeam) - require.Empty(t, session.teams) - }) - - t.Run("Should correctly subscribe to a team", func(t *testing.T) { - require.False(t, session.isSubscribedToTeam(teamID)) - - server.subscribeListenerToTeam(session, teamID) - - require.Len(t, server.listenersByTeam[teamID], 1) - require.Contains(t, server.listenersByTeam[teamID], session) - require.Len(t, session.teams, 1) - require.Contains(t, session.teams, teamID) - - require.True(t, session.isSubscribedToTeam(teamID)) - }) - - t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) { - require.True(t, session.isSubscribedToTeam(teamID)) - - server.subscribeListenerToTeam(session, teamID) - - require.Len(t, server.listenersByTeam[teamID], 1) - require.Contains(t, server.listenersByTeam[teamID], session) - require.Len(t, session.teams, 1) - require.Contains(t, session.teams, teamID) - - require.True(t, session.isSubscribedToTeam(teamID)) - }) - - t.Run("Should correctly unsubscribe to a team", func(t *testing.T) { - require.True(t, session.isSubscribedToTeam(teamID)) - - server.unsubscribeListenerFromTeam(session, teamID) - - require.Empty(t, server.listenersByTeam[teamID]) - require.Empty(t, session.teams) - - require.False(t, session.isSubscribedToTeam(teamID)) - }) - - t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) { - require.False(t, session.isSubscribedToTeam(teamID)) - - server.unsubscribeListenerFromTeam(session, teamID) - - require.Empty(t, server.listenersByTeam[teamID]) - require.Empty(t, session.teams) - - require.False(t, session.isSubscribedToTeam(teamID)) - }) - - t.Run("Should correctly be removed from the server", func(t *testing.T) { - server.removeListener(session) - - require.Empty(t, server.listeners) - }) - - t.Run("If subscribed to teams and removed, should be removed from the teams subscription list", func(t *testing.T) { - teamID2 := "other-fake-team-id" - - server.addListener(session) - server.subscribeListenerToTeam(session, teamID) - server.subscribeListenerToTeam(session, teamID2) - - require.Len(t, server.listeners, 1) - require.Contains(t, server.listenersByTeam[teamID], session) - require.Contains(t, server.listenersByTeam[teamID2], session) - - server.removeListener(session) - - require.Empty(t, server.listeners) - require.Empty(t, server.listenersByTeam[teamID]) - require.Empty(t, server.listenersByTeam[teamID2]) - }) -} - -func TestBlocksSubscription(t *testing.T) { - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) - session := &websocketSession{ - conn: &websocket.Conn{}, - mu: sync.Mutex{}, - teams: []string{}, - blocks: []string{}, - } - blockID1 := "block1" - blockID2 := "block2" - blockID3 := "block3" - blockIDs := []string{blockID1, blockID2, blockID3} - - t.Run("Should correctly add a session", func(t *testing.T) { - server.addListener(session) - require.Len(t, server.listeners, 1) - require.Empty(t, server.listenersByTeam) - require.Empty(t, session.teams) - }) - - t.Run("Should correctly subscribe to a set of blocks", func(t *testing.T) { - require.False(t, session.isSubscribedToBlock(blockID1)) - require.False(t, session.isSubscribedToBlock(blockID2)) - require.False(t, session.isSubscribedToBlock(blockID3)) - - server.subscribeListenerToBlocks(session, blockIDs) - - require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], session) - require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], session) - require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], session) - require.Len(t, session.blocks, 3) - require.ElementsMatch(t, blockIDs, session.blocks) - - require.True(t, session.isSubscribedToBlock(blockID1)) - require.True(t, session.isSubscribedToBlock(blockID2)) - require.True(t, session.isSubscribedToBlock(blockID3)) - - t.Run("Subscribing again to a subscribed block would have no effect", func(t *testing.T) { - require.True(t, session.isSubscribedToBlock(blockID1)) - require.True(t, session.isSubscribedToBlock(blockID2)) - require.True(t, session.isSubscribedToBlock(blockID3)) - - server.subscribeListenerToBlocks(session, blockIDs) - - require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], session) - require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], session) - require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], session) - require.Len(t, session.blocks, 3) - require.ElementsMatch(t, blockIDs, session.blocks) - - require.True(t, session.isSubscribedToBlock(blockID1)) - require.True(t, session.isSubscribedToBlock(blockID2)) - require.True(t, session.isSubscribedToBlock(blockID3)) - }) - }) - - t.Run("Should correctly unsubscribe to a set of blocks", func(t *testing.T) { - require.True(t, session.isSubscribedToBlock(blockID1)) - require.True(t, session.isSubscribedToBlock(blockID2)) - require.True(t, session.isSubscribedToBlock(blockID3)) - - server.unsubscribeListenerFromBlocks(session, blockIDs) - - require.Empty(t, server.listenersByBlock[blockID1]) - require.Empty(t, server.listenersByBlock[blockID2]) - require.Empty(t, server.listenersByBlock[blockID3]) - require.Empty(t, session.blocks) - - require.False(t, session.isSubscribedToBlock(blockID1)) - require.False(t, session.isSubscribedToBlock(blockID2)) - require.False(t, session.isSubscribedToBlock(blockID3)) - }) - - t.Run("Unsubscribing again to an unsubscribed block would have no effect", func(t *testing.T) { - require.False(t, session.isSubscribedToBlock(blockID1)) - - server.unsubscribeListenerFromBlocks(session, []string{blockID1}) - - require.Empty(t, server.listenersByBlock[blockID1]) - require.Empty(t, session.blocks) - - require.False(t, session.isSubscribedToBlock(blockID1)) - }) - - t.Run("Should correctly be removed from the server", func(t *testing.T) { - server.removeListener(session) - - require.Empty(t, server.listeners) - }) - - t.Run("If subscribed to blocks and removed, should be removed from the blocks subscription list", func(t *testing.T) { - server.addListener(session) - server.subscribeListenerToBlocks(session, blockIDs) - - require.Len(t, server.listeners, 1) - require.Len(t, server.listenersByBlock[blockID1], 1) - require.Contains(t, server.listenersByBlock[blockID1], session) - require.Len(t, server.listenersByBlock[blockID2], 1) - require.Contains(t, server.listenersByBlock[blockID2], session) - require.Len(t, server.listenersByBlock[blockID3], 1) - require.Contains(t, server.listenersByBlock[blockID3], session) - require.Len(t, session.blocks, 3) - require.ElementsMatch(t, blockIDs, session.blocks) - - server.removeListener(session) - - require.Empty(t, server.listeners) - require.Empty(t, server.listenersByBlock[blockID1]) - require.Empty(t, server.listenersByBlock[blockID2]) - require.Empty(t, server.listenersByBlock[blockID3]) - }) -} - -func TestGetUserIDForTokenInSingleUserMode(t *testing.T) { - singleUserToken := "single-user-token" - server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil) - server.singleUserToken = singleUserToken - - t.Run("Should return nothing if the token is empty", func(t *testing.T) { - require.Empty(t, server.getUserIDForToken("")) - }) - - t.Run("Should return nothing if the token is invalid", func(t *testing.T) { - require.Empty(t, server.getUserIDForToken("invalid-token")) - }) - - t.Run("Should return the single user ID if the token is correct", func(t *testing.T) { - require.Equal(t, model.SingleUser, server.getUserIDForToken(singleUserToken)) - }) -} diff --git a/server/channels/api4/apitestlib.go b/server/channels/api4/apitestlib.go index b581a13b03..629557bee3 100644 --- a/server/channels/api4/apitestlib.go +++ b/server/channels/api4/apitestlib.go @@ -289,7 +289,6 @@ func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelpe dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - mainHelper.PreloadBoardsMigrationsIfNeeded() searchEngine := mainHelper.GetSearchEngine() th := setupTestHelper(dbStore, searchEngine, false, true, updateConfig, nil) th.InitLogin() diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index ea5f74426f..1ab0e356c3 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -871,7 +871,6 @@ type AppIface interface { HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) HandleIncomingWebhook(c *request.Context, hookID string, req *model.IncomingWebhookRequest) *model.AppError HandleMessageExportConfig(cfg *model.Config, appCfg *model.Config) - HasBoardProduct() (bool, error) HasPermissionTo(askingUserId string, permission *model.Permission) bool HasPermissionToChannel(c request.CTX, askingUserId string, channelID string, permission *model.Permission) bool HasPermissionToChannelByPost(askingUserId string, postID string, permission *model.Permission) bool diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index d332d6809b..ff3ce167ce 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -159,8 +159,6 @@ func SetupWithoutPreloadMigrations(tb testing.TB) *TestHelper { dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - // Only boards migrations are applied - mainHelper.PreloadBoardsMigrationsIfNeeded() return setupTestHelper(dbStore, false, true, nil, tb) } diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index a9f6b929e0..a3bdadab5d 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -11492,28 +11492,6 @@ func (a *OpenTracingAppLayer) HandleMessageExportConfig(cfg *model.Config, appCf a.app.HandleMessageExportConfig(cfg, appCfg) } -func (a *OpenTracingAppLayer) HasBoardProduct() (bool, error) { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasBoardProduct") - - a.ctx = newCtx - a.app.Srv().Store().SetContext(newCtx) - defer func() { - a.app.Srv().Store().SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0, resultVar1 := a.app.HasBoardProduct() - - if resultVar1 != nil { - span.LogFields(spanlog.Error(resultVar1)) - ext.Error.Set(span, true) - } - - return resultVar0, resultVar1 -} - func (a *OpenTracingAppLayer) HasPermissionTo(askingUserId string, permission *model.Permission) bool { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.HasPermissionTo") diff --git a/server/channels/app/product.go b/server/channels/app/product.go index 82399bcec7..073c198317 100644 --- a/server/channels/app/product.go +++ b/server/channels/app/product.go @@ -4,9 +4,7 @@ package app import ( - "errors" "fmt" - "os" "strings" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -77,30 +75,5 @@ func (s *Server) shouldStart(product string) bool { return false } - if product == "boards" { - if os.Getenv("MM_DISABLE_BOARDS") == "true" { - s.Log().Warn("Skipping Boards start: disabled via env var") - return false - } - } - return true } - -func (s *Server) HasBoardProduct() (bool, error) { - prod, exists := s.services[product.BoardsKey] - if !exists { - return false, nil - } - if prod == nil { - return false, errors.New("board product is nil") - } - if _, ok := prod.(product.BoardsService); !ok { - return false, errors.New("board product key does not match its definition") - } - return true, nil -} - -func (a *App) HasBoardProduct() (bool, error) { - return a.Srv().HasBoardProduct() -} diff --git a/server/channels/app/slashcommands/helper_test.go b/server/channels/app/slashcommands/helper_test.go index 60e5d4d56c..bd0f88668a 100644 --- a/server/channels/app/slashcommands/helper_test.go +++ b/server/channels/app/slashcommands/helper_test.go @@ -143,7 +143,6 @@ func setup(tb testing.TB) *TestHelper { dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - mainHelper.PreloadBoardsMigrationsIfNeeded() return setupTestHelper(dbStore, false, true, tb, nil) } diff --git a/server/channels/product/api.go b/server/channels/product/api.go index 4334d6ba28..de886aae84 100644 --- a/server/channels/product/api.go +++ b/server/channels/product/api.go @@ -12,8 +12,6 @@ import ( "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/app/request" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" - - fb_model "github.com/mattermost/mattermost/server/v8/boards/model" ) // RouterService enables registering the product router to the server. After registering the @@ -214,26 +212,6 @@ type PreferencesService interface { DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError } -// BoardsService is the API for accessing Boards service APIs. -// -// The service shall be registered via app.BoardsKey service key. -type BoardsService interface { - GetTemplates(teamID string, userID string) ([]*fb_model.Board, error) - GetBoard(boardID string) (*fb_model.Board, error) - CreateBoard(board *fb_model.Board, userID string, addmember bool) (*fb_model.Board, error) - PatchBoard(boardPatch *fb_model.BoardPatch, boardID string, userID string) (*fb_model.Board, error) - DeleteBoard(boardID string, userID string) error - SearchBoards(searchTerm string, searchField fb_model.BoardSearchField, userID string, includePublicBoards bool) ([]*fb_model.Board, error) - LinkBoardToChannel(boardID string, channelID string, userID string) (*fb_model.Board, error) - GetCards(boardID string) ([]*fb_model.Card, error) - GetCard(cardID string) (*fb_model.Card, error) - CreateCard(card *fb_model.Card, boardID string, userID string) (*fb_model.Card, error) - PatchCard(cardPatch *fb_model.CardPatch, cardID string, userID string) (*fb_model.Card, error) - DeleteCard(cardID string, userID string) error - HasPermissionToBoard(userID, boardID string, permission *model.Permission) bool - DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*fb_model.BoardsAndBlocks, []*fb_model.BoardMember, error) -} - // SessionService is the API for accessing the session. // // The service shall be registered via app.SessionKey service key. diff --git a/server/channels/product/service.go b/server/channels/product/service.go index 7e629fc488..cc40205d0a 100644 --- a/server/channels/product/service.go +++ b/server/channels/product/service.go @@ -25,7 +25,6 @@ const ( StoreKey ServiceKey = "storekey" SystemKey ServiceKey = "systemkey" PreferencesKey ServiceKey = "preferenceskey" - BoardsKey ServiceKey = "boards" SessionKey ServiceKey = "sessionkey" FrontendKey ServiceKey = "frontendkey" CommandKey ServiceKey = "commandkey" diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index 3ea68e4605..bd33da1ded 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -1078,7 +1078,6 @@ func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore { } func (ss *SqlStore) DropAllTables() { - var tableSchemaFn string if ss.DriverName() == model.DatabaseDriverPostgres { ss.masterX.Exec(`DO $func$ @@ -1088,57 +1087,19 @@ func (ss *SqlStore) DropAllTables() { FROM pg_class WHERE relkind = 'r' -- only tables AND relnamespace = 'public'::regnamespace - AND NOT ( - relname = 'db_migrations' OR - relname = 'focalboard_schema_migrations' OR - relname = 'focalboard_boards' OR - relname = 'focalboard_blocks' - ) + AND NOT relname = 'db_migrations' ); END $func$;`) - tableSchemaFn = "current_schema()" } else { tables := []string{} ss.masterX.Select(&tables, `show tables`) for _, t := range tables { - if t != "db_migrations" && - t != "focalboard_schema_migrations" && - t != "focalboard_boards" && - t != "focalboard_blocks" { + if t != "db_migrations" { ss.masterX.Exec(`TRUNCATE TABLE ` + t) } } - tableSchemaFn = "DATABASE()" - } - - var boardsTableCount int - err := ss.masterX.Get(&boardsTableCount, ` - SELECT COUNT(*) - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = `+tableSchemaFn+` - AND TABLE_NAME = 'focalboard_schema_migrations'`) - if err != nil { - panic(errors.Wrap(err, "Error dropping all tables. Cannot query INFORMATION_SCHEMA table to check for focalboard_schema_migrations table")) - } - - if boardsTableCount != 0 { - _, blErr := ss.masterX.Exec(` - DELETE FROM focalboard_blocks - WHERE board_id IN ( - SELECT id - FROM focalboard_boards - WHERE NOT is_template - )`) - if blErr != nil { - panic(errors.Wrap(blErr, "Error deleting all non-template blocks")) - } - - _, boErr := ss.masterX.Exec(`DELETE FROM focalboard_boards WHERE NOT is_template`) - if boErr != nil { - panic(errors.Wrap(boErr, "Error delegint all non-template boards")) - } } } diff --git a/server/channels/testlib/helper.go b/server/channels/testlib/helper.go index bd50adccd9..e3deae4c4b 100644 --- a/server/channels/testlib/helper.go +++ b/server/channels/testlib/helper.go @@ -198,67 +198,6 @@ func (h *MainHelper) PreloadMigrations() { if err != nil { panic(errors.Wrap(err, "Error preloading migrations. Check if you have &multiStatements=true in your DSN if you are using MySQL. Or perhaps the schema changed? If yes, then update the warmup files accordingly")) } - - h.PreloadBoardsMigrationsIfNeeded() -} - -// PreloadBoardsMigrationsIfNeeded loads boards migrations if the -// focalboard_schema_migrations table exists already. -// Besides this, the same compatibility and breaking conditions that -// PreloadMigrations has apply here. -// -// Re-generate the files with: -// pg_dump -a -h localhost -U mmuser -d <> --no-comments --inserts -t focalboard_system_settings -// mysqldump -u root -p <> --no-create-info --extended-insert=FALSE focalboard_system_settings -func (h *MainHelper) PreloadBoardsMigrationsIfNeeded() { - tableSchemaFn := "current_schema()" - if *h.Settings.DriverName == model.DatabaseDriverMysql { - tableSchemaFn = "DATABASE()" - } - - basePath := os.Getenv("MM_SERVER_PATH") - if basePath == "" { - _, errFile := os.Stat("mattermost-server/server") - if os.IsNotExist(errFile) { - basePath = "mattermost/server" - } else { - basePath = "mattermost-server/server" - } - } - relPath := "channels/testlib/testdata" - - handle := h.SQLStore.GetMasterX() - var boardsTableCount int - gErr := handle.Get(&boardsTableCount, ` - SELECT COUNT(*) - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = `+tableSchemaFn+` - AND TABLE_NAME = 'focalboard_schema_migrations'`) - if gErr != nil { - panic(errors.Wrap(gErr, "Error preloading migrations. Cannot query INFORMATION_SCHEMA table to check for focalboard_schema_migrations table")) - } - - var buf []byte - var err error - if boardsTableCount != 0 { - switch *h.Settings.DriverName { - case model.DatabaseDriverPostgres: - boardsFinalPath := filepath.Join(basePath, relPath, "boards_postgres_migration_warmup.sql") - buf, err = os.ReadFile(boardsFinalPath) - if err != nil { - panic(fmt.Errorf("cannot read file: %v", err)) - } - case model.DatabaseDriverMysql: - boardsFinalPath := filepath.Join(basePath, relPath, "boards_mysql_migration_warmup.sql") - buf, err = os.ReadFile(boardsFinalPath) - if err != nil { - panic(fmt.Errorf("cannot read file: %v", err)) - } - } - if _, err := handle.Exec(string(buf)); err != nil { - panic(errors.Wrap(err, "Error preloading boards migrations. Check if you have &multiStatements=true in your DSN if you are using MySQL. Or perhaps the schema changed? If yes, then update the warmup files accordingly")) - } - } } func (h *MainHelper) Close() error { diff --git a/server/channels/web/web_test.go b/server/channels/web/web_test.go index e65f701ac5..11ebf92885 100644 --- a/server/channels/web/web_test.go +++ b/server/channels/web/web_test.go @@ -68,7 +68,6 @@ func Setup(tb testing.TB) *TestHelper { } store := mainHelper.GetStore() store.DropAllTables() - mainHelper.PreloadBoardsMigrationsIfNeeded() return setupTestHelper(tb, true, nil) } diff --git a/server/cmd/mattermost/main.go b/server/cmd/mattermost/main.go index 5e4d43aa57..4ff658560e 100644 --- a/server/cmd/mattermost/main.go +++ b/server/cmd/mattermost/main.go @@ -14,9 +14,6 @@ import ( // Enterprise Imports _ "github.com/mattermost/mattermost/server/v8/channels/imports" - - // Blank imports for each product to register themselves - _ "github.com/mattermost/mattermost/server/v8/boards/product" ) func main() { diff --git a/server/go.mod b/server/go.mod index 0ecf7760df..4c1e16513e 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( code.sajari.com/docconv v1.3.5 github.com/Masterminds/semver/v3 v3.2.1 - github.com/Masterminds/squirrel v1.5.4 github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 github.com/aws/aws-sdk-go v1.44.293 github.com/blang/semver v3.5.1+incompatible @@ -36,7 +35,6 @@ require ( github.com/isacikgoz/prompt v0.1.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/jmoiron/sqlx v1.3.5 - github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 github.com/lib/pq v1.10.9 github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 @@ -48,21 +46,17 @@ require ( github.com/mattermost/morph v1.0.5-0.20230511171014-e76e25978d56 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 github.com/mattermost/squirrel v0.2.0 - github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.24 github.com/minio/minio-go/v7 v7.0.59 - github.com/oklog/run v1.1.0 github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb github.com/opentracing/opentracing-go v1.2.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.16.0 github.com/reflog/dateconstraints v0.2.1 - github.com/rivo/uniseg v0.4.4 github.com/rs/cors v1.9.0 github.com/rudderlabs/analytics-go v3.3.3+incompatible github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd - github.com/sergi/go-diff v1.3.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 @@ -179,6 +173,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/nwaples/rardecode v1.1.3 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olivere/elastic v6.2.37+incompatible // indirect github.com/otiai10/gosseract/v2 v2.4.0 // indirect @@ -195,6 +190,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/xid v1.5.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/server/go.sum b/server/go.sum index 2212a38005..5b5eee672f 100644 --- a/server/go.sum +++ b/server/go.sum @@ -69,8 +69,6 @@ github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 h1:8T2zMbhLBbH951 github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052/go.mod h1:0SURuH1rsE8aVWvutuMZghRNrNrYEUzibzJfhEYR8L0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= @@ -482,8 +480,6 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 h1:+AIlO01SKT9sfWU5CLWi0cfHc7dQwgGz3FhFRzXLoMg= -github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94/go.mod h1:TcE3PIIkVWbP/HjhRAafgCjRKvDOi086iqp9VkNX/ng= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -545,8 +541,6 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538 h1:6mFhRD89wtsxh7g8V6og9DR4y/UGlzzehc1c3O9tbMQ= -github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538/go.mod h1:ZwobEfNHde7sU2pGybCWEnSlQ2r+MGrHGOKLphHZ42g= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= @@ -694,8 +688,6 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= diff --git a/server/public/model/cloud.go b/server/public/model/cloud.go index 7693654e6b..97b27bcd32 100644 --- a/server/public/model/cloud.go +++ b/server/public/model/cloud.go @@ -272,16 +272,6 @@ type SubscriptionChange struct { ShippingAddress *Address `json:"shipping_address"` } -// TODO remove BoardsLimits. -// It is not used for real. -// Focalboard has some lingering code using this struct -// https://github.com/mattermost/mattermost/server/v8/boards/blob/fd4cf95f8ac9ba616864b25bf91bb1e4ec21335a/server/app/cloud.go#L86 -// we should remove this struct once that code is removed. -type BoardsLimits struct { - Cards *int `json:"cards"` - Views *int `json:"views"` -} - type FilesLimits struct { TotalStorage *int64 `json:"total_storage"` } @@ -295,12 +285,6 @@ type TeamsLimits struct { } type ProductLimits struct { - // TODO remove Boards property. - // It is not used for real. - // Focalboard has some lingering code using this property - // https://github.com/mattermost/mattermost/server/v8/boards/blob/fd4cf95f8ac9ba616864b25bf91bb1e4ec21335a/server/app/cloud.go#L86 - // we should remove this property once that code is removed. - Boards *BoardsLimits `json:"boards,omitempty"` Files *FilesLimits `json:"files,omitempty"` Messages *MessagesLimits `json:"messages,omitempty"` Teams *TeamsLimits `json:"teams,omitempty"` diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index 5f2411330c..7280abb2b6 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -33,9 +33,6 @@ type FeatureFlags struct { // CallsEnabled controls whether or not the Calls plugin should be enabled CallsEnabled bool - // A dash separated list for feature flags to turn on for Boards - BoardsFeatureFlags string - // Enable DataRetention for Boards BoardsDataRetention bool @@ -71,7 +68,6 @@ func (f *FeatureFlags) SetDefaults() { f.AppsEnabled = true f.PluginApps = "" f.PluginFocalboard = "" - f.BoardsFeatureFlags = "" f.BoardsDataRetention = false f.NormalizeLdapDNs = false f.GraphQL = false diff --git a/webapp/boards/.eslintignore b/webapp/boards/.eslintignore deleted file mode 100644 index c2658d7d1b..0000000000 --- a/webapp/boards/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/webapp/boards/.eslintrc.json b/webapp/boards/.eslintrc.json deleted file mode 100644 index 852eef7c78..0000000000 --- a/webapp/boards/.eslintrc.json +++ /dev/null @@ -1,754 +0,0 @@ -{ - "extends": [ - "plugin:react/recommended" - ], - "plugins": [ - "react", - "import", - "no-only-tests", - "header", - "formatjs", - "unused-imports", - "no-relative-import-paths", - "import-newlines", - "eslint-comments" - ], - "parser": "@typescript-eslint/parser", - "settings": { - "import/resolver": "webpack", - "react": { - "pragma": "React", - "version": "detect" - } - }, - "rules": { - "header/header": [ - 2, - "line", - " Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\n See LICENSE.txt for license information." - ], - "array-bracket-spacing": [ - 2, - "never" - ], - "array-callback-return": 2, - "arrow-body-style": 0, - "arrow-parens": [ - 2, - "always" - ], - "arrow-spacing": [ - 2, - { - "before": true, - "after": true - } - ], - "block-scoped-var": 2, - "brace-style": [ - 2, - "1tbs", - { - "allowSingleLine": false - } - ], - "capitalized-comments": 0, - "class-methods-use-this": 0, - "comma-dangle": [ - 2, - "always-multiline" - ], - "comma-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "comma-style": [ - 2, - "last" - ], - "complexity": [ - 0, - 10 - ], - "computed-property-spacing": [ - 2, - "never" - ], - "consistent-return": 2, - "consistent-this": [ - 2, - "self" - ], - "constructor-super": 2, - "curly": [ - 2, - "all" - ], - "dot-location": [ - 2, - "property" - ], - "dot-notation": 2, - "eqeqeq": [ - 2, - "smart" - ], - "func-call-spacing": [ - 2, - "never" - ], - "func-name-matching": 0, - "func-names": 2, - "func-style": [ - 2, - "declaration", - { - "allowArrowFunctions": true - } - ], - "generator-star-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "global-require": 2, - "guard-for-in": 2, - "id-blacklist": 0, - "import/no-unresolved": 0, // ts handles this better - "import/order": [ - "error", - { - "newlines-between": "always-and-inside-groups", - "groups": [ - "builtin", - "external", - [ - "internal", - "parent" - ], - "sibling", - "index" - ] - } - ], - "indent": 0, // ts handles this - "jsx-quotes": [ - 2, - "prefer-single" - ], - "key-spacing": [ - 2, - { - "beforeColon": false, - "afterColon": true, - "mode": "strict" - } - ], - "keyword-spacing": [ - 2, - { - "before": true, - "after": true, - "overrides": {} - } - ], - "line-comment-position": 0, - "linebreak-style": 2, - "lines-around-comment": [ - 2, - { - "beforeBlockComment": true, - "beforeLineComment": true, - "allowBlockStart": true, - "allowBlockEnd": true - } - ], - "max-lines": [ - 2, - { - "max": 800, - "skipBlankLines": true, - "skipComments": true - } - ], - "max-statements-per-line": [ - 2, - { - "max": 1 - } - ], - "multiline-ternary": [ - 1, - "never" - ], - "new-cap": 2, - "new-parens": 2, - "padding-line-between-statements": [ - 2, - { - "blankLine": "always", - "prev": "*", - "next": "return" - } - ], - "newline-per-chained-call": 0, - "no-alert": 2, - "no-array-constructor": 2, - "no-await-in-loop": 2, - "no-caller": 2, - "no-case-declarations": 2, - "no-class-assign": 2, - "no-compare-neg-zero": 2, - "no-cond-assign": [ - 2, - "except-parens" - ], - "no-confusing-arrow": 2, - "no-console": 2, - "no-const-assign": 2, - "no-constant-condition": 2, - "no-debugger": 2, - "no-div-regex": 2, - "no-dupe-args": 2, - "no-dupe-class-members": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-duplicate-imports": [ - 2, - { - "includeExports": true - } - ], - "no-else-return": 2, - "no-empty": 2, - "no-empty-function": 2, - "no-empty-pattern": 2, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-label": 2, - "no-extra-parens": 0, - "no-extra-semi": 2, - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-global-assign": 2, - "no-implicit-coercion": 2, - "no-implicit-globals": 0, - "no-implied-eval": 2, - "no-inner-declarations": 0, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-lonely-if": 2, - "no-loop-func": 2, - "no-magic-numbers": [ - 0, - { - "ignore": [ - -1, - 0, - 1, - 2 - ], - "enforceConst": true, - "detectObjects": true - } - ], - "no-mixed-operators": [ - 2, - { - "allowSamePrecedence": false - } - ], - "no-mixed-spaces-and-tabs": 2, - "no-multi-assign": 2, - "no-multi-spaces": [ - 2, - { - "exceptions": { - "Property": false - } - } - ], - "no-multi-str": 0, - "no-multiple-empty-lines": [ - 2, - { - "max": 1 - } - ], - "no-native-reassign": 2, - "no-negated-condition": 2, - "no-nested-ternary": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-symbol": 2, - "no-new-wrappers": 2, - "no-octal-escape": 2, - "no-param-reassign": 2, - "no-process-env": 2, - "no-process-exit": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-return-assign": [ - 2, - "always" - ], - "no-return-await": 2, - "no-script-url": 2, - "no-self-assign": [ - 2, - { - "props": true - } - ], - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-tabs": 0, - "no-template-curly-in-string": 2, - "no-ternary": 0, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-trailing-spaces": [ - 2, - { - "skipBlankLines": false - } - ], - "no-undef-init": 2, - "no-undefined": 0, - "no-underscore-dangle": 2, - "no-unexpected-multiline": 2, - "no-unmodified-loop-condition": 2, - "no-unneeded-ternary": [ - 2, - { - "defaultAssignment": false - } - ], - "no-unreachable": 2, - "no-unsafe-finally": 2, - "no-unsafe-negation": 2, - "no-unused-expressions": 2, - "no-unused-vars": [ - 2, - { - "vars": "all", - "args": "after-used" - } - ], - "no-use-before-define": 0, - "no-useless-computed-key": 2, - "no-useless-concat": 2, - "no-useless-constructor": 2, - "no-useless-escape": 2, - "no-useless-rename": 2, - "no-useless-return": 2, - "no-var": 0, - "no-void": 2, - "no-warning-comments": 1, - "no-whitespace-before-property": 2, - "no-with": 2, - "object-curly-newline": 0, - "object-curly-spacing": [ - 2, - "never" - ], - "object-property-newline": [ - 2, - { - "allowMultiplePropertiesPerLine": true - } - ], - "object-shorthand": [ - 2, - "always" - ], - "one-var": [ - 2, - "never" - ], - "one-var-declaration-per-line": 0, - "operator-assignment": [ - 2, - "always" - ], - "operator-linebreak": [ - 2, - "after" - ], - "padded-blocks": [ - 2, - "never" - ], - "prefer-arrow-callback": 2, - "prefer-const": 2, - "prefer-destructuring": 0, - "prefer-numeric-literals": 2, - "prefer-promise-reject-errors": 2, - "prefer-rest-params": 2, - "prefer-spread": 2, - "prefer-template": 0, - "quote-props": [ - 2, - "as-needed" - ], - "quotes": [ - 2, - "single", - "avoid-escape" - ], - "radix": 2, - "react/display-name": [ - 0, - { - "ignoreTranspilerName": false - } - ], - "react/forbid-component-props": 0, - "react/forbid-elements": [ - 2, - { - "forbid": [ - "embed" - ] - } - ], - "react/jsx-boolean-value": [ - 2, - "always" - ], - "react/jsx-closing-bracket-location": [ - 2, - { - "location": "tag-aligned" - } - ], - "react/jsx-curly-spacing": [ - 2, - "never" - ], - "react/jsx-equals-spacing": [ - 2, - "never" - ], - "react/jsx-filename-extension": 2, - "react/jsx-first-prop-new-line": [ - 2, - "multiline" - ], - "react/jsx-handler-names": 0, - "react/jsx-indent": [ - 2, - 4 - ], - "react/jsx-indent-props": [ - 2, - 4 - ], - "react/jsx-key": 2, - "react/jsx-max-props-per-line": [ - 2, - { - "maximum": 1 - } - ], - "react/jsx-no-bind": 0, - "react/jsx-no-comment-textnodes": 2, - "react/jsx-no-duplicate-props": [ - 2, - { - "ignoreCase": false - } - ], - "react/jsx-no-literals": 2, - "react/jsx-no-target-blank": 2, - "react/jsx-no-undef": 2, - "react/jsx-pascal-case": 2, - "react/jsx-tag-spacing": [ - 2, - { - "closingSlash": "never", - "beforeSelfClosing": "never", - "afterOpening": "never" - } - ], - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/jsx-wrap-multilines": 2, - "react/no-array-index-key": 1, - "react/no-children-prop": 2, - "react/no-danger": 0, - "react/no-danger-with-children": 2, - "react/no-deprecated": 1, - "react/no-did-mount-set-state": 2, - "react/no-did-update-set-state": 2, - "react/no-direct-mutation-state": 2, - "react/no-find-dom-node": 1, - "react/no-is-mounted": 2, - "react/no-multi-comp": [ - 2, - { - "ignoreStateless": true - } - ], - "react/no-render-return-value": 2, - "react/no-set-state": 0, - "react/no-string-refs": 0, - "react/no-unescaped-entities": 2, - "react/no-unknown-property": 2, - "react/no-unused-prop-types": [ - 1, - { - "skipShapeProps": true - } - ], - "react/prefer-es6-class": 2, - "react/prefer-stateless-function": 2, - "react/prop-types": [ - 2, - { - "ignore": [ - "location", - "history", - "component", - "className" - ] - } - ], - "react/require-default-props": 0, - "react/require-optimization": 1, - "react/require-render-return": 2, - "react/self-closing-comp": 2, - "react/sort-comp": 0, - "react/style-prop-object": [ - 2, - { - "allow": [ - "FormattedNumber", - "FormattedDuration", - "FormattedRelativeTime", - "Timestamp" - ] - } - ], - "max-nested-callbacks": [ - 2, - { - "max": 5 - } - ], - "no-shadow": 0, - "@typescript-eslint/no-shadow": 2, - "unused-imports/no-unused-imports": 2, - "no-relative-import-paths/no-relative-import-paths": [ - "error", - { "allowSameFolder": true, "rootDir": "webapp/boards"} - ], - "import-newlines/enforce": [ - 2, - 3 - ], - "formatjs/no-multiple-whitespaces": 2, - "require-yield": 2, - "rest-spread-spacing": [ - 2, - "never" - ], - "semi": [ - 2, - "always" - ], - "semi-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "sort-imports": [ - 2, - { - "ignoreDeclarationSort": true - } - ], - "sort-keys": 0, - "space-before-blocks": [ - 2, - "always" - ], - "space-before-function-paren": [ - 2, - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ], - "space-in-parens": [ - 2, - "never" - ], - "space-infix-ops": 2, - "space-unary-ops": [ - 2, - { - "words": true, - "nonwords": false - } - ], - "symbol-description": 2, - "template-curly-spacing": [ - 2, - "never" - ], - "valid-typeof": [ - 2, - { - "requireStringLiterals": false - } - ], - "vars-on-top": 0, - "wrap-iife": [ - 2, - "outside" - ], - "wrap-regex": 2, - "yoda": [ - 2, - "never", - { - "exceptRange": false, - "onlyEquality": false - } - ], - "eol-last": [ - 2, - "always" - ], - "eslint-comments/no-unused-disable": 2 - }, - "overrides": [ - { - "files": [ - "**/*.tsx", - "**/*.ts" - ], - "extends": [ - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "import/no-unresolved": 0, // ts handles this better - "camelcase": 0, - "semi": "off", - "@typescript-eslint/naming-convention": [ - 2, - { - "selector": "function", - "format": [ - "camelCase", - "PascalCase" - ] - }, - { - "selector": "variable", - "format": [ - "camelCase", - "PascalCase", - "UPPER_CASE" - ] - }, - { - "selector": "parameter", - "format": [ - "camelCase", - "PascalCase" - ], - "leadingUnderscore": "allow" - }, - { - "selector": "typeLike", - "format": [ - "PascalCase" - ] - } - ], - "@typescript-eslint/no-non-null-assertion": 0, - "@typescript-eslint/no-unused-vars": [ - 2, - { - "vars": "all", - "args": "after-used" - } - ], - "@typescript-eslint/member-delimiter-style": [ - 2, - { - "multiline": { - "delimiter": "none" - }, - "singleline": { - "delimiter": "comma" - } - } - ], - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-empty-function": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/semi": [ - 2, - "never" - ], - "@typescript-eslint/indent": [ - 2, - 4, - { - "SwitchCase": 0 - } - ], - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": [ - 2, - { - "classes": false, - "functions": false, - "variables": false - } - ], - "no-useless-constructor": 0, - "@typescript-eslint/no-useless-constructor": 2, - "react/jsx-filename-extension": 0 - } - }, - { - "files": [ - "test/**", - "**/*.test.*" - ], - "env": { - "jest": true - }, - "rules": { - "func-names": 0, - "global-require": 0, - "new-cap": 0, - "prefer-arrow-callback": 0, - "no-import-assign": 0, - "no-console": 0, - "max-lines": 0 - } - } - ] -} diff --git a/webapp/boards/.gitignore b/webapp/boards/.gitignore deleted file mode 100644 index ed9f9cc128..0000000000 --- a/webapp/boards/.gitignore +++ /dev/null @@ -1 +0,0 @@ -coverage \ No newline at end of file diff --git a/webapp/boards/.npmrc b/webapp/boards/.npmrc deleted file mode 100644 index cffe8cdef1..0000000000 --- a/webapp/boards/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/webapp/boards/.prettierignore b/webapp/boards/.prettierignore deleted file mode 100644 index 2a631c56a0..0000000000 --- a/webapp/boards/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore all js/ts files: -*.ts -*.tsx -*.js \ No newline at end of file diff --git a/webapp/boards/.prettierrc.json b/webapp/boards/.prettierrc.json deleted file mode 100644 index cef22bea2c..0000000000 --- a/webapp/boards/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "tabWidth": 4 -} diff --git a/webapp/boards/.stylelintrc.json b/webapp/boards/.stylelintrc.json deleted file mode 100644 index 762b8934a9..0000000000 --- a/webapp/boards/.stylelintrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "stylelint-config-sass-guidelines", - "rules": { - "indentation": 4, - "selector-class-pattern": "[a-zA-Z_-]+", - "max-nesting-depth": 4, - "selector-max-compound-selectors": 6, - "selector-max-id": 1, - "selector-no-qualifying-type": null, - "order/properties-alphabetical-order": null, - "declaration-block-no-duplicate-properties": true, - "property-disallowed-list": ["z-index"] - } -} diff --git a/webapp/boards/NOTICE.txt b/webapp/boards/NOTICE.txt deleted file mode 100644 index acebeb67ec..0000000000 --- a/webapp/boards/NOTICE.txt +++ /dev/null @@ -1,4465 +0,0 @@ -Focalboard -© 2015-present Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information. - -NOTICES: --------- - -This document includes a list of open source components used in Focalboard web app, including those that have been modified. - ------ - -The following software may be included in this product: @babel/code-frame, @babel/helper-module-imports, @babel/helper-validator-identifier, @babel/highlight, @babel/runtime, @babel/types. A copy of the source code may be downloaded from https://github.com/babel/babel.git (@babel/code-frame), https://github.com/babel/babel.git (@babel/helper-module-imports), https://github.com/babel/babel.git (@babel/helper-validator-identifier), https://github.com/babel/babel.git (@babel/highlight), https://github.com/babel/babel.git (@babel/runtime), https://github.com/babel/babel.git (@babel/types). This software contains the following license and notice below: - -MIT License - -Copyright (c) 2014-present Sebastian McKenzie and other contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: @cypress/listr-verbose-renderer, listr, listr-silent-renderer, listr-update-renderer, listr-verbose-renderer. A copy of the source code may be downloaded from https://github.com/SamVerschueren/listr-verbose-renderer.git (@cypress/listr-verbose-renderer), https://github.com/SamVerschueren/listr.git (listr), https://github.com/SamVerschueren/listr-silent-renderer.git (listr-silent-renderer), https://github.com/SamVerschueren/listr-update-renderer.git (listr-update-renderer), https://github.com/SamVerschueren/listr-verbose-renderer.git (listr-verbose-renderer). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Sam Verschueren (github.com/SamVerschueren) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: @cypress/request, aws-sign2, forever-agent, oauth-sign, tunnel-agent. A copy of the source code may be downloaded from https://github.com/cypress-io/request.git (@cypress/request), https://github.com/mikeal/aws-sign (aws-sign2), https://github.com/mikeal/forever-agent (forever-agent), https://github.com/mikeal/oauth-sign (oauth-sign), https://github.com/mikeal/tunnel-agent (tunnel-agent). This software contains the following license and notice below: - -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - ------ - -The following software may be included in this product: @cypress/xvfb. A copy of the source code may be downloaded from https://github.com/cypress-io/xvfb.git. This software contains the following license and notice below: - -Original Work Copyright (C) 2012 ProxV, Inc. -Modified Work Copyright (c) 2015 Cypress.io, LLC - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: @emotion/cache, @emotion/core, @emotion/css, @emotion/hash, @emotion/memoize, @emotion/serialize, @emotion/sheet, @emotion/stylis, @emotion/unitless, @emotion/utils, @emotion/weak-memoize, babel-plugin-emotion. A copy of the source code may be downloaded from https://github.com/emotion-js/emotion/tree/master/packages/cache (@emotion/cache), https://github.com/emotion-js/emotion/tree/master/packages/core (@emotion/core), https://github.com/emotion-js/emotion/tree/master/packages/css (@emotion/css), https://github.com/emotion-js/emotion/tree/master/packages/hash (@emotion/hash), https://github.com/emotion-js/emotion/tree/master/packages/memoize (@emotion/memoize), https://github.com/emotion-js/emotion/tree/master/packages/serialize (@emotion/serialize), https://github.com/emotion-js/emotion/tree/master/packages/sheet (@emotion/sheet), https://github.com/emotion-js/emotion/tree/master/packages/stylis (@emotion/stylis), https://github.com/emotion-js/emotion/tree/master/packages/unitless (@emotion/unitless), https://github.com/emotion-js/emotion/tree/master/packages/serialize (@emotion/utils), https://github.com/emotion-js/emotion/tree/master/packages/weak-memoize (@emotion/weak-memoize), https://github.com/emotion-js/emotion/tree/master/packages/babel-plugin-emotion (babel-plugin-emotion). This software contains the following license and notice below: - -MIT License - -Copyright (c) Emotion team and other contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: @formatjs/ecma402-abstract, @formatjs/intl, @formatjs/intl-datetimeformat, @formatjs/intl-displaynames, @formatjs/intl-listformat, @formatjs/intl-relativetimeformat. A copy of the source code may be downloaded from git@github.com:formatjs/formatjs.git (@formatjs/ecma402-abstract), git@github.com:formatjs/formatjs.git (@formatjs/intl), git+https://github.com/formatjs/formatjs.git (@formatjs/intl-datetimeformat), git+https://github.com/formatjs/formatjs.git (@formatjs/intl-displaynames), git@github.com:formatjs/formatjs.git (@formatjs/intl-listformat), git@github.com:formatjs/formatjs.git (@formatjs/intl-relativetimeformat). This software contains the following license and notice below: - -MIT License - -Copyright (c) 2019 FormatJS - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: @samverschueren/stream-to-observable. A copy of the source code may be downloaded from https://github.com/SamVerschueren/stream-to-observable.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) James Talmage (github.com/jamestalmage), Sam Verschueren (github.com/SamVerschueren) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: @types/codemirror, @types/hoist-non-react-statics, @types/parse-json, @types/prop-types, @types/sizzle, @types/tern. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/codemirror), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/sizzle), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/tern). This software contains the following license and notice below: - -MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - ------ - -The following software may be included in this product: @types/estree, @types/marked, @types/react, @types/sinonjs__fake-timers. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/estree), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/marked), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/sinonjs__fake-timers). This software contains the following license and notice below: - -MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - ------ - -The following software may be included in this product: ajv. A copy of the source code may be downloaded from https://github.com/ajv-validator/ajv.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015-2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: ansi-escapes, ansi-regex, ansi-styles, any-observable, callsites, chalk, has-flag, indent-string, is-fullwidth-code-point, is-installed-globally, is-observable, is-path-inside, is-stream, log-update, mimic-fn, npm-run-path, p-map, parent-module, path-key, path-type, resolve-from, shebang-regex, string-width, strip-ansi, strip-final-newline, supports-color, untildify, wrap-ansi. A copy of the source code may be downloaded from https://github.com/sindresorhus/ansi-escapes.git (ansi-escapes), https://github.com/chalk/ansi-regex.git (ansi-regex), https://github.com/chalk/ansi-styles.git (ansi-styles), https://github.com/sindresorhus/any-observable.git (any-observable), https://github.com/sindresorhus/callsites.git (callsites), https://github.com/chalk/chalk.git (chalk), https://github.com/sindresorhus/has-flag.git (has-flag), https://github.com/sindresorhus/indent-string.git (indent-string), https://github.com/sindresorhus/is-fullwidth-code-point.git (is-fullwidth-code-point), https://github.com/sindresorhus/is-installed-globally.git (is-installed-globally), https://github.com/sindresorhus/is-observable.git (is-observable), https://github.com/sindresorhus/is-path-inside.git (is-path-inside), https://github.com/sindresorhus/is-stream.git (is-stream), https://github.com/sindresorhus/log-update.git (log-update), https://github.com/sindresorhus/mimic-fn.git (mimic-fn), https://github.com/sindresorhus/npm-run-path.git (npm-run-path), https://github.com/sindresorhus/p-map.git (p-map), https://github.com/sindresorhus/parent-module.git (parent-module), https://github.com/sindresorhus/path-key.git (path-key), https://github.com/sindresorhus/path-type.git (path-type), https://github.com/sindresorhus/resolve-from.git (resolve-from), https://github.com/sindresorhus/shebang-regex.git (shebang-regex), https://github.com/sindresorhus/string-width.git (string-width), https://github.com/chalk/strip-ansi.git (strip-ansi), https://github.com/sindresorhus/strip-final-newline.git (strip-final-newline), https://github.com/chalk/supports-color.git (supports-color), https://github.com/sindresorhus/untildify.git (untildify), https://github.com/chalk/wrap-ansi.git (wrap-ansi). This software contains the following license and notice below: - -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: ansi-regex, ansi-styles, chalk, cli-cursor, cli-truncate, code-point-at, elegant-spinner, escape-string-regexp, figures, has-ansi, is-fullwidth-code-point, is-stream, log-symbols, number-is-nan, object-assign, onetime, path-is-absolute, pify, restore-cursor, string-width, strip-ansi, supports-color. A copy of the source code may be downloaded from https://github.com/chalk/ansi-regex.git (ansi-regex), https://github.com/chalk/ansi-styles.git (ansi-styles), https://github.com/chalk/chalk.git (chalk), https://github.com/sindresorhus/cli-cursor.git (cli-cursor), https://github.com/sindresorhus/cli-truncate.git (cli-truncate), https://github.com/sindresorhus/code-point-at.git (code-point-at), https://github.com/sindresorhus/elegant-spinner.git (elegant-spinner), https://github.com/sindresorhus/escape-string-regexp.git (escape-string-regexp), https://github.com/sindresorhus/figures.git (figures), https://github.com/sindresorhus/has-ansi.git (has-ansi), https://github.com/sindresorhus/is-fullwidth-code-point.git (is-fullwidth-code-point), https://github.com/sindresorhus/is-stream.git (is-stream), https://github.com/sindresorhus/log-symbols.git (log-symbols), https://github.com/sindresorhus/number-is-nan.git (number-is-nan), https://github.com/sindresorhus/object-assign.git (object-assign), https://github.com/sindresorhus/onetime.git (onetime), https://github.com/sindresorhus/path-is-absolute.git (path-is-absolute), https://github.com/sindresorhus/pify.git (pify), https://github.com/sindresorhus/restore-cursor.git (restore-cursor), https://github.com/sindresorhus/string-width.git (string-width), https://github.com/chalk/strip-ansi.git (strip-ansi), https://github.com/chalk/supports-color.git (supports-color). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: arch. A copy of the source code may be downloaded from git://github.com/feross/arch.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Feross Aboukhadijeh - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: asn1. A copy of the source code may be downloaded from git://github.com/joyent/node-asn1.git. This software contains the following license and notice below: - -Copyright (c) 2011 Mark Cavage, All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE - ------ - -The following software may be included in this product: async. A copy of the source code may be downloaded from https://github.com/caolan/async.git. This software contains the following license and notice below: - -Copyright (c) 2010-2018 Caolan McMahon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: asynckit. A copy of the source code may be downloaded from git+https://github.com/alexindigo/asynckit.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016 Alex Indigo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: at-least-node. A copy of the source code may be downloaded from git+https://github.com/RyanZim/at-least-node.git. This software contains the following license and notice below: - -The ISC License -Copyright (c) 2020 Ryan Zimmerman - -Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: aws4. A copy of the source code may be downloaded from https://github.com/mhart/aws4.git. This software contains the following license and notice below: - -Copyright 2013 Michael Hart (michael.hart.au@gmail.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: babel-plugin-macros. A copy of the source code may be downloaded from https://github.com/kentcdodds/babel-plugin-macros.git. This software contains the following license and notice below: - -The MIT License (MIT) -Copyright (c) 2017 Kent C. Dodds - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: balanced-match. A copy of the source code may be downloaded from git://github.com/juliangruber/balanced-match.git. This software contains the following license and notice below: - -(MIT) - -Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: bcrypt-pbkdf. A copy of the source code may be downloaded from git://github.com/joyent/node-bcrypt-pbkdf.git. This software contains the following license and notice below: - -The Blowfish portions are under the following license: - -Blowfish block cipher for OpenBSD -Copyright 1997 Niels Provos -All rights reserved. - -Implementation advice by David Mazieres . - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - - -The bcrypt_pbkdf portions are under the following license: - -Copyright (c) 2013 Ted Unangst - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - - -Performance improvements (Javascript-specific): - -Copyright 2016, Joyent Inc -Author: Alex Wilson - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: blob-util. A copy of the source code may be downloaded from git://github.com/nolanlawson/blob-util.git. This software contains the following license and notice below: - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ------ - -The following software may be included in this product: bluebird. A copy of the source code may be downloaded from git://github.com/petkaantonov/bluebird.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2013-2018 Petka Antonov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: brace-expansion. A copy of the source code may be downloaded from git://github.com/juliangruber/brace-expansion.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2013 Julian Gruber - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: buffer-crc32. A copy of the source code may be downloaded from git://github.com/brianloveswords/buffer-crc32.git. This software contains the following license and notice below: - -The MIT License - -Copyright (c) 2013 Brian J. Brennan - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: buffer-from. A copy of the source code may be downloaded from https://github.com/LinusU/buffer-from.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2016, 2018 Linus Unnebäck - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: cachedir. A copy of the source code may be downloaded from https://github.com/LinusU/node-cachedir.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2013-2014, 2016, 2018 Linus Unnebäck - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: caseless. A copy of the source code may be downloaded from https://github.com/mikeal/caseless. This software contains the following license and notice below: - -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS - ------ - -The following software may be included in this product: check-more-types. A copy of the source code may be downloaded from https://github.com/kensho/check-more-types.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Kensho - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: ci-info, is-ci. A copy of the source code may be downloaded from https://github.com/watson/ci-info.git (ci-info), https://github.com/watson/is-ci.git (is-ci). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016-2018 Thomas Watson Steen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: cli-table3. A copy of the source code may be downloaded from https://github.com/cli-table/cli-table3.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2014 James Talmage - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: codemirror. A copy of the source code may be downloaded from https://github.com/codemirror/CodeMirror.git. This software contains the following license and notice below: - -MIT License - -Copyright (C) 2017 by Marijn Haverbeke and others - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: codemirror-spell-checker. A copy of the source code may be downloaded from https://github.com/NextStepWebs/codemirror-spell-checker. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Wes Cossick - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: color-convert. A copy of the source code may be downloaded from https://github.com/Qix-/color-convert.git. This software contains the following license and notice below: - -Copyright (c) 2011-2016 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: color-name. A copy of the source code may be downloaded from git@github.com:colorjs/color-name.git. This software contains the following license and notice below: - -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: colors. A copy of the source code may be downloaded from http://github.com/Marak/colors.js.git. This software contains the following license and notice below: - -MIT License - -Original Library - - Copyright (c) Marak Squires - -Additional Functionality - - Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: combined-stream, delayed-stream. A copy of the source code may be downloaded from git://github.com/felixge/node-combined-stream.git (combined-stream), git://github.com/felixge/node-delayed-stream.git (delayed-stream). This software contains the following license and notice below: - -Copyright (c) 2011 Debuggable Limited - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: commander. A copy of the source code may be downloaded from https://github.com/tj/commander.js.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2011 TJ Holowaychuk - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: common-tags. A copy of the source code may be downloaded from https://github.com/declandewet/common-tags. This software contains the following license and notice below: - -License (MIT) -------------- - -Copyright © Declan de Wet - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: concat-map, is-typedarray, minimist. A copy of the source code may be downloaded from git://github.com/substack/node-concat-map.git (concat-map), git://github.com/hughsk/is-typedarray.git (is-typedarray), git://github.com/substack/minimist.git (minimist). This software contains the following license and notice below: - -This software is released under the MIT license: - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: concat-stream. A copy of the source code may be downloaded from http://github.com/maxogden/concat-stream.git. This software contains the following license and notice below: - -The MIT License - -Copyright (c) 2013 Max Ogden - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: convert-source-map. A copy of the source code may be downloaded from git://github.com/thlorenz/convert-source-map.git. This software contains the following license and notice below: - -Copyright 2013 Thorsten Lorenz. -All rights reserved. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: core-util-is. A copy of the source code may be downloaded from git://github.com/isaacs/core-util-is. This software contains the following license and notice below: - -Copyright Node.js contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. - ------ - -The following software may be included in this product: cosmiconfig. A copy of the source code may be downloaded from git+https://github.com/davidtheclark/cosmiconfig.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 David Clark - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: cross-spawn. A copy of the source code may be downloaded from git@github.com:moxystudio/node-cross-spawn.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2018 Made With MOXY Lda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: csstype. A copy of the source code may be downloaded from https://github.com/frenic/csstype. This software contains the following license and notice below: - -Copyright (c) 2017-2018 Fredrik Nicol - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: dashdash. A copy of the source code may be downloaded from git://github.com/trentm/node-dashdash.git. This software contains the following license and notice below: - -# This is the MIT license - -Copyright (c) 2013 Trent Mick. All rights reserved. -Copyright (c) 2013 Joyent Inc. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: date-fns. A copy of the source code may be downloaded from https://github.com/date-fns/date-fns. This software contains the following license and notice below: - -# License - -date-fns is licensed under the [MIT license](http://kossnocorp.mit-license.org). -Read more about MIT at [TLDRLegal](https://tldrlegal.com/license/mit-license). - ------ - -The following software may be included in this product: debug. A copy of the source code may be downloaded from git://github.com/visionmedia/debug.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2014 TJ Holowaychuk - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the 'Software'), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: dom-helpers. A copy of the source code may be downloaded from git+https://github.com/react-bootstrap/dom-helpers.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Jason Quense - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: easymde. A copy of the source code may be downloaded from https://github.com/Ionaru/easy-markdown-editor.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Sparksuite, Inc. -Copyright (c) 2017 Jeroen Akkerman. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: ecc-jsbn. A copy of the source code may be downloaded from https://github.com/quartzjer/ecc-jsbn.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Jeremie Miller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: emoji-mart. A copy of the source code may be downloaded from git@github.com:missive/emoji-mart.git. This software contains the following license and notice below: - -Copyright (c) 2016, Missive -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: end-of-stream, pump. A copy of the source code may be downloaded from git://github.com/mafintosh/end-of-stream.git (end-of-stream), git://github.com/mafintosh/pump.git (pump). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Mathias Buus - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: error-ex, is-arrayish. A copy of the source code may be downloaded from https://github.com/qix-/node-error-ex.git (error-ex), https://github.com/qix-/node-is-arrayish.git (is-arrayish). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 JD Ballard - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: eventemitter2. A copy of the source code may be downloaded from git://github.com/hij1nx/EventEmitter2.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016 Paolo Fragomeni and Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN -AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: execa, get-stream, global-dirs, import-fresh, log-symbols, onetime, parse-json, pretty-bytes. A copy of the source code may be downloaded from https://github.com/sindresorhus/execa.git (execa), https://github.com/sindresorhus/get-stream.git (get-stream), https://github.com/sindresorhus/global-dirs.git (global-dirs), https://github.com/sindresorhus/import-fresh.git (import-fresh), https://github.com/sindresorhus/log-symbols.git (log-symbols), https://github.com/sindresorhus/onetime.git (onetime), https://github.com/sindresorhus/parse-json.git (parse-json), https://github.com/sindresorhus/pretty-bytes.git (pretty-bytes). This software contains the following license and notice below: - -MIT License - -Copyright (c) Sindre Sorhus (https://sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: executable. A copy of the source code may be downloaded from https://github.com/kevva/executable.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Kevin Mårtensson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: extend. A copy of the source code may be downloaded from https://github.com/justmoon/node-extend.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Stefan Thomas - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: extract-zip. A copy of the source code may be downloaded from https://github.com/maxogden/extract-zip.git. This software contains the following license and notice below: - -Copyright (c) 2014 Max Ogden and other contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: extsprintf, jsprim. A copy of the source code may be downloaded from git://github.com/davepacheco/node-extsprintf.git (extsprintf), git://github.com/joyent/node-jsprim.git (jsprim). This software contains the following license and notice below: - -Copyright (c) 2012, Joyent, Inc. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE - ------ - -The following software may be included in this product: fast-deep-equal, json-schema-traverse. A copy of the source code may be downloaded from git+https://github.com/epoberezkin/fast-deep-equal.git (fast-deep-equal), git+https://github.com/epoberezkin/json-schema-traverse.git (json-schema-traverse). This software contains the following license and notice below: - -MIT License - -Copyright (c) 2017 Evgeny Poberezkin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: fast-json-stable-stringify. A copy of the source code may be downloaded from git://github.com/epoberezkin/fast-json-stable-stringify.git. This software contains the following license and notice below: - -This software is released under the MIT license: - -Copyright (c) 2017 Evgeny Poberezkin -Copyright (c) 2013 James Halliday - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: fast-memoize. A copy of the source code may be downloaded from git+https://github.com/caiogondim/fast-memoize.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016 Caio Gondim - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: fd-slicer. A copy of the source code may be downloaded from git://github.com/andrewrk/node-fd-slicer.git. This software contains the following license and notice below: - -Copyright (c) 2014 Andrew Kelley - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: find-root. A copy of the source code may be downloaded from git@github.com:js-n/find-root.git. This software contains the following license and notice below: - -Copyright © 2017 jsdnxx - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: form-data. A copy of the source code may be downloaded from git://github.com/form-data/form-data.git. This software contains the following license and notice below: - -Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - ------ - -The following software may be included in this product: fs-extra. A copy of the source code may be downloaded from https://github.com/jprichardson/node-fs-extra. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2011-2017 JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: fs.realpath. A copy of the source code may be downloaded from git+https://github.com/isaacs/fs.realpath.git. This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ----- - -This library bundles a version of the `fs.realpath` and `fs.realpathSync` -methods from Node.js v0.10 under the terms of the Node.js MIT license. - -Node's license follows, also included at the header of `old.js` which contains -the licensed code: - - Copyright Joyent, Inc. and other Node contributors. - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: function-bind. A copy of the source code may be downloaded from git://github.com/Raynos/function-bind.git. This software contains the following license and notice below: - -Copyright (c) 2013 Raynos. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: getos. A copy of the source code may be downloaded from https://github.com/retrohacker/getos.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016 William Blankenship - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: getpass, http-signature, sshpk. A copy of the source code may be downloaded from https://github.com/arekinath/node-getpass.git (getpass), git://github.com/joyent/node-http-signature.git (http-signature), git+https://github.com/joyent/node-sshpk.git (sshpk). This software contains the following license and notice below: - -Copyright Joyent, Inc. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. - ------ - -The following software may be included in this product: glob. A copy of the source code may be downloaded from git://github.com/isaacs/node-glob.git. This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -## Glob Logo - -Glob's logo created by Tanya Brassie , licensed -under a Creative Commons Attribution-ShareAlike 4.0 International License -https://creativecommons.org/licenses/by-sa/4.0/ - ------ - -The following software may be included in this product: graceful-fs. A copy of the source code may be downloaded from https://github.com/isaacs/node-graceful-fs. This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: har-schema. A copy of the source code may be downloaded from https://github.com/ahmadnassri/har-schema.git. This software contains the following license and notice below: - -Copyright (c) 2015, Ahmad Nassri - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: har-validator. A copy of the source code may be downloaded from https://github.com/ahmadnassri/node-har-validator.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2018 Ahmad Nassri - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: history, react-router, react-router-dom. A copy of the source code may be downloaded from https://github.com/ReactTraining/history.git (history), https://github.com/ReactTraining/react-router.git (react-router), https://github.com/ReactTraining/react-router.git (react-router-dom). This software contains the following license and notice below: - -MIT License - -Copyright (c) React Training 2016-2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: hoist-non-react-statics. A copy of the source code may be downloaded from git://github.com/mridgway/hoist-non-react-statics.git. This software contains the following license and notice below: - -Software License Agreement (BSD License) -======================================== - -Copyright (c) 2015, Yahoo! Inc. All rights reserved. ----------------------------------------------------- - -Redistribution and use of this software in source and binary forms, with or -without modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be - used to endorse or promote products derived from this software without - specific prior written permission of Yahoo! Inc. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: human-signals. A copy of the source code may be downloaded from https://github.com/ehmicky/human-signals.git. This software contains the following license and notice below: - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019 ehmicky - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ------ - -The following software may be included in this product: inflight. A copy of the source code may be downloaded from https://github.com/npm/inflight.git. This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: inherits. A copy of the source code may be downloaded from git://github.com/isaacs/inherits. This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: ini, isexe, json-stringify-safe, minimatch, once, rimraf, which, wrappy. A copy of the source code may be downloaded from git://github.com/isaacs/ini.git (ini), git+https://github.com/isaacs/isexe.git (isexe), git://github.com/isaacs/json-stringify-safe (json-stringify-safe), git://github.com/isaacs/minimatch.git (minimatch), git://github.com/isaacs/once (once), git://github.com/isaacs/rimraf.git (rimraf), git://github.com/isaacs/node-which.git (which), https://github.com/npm/wrappy (wrappy). This software contains the following license and notice below: - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: intl-messageformat, intl-messageformat-parser. A copy of the source code may be downloaded from git@github.com:formatjs/formatjs.git (intl-messageformat), git://github.com/formatjs/formatjs.git (intl-messageformat-parser). This software contains the following license and notice below: - -Copyright (c) 2019, Oath Inc. - -Licensed under the terms of the New BSD license. See below for terms. - -Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the following -conditions are met: - -- Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -- Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -- Neither the name of Oath Inc. nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission of Oath Inc. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: is-core-module. A copy of the source code may be downloaded from git+https://github.com/inspect-js/is-core-module.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Dave Justice - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: is-promise. A copy of the source code may be downloaded from https://github.com/then/is-promise.git. This software contains the following license and notice below: - -Copyright (c) 2014 Forbes Lindesay - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: isstream. A copy of the source code may be downloaded from https://github.com/rvagg/isstream.git. This software contains the following license and notice below: - -The MIT License (MIT) -===================== - -Copyright (c) 2015 Rod Vagg ---------------------------- - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: js-tokens. A copy of the source code may be downloaded from https://github.com/lydell/js-tokens.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: jsbn. A copy of the source code may be downloaded from https://github.com/andyperlitch/jsbn.git. This software contains the following license and notice below: - -Licensing ---------- - -This software is covered under the following copyright: - -/* - * Copyright (c) 2003-2005 Tom Wu - * All Rights Reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, - * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY - * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. - * - * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, - * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER - * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF - * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT - * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * In addition, the following condition applies: - * - * All redistributions must retain an intact copy of this copyright notice - * and disclaimer. - */ - -Address all questions regarding this license to: - - Tom Wu - tjw@cs.Stanford.EDU - ------ - -The following software may be included in this product: json-parse-even-better-errors. A copy of the source code may be downloaded from https://github.com/npm/json-parse-even-better-errors. This software contains the following license and notice below: - -Copyright 2017 Kat Marchán -Copyright npm, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - ---- - -This library is a fork of 'better-json-errors' by Kat Marchán, extended and -distributed under the terms of the MIT license above. - ------ - -The following software may be included in this product: jsonfile. A copy of the source code may be downloaded from git@github.com:jprichardson/node-jsonfile.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2012-2015, JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: lazy-ass. A copy of the source code may be downloaded from https://github.com/bahmutov/lazy-ass.git. This software contains the following license and notice below: - -Copyright (c) 2014 Gleb Bahmutov - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: lines-and-columns. A copy of the source code may be downloaded from https://github.com/eventualbuddha/lines-and-columns.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Brian Donovan - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: lodash. A copy of the source code may be downloaded from https://github.com/lodash/lodash.git. This software contains the following license and notice below: - -Copyright OpenJS Foundation and other contributors - -Based on Underscore.js, copyright Jeremy Ashkenas, -DocumentCloud and Investigative Reporters & Editors - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/lodash/lodash - -The following license applies to all parts of this software except as -documented below: - -==== - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== - -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code displayed within the prose of the -documentation. - -CC0: http://creativecommons.org/publicdomain/zero/1.0/ - -==== - -Files located in the node_modules and vendor directories are externally -maintained libraries used by this software which have their own -licenses; we recommend you read them, as their terms may differ from the -terms above. - ------ - -The following software may be included in this product: lodash.once. A copy of the source code may be downloaded from https://github.com/lodash/lodash.git. This software contains the following license and notice below: - -Copyright jQuery Foundation and other contributors - -Based on Underscore.js, copyright Jeremy Ashkenas, -DocumentCloud and Investigative Reporters & Editors - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/lodash/lodash - -The following license applies to all parts of this software except as -documented below: - -==== - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== - -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code displayed within the prose of the -documentation. - -CC0: http://creativecommons.org/publicdomain/zero/1.0/ - -==== - -Files located in the node_modules and vendor directories are externally -maintained libraries used by this software which have their own -licenses; we recommend you read them, as their terms may differ from the -terms above. - ------ - -The following software may be included in this product: loose-envify. A copy of the source code may be downloaded from git://github.com/zertosh/loose-envify.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Andres Suarez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: marked. A copy of the source code may be downloaded from git://github.com/markedjs/marked.git. This software contains the following license and notice below: - -# License information - -## Contribution License Agreement - -If you contribute code to this project, you are implicitly allowing your code -to be distributed under the MIT license. You are also implicitly verifying that -all code is your original work. `` - -## Marked - -Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) -Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -## Markdown - -Copyright © 2004, John Gruber -http://daringfireball.net/ -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. - ------ - -The following software may be included in this product: memoize-one, tiny-invariant, tiny-warning. A copy of the source code may be downloaded from https://github.com/alexreardon/memoize-one.git (memoize-one), https://github.com/alexreardon/tiny-invariant.git (tiny-invariant), https://github.com/alexreardon/tiny-warning.git (tiny-warning). This software contains the following license and notice below: - -MIT License - -Copyright (c) 2019 Alexander Reardon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: merge-stream. A copy of the source code may be downloaded from https://github.com/grncdr/merge-stream.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Stephen Sugden (stephensugden.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: mime-db. A copy of the source code may be downloaded from https://github.com/jshttp/mime-db.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Jonathan Ong me@jongleberry.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: mime-types. A copy of the source code may be downloaded from https://github.com/jshttp/mime-types.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2014 Jonathan Ong -Copyright (c) 2015 Douglas Christopher Wilson - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: mkdirp. A copy of the source code may be downloaded from https://github.com/substack/node-mkdirp.git. This software contains the following license and notice below: - -Copyright 2010 James Halliday (mail@substack.net) - -This project is free software released under the MIT/X11 license: - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: moment. A copy of the source code may be downloaded from https://github.com/moment/moment.git. This software contains the following license and notice below: - -Copyright (c) JS Foundation and other contributors - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: ms. A copy of the source code may be downloaded from https://github.com/vercel/ms.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2020 Vercel, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: ms. A copy of the source code may be downloaded from https://github.com/zeit/ms.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2016 Zeit, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: nanoevents. A copy of the source code may be downloaded from https://github.com/ai/nanoevents.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright 2016 Andrey Sitnik - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: path-parse. A copy of the source code may be downloaded from https://github.com/jbgutierrez/path-parse.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2015 Javier Blanco - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: path-to-regexp. A copy of the source code may be downloaded from https://github.com/pillarjs/path-to-regexp.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: pend. A copy of the source code may be downloaded from git://github.com/andrewrk/node-pend.git. This software contains the following license and notice below: - -The MIT License (Expat) - -Copyright (c) 2014 Andrew Kelley - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: performance-now. A copy of the source code may be downloaded from git://github.com/braveg1rl/performance-now.git. This software contains the following license and notice below: - -Copyright (c) 2013 Braveg1rl - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: process-nextick-args. A copy of the source code may be downloaded from https://github.com/calvinmetcalf/process-nextick-args.git. This software contains the following license and notice below: - -# Copyright (c) 2015 Calvin Metcalf - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.** - ------ - -The following software may be included in this product: prop-types. A copy of the source code may be downloaded from https://github.com/facebook/prop-types.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2013-present, Facebook, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: psl. A copy of the source code may be downloaded from git@github.com:lupomontero/psl.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2017 Lupo Montero lupomontero@gmail.com - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: qs. A copy of the source code may be downloaded from https://github.com/ljharb/qs.git. This software contains the following license and notice below: - -Copyright (c) 2014 Nathan LaFreniere and other contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * The names of any contributors may not be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - * * * - -The complete list of contributors can be found at: https://github.com/hapijs/qs/graphs/contributors - ------ - -The following software may be included in this product: querystring. A copy of the source code may be downloaded from git://github.com/Gozala/querystring.git. This software contains the following license and notice below: - -Copyright 2012 Irakli Gozalishvili. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. - ------ - -The following software may be included in this product: ramda. A copy of the source code may be downloaded from git://github.com/ramda/ramda.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2013-2018 Scott Sauyet and Michael Hurley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: react, react-dom, react-is, scheduler. A copy of the source code may be downloaded from git+https://github.com/facebook/react.git (react), git+https://github.com/facebook/react.git (react-dom), https://github.com/facebook/react.git (react-is), https://github.com/facebook/react.git (scheduler). This software contains the following license and notice below: - -MIT License - -Copyright (c) Facebook, Inc. and its affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: react-input-autosize. A copy of the source code may be downloaded from https://github.com/JedWatson/react-input-autosize.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2018 Jed Watson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: react-intl. A copy of the source code may be downloaded from git@github.com:formatjs/formatjs.git. This software contains the following license and notice below: - -Copyright 2019 Oath Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - * Neither the name of the Oath Inc. nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL Oath INC. BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: react-simplemde-editor. A copy of the source code may be downloaded from https://github.com/RIP21/react-simplemde-editor. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2017 Ben Lodge and Andrii Los - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: react-transition-group. A copy of the source code may be downloaded from https://github.com/reactjs/react-transition-group.git. This software contains the following license and notice below: - -BSD 3-Clause License - -Copyright (c) 2018, React Community -Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: readable-stream. A copy of the source code may be downloaded from git://github.com/nodejs/readable-stream. This software contains the following license and notice below: - -Node.js is licensed for use as follows: - -""" -Copyright Node.js contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. -""" - -This license applies to parts of Node.js originating from the -https://github.com/joyent/node repository: - -""" -Copyright Joyent, Inc. and other Node contributors. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. -""" - ------ - -The following software may be included in this product: regenerator-runtime. A copy of the source code may be downloaded from https://github.com/facebook/regenerator/tree/master/packages/regenerator-runtime. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2014-present, Facebook, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: request-progress. A copy of the source code may be downloaded from git://github.com/IndigoUnited/node-request-progress. This software contains the following license and notice below: - -Copyright (c) 2012 IndigoUnited - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: resolve. A copy of the source code may be downloaded from git://github.com/browserify/resolve.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2012 James Halliday - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: resolve-pathname, value-equal. A copy of the source code may be downloaded from https://github.com/mjackson/resolve-pathname.git (resolve-pathname), https://github.com/mjackson/value-equal.git (value-equal). This software contains the following license and notice below: - -MIT License - -Copyright (c) Michael Jackson 2016-2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: rxjs. A copy of the source code may be downloaded from https://github.com/reactivex/rxjs.git. This software contains the following license and notice below: - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ------ - -The following software may be included in this product: safe-buffer. A copy of the source code may be downloaded from git://github.com/feross/safe-buffer.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Feross Aboukhadijeh - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: safer-buffer. A copy of the source code may be downloaded from git+https://github.com/ChALkeR/safer-buffer.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2018 Nikita Skovoroda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: shallow-equal. A copy of the source code may be downloaded from https://github.com/moroshko/shallow-equal.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright © 2016 Misha Moroshko - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the “Software”), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: shebang-command. A copy of the source code may be downloaded from https://github.com/kevva/shebang-command.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) Kevin Mårtensson (github.com/kevva) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: signal-exit. A copy of the source code may be downloaded from https://github.com/tapjs/signal-exit.git. This software contains the following license and notice below: - -The ISC License - -Copyright (c) 2015, Contributors - -Permission to use, copy, modify, and/or distribute this software -for any purpose with or without fee is hereby granted, provided -that the above copyright notice and this permission notice -appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE -LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: slice-ansi. A copy of the source code may be downloaded from https://github.com/chalk/slice-ansi.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2015 DC - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: source-map. A copy of the source code may be downloaded from http://github.com/mozilla/source-map.git. This software contains the following license and notice below: - -Copyright (c) 2009-2011, Mozilla Foundation and contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the names of the Mozilla Foundation nor the names of project - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: string_decoder. A copy of the source code may be downloaded from git://github.com/nodejs/string_decoder.git. This software contains the following license and notice below: - -Node.js is licensed for use as follows: - -""" -Copyright Node.js contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. -""" - -This license applies to parts of Node.js originating from the -https://github.com/joyent/node repository: - -""" -Copyright Joyent, Inc. and other Node contributors. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. -""" - ------ - -The following software may be included in this product: symbol-observable. A copy of the source code may be downloaded from https://github.com/blesh/symbol-observable.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) Sindre Sorhus (sindresorhus.com) -Copyright (c) Ben Lesh - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------ - -The following software may be included in this product: tmp. A copy of the source code may be downloaded from https://github.com/raszi/node-tmp.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 KARASZI István - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: to-fast-properties. A copy of the source code may be downloaded from https://github.com/sindresorhus/to-fast-properties.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2014 Petka Antonov - 2015 Sindre Sorhus - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: tough-cookie. A copy of the source code may be downloaded from git://github.com/salesforce/tough-cookie.git. This software contains the following license and notice below: - -Copyright (c) 2015, Salesforce.com, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: tslib. A copy of the source code may be downloaded from https://github.com/Microsoft/tslib.git. This software contains the following license and notice below: - -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - ------ - -The following software may be included in this product: tweetnacl. A copy of the source code may be downloaded from https://github.com/dchest/tweetnacl-js.git. This software contains the following license and notice below: - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to - ------ - -The following software may be included in this product: typedarray. A copy of the source code may be downloaded from git://github.com/substack/typedarray.git. This software contains the following license and notice below: - -/* - Copyright (c) 2010, Linden Research, Inc. - Copyright (c) 2012, Joshua Bell - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - $/LicenseInfo$ - */ - -// Original can be found at: -// https://bitbucket.org/lindenlab/llsd -// Modifications by Joshua Bell inexorabletash@gmail.com -// https://github.com/inexorabletash/polyfill - -// ES3/ES5 implementation of the Krhonos Typed Array Specification -// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ -// Date: 2011-02-01 -// -// Variations: -// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) - ------ - -The following software may be included in this product: universalify. A copy of the source code may be downloaded from git+https://github.com/RyanZim/universalify.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2017, Ryan Zimmerman - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: uri-js. A copy of the source code may be downloaded from http://github.com/garycourt/uri-js. This software contains the following license and notice below: - -Copyright 2011 Gary Court. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY GARY COURT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Gary Court. - ------ - -The following software may be included in this product: url. A copy of the source code may be downloaded from https://github.com/defunctzombie/node-url.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright Joyent, Inc. and other Node contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: util-deprecate. A copy of the source code may be downloaded from git://github.com/TooTallNate/util-deprecate.git. This software contains the following license and notice below: - -(The MIT License) - -Copyright (c) 2014 Nathan Rajlich - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - ------ - -The following software may be included in this product: uuid. A copy of the source code may be downloaded from https://github.com/uuidjs/uuid.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2010-2016 Robert Kieffer and other contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: verror. A copy of the source code may be downloaded from git://github.com/davepacheco/node-verror.git. This software contains the following license and notice below: - -Copyright (c) 2016, Joyent, Inc. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE - ------ - -The following software may be included in this product: yaml. A copy of the source code may be downloaded from https://github.com/eemeli/yaml.git. This software contains the following license and notice below: - -Copyright 2018 Eemeli Aro - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - ------ - -The following software may be included in this product: yauzl. A copy of the source code may be downloaded from https://github.com/thejoshwolfe/yauzl.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Josh Wolfe - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - -The following software may be included in this product: react-day-picker. A copy of the source code may be downloaded from https://github.com/gpbl/react-day-picker.git. This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014 Giampaolo Bellavite - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/webapp/boards/__mocks__/fileMock.js b/webapp/boards/__mocks__/fileMock.js deleted file mode 100644 index fe29b3a274..0000000000 --- a/webapp/boards/__mocks__/fileMock.js +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -module.exports = 'test-file-stub'; diff --git a/webapp/boards/__mocks__/styleMock.js b/webapp/boards/__mocks__/styleMock.js deleted file mode 100644 index a8ab4d2522..0000000000 --- a/webapp/boards/__mocks__/styleMock.js +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -module.exports = {}; diff --git a/webapp/boards/i18n/ar.json b/webapp/boards/i18n/ar.json deleted file mode 100644 index 622352141c..0000000000 --- a/webapp/boards/i18n/ar.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "AppBar.Tooltip": "اخيار الالواح المرتبطة", - "Attachment.Attachment-title": "المرفق", - "AttachmentBlock.DeleteAction": "حذف", - "AttachmentBlock.addElement": "اضافة {type}", - "AttachmentBlock.delete": "تم حذف المرفق.", - "AttachmentBlock.failed": "تعذر تحميل هذا الملف حيث تم الوصول إلى الحد الأقصى لحجم الملفات.", - "AttachmentBlock.upload": "يتم الآن تحميل المرفق.", - "AttachmentBlock.uploadSuccess": "تم تحميل المرفق.", - "AttachmentElement.delete-confirmation-dialog-button-text": "حذف", - "AttachmentElement.download": "تحميل", - "AttachmentElement.upload-percentage": "جاري التحمي... ({uploadPercent}%)", - "BoardComponent.add-a-group": "+ إضافة مجموعة", - "BoardComponent.delete": "حذف", - "BoardComponent.hidden-columns": "الأعمدة المخفية", - "BoardComponent.hide": "إخفاء", - "BoardComponent.new": "+ جديد", - "BoardComponent.no-property": "لا يوجد {property}", - "BoardComponent.no-property-title": "يستخدم هذا العمود للعناصر بلا خواص{property} مبينة . و لا يمكن ازالته.", - "BoardComponent.show": "عرض", - "BoardMember.schemeAdmin": "المسؤول", - "BoardMember.schemeCommenter": "المعلق", - "BoardMember.schemeEditor": "محرر", - "BoardMember.schemeNone": "لا شيء", - "BoardMember.schemeViewer": "مشاهد", - "BoardMember.unlinkChannel": "إلغاء الربط", - "BoardPage.newVersion": "يتوفر نسخة جديدة من الألواح. اضغط هنا للتحميل.", - "BoardPage.syncFailed": "تعذر الوصول إلي اللوح , ربما يكون محذوفا .", - "BoardTemplateSelector.add-template": "قالب جديد", - "BoardTemplateSelector.create-empty-board": "أنشيء لوح فارغ", - "BoardTemplateSelector.delete-template": "حذف", - "BoardTemplateSelector.description": "اضافة لوح جديد للقائمة الجانبية يمكنك استخدام اي قالب من القوالب المتاحة او انشا ء لوح جديد.", - "BoardTemplateSelector.edit-template": "تحرير", - "BoardTemplateSelector.plugin.no-content-description": "اضافة لوح جديد للقائمة الجانبية يمكنك استخدام اي قالب من القوالب المتاحة او انشا ء لوح جديد.", - "BoardTemplateSelector.plugin.no-content-title": "لوح جديد", - "BoardTemplateSelector.title": "لوح جديد", - "BoardTemplateSelector.use-this-template": "استخدم هذا القالب", - "BoardsSwitcher.Title": "بحث في الالواح", - "BoardsUnfurl.Limited": "تعزر اظهار بيانات اخرى بسبب ارشفة الكارت", - "BoardsUnfurl.Remainder": "{remainder}+ المزيد", - "BoardsUnfurl.Updated": "تم التحديث {time}", - "Calculations.Options.average.displayName": "متوسط", - "Calculations.Options.average.label": "متوسط", - "Calculations.Options.count.displayName": "الاجمالي", - "Calculations.Options.count.label": "الاجمالي", - "Calculations.Options.countChecked.displayName": "تم التدقيق", - "Calculations.Options.countChecked.label": "اجمالي ما تم تدقيقه", - "Calculations.Options.countUnchecked.displayName": "لم يتم تدقيقه", - "Calculations.Options.countUnchecked.label": "اجمالي ما تم تدقيقه", - "Calculations.Options.countUniqueValue.displayName": "فريد", - "Calculations.Options.countUniqueValue.label": "اجمالي القيم الفريدة", - "Calculations.Options.countValue.displayName": "القيم", - "Calculations.Options.countValue.label": "اجمالي القيم", - "Calculations.Options.dateRange.displayName": "النطاق الزمني", - "Calculations.Options.dateRange.label": "النطاق الزمني", - "Calculations.Options.earliest.displayName": "الاقدم", - "Calculations.Options.earliest.label": "الاقدم", - "Calculations.Options.latest.displayName": "الأحدث", - "Calculations.Options.latest.label": "الأحدث", - "Calculations.Options.max.displayName": "الاقصى", - "Calculations.Options.max.label": "الاقصى", - "Calculations.Options.median.displayName": "المتوسط", - "Calculations.Options.median.label": "المتوسط", - "Calculations.Options.min.displayName": "الأدنى", - "Calculations.Options.min.label": "الأدنى", - "Calculations.Options.none.displayName": "احسب", - "Calculations.Options.none.label": "لا شيء", - "Calculations.Options.percentChecked.displayName": "تم التدقيق", - "Calculations.Options.percentChecked.label": "نسبة ما تم تدقيقه", - "Calculations.Options.percentUnchecked.displayName": "لم يتم تدقيقه", - "Calculations.Options.percentUnchecked.label": "سبة ما لم يتم تدقيقه", - "Calculations.Options.range.displayName": "النطاق الزمني", - "Calculations.Options.range.label": "النطاق الزمني", - "Calculations.Options.sum.displayName": "مجموع", - "Calculations.Options.sum.label": "مجموع", - "CalendarCard.untitled": "بدون اسم", - "CardActionsMenu.copiedLink": "تم النسخ!", - "CardActionsMenu.copyLink": "نسخ الرابط", - "CardActionsMenu.delete": "حذف", - "CardActionsMenu.duplicate": "تكرار", - "CardBadges.title-checkboxes": "تم اخيتاره", - "CardBadges.title-comments": "التعليقات", - "CardBadges.title-description": "البطاقة لها تفاصيل", - "CardDetail.Follow": "يتبع", - "CardDetail.Following": "التالية", - "CardDetail.add-content": "إضافة محتوى", - "CardDetail.add-icon": "إضافة أيقونة", - "CardDetail.add-property": "اضافة خاصية", - "CardDetail.addCardText": "اضافة محوى البطاقة", - "CardDetail.limited-body": "تحتاج رخصة Professional او Enterprise لعرض البطاقات المؤرشفة , حدث الآن و تمتع بعدد غير محدود من الالواح و البطاقات و المزيد.", - "CardDetail.limited-button": "الترقية", - "CardDetail.limited-title": "هذه البطاقة مخفية", - "CardDetail.moveContent": "نقل بيانات البطاقة", - "CardDetail.new-comment-placeholder": "إضافة تعليق…", - "CardDetailProperty.confirm-delete-heading": "سيتم حذف هذه الخاصية", - "CardDetailProperty.confirm-delete-subtext": "هل تريد حذف هذه الخاصية {propertyName}؟ حذف سيتم علي جميع البطاقات في هذا اللوح.", - "CardDetailProperty.confirm-property-name-change-subtext": "هل تريد تعديل هذه الخاصية \"{propertyName}\" {customText}؟ حذف سيتم علي {numOfCards} من البطاقات في هذا اللوح, و قد يتسبب في خسارة بعض البيانات.", - "CardDetailProperty.confirm-property-type-change": "سيتم تعديل نوع الخاصية", - "CardDetailProperty.delete-action-button": "حذف", - "CardDetailProperty.property-change-action-button": "تغيير نوع الخاصية", - "CardDetailProperty.property-changed": "تم تعديل الخاصية بنجاح!", - "CardDetailProperty.property-deleted": "تم حذف {propertyName} بنجاح!", - "CardDetailProperty.property-name-change-subtext": "النوع من \"{oldPropType}\" إلى \"{newPropType}\"", - "CardDetial.limited-link": "لمعرفة المزيد حول اسعارنا.", - "CardDialog.delete-confirmation-dialog-button-text": "حذف", - "CardDialog.delete-confirmation-dialog-heading": "سيتم حذف البطاقة!", - "CardDialog.editing-template": "انت الآن تقوم بتعديل قالب.", - "CardDialog.nocard": "تعذر الوصول للبطاقة او بطاقة غير موجودة.", - "Categories.CreateCategoryDialog.CancelText": "إلغاء", - "Categories.CreateCategoryDialog.CreateText": "إنشاء", - "Categories.CreateCategoryDialog.Placeholder": "عرف فئتك", - "Categories.CreateCategoryDialog.UpdateText": "تحديث", - "CenterPanel.Login": "تسجيل الدخول", - "CenterPanel.Share": "مشاركة", - "ColorOption.selectColor": "اختر {color} اللون", - "Comment.delete": "حذف", - "CommentsList.send": "إرسال", - "ConfirmationDialog.cancel-action": "إلغاء", - "ConfirmationDialog.confirm-action": "تأكيد", - "ContentBlock.Delete": "حذف", - "ContentBlock.DeleteAction": "حذف", - "ContentBlock.addElement": "إضافة {type}", - "ContentBlock.checkbox": "اختيار", - "ContentBlock.divider": "فاصل", - "ContentBlock.editCardCheckbox": "مختار", - "ContentBlock.editCardCheckboxText": "تحرير محوى البطاقة", - "ContentBlock.editCardText": "تحرير محوى البطاقة", - "ContentBlock.editText": "تحرير المحتوى...", - "ContentBlock.image": "صورة", - "ContentBlock.insertAbove": "إدراج بالأعلى", - "ContentBlock.moveDown": "ازاحة للأسفل", - "ContentBlock.moveUp": "إزاحة للأعلى", - "ContentBlock.text": "نص", - "DateRange.clear": "مسح", - "DateRange.empty": "فارغ", - "DateRange.endDate": "تاريخ الإنتهاء", - "DateRange.today": "اليوم", - "DeleteBoardDialog.confirm-cancel": "إلغاء", - "DeleteBoardDialog.confirm-delete": "حذف", - "DeleteBoardDialog.confirm-info": "هل تريد حذف اللوح \"{boardTitle}\"؟ سيتم حذف جميع البطاقات داخل اللوح.", - "DeleteBoardDialog.confirm-info-template": "هل تريد حذف قالب اللوح \"{boardTitle}\"؟", - "DeleteBoardDialog.confirm-tite": "نعم احذف اللوح", - "DeleteBoardDialog.confirm-tite-template": "نعم احذف قالب اللوح", - "Dialog.closeDialog": "اغلق", - "EditableDayPicker.today": "اليوم", - "Error.websocket-closed": "مشكلة في الاتصال بالخادم, برجاء التأكد من الشبكة . أو تأكد من إعدادات الخادم او الوكيل.", - "Filter.contains": "يحتوي على", - "Filter.ends-with": "ينتهي بـ", - "Filter.includes": "يحتوي على", - "Filter.is": "يطابق", - "Filter.is-empty": "فارغ", - "Filter.is-not-empty": "ليس فارغًا", - "Filter.is-not-set": "بدون تحديد", - "Filter.is-set": "معين", - "Filter.not-contains": "لا يحتوى على", - "Filter.not-ends-with": "لا ينتهى بـ", - "Filter.not-includes": "لا تشمل", - "Filter.not-starts-with": "لا تبدأ بـ", - "Filter.starts-with": "تبدأ بـ", - "FilterByText.placeholder": "فرز", - "FilterComponent.add-filter": "+ إضافة فلتر", - "FilterComponent.delete": "حذف", - "FindBoardsDialog.IntroText": "البحث في الألواح", - "FindBoardsDialog.NoResultsFor": "لا يوجد نتيجة للبحث \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "اختر بحث آخر أو تأكد من الأخطاء الإملائية.", - "FindBoardsDialog.Title": "البحث عن ألواح", - "GroupBy.ungroup": "إلغاء التجميع", - "KanbanCard.untitled": "بدون عنوان", - "Mutator.new-card-from-template": "بطاقة جديدة من نموذج", - "Mutator.new-template-from-card": "نموذج جديد من بطاقة", - "OnboardingTour.AddComments.Title": "إضافة تعليقات", - "OnboardingTour.AddDescription.Title": "اضافة وصف", - "OnboardingTour.AddProperties.Title": "إضافة خواص", - "OnboardingTour.AddView.Body": "انتقل هنا لإنشاء عرض جديد لتنظيم لوحتك باستخدام تخطيطات مختلفة.", - "OnboardingTour.AddView.Title": "إضافة عرض جديد", - "OnboardingTour.CopyLink.Title": "نسخ الرابط", - "PropertyMenu.Delete": "حذف", - "PropertyMenu.changeType": "تغيير نوع الخاصية", - "PropertyMenu.selectType": "اختيار نوع الخاصية", - "PropertyMenu.typeTitle": "النوع", - "PropertyType.CreatedBy": "تم الإنشاء بواسطة", - "PropertyType.Date": "التاريخ", - "PropertyType.Email": "البريد الإلكتروني", - "PropertyType.Number": "رقم", - "PropertyType.Person": "شخص", - "PropertyType.Text": "نص", - "RegistrationLink.copiedLink": "منسوخ!", - "RegistrationLink.copyLink": "انسخ الرابط", - "ShareBoard.copiedLink": "منسوخ!", - "ShareBoard.copyLink": "انسخ الرابط", - "Sidebar.about": "عن Focalboard", - "Sidebar.changePassword": "تغيير الكلمة السرية", - "Sidebar.invite-users": "دعوة المستخدمين", - "Sidebar.logout": "الخروج", - "Sidebar.set-language": "ضبط اللغة", - "Sidebar.settings": "الإعدادت", - "TableComponent.add-icon": "إضافة أيقونة", - "TableComponent.name": "الإسم", - "TableComponent.plus-new": "+ جديد", - "TableHeaderMenu.delete": "حذف", - "TableHeaderMenu.hide": "إخفاء", - "TableRow.open": "افتح", - "View.Table": "جدول", - "ViewHeader.add-template": "نموذج جديد", - "ViewHeader.delete-template": "حذف", - "ViewHeader.new": "جديز", - "ViewTitle.pick-icon": "اختر أيقونة", - "default-properties.badges": "التعليقات و التفاصيل", - "default-properties.title": "العنوان", - "error.back-to-home": "الرجوع للواجهة الأساسية", - "error.back-to-team": "الرجوع إلي الفريق", - "error.board-not-found": "لوح غير موجود.", - "error.go-login": "تسجيل الدخول", - "login.log-in-button": "لِج", - "login.log-in-title": "لِج" -} diff --git a/webapp/boards/i18n/ars.json b/webapp/boards/i18n/ars.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/webapp/boards/i18n/ars.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/webapp/boards/i18n/ca.json b/webapp/boards/i18n/ca.json deleted file mode 100644 index b6386314c3..0000000000 --- a/webapp/boards/i18n/ca.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - "Attachment.Attachment-title": "Adjunt", - "AttachmentBlock.DeleteAction": "esborra", - "AttachmentBlock.addElement": "afegir {type}", - "AttachmentBlock.delete": "Adjunt esborrat.", - "AttachmentBlock.failed": "Aquest fitxer no pot ser afegit ja que el límit de tamany de fitxer ha estat assolit.", - "AttachmentBlock.upload": "Adjunt afegint-se.", - "AttachmentBlock.uploadSuccess": "Adjunt afegit.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Esborra", - "AttachmentElement.download": "Descarrega", - "AttachmentElement.upload-percentage": "Afegint...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Afegir un grup", - "BoardComponent.delete": "Eliminar", - "BoardComponent.hidden-columns": "Columnes ocultes", - "BoardComponent.hide": "Amagar", - "BoardComponent.new": "+ Nou", - "BoardComponent.no-property": "Sense {property}", - "BoardComponent.no-property-title": "Els elements amb una propietat {property} buida anirán aquí. Aquesta col·lumna no pot eliminar-se.", - "BoardComponent.show": "Mostrar", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Comentarista", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "Cap", - "BoardMember.schemeViewer": "Visualitzador", - "BoardPage.newVersion": "Una nova versió de Boards és disponible, clica aquí per recarregar.", - "BoardPage.syncFailed": "El tauler podria ser eliminat o revocat l'accés.", - "BoardTemplateSelector.add-template": "Crea una nova plantilla", - "BoardTemplateSelector.create-empty-board": "Crea un taulell buit", - "BoardTemplateSelector.delete-template": "Esborra", - "BoardTemplateSelector.description": "Afegeix el taulell a la barra lateral usant alguna de les plantilles definides a sota o comença des de zero.", - "BoardTemplateSelector.edit-template": "Edita", - "BoardTemplateSelector.plugin.no-content-description": "Afegeix el taulell a la barra lateral usant alguna de les plantilles definides a sota o comença des de zero.", - "BoardTemplateSelector.plugin.no-content-title": "Crea un taulell", - "BoardTemplateSelector.title": "Crea un taulell", - "BoardTemplateSelector.use-this-template": "Utilitza aquesta plantilla", - "BoardsSwitcher.Title": "Busca taulells", - "BoardsUnfurl.Updated": "Actualitzat {time}", - "Calculations.Options.average.displayName": "Promig", - "Calculations.Options.average.label": "Promig", - "Calculations.Options.countChecked.displayName": "Comprovat", - "Calculations.Options.countUniqueValue.displayName": "Únic", - "Calculations.Options.countUniqueValue.label": "Compta valors únics", - "Calculations.Options.countValue.displayName": "Valors", - "Calculations.Options.dateRange.displayName": "Rang", - "Calculations.Options.dateRange.label": "Rang", - "Calculations.Options.earliest.displayName": "Proper", - "Calculations.Options.earliest.label": "Proper", - "Calculations.Options.latest.displayName": "Últim", - "Calculations.Options.latest.label": "Últim", - "Calculations.Options.max.displayName": "Màxim", - "Calculations.Options.max.label": "Màxim", - "Calculations.Options.min.displayName": "Mínim", - "Calculations.Options.min.label": "Mínim", - "Calculations.Options.none.displayName": "Calcula", - "Calculations.Options.none.label": "Cap", - "Calculations.Options.percentChecked.displayName": "Completat", - "Calculations.Options.percentChecked.label": "Percentatge completat", - "Calculations.Options.percentUnchecked.displayName": "No finalitzat", - "Calculations.Options.percentUnchecked.label": "Percentatge no finalitzat", - "Calculations.Options.range.displayName": "Rang", - "Calculations.Options.range.label": "Rang", - "Calculations.Options.sum.displayName": "Suma", - "Calculations.Options.sum.label": "Suma", - "CalendarCard.untitled": "Sense títol", - "CardActionsMenu.copiedLink": "Copiat!", - "CardActionsMenu.copyLink": "Còpia l'enllaç", - "CardActionsMenu.delete": "Esborra", - "CardActionsMenu.duplicate": "Duplica", - "CardBadges.title-comments": "Comentaris", - "CardBadges.title-description": "Aquesta tarjeta té una descripció", - "CardDetail.Attach": "Adjunta", - "CardDetail.Follow": "Segueix", - "CardDetail.Following": "Segueix", - "CardDetail.add-content": "Afegeix contingut", - "CardDetail.add-icon": "Afegeix icona", - "CardDetail.add-property": "+ Afegeix propietat", - "CardDetail.addCardText": "afegeix text a la targeta", - "CardDetail.moveContent": "Mou el contingut de la targeta", - "CardDetail.new-comment-placeholder": "Afegeix un comentari...", - "CardDialog.editing-template": "Estas editant una plantilla.", - "CardDialog.nocard": "Aquesta targeta no existeix o és innaccesible.", - "Comment.delete": "Eliminar", - "CommentsList.send": "Enviar", - "ContentBlock.Delete": "Eliminar", - "ContentBlock.DeleteAction": "eliminar", - "ContentBlock.addElement": "afegeix {type}", - "ContentBlock.checkbox": "casella de verificació", - "ContentBlock.divider": "divisor", - "ContentBlock.editCardCheckbox": "casella de verificació conmutada", - "ContentBlock.editCardCheckboxText": "edita el contigut de la targeta", - "ContentBlock.editCardText": "edita el text de la targeta", - "ContentBlock.editText": "Edita el text...", - "ContentBlock.image": "imatge", - "ContentBlock.insertAbove": "Insereix damunt", - "ContentBlock.moveDown": "Mou abaix", - "ContentBlock.moveUp": "Mou adalt", - "ContentBlock.text": "text", - "Dialog.closeDialog": "Tanca la finestra", - "EditableDayPicker.today": "Avui", - "Filter.includes": "inclou", - "Filter.is-empty": "esta buit", - "Filter.is-not-empty": "no està buit", - "Filter.not-includes": "no inclou", - "FilterComponent.add-filter": "+ Afegeix filtre", - "FilterComponent.delete": "Eliminar", - "GroupBy.ungroup": "Desagrupar", - "KanbanCard.untitled": "Sense títol", - "Mutator.new-card-from-template": "nova targeta des de plantilla", - "Mutator.new-template-from-card": "nova plantilla des de targeta", - "PropertyMenu.Delete": "Eliminar", - "PropertyMenu.changeType": "Canviar el tipus de propietat", - "PropertyMenu.typeTitle": "Tipus", - "PropertyType.Checkbox": "casella de verificació", - "PropertyType.CreatedBy": "Creada per", - "PropertyType.CreatedTime": "Moment de creació", - "PropertyType.Date": "Data", - "PropertyType.Email": "Correu electrònic", - "PropertyType.MultiSelect": "Selecció múltiple", - "PropertyType.Number": "Número", - "PropertyType.Person": "Persona", - "PropertyType.Phone": "Telèfon", - "PropertyType.Select": "Selecciona", - "PropertyType.Text": "Text", - "PropertyType.UpdatedBy": "Última actualització feta per", - "PropertyType.UpdatedTime": "Moment d'actualització", - "RegistrationLink.confirmRegenerateToken": "Això invalidarà enllaços compartits anteriorment. Continuar?", - "RegistrationLink.copiedLink": "Copiat!", - "RegistrationLink.copyLink": "Copiar enllaç", - "RegistrationLink.description": "Comparteix aquest enllaç per crear comptes per altres:", - "RegistrationLink.regenerateToken": "Regenerar token", - "RegistrationLink.tokenRegenerated": "Enllaç de registre regenerat", - "ShareBoard.confirmRegenerateToken": "Això invalidarà enllaços compartits anteriorment. Continuar?", - "ShareBoard.copiedLink": "Copiat!", - "ShareBoard.copyLink": "Copiar enllaç", - "ShareBoard.tokenRegenrated": "Token regenerat", - "Sidebar.about": "Sobre Focalboard", - "Sidebar.add-board": "+ Afegir tauler", - "Sidebar.changePassword": "Canvi de contrasenya", - "Sidebar.delete-board": "Eliminar el tauler", - "Sidebar.export-archive": "Arxiu d'exportació", - "Sidebar.import-archive": "Arxiu d'importació", - "Sidebar.invite-users": "Convida usuaris", - "Sidebar.logout": "Tanca sessió", - "Sidebar.random-icons": "Icones aleatòries", - "Sidebar.set-language": "Seleccionar idioma", - "Sidebar.set-theme": "Definir un tema", - "Sidebar.settings": "Paràmetres", - "Sidebar.untitled-board": "(Tauler sense títol)", - "TableComponent.add-icon": "Afegeix icona", - "TableComponent.name": "Nom", - "TableComponent.plus-new": "+ Nou", - "TableHeaderMenu.delete": "Eliminar", - "TableHeaderMenu.duplicate": "Duplicar", - "TableHeaderMenu.hide": "Amagar", - "TableHeaderMenu.insert-left": "Insereix a l'esquerra", - "TableHeaderMenu.insert-right": "Insereix a la dreta", - "TableHeaderMenu.sort-ascending": "Ordena ascendent", - "TableHeaderMenu.sort-descending": "Ordena descendent", - "TableRow.open": "Obrir", - "View.AddView": "Afegeix vista", - "View.Board": "Tauler", - "View.DeleteView": "Eliminar vista", - "View.DuplicateView": "Duplicar vista", - "View.NewBoardTitle": "Vista de tauler", - "View.NewCalendarTitle": "Vista de calendari", - "View.NewGalleryTitle": "Vista de galeria", - "View.NewTableTitle": "Vista de tauler", - "View.Table": "Taula", - "ViewHeader.add-template": "Nova plantilla", - "ViewHeader.delete-template": "Eliminar", - "ViewHeader.edit-template": "Editar", - "ViewHeader.empty-card": "Targeta buida", - "ViewHeader.export-complete": "Exportació completada!", - "ViewHeader.export-csv": "Exportació a CSV", - "ViewHeader.export-failed": "L'exportació ha fallat!", - "ViewHeader.filter": "Filtrar", - "ViewHeader.group-by": "Agrupar per: {property}", - "ViewHeader.new": "Nou", - "ViewHeader.properties": "Propietats", - "ViewHeader.search-text": "Cerca tarjetes", - "ViewHeader.select-a-template": "Selecciona una plantilla", - "ViewHeader.sort": "Ordenar", - "ViewHeader.untitled": "Sense títol", - "ViewTitle.hide-description": "amagar descripció", - "ViewTitle.pick-icon": "Seleccionar icona", - "ViewTitle.random-icon": "Aleatori", - "ViewTitle.remove-icon": "Eliminar icona", - "ViewTitle.show-description": "mostra la descripció", - "ViewTitle.untitled-board": "Tauler sense títol", - "Workspace.editing-board-template": "Estàs editant una plantilla de tauler.", - "default-properties.title": "Títol", - "login.log-in-button": "Inicia sessió", - "login.log-in-title": "Inicia sessió", - "login.register-button": "o crea un compte si no en tens", - "register.login-button": "o inicia sessió si ja tens un compte", - "register.signup-title": "Registrat un compte" -} diff --git a/webapp/boards/i18n/de.json b/webapp/boards/i18n/de.json deleted file mode 100644 index 5a2835d428..0000000000 --- a/webapp/boards/i18n/de.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrator", - "AdminBadge.TeamAdmin": "Teamadministrator", - "AppBar.Tooltip": "Verknüpfte Boards umschalten", - "Attachment.Attachment-title": "Anhang", - "AttachmentBlock.DeleteAction": "Löschen", - "AttachmentBlock.addElement": "{type} hinzufügen", - "AttachmentBlock.delete": "Anhang gelöscht.", - "AttachmentBlock.failed": "Kann Datei nicht hochladen, da das Limit für Dateigröße erreicht ist.", - "AttachmentBlock.upload": "Anhang wird hochgeladen.", - "AttachmentBlock.uploadSuccess": "Anhang hochgeladen.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Löschen", - "AttachmentElement.download": "Herunterladen", - "AttachmentElement.upload-percentage": "Hochladen...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Hinzufügen einer Gruppe", - "BoardComponent.delete": "Löschen", - "BoardComponent.hidden-columns": "Versteckte Spalten", - "BoardComponent.hide": "Ausblenden", - "BoardComponent.new": "+ Neu", - "BoardComponent.no-property": "Keine {property}", - "BoardComponent.no-property-title": "Elemente mit einer leeren {property} Eigenschaft erscheinen hier. Diese Spalte kann nicht entfernt werden.", - "BoardComponent.show": "Anzeigen", - "BoardMember.schemeAdmin": "Administrator", - "BoardMember.schemeCommenter": "Kommentator", - "BoardMember.schemeEditor": "Bearbeiter", - "BoardMember.schemeNone": "Keine", - "BoardMember.schemeViewer": "Leser", - "BoardMember.unlinkChannel": "Verknüpfung aufheben", - "BoardPage.newVersion": "Eine neue Version von Boards ist verfügbar, klicke hier, um neu zu laden.", - "BoardPage.syncFailed": "Das Board kann gelöscht oder der Zugang entzogen werden.", - "BoardTemplateSelector.add-template": "Neue Vorlage erstellen", - "BoardTemplateSelector.create-empty-board": "Leeres Board erstellen", - "BoardTemplateSelector.delete-template": "Löschen", - "BoardTemplateSelector.description": "Füge ein Board hinzu, indem du eine der unten definierten Vorlagen verwendest oder ganz neu beginnst.", - "BoardTemplateSelector.edit-template": "Bearbeiten", - "BoardTemplateSelector.plugin.no-content-description": "Füge ein Board zur Seitenleiste hinzu, indem du eine der Vorlagen unten verwendest oder starte mit einem leeren Board.", - "BoardTemplateSelector.plugin.no-content-title": "Erstelle ein Board", - "BoardTemplateSelector.title": "Erstelle ein Board", - "BoardTemplateSelector.use-this-template": "Verwende diese Vorlage", - "BoardsSwitcher.Title": "Finde Boards", - "BoardsUnfurl.Limited": "Weitere Details sind versteckt, da die Karte archiviert wurde", - "BoardsUnfurl.Remainder": "+{remainder} mehr", - "BoardsUnfurl.Updated": "Aktualisiert {time}", - "Calculations.Options.average.displayName": "Durchschnitt", - "Calculations.Options.average.label": "Durchschnitt", - "Calculations.Options.count.displayName": "Zählen", - "Calculations.Options.count.label": "Zählen", - "Calculations.Options.countChecked.displayName": "Geprüft", - "Calculations.Options.countChecked.label": "Zähle Markierte", - "Calculations.Options.countUnchecked.displayName": "Ungeprüft", - "Calculations.Options.countUnchecked.label": "Zähle Unmarkierte", - "Calculations.Options.countUniqueValue.displayName": "Eindeutig", - "Calculations.Options.countUniqueValue.label": "Zähle eindeutige Werte", - "Calculations.Options.countValue.displayName": "Werte", - "Calculations.Options.countValue.label": "Zähle Wert", - "Calculations.Options.dateRange.displayName": "Bereich", - "Calculations.Options.dateRange.label": "Bereich", - "Calculations.Options.earliest.displayName": "Früheste", - "Calculations.Options.earliest.label": "Früheste", - "Calculations.Options.latest.displayName": "Neueste", - "Calculations.Options.latest.label": "Neueste", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Berechnen", - "Calculations.Options.none.label": "Keine", - "Calculations.Options.percentChecked.displayName": "Geprüft", - "Calculations.Options.percentChecked.label": "Prozentsatz Markiert", - "Calculations.Options.percentUnchecked.displayName": "Ungeprüft", - "Calculations.Options.percentUnchecked.label": "Prozentsatz Unmarkiert", - "Calculations.Options.range.displayName": "Bereich", - "Calculations.Options.range.label": "Bereich", - "Calculations.Options.sum.displayName": "Summe", - "Calculations.Options.sum.label": "Summe", - "CalendarCard.untitled": "Ohne Titel", - "CardActionsMenu.copiedLink": "Kopiert!", - "CardActionsMenu.copyLink": "Verknüpfung kopieren", - "CardActionsMenu.delete": "Löschen", - "CardActionsMenu.duplicate": "Duplizieren", - "CardBadges.title-checkboxes": "Checkboxen", - "CardBadges.title-comments": "Kommentare", - "CardBadges.title-description": "Diese Karte hat eine Beschreibung", - "CardDetail.Attach": "Anhängen", - "CardDetail.Follow": "Folgen", - "CardDetail.Following": "Folgend", - "CardDetail.add-content": "Inhalt hinzufügen", - "CardDetail.add-icon": "Symbol hinzufügen", - "CardDetail.add-property": "+ Eigenschaft hinzufügen", - "CardDetail.addCardText": "Kartentext hinzufügen", - "CardDetail.limited-body": "Aktualisiere auf unseren Professional oder Enterprise Plan.", - "CardDetail.limited-button": "Aktualisiere", - "CardDetail.limited-title": "Diese Karte ist versteckt", - "CardDetail.moveContent": "Karteninhalt verschieben", - "CardDetail.new-comment-placeholder": "Kommentar hinzufügen...", - "CardDetailProperty.confirm-delete-heading": "Eigenschaft löschen bestätigen", - "CardDetailProperty.confirm-delete-subtext": "Bist du sicher, dass du die Eigenschaft \"{propertyName}\" löschen möchtest? Wenn du diese löscht, wird die Eigenschaft von allen Karten in diesem Board gelöscht.", - "CardDetailProperty.confirm-property-name-change-subtext": "Bist du sicher, dass du die Eigenschaft \"{propertyName}\" {customText} ändern möchtest? Dies wird Werte auf {numOfCards} Karten in diesem Board ändern und kann dazu führen, dass diese verloren gehen.", - "CardDetailProperty.confirm-property-type-change": "Bestätige Eigenschaftsänderung", - "CardDetailProperty.delete-action-button": "Löschen", - "CardDetailProperty.property-change-action-button": "Ändere Eigenschaft", - "CardDetailProperty.property-changed": "Eigenschaft erfolgreich geändert!", - "CardDetailProperty.property-deleted": "{propertyName} erfolgreich gelöscht!", - "CardDetailProperty.property-name-change-subtext": "Typ von \"{oldPropType}\" zu \"{newPropType}\"", - "CardDetial.limited-link": "Erfahre mehr über unsere Pläne.", - "CardDialog.delete-confirmation-dialog-attachment": "Löschen des Anhangs bestätigen", - "CardDialog.delete-confirmation-dialog-button-text": "Löschen", - "CardDialog.delete-confirmation-dialog-heading": "Karte löschen bestätigen", - "CardDialog.editing-template": "Du bearbeitest eine Vorlage.", - "CardDialog.nocard": "Diese Karte existiert nicht oder ist nicht verfügbar.", - "Categories.CreateCategoryDialog.CancelText": "Abbrechen", - "Categories.CreateCategoryDialog.CreateText": "Erstellen", - "Categories.CreateCategoryDialog.Placeholder": "Benenne deine Kategorie", - "Categories.CreateCategoryDialog.UpdateText": "Aktualisieren", - "CenterPanel.Login": "Anmeldung", - "CenterPanel.Share": "Teilen", - "ChannelIntro.CreateBoard": "Erstelle ein Board", - "ColorOption.selectColor": "Wähle Farbe {color}", - "Comment.delete": "Löschen", - "CommentsList.send": "Abschicken", - "ConfirmPerson.empty": "Leer", - "ConfirmPerson.search": "Suche...", - "ConfirmationDialog.cancel-action": "Abbrechen", - "ConfirmationDialog.confirm-action": "Bestätigen", - "ContentBlock.Delete": "Löschen", - "ContentBlock.DeleteAction": "löschen", - "ContentBlock.addElement": "{type} hinzufügen", - "ContentBlock.checkbox": "Checkbox", - "ContentBlock.divider": "Teiler", - "ContentBlock.editCardCheckbox": "Umschaltbare Checkbox", - "ContentBlock.editCardCheckboxText": "Kartentext bearbeiten", - "ContentBlock.editCardText": "Kartentext bearbeiten", - "ContentBlock.editText": "Text bearbeiten ...", - "ContentBlock.image": "Bild", - "ContentBlock.insertAbove": "Darüber einfügen", - "ContentBlock.moveBlock": "Karteninhalt verschieben", - "ContentBlock.moveDown": "Nach unten bewegen", - "ContentBlock.moveUp": "Nach oben bewegen", - "ContentBlock.text": "Text", - "DateFilter.empty": "Leer", - "DateRange.clear": "Leeren", - "DateRange.empty": "Leer", - "DateRange.endDate": "Enddatum", - "DateRange.today": "Heute", - "DeleteBoardDialog.confirm-cancel": "Abbrechen", - "DeleteBoardDialog.confirm-delete": "Löschen", - "DeleteBoardDialog.confirm-info": "Bist du sicher, dass du das Board \"{boardTitle}\" löschen möchtest? Wenn du es löschen, werden allen Karten auf diesem Board gelöscht.", - "DeleteBoardDialog.confirm-info-template": "Bist du sicher, dass du die Board-Vorlage \"{boardTitle}\" löschen willst?", - "DeleteBoardDialog.confirm-tite": "Board löschen bestätigen", - "DeleteBoardDialog.confirm-tite-template": "Board Vorlage wirklich löschen", - "Dialog.closeDialog": "Dialog schließen", - "EditableDayPicker.today": "Heute", - "Error.mobileweb": "Die Unterstützung für das mobile Web befindet sich derzeit in einer frühen Betaphase. Möglicherweise sind nicht alle Funktionen vorhanden.", - "Error.websocket-closed": "Websocket-Verbindung geschlossen, Verbindung unterbrochen. Wenn dieses Problem weiterhin besteht, überprüfe bitte die Konfiguration deines Servers oder Web-Proxys.", - "Filter.contains": "enthält", - "Filter.ends-with": "endet mit", - "Filter.includes": "beinhaltet", - "Filter.is": "ist", - "Filter.is-after": "ist nach", - "Filter.is-before": "ist vor", - "Filter.is-empty": "ist leer", - "Filter.is-not-empty": "ist nicht leer", - "Filter.is-not-set": "ist nicht gesetzt", - "Filter.is-set": "ist gesetzt", - "Filter.isafter": "ist nach", - "Filter.isbefore": "ist vor", - "Filter.not-contains": "enthält nicht", - "Filter.not-ends-with": "endet nicht mit", - "Filter.not-includes": "beinhaltet nicht", - "Filter.not-starts-with": "beginnt nicht mit", - "Filter.starts-with": "beginnt mit", - "FilterByText.placeholder": "Filtertext", - "FilterComponent.add-filter": "+ Filter hinzufügen", - "FilterComponent.delete": "Löschen", - "FilterValue.empty": "(leer)", - "FindBoardsDialog.IntroText": "Suche nach Boards", - "FindBoardsDialog.NoResultsFor": "Keine Ergebnisse für \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Prüfe die Schreibweise oder versuche eine weitere Suche.", - "FindBoardsDialog.SubTitle": "Tippe um ein Board zu finden. Benutze HOCH/RUNTER zum Browsen. ENTER zur Auswahl, ESC zum Schließen", - "FindBoardsDialog.Title": "Finde Boards", - "GroupBy.hideEmptyGroups": "Verstecke {count} leere Gruppen", - "GroupBy.showHiddenGroups": "Zeige {count} versteckte Gruppen", - "GroupBy.ungroup": "Gruppierung aufheben", - "HideBoard.MenuOption": "Board verstecken", - "KanbanCard.untitled": "Unbenannt", - "MentionSuggestion.is-not-board-member": "(kein Board Mitglied)", - "Mutator.new-board-from-template": "Neues Board aus Vorlage", - "Mutator.new-card-from-template": "neue Karte aus Vorlage", - "Mutator.new-template-from-card": "neue Vorlage aus Karte", - "OnboardingTour.AddComments.Body": "Du kannst Themen kommentieren und sogar deine Mattermost-Kollegen @erwähnen, um deren Aufmerksamkeit zu erhalten.", - "OnboardingTour.AddComments.Title": "Kommentare hinzufügen", - "OnboardingTour.AddDescription.Body": "Füge deiner Karte eine Beschreibung hinzu, damit deine Teamkollegen wissen, worum es in der Karte geht.", - "OnboardingTour.AddDescription.Title": "Beschreibung hinzufügen", - "OnboardingTour.AddProperties.Body": "Füge den Karten verschiedene Eigenschaften hinzu, um sie noch leistungsfähiger zu machen.", - "OnboardingTour.AddProperties.Title": "Eigenschaften hinzufügen", - "OnboardingTour.AddView.Body": "Hier kannst Du eine neue Ansicht erstellen, um dein Board mit verschiedenen Layouts zu organisieren.", - "OnboardingTour.AddView.Title": "Eine neue Ansicht hinzufügen", - "OnboardingTour.CopyLink.Body": "Du kannst deine Karten mit Teamkollegen teilen, indem Du den Link kopierst und in einen Kanal, eine Direktnachricht oder eine Gruppennachricht einfügst.", - "OnboardingTour.CopyLink.Title": "Link kopieren", - "OnboardingTour.OpenACard.Body": "Öffne eine Karte und entdecke die Möglichkeiten, die Boards bei der Organisation deiner Arbeit bietet.", - "OnboardingTour.OpenACard.Title": "Eine Karte öffnen", - "OnboardingTour.ShareBoard.Body": "Du kannst dein Board intern, innerhalb deines Teams, freigeben oder es öffentlich veröffentlichen, damit es auch außerhalb deines Unternehmens sichtbar ist.", - "OnboardingTour.ShareBoard.Title": "Board teilen", - "PersonProperty.board-members": "Board Mitglieder", - "PersonProperty.me": "Ich", - "PersonProperty.non-board-members": "Keine Board Mitglieder", - "PropertyMenu.Delete": "Löschen", - "PropertyMenu.changeType": "Eigenschaftstyp ändern", - "PropertyMenu.selectType": "Eigenschaftstyp auswählen", - "PropertyMenu.typeTitle": "Art", - "PropertyType.Checkbox": "Checkbox", - "PropertyType.CreatedBy": "Erstellt von", - "PropertyType.CreatedTime": "Erstellzeit", - "PropertyType.Date": "Datum", - "PropertyType.Email": "E-Mail", - "PropertyType.MultiPerson": "Mehrere Personen", - "PropertyType.MultiSelect": "Mehrfachauswahl", - "PropertyType.Number": "Zahl", - "PropertyType.Person": "Person", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Auswählen", - "PropertyType.Text": "Text", - "PropertyType.Unknown": "Unbekannt", - "PropertyType.UpdatedBy": "Aktualisiert durch", - "PropertyType.UpdatedTime": "Letzte Aktualisierung", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Leer", - "RegistrationLink.confirmRegenerateToken": "Diese Aktion widerruft zuvor geteilte Links. Trotzdem fortfahren?", - "RegistrationLink.copiedLink": "Kopiert!", - "RegistrationLink.copyLink": "Link kopieren", - "RegistrationLink.description": "Teile diesen Link mit anderen zur Accounterstellung:", - "RegistrationLink.regenerateToken": "Token neu generieren", - "RegistrationLink.tokenRegenerated": "Registrierungslink neu generiert", - "ShareBoard.PublishDescription": "Veröffentliche und teile einen \"Nur Lesen\"-Link mit jedem im Web.", - "ShareBoard.PublishTitle": "Im Web veröffentlichen", - "ShareBoard.ShareInternal": "Intern teilen", - "ShareBoard.ShareInternalDescription": "Benutzer mit Berechtigungen können diesen Link benutzen.", - "ShareBoard.Title": "Board teilen", - "ShareBoard.confirmRegenerateToken": "Diese Aktion invalidiert zuvor geteilte Links. Trotzdem fortfahren?", - "ShareBoard.copiedLink": "Kopiert!", - "ShareBoard.copyLink": "Link kopieren", - "ShareBoard.regenerate": "Token neu erstellen", - "ShareBoard.searchPlaceholder": "Suche nach Personen und Kanälen", - "ShareBoard.teamPermissionsText": "Jeder im {teamName} Team", - "ShareBoard.tokenRegenrated": "Token neu generiert", - "ShareBoard.userPermissionsRemoveMemberText": "Mitglied entfernen", - "ShareBoard.userPermissionsYouText": "(Du)", - "ShareTemplate.Title": "Vorlage teilen", - "ShareTemplate.searchPlaceholder": "Benutzer suchen", - "Sidebar.about": "Über Focalboard", - "Sidebar.add-board": "+ Board hinzufügen", - "Sidebar.changePassword": "Passwort ändern", - "Sidebar.delete-board": "Board löschen", - "Sidebar.duplicate-board": "Board kopieren", - "Sidebar.export-archive": "Archiv exportieren", - "Sidebar.import": "Importieren", - "Sidebar.import-archive": "Archiv importieren", - "Sidebar.invite-users": "Nutzer einladen", - "Sidebar.logout": "Ausloggen", - "Sidebar.new-category.badge": "Neu", - "Sidebar.new-category.drag-boards-cta": "Board hierher ziehen...", - "Sidebar.no-boards-in-category": "Keine Boards vorhanden", - "Sidebar.product-tour": "Produkttour", - "Sidebar.random-icons": "Zufällige Icons", - "Sidebar.set-language": "Sprache übernehmen", - "Sidebar.set-theme": "Theme übernehmen", - "Sidebar.settings": "Einstellungen", - "Sidebar.template-from-board": "Neue Vorlage aus Board", - "Sidebar.untitled-board": "(Unbenanntes Board)", - "Sidebar.untitled-view": "(Ansicht ohne Titel)", - "SidebarCategories.BlocksMenu.Move": "Bewege nach...", - "SidebarCategories.CategoryMenu.CreateNew": "Erstelle neue Kategorie", - "SidebarCategories.CategoryMenu.Delete": "Lösche Kategorie", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in {categoryName} werden zurück zu den Board-Kategorien bewegt. Du wirst von keinen Boards entfernt.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Diese Kategorie löschen?", - "SidebarCategories.CategoryMenu.Update": "Kategorie umbenennen", - "SidebarTour.ManageCategories.Body": "Erstelle und verwalte eigene Kategorien. Diese sind benutzer-spezifisch, daher beeinflusst das Verschieben eines Boards in deine Kategorie andere Mitglieder, die das gleiche Board nutzen, nicht.", - "SidebarTour.ManageCategories.Title": "Verwalte Kategorien", - "SidebarTour.SearchForBoards.Body": "Öffne den Board Wechsler /Cmd/Strg + K) um schnell Boards zu finden und zu deiner Seitenleiste hinzuzufügen.", - "SidebarTour.SearchForBoards.Title": "Suche nach Boards", - "SidebarTour.SidebarCategories.Body": "Alle deine Boards sind jetzt unter deiner neuen Seitenleiste organisiert. Kein Wechseln mehr zwischen Arbeitsbereichen. Eigene Kategorien werden einmalig auf Basis deiner bisherigen Arbeitsbereiche automatisch im Rahmen des Upgrades auf 7.2 erstellt. Diese können entfernt oder nach deinem Bedarf bearbeitet werden.", - "SidebarTour.SidebarCategories.Link": "Erfahre mehr", - "SidebarTour.SidebarCategories.Title": "Seitenleisten Kategorien", - "SiteStats.total_boards": "Boards gesamt", - "SiteStats.total_cards": "Karten gesamt", - "TableComponent.add-icon": "Symbol hinzufügen", - "TableComponent.name": "Name", - "TableComponent.plus-new": "+ Neu", - "TableHeaderMenu.delete": "Löschen", - "TableHeaderMenu.duplicate": "Duplizieren", - "TableHeaderMenu.hide": "Verstecken", - "TableHeaderMenu.insert-left": "Links einfügen", - "TableHeaderMenu.insert-right": "Rechts einfügen", - "TableHeaderMenu.sort-ascending": "Aufsteigend sortieren", - "TableHeaderMenu.sort-descending": "Absteigend sortieren", - "TableRow.DuplicateCard": "Kopiere Karte", - "TableRow.MoreOption": "Weitere Aktionen", - "TableRow.open": "Öffnen", - "TopBar.give-feedback": "Feedback geben", - "URLProperty.copiedLink": "Kopiert!", - "URLProperty.copy": "Kopieren", - "URLProperty.edit": "Bearbeiten", - "UndoRedoHotKeys.canRedo": "Wiederherstellen", - "UndoRedoHotKeys.canRedo-with-description": "{description} wiederherstellen", - "UndoRedoHotKeys.canUndo": "Rückgängig", - "UndoRedoHotKeys.canUndo-with-description": "{description} rückgängig machen", - "UndoRedoHotKeys.cannotRedo": "Nichts wiederherstellbar", - "UndoRedoHotKeys.cannotUndo": "Nichts zum rückgängig machen", - "ValueSelector.noOptions": "Keine Optionen. Fange an zu tippen, um die erste Option hinzuzufügen!", - "ValueSelector.valueSelector": "Werteselektor", - "ValueSelectorLabel.openMenu": "Menü öffnen", - "VersionMessage.help": "Finde raus, was es Neues in dieser Version gibt.", - "VersionMessage.learn-more": "Erfahre mehr", - "View.AddView": "Ansicht hinzufügen", - "View.Board": "Board", - "View.DeleteView": "Ansicht löschen", - "View.DuplicateView": "Ansicht duplizieren", - "View.Gallery": "Galerie", - "View.NewBoardTitle": "Boardansicht", - "View.NewCalendarTitle": "Kalenderansicht", - "View.NewGalleryTitle": "Galerie Ansicht", - "View.NewTableTitle": "Tabellenansicht", - "View.NewTemplateDefaultTitle": "Unbenannte Vorlage", - "View.NewTemplateTitle": "Unbenannt", - "View.Table": "Tabelle", - "ViewHeader.add-template": "+ Neue Vorlage", - "ViewHeader.delete-template": "Löschen", - "ViewHeader.display-by": "Anzeige durch: {property}", - "ViewHeader.edit-template": "Bearbeiten", - "ViewHeader.empty-card": "Leere Karte", - "ViewHeader.export-board-archive": "Board Archiv exportieren", - "ViewHeader.export-complete": "Export abgeschlossen!", - "ViewHeader.export-csv": "Als CSV exportieren", - "ViewHeader.export-failed": "Export fehlgeschlagen!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Gruppiere nach: {property}", - "ViewHeader.new": "Neu", - "ViewHeader.properties": "Eigenschaften", - "ViewHeader.properties-menu": "Eigenschaften Menü", - "ViewHeader.search-text": "Suche Karten", - "ViewHeader.select-a-template": "Vorlage auswählen", - "ViewHeader.set-default-template": "Als Standard eingestellt", - "ViewHeader.sort": "Sortieren", - "ViewHeader.untitled": "Unbenannt", - "ViewHeader.view-header-menu": "Kopfmenü ansehen", - "ViewHeader.view-menu": "Ansichten Menü", - "ViewLimitDialog.Heading": "Ansichten pro Board Limit erreicht", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Aktualisieren", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Admin benachrichtigen", - "ViewLimitDialog.Subtext.Admin": "Aktualisiere auf unseren Professional oder Enterprise Plan.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Erfahre mehr über unsere Pläne.", - "ViewLimitDialog.Subtext.RegularUser": "Benachrichtige deinen Admin um auf unseren Professional oder Enterprise Plan zu aktualisieren.", - "ViewLimitDialog.UpgradeImg.AltText": "Bild aktualisieren", - "ViewLimitDialog.notifyAdmin.Success": "Dein Admin wurde benachrichtigt", - "ViewTitle.hide-description": "Beschreibung ausblenden", - "ViewTitle.pick-icon": "Symbol auswählen", - "ViewTitle.random-icon": "Zufällig", - "ViewTitle.remove-icon": "Symbol entfernen", - "ViewTitle.show-description": "Beschreibung anzeigen", - "ViewTitle.untitled-board": "Unbenanntes Board", - "WelcomePage.Description": "Boards ist ein Projektmanagement-Tool, das die Definition, Organisation, Verfolgung und Verwaltung von Arbeit in verschiedenen Teams mit Hilfe einer vertrauten Kanban-Board-Ansicht unterstützt.", - "WelcomePage.Explore.Button": "Rundgang", - "WelcomePage.Heading": "Willkommen bei Boards", - "WelcomePage.NoThanks.Text": "Nein danke, ich werde es selbst herausfinden", - "WelcomePage.StartUsingIt.Text": "Verwende es", - "Workspace.editing-board-template": "Du bearbeitest eine Board Vorlage.", - "badge.guest": "Gast", - "boardPage.confirm-join-button": "Teilnehmen", - "boardPage.confirm-join-text": "Du bist dabei einem privaten Board zu betreten, ohne dass du explizit durch den Board-Administrator hinzugefügt wurdest. Bist du sicher, dass du diesem privaten Board beitreten willst?", - "boardPage.confirm-join-title": "Privatem Board beitreten", - "boardSelector.confirm-link-board": "Verknüpfe Board mit Kanal", - "boardSelector.confirm-link-board-button": "Ja, verknüpfe Board", - "boardSelector.confirm-link-board-subtext": "Wenn du \"{boardName}\" mit diesem Kanal verknüpfst, werden alle Mitglieder des Kanals (aktuelle und neue) das Board bearbeiten können. Dies schließt Mitglieder aus, die Gast sind. Du kannst die Verknüpfung eine Boards mit einem Kanal jederzeit entfernen.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Wenn du \"{boardName}\" mit dem Kanal verknüpfst, werden alle Mitglieder des Kanals (aktuelle und neue) das Board bearbeiten können. Dies schließt Mitglieder aus, die Gast sind.{lineBreak} Dieses Board ist mit einem anderen Kanal verknüpft. Die Verknüpfung wird getrennt, wenn du es hier verknüpfst.", - "boardSelector.create-a-board": "Erstelle ein Board", - "boardSelector.link": "Verknüpfung", - "boardSelector.search-for-boards": "Suche nach Boards", - "boardSelector.title": "Verknüpfe Boards", - "boardSelector.unlink": "Verknüpfung aufheben", - "calendar.month": "Monat", - "calendar.today": "HEUTE", - "calendar.week": "Woche", - "centerPanel.undefined": "Kein(e) {propertyName}", - "centerPanel.unknown-user": "Unbekannter Benutzer", - "cloudMessage.learn-more": "Erfahre mehr", - "createImageBlock.failed": "Kann Datei nicht hochladen, da das Limit für Dateigröße erreicht ist.", - "default-properties.badges": "Kommentare und Beschreibung", - "default-properties.title": "Titel", - "error.back-to-home": "Zurück zur Startseite", - "error.back-to-team": "Zurück zum Team", - "error.board-not-found": "Board nicht gefunden.", - "error.go-login": "Anmeldung", - "error.invalid-read-only-board": "Du hast keine Zugriff auf dieses Board. Melde dich an um auf das Board zu zugreifen.", - "error.not-logged-in": "Deine Sitzung könnte abgelaufen sein oder du bist nicht angemeldet. Melde dich nochmal an um auf das Board zuzugreifen.", - "error.page.title": "Entschuldigung, etwas ist schief gelaufen", - "error.team-undefined": "Kein gültiges Team.", - "error.unknown": "Ein Fehler ist aufgetreten.", - "generic.previous": "Zurück", - "guest-no-board.subtitle": "Du hast noch keinen Zugang zu einem Board in diesem Team. Bitte warte, bis dich jemand zu einem Board hinzufügt.", - "guest-no-board.title": "Noch keine Boards", - "imagePaste.upload-failed": "Einige Dateien nicht hochgeladen, da das Limit für Dateigröße erreicht ist.", - "limitedCard.title": "Versteckte Karten", - "login.log-in-button": "Anmelden", - "login.log-in-title": "Anmelden", - "login.register-button": "oder erstelle einen Account wenn du noch keines hast", - "new_channel_modal.create_board.empty_board_description": "Neues leeres Board erstellen", - "new_channel_modal.create_board.empty_board_title": "Leeres Board", - "new_channel_modal.create_board.select_template_placeholder": "Vorlage auswählen", - "new_channel_modal.create_board.title": "Erstelle ein Board für diesen Kanal", - "notification-box-card-limit-reached.close-tooltip": "Für 10 Tage schlummern", - "notification-box-card-limit-reached.contact-link": "Benachrichtige deinen Admin", - "notification-box-card-limit-reached.link": "Wechsel auf einen kostenpflichtigen Plan", - "notification-box-card-limit-reached.title": "{cards} vom Board versteckte Karten", - "notification-box-cards-hidden.title": "Diese Aktion verdeckt eine andere Karte", - "notification-box.card-limit-reached.not-admin.text": "Um auf archivierte Karten zuzugreifen, kannst du {contactLink} um auf einen bezahlten Plan zu wechseln.", - "notification-box.card-limit-reached.text": "Kartenlimit erreicht. Um ältere Karten zu betrachten, {link}", - "person.add-user-to-board": "Füge {username} zum Board hinzu", - "person.add-user-to-board-confirm-button": "Zum Board hinzufügen", - "person.add-user-to-board-permissions": "Berechtigungen", - "person.add-user-to-board-question": "Möchtest du {username} zum Board hinzufügen?", - "person.add-user-to-board-warning": "{username} ist kein Mitglied des Boards und wird keine Benachrichtigungen darüber erhalten.", - "register.login-button": "oder melde dich an, wenn du bereits ein Konto hast", - "register.signup-title": "Registriere dich für deinen Account", - "rhs-board-non-admin-msg": "Du bist kein Administrator des Boards", - "rhs-boards.add": "Hinzufügen", - "rhs-boards.dm": "DN", - "rhs-boards.gm": "GN", - "rhs-boards.header.dm": "diese Direktnachricht", - "rhs-boards.header.gm": "diese Gruppennachricht", - "rhs-boards.last-update-at": "Letzte Aktualisierung um: {datetime}", - "rhs-boards.link-boards-to-channel": "Verknüpfe Board mit {channelName}", - "rhs-boards.linked-boards": "Verknüpfte Boards", - "rhs-boards.no-boards-linked-to-channel": "Bisher sind keine Boards mit {channelName} verknüpft", - "rhs-boards.no-boards-linked-to-channel-description": "Boards ist ein Projektmanagement Werkzeug, das hilft Aufgaben über Teams hinweg zu definieren, organisieren, verfolgen und verwalten, ähnlich den bekannten Kanban Boards.", - "rhs-boards.unlink-board": "Board Verknüpfung aufheben", - "rhs-boards.unlink-board1": "Board Verknüpfung aufheben", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Veröffentlichen", - "share-board.share": "Teilen", - "shareBoard.channels-select-group": "Kanäle", - "shareBoard.confirm-change-team-role.body": "Jeder in diesem Board, der eine niedrigere Berechtigung als die Rolle \"{role}\" hat, wird nun zu {role} befördert. Bist du sicher, dass du die Mindestrolle für das Board ändern willst?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Minimale Rolle des Boards ändern", - "shareBoard.confirm-change-team-role.title": "Minimale Rolle des Boards ändern", - "shareBoard.confirm-link-channel": "Verknüpfe Board mit Kanal", - "shareBoard.confirm-link-channel-button": "Verknüpfe Kanal", - "shareBoard.confirm-link-channel-button-with-other-channel": "Verknüpfung lösen und hier verknüpfen", - "shareBoard.confirm-link-channel-subtext": "Wenn du einen Kanal mit einem Board verknüpfst, werden alle Mitglieder des Kanals (aktuelle und neue) das Board bearbeiten können. Dies schließt Mitglieder aus, die Gast sind.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Wenn du einen Kanal mit einem Board verknüpfst, werden alle Mitglieder des Kanals (aktuelle und neue) dies bearbeiten können. Dies schließt Mitglieder aus, die Gast sind.{lineBreak}Dieses Board ist aktuell mit einem anderen Kanal verknüpft. Die Verknüpfung wird aufgehoben, wenn du es hier verknüpfst.", - "shareBoard.confirm-unlink.body": "Wenn du einen Kanal von einem Board trennst, werden alle Mitglieder des Kanals (aktuelle und neue) den Zugriff auf das Board verlieren außer die Berechtigungen wurden individuell vergeben.", - "shareBoard.confirm-unlink.confirmBtnText": "Kanal trennen", - "shareBoard.confirm-unlink.title": "Kanal vom Board trennen", - "shareBoard.lastAdmin": "Boards müssen mindestens eine Administrator haben", - "shareBoard.members-select-group": "Mitglieder", - "shareBoard.unknown-channel-display-name": "Unbekannter Kanal", - "tutorial_tip.finish_tour": "Erledigt", - "tutorial_tip.got_it": "Alles klar", - "tutorial_tip.ok": "Weiter", - "tutorial_tip.out": "Diese Tipps nicht mehr anzeigen.", - "tutorial_tip.seen": "Schon mal gesehen?" -} diff --git a/webapp/boards/i18n/el.json b/webapp/boards/i18n/el.json deleted file mode 100644 index 4ab7fa59c6..0000000000 --- a/webapp/boards/i18n/el.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Προσθήκη ομάδας", - "BoardComponent.delete": "Διαγραφή", - "BoardComponent.hidden-columns": "Κρυφές στήλες", - "BoardComponent.hide": "Απόκρυψη", - "BoardComponent.new": "+ Νέο", - "BoardComponent.show": "Εμφάνιση", - "CardDetail.add-content": "Προσθήκη περιεχομένου", - "CardDetail.add-icon": "Προσθήκη εικονιδίου", - "CardDetail.new-comment-placeholder": "Προσθήκη σχολίου ...", - "CardDialog.nocard": "Αυτή η κάρτα δεν υπάρχει ή δεν είναι προσβάσιμη", - "Comment.delete": "Διαγραφή", - "CommentsList.send": "Αποστολή", - "ContentBlock.Delete": "Διαγραφή", - "ContentBlock.DeleteAction": "διαγραφή", - "ContentBlock.editText": "Επεξεργασία κειμένου ...", - "ContentBlock.image": "εικόνα", - "ContentBlock.insertAbove": "Εισαγωγή από πάνω", - "ContentBlock.moveDown": "Μετακίνηση κάτω", - "ContentBlock.moveUp": "Μετακίνηση επάνω", - "ContentBlock.text": "κείμενο", - "EditableDayPicker.today": "Σήμερα", - "Filter.includes": "περιέχει", - "Filter.is-empty": "είναι άδειο", - "Filter.is-not-empty": "δεν είναι άδειο", - "Filter.not-includes": "δεν περιέχει", - "FilterComponent.add-filter": "+ Προσθήκη φίλτρου", - "FilterComponent.delete": "Διαγραφή", - "KanbanCard.untitled": "Χωρίς τίτλο", - "Mutator.new-card-from-template": "νέα κάρτα από πρότυπο", - "Mutator.new-template-from-card": "νέο πρότυπο από την κάρτα", - "PropertyMenu.Delete": "Διαγραφή", - "PropertyMenu.typeTitle": "Τύπος", - "PropertyType.CreatedBy": "Δημιουργήθηκε από", - "PropertyType.CreatedTime": "Χρόνος δημιουργίας", - "PropertyType.Date": "Ημερομηνία", - "PropertyType.Email": "Email", - "PropertyType.Number": "Αριθμός", - "PropertyType.Phone": "Τηλέφωνο", - "PropertyType.Text": "Κείμενο", - "PropertyType.UpdatedBy": "Ενημερώθηκε από", - "PropertyType.UpdatedTime": "Ώρα Ενημέρωσης", - "RegistrationLink.copyLink": "Αντιγραφή συνδέσμου", - "RegistrationLink.description": "Μοιραστείτε αυτόν τον σύνδεσμο με άλλους για να δημιουργήσουν λογαριασμούς:", - "ViewTitle.pick-icon": "Επιλογή εικονιδίου", - "ViewTitle.random-icon": "Τυχαίο", - "ViewTitle.remove-icon": "Αφαίρεση εικονιδίου", - "default-properties.title": "Τίτλος" -} diff --git a/webapp/boards/i18n/en.json b/webapp/boards/i18n/en.json deleted file mode 100644 index 26f1fd701f..0000000000 --- a/webapp/boards/i18n/en.json +++ /dev/null @@ -1,455 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Admin", - "AdminBadge.TeamAdmin": "Team Admin", - "AppBar.Tooltip": "Toggle linked boards", - "Attachment.Attachment-title": "Attachment", - "AttachmentBlock.DeleteAction": "delete", - "AttachmentBlock.addElement": "add {type}", - "AttachmentBlock.delete": "Attachment deleted.", - "AttachmentBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.", - "AttachmentBlock.upload": "Attachment uploading.", - "AttachmentBlock.uploadSuccess": "Attachment uploaded.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Delete", - "AttachmentElement.download": "Download", - "AttachmentElement.upload-percentage": "Uploading...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Add a group", - "BoardComponent.delete": "Delete", - "BoardComponent.hidden-columns": "Hidden columns", - "BoardComponent.hide": "Hide", - "BoardComponent.new": "+ New", - "BoardComponent.no-property": "No {property}", - "BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column can't be removed.", - "BoardComponent.show": "Show", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Commenter", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "None", - "BoardMember.schemeViewer": "Viewer", - "BoardMember.unlinkChannel": "Unlink", - "BoardPage.newVersion": "A new version of Boards is available, click here to reload.", - "BoardPage.syncFailed": "Board may be deleted or access revoked.", - "BoardTemplateSelector.add-template": "Create new template", - "BoardTemplateSelector.create-empty-board": "Create an empty board", - "BoardTemplateSelector.delete-template": "Delete", - "BoardTemplateSelector.description": "Add a board to the sidebar using any of the templates defined below or start from scratch.", - "BoardTemplateSelector.edit-template": "Edit", - "BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.", - "BoardTemplateSelector.plugin.no-content-title": "Create a board", - "BoardTemplateSelector.title": "Create a board", - "BoardTemplateSelector.use-this-template": "Use this template", - "BoardsSwitcher.Title": "Find boards", - "BoardsUnfurl.Limited": "Additional details are hidden due to the card being archived", - "BoardsUnfurl.Remainder": "+{remainder} more", - "BoardsUnfurl.Updated": "Updated {time}", - "Calculations.Options.average.displayName": "Average", - "Calculations.Options.average.label": "Average", - "Calculations.Options.count.displayName": "Count", - "Calculations.Options.count.label": "Count", - "Calculations.Options.countChecked.displayName": "Checked", - "Calculations.Options.countChecked.label": "Count checked", - "Calculations.Options.countUnchecked.displayName": "Unchecked", - "Calculations.Options.countUnchecked.label": "Count unchecked", - "Calculations.Options.countUniqueValue.displayName": "Unique", - "Calculations.Options.countUniqueValue.label": "Count unique values", - "Calculations.Options.countValue.displayName": "Values", - "Calculations.Options.countValue.label": "Count value", - "Calculations.Options.dateRange.displayName": "Range", - "Calculations.Options.dateRange.label": "Range", - "Calculations.Options.earliest.displayName": "Earliest", - "Calculations.Options.earliest.label": "Earliest", - "Calculations.Options.latest.displayName": "Latest", - "Calculations.Options.latest.label": "Latest", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Calculate", - "Calculations.Options.none.label": "None", - "Calculations.Options.percentChecked.displayName": "Checked", - "Calculations.Options.percentChecked.label": "Percent checked", - "Calculations.Options.percentUnchecked.displayName": "Unchecked", - "Calculations.Options.percentUnchecked.label": "Percent unchecked", - "Calculations.Options.range.displayName": "Range", - "Calculations.Options.range.label": "Range", - "Calculations.Options.sum.displayName": "Sum", - "Calculations.Options.sum.label": "Sum", - "CalendarCard.untitled": "Untitled", - "CardActionsMenu.copiedLink": "Copied!", - "CardActionsMenu.copyLink": "Copy link", - "CardActionsMenu.delete": "Delete", - "CardActionsMenu.duplicate": "Duplicate", - "CardBadges.title-checkboxes": "Checkboxes", - "CardBadges.title-comments": "Comments", - "CardBadges.title-description": "This card has a description", - "CardDetail.Attach": "Attach", - "CardDetail.Follow": "Follow", - "CardDetail.Following": "Following", - "CardDetail.add-content": "Add content", - "CardDetail.add-icon": "Add icon", - "CardDetail.add-property": "+ Add a property", - "CardDetail.addCardText": "add card text", - "CardDetail.limited-body": "Upgrade to our Professional or Enterprise plan.", - "CardDetail.limited-button": "Upgrade", - "CardDetail.limited-title": "This card is hidden", - "CardDetail.moveContent": "Move card content", - "CardDetail.new-comment-placeholder": "Add a comment...", - "CardDetailProperty.confirm-delete-heading": "Confirm delete property", - "CardDetailProperty.confirm-delete-subtext": "Are you sure you want to delete the property \"{propertyName}\"? Deleting it will delete the property from all cards in this board.", - "CardDetailProperty.confirm-property-name-change-subtext": "Are you sure you want to change property \"{propertyName}\" {customText}? This will affect value(s) across {numOfCards} card(s) in this board, and can result in data loss.", - "CardDetailProperty.confirm-property-type-change": "Confirm property type change", - "CardDetailProperty.delete-action-button": "Delete", - "CardDetailProperty.property-change-action-button": "Change property", - "CardDetailProperty.property-changed": "Changed property successfully!", - "CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!", - "CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"", - "CardDetial.limited-link": "Learn more about our plans.", - "CardDialog.delete-confirmation-dialog-attachment": "Confirm attachment delete", - "CardDialog.delete-confirmation-dialog-button-text": "Delete", - "CardDialog.delete-confirmation-dialog-heading": "Confirm card delete", - "CardDialog.editing-template": "You're editing a template.", - "CardDialog.nocard": "This card doesn't exist or is inaccessible.", - "Categories.CreateCategoryDialog.CancelText": "Cancel", - "Categories.CreateCategoryDialog.CreateText": "Create", - "Categories.CreateCategoryDialog.Placeholder": "Name your category", - "Categories.CreateCategoryDialog.UpdateText": "Update", - "CenterPanel.Login": "Login", - "CenterPanel.Share": "Share", - "ChannelIntro.CreateBoard": "Create a board", - "ColorOption.selectColor": "Select {color} Color", - "Comment.delete": "Delete", - "CommentsList.send": "Send", - "ConfirmPerson.empty": "Empty", - "ConfirmPerson.search": "Search...", - "ConfirmationDialog.cancel-action": "Cancel", - "ConfirmationDialog.confirm-action": "Confirm", - "ContentBlock.Delete": "Delete", - "ContentBlock.DeleteAction": "delete", - "ContentBlock.addElement": "add {type}", - "ContentBlock.checkbox": "checkbox", - "ContentBlock.divider": "divider", - "ContentBlock.editCardCheckbox": "toggled-checkbox", - "ContentBlock.editCardCheckboxText": "edit card text", - "ContentBlock.editCardText": "edit card text", - "ContentBlock.editText": "Edit text...", - "ContentBlock.image": "image", - "ContentBlock.insertAbove": "Insert above", - "ContentBlock.moveBlock": "move card content", - "ContentBlock.moveDown": "Move down", - "ContentBlock.moveUp": "Move up", - "ContentBlock.text": "text", - "DateFilter.empty": "Empty", - "DateRange.clear": "Clear", - "DateRange.empty": "Empty", - "DateRange.endDate": "End date", - "DateRange.today": "Today", - "DeleteBoardDialog.confirm-cancel": "Cancel", - "DeleteBoardDialog.confirm-delete": "Delete", - "DeleteBoardDialog.confirm-info": "Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete all cards in the board.", - "DeleteBoardDialog.confirm-info-template": "Are you sure you want to delete the board template “{boardTitle}”?", - "DeleteBoardDialog.confirm-tite": "Confirm delete board", - "DeleteBoardDialog.confirm-tite-template": "Confirm delete board template", - "Dialog.closeDialog": "Close dialog", - "EditableDayPicker.today": "Today", - "Error.mobileweb": "Mobile web support is currently in early beta. Not all functionality may be present.", - "Error.websocket-closed": "Websocket connection closed, connection interrupted. If this persists, check your server or web proxy configuration.", - "Filter.contains": "contains", - "Filter.ends-with": "ends with", - "Filter.includes": "includes", - "Filter.is": "is", - "Filter.is-after": "is after", - "Filter.is-before": "is before", - "Filter.is-empty": "is empty", - "Filter.is-not-empty": "is not empty", - "Filter.is-not-set": "is not set", - "Filter.is-set": "is set", - "Filter.isafter": "is after", - "Filter.isbefore": "is before", - "Filter.not-contains": "doesn't contain", - "Filter.not-ends-with": "doesn't end with", - "Filter.not-includes": "doesn't include", - "Filter.not-starts-with": "doesn't start with", - "Filter.starts-with": "starts with", - "FilterByText.placeholder": "filter text", - "FilterComponent.add-filter": "+ Add filter", - "FilterComponent.delete": "Delete", - "FilterValue.empty": "(empty)", - "FindBoardsDialog.IntroText": "Search for boards", - "FindBoardsDialog.NoResultsFor": "No results for \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.", - "FindBoardsDialog.SubTitle": "Type to find a board. Use UP/DOWN to browse. ENTER to select, ESC to dismiss", - "FindBoardsDialog.Title": "Find Boards", - "GroupBy.hideEmptyGroups": "Hide {count} empty groups", - "GroupBy.showHiddenGroups": "Show {count} hidden groups", - "GroupBy.ungroup": "Ungroup", - "HideBoard.MenuOption": "Hide board", - "KanbanCard.untitled": "Untitled", - "MentionSuggestion.is-not-board-member": "(not board member)", - "Mutator.new-board-from-template": "new board from template", - "Mutator.new-card-from-template": "new card from template", - "Mutator.new-template-from-card": "new template from card", - "OnboardingTour.AddComments.Body": "You can comment on issues, and even @mention your fellow Mattermost users to get their attention.", - "OnboardingTour.AddComments.Title": "Add comments", - "OnboardingTour.AddDescription.Body": "Add a description to your card so your teammates know what the card is about.", - "OnboardingTour.AddDescription.Title": "Add description", - "OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful.", - "OnboardingTour.AddProperties.Title": "Add properties", - "OnboardingTour.AddView.Body": "Go here to create a new view to organise your board using different layouts.", - "OnboardingTour.AddView.Title": "Add a new view", - "OnboardingTour.CopyLink.Body": "You can share your cards with teammates by copying the link and pasting it in a channel, direct message, or group message.", - "OnboardingTour.CopyLink.Title": "Copy link", - "OnboardingTour.OpenACard.Body": "Open a card to explore the powerful ways that Boards can help you organize your work.", - "OnboardingTour.OpenACard.Title": "Open a card", - "OnboardingTour.ShareBoard.Body": "You can share your board internally, within your team, or publish it publicly for visibility outside of your organization.", - "OnboardingTour.ShareBoard.Title": "Share board", - "PersonProperty.board-members": "Board members", - "PersonProperty.me": "Me", - "PersonProperty.non-board-members": "Not board members", - "PropertyMenu.Delete": "Delete", - "PropertyMenu.changeType": "Change property type", - "PropertyMenu.selectType": "Select property type", - "PropertyMenu.typeTitle": "Type", - "PropertyType.Checkbox": "Checkbox", - "PropertyType.CreatedBy": "Created by", - "PropertyType.CreatedTime": "Created time", - "PropertyType.Date": "Date", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Multi person", - "PropertyType.MultiSelect": "Multi select", - "PropertyType.Number": "Number", - "PropertyType.Person": "Person", - "PropertyType.Phone": "Phone", - "PropertyType.Select": "Select", - "PropertyType.Text": "Text", - "PropertyType.Unknown": "Unknown", - "PropertyType.UpdatedBy": "Last updated by", - "PropertyType.UpdatedTime": "Last updated time", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Empty", - "RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", - "RegistrationLink.copiedLink": "Copied!", - "RegistrationLink.copyLink": "Copy link", - "RegistrationLink.description": "Share this link for others to create accounts:", - "RegistrationLink.regenerateToken": "Regenerate token", - "RegistrationLink.tokenRegenerated": "Registration link regenerated", - "ShareBoard.PublishDescription": "Publish and share a read-only link with everyone on the web.", - "ShareBoard.PublishTitle": "Publish to the web", - "ShareBoard.ShareInternal": "Share internally", - "ShareBoard.ShareInternalDescription": "Users who have permissions will be able to use this link.", - "ShareBoard.Title": "Share Board", - "ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", - "ShareBoard.copiedLink": "Copied!", - "ShareBoard.copyLink": "Copy link", - "ShareBoard.regenerate": "Regenerate token", - "ShareBoard.searchPlaceholder": "Search for people and channels", - "ShareBoard.teamPermissionsText": "Everyone at {teamName} team", - "ShareBoard.tokenRegenrated": "Token regenerated", - "ShareBoard.userPermissionsRemoveMemberText": "Remove member", - "ShareBoard.userPermissionsYouText": "(You)", - "ShareTemplate.Title": "Share template", - "ShareTemplate.searchPlaceholder": "Search for people", - "Sidebar.delete-board": "Delete board", - "Sidebar.duplicate-board": "Duplicate board", - "Sidebar.export-archive": "Export archive", - "Sidebar.import": "Import", - "Sidebar.import-archive": "Import archive", - "Sidebar.new-category.badge": "New", - "Sidebar.new-category.drag-boards-cta": "Drag boards here...", - "Sidebar.no-boards-in-category": "No boards inside", - "Sidebar.product-tour": "Product tour", - "Sidebar.random-icons": "Random icons", - "Sidebar.set-theme": "Set theme", - "Sidebar.settings": "Settings", - "Sidebar.template-from-board": "New template from board", - "Sidebar.untitled-board": "(Untitled Board)", - "Sidebar.untitled-view": "(Untitled View)", - "SidebarCategories.BlocksMenu.Move": "Move To...", - "SidebarCategories.CategoryMenu.CreateNew": "Create New Category", - "SidebarCategories.CategoryMenu.Delete": "Delete Category", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in {categoryName} will move back to the Boards categories. You're not removed from any boards.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?", - "SidebarCategories.CategoryMenu.Update": "Rename Category", - "SidebarTour.ManageCategories.Body": "Create and manage custom categories. Categories are user-specific, so moving a board to your category won’t impact other members using the same board.", - "SidebarTour.ManageCategories.Title": "Manage categories", - "SidebarTour.SearchForBoards.Body": "Open the board switcher (Cmd/Ctrl + K) to quickly search and add boards to your sidebar.", - "SidebarTour.SearchForBoards.Title": "Search for boards", - "SidebarTour.SidebarCategories.Body": "All your boards are now organized under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.", - "SidebarTour.SidebarCategories.Link": "Learn more", - "SidebarTour.SidebarCategories.Title": "Sidebar categories", - "SiteStats.total_boards": "Total boards", - "SiteStats.total_cards": "Total cards", - "TableComponent.add-icon": "Add icon", - "TableComponent.name": "Name", - "TableComponent.plus-new": "+ New", - "TableHeaderMenu.delete": "Delete", - "TableHeaderMenu.duplicate": "Duplicate", - "TableHeaderMenu.hide": "Hide", - "TableHeaderMenu.insert-left": "Insert left", - "TableHeaderMenu.insert-right": "Insert right", - "TableHeaderMenu.sort-ascending": "Sort ascending", - "TableHeaderMenu.sort-descending": "Sort descending", - "TableRow.DuplicateCard": "duplicate card", - "TableRow.MoreOption": "More actions", - "TableRow.open": "Open", - "TopBar.give-feedback": "Give feedback", - "URLProperty.copiedLink": "Copied!", - "URLProperty.copy": "Copy", - "URLProperty.edit": "Edit", - "UndoRedoHotKeys.canRedo": "Redo", - "UndoRedoHotKeys.canRedo-with-description": "Redo {description}", - "UndoRedoHotKeys.canUndo": "Undo", - "UndoRedoHotKeys.canUndo-with-description": "Undo {description}", - "UndoRedoHotKeys.cannotRedo": "Nothing to Redo", - "UndoRedoHotKeys.cannotUndo": "Nothing to Undo", - "ValueSelector.noOptions": "No options. Start typing to add the first one!", - "ValueSelector.valueSelector": "Value selector", - "ValueSelectorLabel.openMenu": "Open menu", - "VersionMessage.help": "Check out what's new in this version.", - "VersionMessage.learn-more": "Learn more", - "View.AddView": "Add view", - "View.Board": "Board", - "View.DeleteView": "Delete view", - "View.DuplicateView": "Duplicate view", - "View.Gallery": "Gallery", - "View.NewBoardTitle": "Board view", - "View.NewCalendarTitle": "Calendar view", - "View.NewGalleryTitle": "Gallery view", - "View.NewTableTitle": "Table view", - "View.NewTemplateDefaultTitle": "Untitled Template", - "View.NewTemplateTitle": "Untitled", - "View.Table": "Table", - "ViewHeader.add-template": "New template", - "ViewHeader.delete-template": "Delete", - "ViewHeader.display-by": "Display by: {property}", - "ViewHeader.edit-template": "Edit", - "ViewHeader.empty-card": "Empty card", - "ViewHeader.export-board-archive": "Export board archive", - "ViewHeader.export-complete": "Export complete!", - "ViewHeader.export-csv": "Export to CSV", - "ViewHeader.export-failed": "Export failed!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Group by: {property}", - "ViewHeader.new": "New", - "ViewHeader.properties": "Properties", - "ViewHeader.properties-menu": "Properties menu", - "ViewHeader.search-text": "Search cards", - "ViewHeader.select-a-template": "Select a template", - "ViewHeader.set-default-template": "Set as default", - "ViewHeader.sort": "Sort", - "ViewHeader.untitled": "Untitled", - "ViewHeader.view-header-menu": "View header menu", - "ViewHeader.view-menu": "View menu", - "ViewLimitDialog.Heading": "Views per board limit reached", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Upgrade", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notify Admin", - "ViewLimitDialog.Subtext.Admin": "Upgrade to our Professional or Enterprise plan.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Learn more about our plans.", - "ViewLimitDialog.Subtext.RegularUser": "Notify your Admin to upgrade to our Professional or Enterprise plan.", - "ViewLimitDialog.UpgradeImg.AltText": "upgrade image", - "ViewLimitDialog.notifyAdmin.Success": "Your admin has been notified", - "ViewTitle.hide-description": "hide description", - "ViewTitle.pick-icon": "Pick icon", - "ViewTitle.random-icon": "Random", - "ViewTitle.remove-icon": "Remove icon", - "ViewTitle.show-description": "show description", - "ViewTitle.untitled-board": "Untitled board", - "WelcomePage.Description": "Boards is a project management tool that helps define, organize, track, and manage work across teams using a familiar Kanban board view.", - "WelcomePage.Explore.Button": "Take a tour", - "WelcomePage.Heading": "Welcome To Boards", - "WelcomePage.NoThanks.Text": "No thanks, I'll figure it out myself", - "WelcomePage.StartUsingIt.Text": "Start using it", - "Workspace.editing-board-template": "You're editing a board template.", - "badge.guest": "Guest", - "boardPage.confirm-join-button": "Join", - "boardPage.confirm-join-text": "You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?", - "boardPage.confirm-join-title": "Join private board", - "boardSelector.confirm-link-board": "Link board to channel", - "boardSelector.confirm-link-board-button": "Yes, link board", - "boardSelector.confirm-link-board-subtext": "When you link \"{boardName}\" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests. You can unlink a board from a channel at any time.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "When you link \"{boardName}\" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.{lineBreak} This board is currently linked to another channel. It will be unlinked if you choose to link it here.", - "boardSelector.create-a-board": "Create a board", - "boardSelector.link": "Link", - "boardSelector.search-for-boards": "Search for boards", - "boardSelector.title": "Link boards", - "boardSelector.unlink": "Unlink", - "calendar.month": "Month", - "calendar.today": "TODAY", - "calendar.week": "Week", - "centerPanel.undefined": "No {propertyName}", - "centerPanel.unknown-user": "Unknown user", - "createImageBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.", - "default-properties.badges": "Comments and description", - "default-properties.title": "Title", - "error.back-to-home": "Back to home", - "error.back-to-team": "Back to team", - "error.board-not-found": "Board not found.", - "error.go-login": "Log in", - "error.invalid-read-only-board": "You don't have access to this board. Log in to access Boards.", - "error.not-logged-in": "Your session may have expired or you're not logged in. Log in again to access Boards.", - "error.page.title": "Sorry, something went wrong", - "error.team-undefined": "Not a valid team.", - "error.unknown": "An error occurred.", - "generic.previous": "Previous", - "guest-no-board.subtitle": "You don't have access to any board in this team yet, please wait until somebody adds you to any board.", - "guest-no-board.title": "No boards yet", - "imagePaste.upload-failed": "Some files weren't uploaded because the file size limit has been reached.", - "limitedCard.title": "Cards hidden", - "login.log-in-button": "Log in", - "login.log-in-title": "Log in", - "login.register-button": "or create an account if you don't have one", - "new_channel_modal.create_board.empty_board_description": "Create a new empty board", - "new_channel_modal.create_board.empty_board_title": "Empty board", - "new_channel_modal.create_board.select_template_placeholder": "Select a template", - "new_channel_modal.create_board.title": "Create a board for this channel", - "notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days", - "notification-box-card-limit-reached.contact-link": "notify your admin", - "notification-box-card-limit-reached.link": "Upgrade to a paid plan", - "notification-box-card-limit-reached.title": "{cards} cards hidden from board", - "notification-box-cards-hidden.title": "This action has hidden another card", - "notification-box.card-limit-reached.not-admin.text": "To access archived cards, you can {contactLink} to upgrade to a paid plan.", - "notification-box.card-limit-reached.text": "Card limit reached, to view older cards, {link}", - "person.add-user-to-board": "Add {username} to board", - "person.add-user-to-board-confirm-button": "Add to board", - "person.add-user-to-board-permissions": "Permissions", - "person.add-user-to-board-question": "Do you want to add {username} to the board?", - "person.add-user-to-board-warning": "{username} isn't a member of the board, and won't receive any notifications about it.", - "register.login-button": "or log in if you already have an account", - "register.signup-title": "Sign up for your account", - "rhs-board-non-admin-msg": "You're not an admin of the board", - "rhs-boards.add": "Add", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "this direct message", - "rhs-boards.header.gm": "this group message", - "rhs-boards.last-update-at": "Last update at: {datetime}", - "rhs-boards.link-boards-to-channel": "Link boards to {channelName}", - "rhs-boards.linked-boards": "Linked boards", - "rhs-boards.no-boards-linked-to-channel": "No boards are linked to {channelName} yet", - "rhs-boards.no-boards-linked-to-channel-description": "Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.", - "rhs-boards.unlink-board": "Unlink board", - "rhs-boards.unlink-board1": "Unlink board", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Publish", - "share-board.share": "Share", - "shareBoard.channels-select-group": "Channels", - "shareBoard.confirm-change-team-role.body": "Everyone on this board with a lower permission than the \"{role}\" role will now be promoted to {role}. Are you sure you want to change the minimum role for the board?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Change minimum board role", - "shareBoard.confirm-change-team-role.title": "Change minimum board role", - "shareBoard.confirm-link-channel": "Link board to channel", - "shareBoard.confirm-link-channel-button": "Link channel", - "shareBoard.confirm-link-channel-button-with-other-channel": "Unlink and link here", - "shareBoard.confirm-link-channel-subtext": "When you link a channel to a board, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "When you link a channel to a board, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.{lineBreak}This board is currently linked to another channel. It will be unlinked if you choose to link it here.", - "shareBoard.confirm-unlink.body": "When you unlink a channel from a board, all members of the channel (existing and new) will lose access to it unless they're given permission separately.", - "shareBoard.confirm-unlink.confirmBtnText": "Unlink channel", - "shareBoard.confirm-unlink.title": "Unlink channel from board", - "shareBoard.lastAdmin": "Boards must have at least one Administrator", - "shareBoard.members-select-group": "Members", - "shareBoard.unknown-channel-display-name": "Unknown channel", - "tutorial_tip.finish_tour": "Done", - "tutorial_tip.got_it": "Got it", - "tutorial_tip.ok": "Next", - "tutorial_tip.out": "Opt out of these tips.", - "tutorial_tip.seen": "Seen this before?" -} diff --git a/webapp/boards/i18n/en_AU.json b/webapp/boards/i18n/en_AU.json deleted file mode 100644 index a998a55e66..0000000000 --- a/webapp/boards/i18n/en_AU.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Admin", - "AdminBadge.TeamAdmin": "Team Admin", - "AppBar.Tooltip": "Toggle Linked Boards", - "Attachment.Attachment-title": "Attachment", - "AttachmentBlock.DeleteAction": "delete", - "AttachmentBlock.addElement": "add {type}", - "AttachmentBlock.delete": "Attachment deleted.", - "AttachmentBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.", - "AttachmentBlock.upload": "Attachment uploading.", - "AttachmentBlock.uploadSuccess": "Attachment uploaded.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Delete", - "AttachmentElement.download": "Download", - "AttachmentElement.upload-percentage": "Uploading...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Add a group", - "BoardComponent.delete": "Delete", - "BoardComponent.hidden-columns": "Hidden columns", - "BoardComponent.hide": "Hide", - "BoardComponent.new": "+ New", - "BoardComponent.no-property": "No {property}", - "BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column can't be removed.", - "BoardComponent.show": "Show", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Commenter", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "None", - "BoardMember.schemeViewer": "Viewer", - "BoardMember.unlinkChannel": "Unlink", - "BoardPage.newVersion": "A new version of Boards is available, click here to reload.", - "BoardPage.syncFailed": "Board may be deleted or access revoked.", - "BoardTemplateSelector.add-template": "Create new template", - "BoardTemplateSelector.create-empty-board": "Create an empty board", - "BoardTemplateSelector.delete-template": "Delete", - "BoardTemplateSelector.description": "Add a board to the sidebar using any of the templates defined below or start from scratch.", - "BoardTemplateSelector.edit-template": "Edit", - "BoardTemplateSelector.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.", - "BoardTemplateSelector.plugin.no-content-title": "Create a board", - "BoardTemplateSelector.title": "Create a board", - "BoardTemplateSelector.use-this-template": "Use this template", - "BoardsSwitcher.Title": "Find boards", - "BoardsUnfurl.Limited": "Additional details are hidden due to the card being archived", - "BoardsUnfurl.Remainder": "+{remainder} more", - "BoardsUnfurl.Updated": "Updated {time}", - "Calculations.Options.average.displayName": "Average", - "Calculations.Options.average.label": "Average", - "Calculations.Options.count.displayName": "Count", - "Calculations.Options.count.label": "Count", - "Calculations.Options.countChecked.displayName": "Checked", - "Calculations.Options.countChecked.label": "Count checked", - "Calculations.Options.countUnchecked.displayName": "Unchecked", - "Calculations.Options.countUnchecked.label": "Count unchecked", - "Calculations.Options.countUniqueValue.displayName": "Unique", - "Calculations.Options.countUniqueValue.label": "Count unique values", - "Calculations.Options.countValue.displayName": "Values", - "Calculations.Options.countValue.label": "Count value", - "Calculations.Options.dateRange.displayName": "Range", - "Calculations.Options.dateRange.label": "Range", - "Calculations.Options.earliest.displayName": "Earliest", - "Calculations.Options.earliest.label": "Earliest", - "Calculations.Options.latest.displayName": "Latest", - "Calculations.Options.latest.label": "Latest", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Calculate", - "Calculations.Options.none.label": "None", - "Calculations.Options.percentChecked.displayName": "Checked", - "Calculations.Options.percentChecked.label": "Percent checked", - "Calculations.Options.percentUnchecked.displayName": "Unchecked", - "Calculations.Options.percentUnchecked.label": "Percent unchecked", - "Calculations.Options.range.displayName": "Range", - "Calculations.Options.range.label": "Range", - "Calculations.Options.sum.displayName": "Sum", - "Calculations.Options.sum.label": "Sum", - "CalendarCard.untitled": "Untitled", - "CardActionsMenu.copiedLink": "Copied", - "CardActionsMenu.copyLink": "Copy link", - "CardActionsMenu.delete": "Delete", - "CardActionsMenu.duplicate": "Duplicate", - "CardBadges.title-checkboxes": "Checkboxes", - "CardBadges.title-comments": "Comments", - "CardBadges.title-description": "This card has a description", - "CardDetail.Attach": "Attach", - "CardDetail.Follow": "Follow", - "CardDetail.Following": "Following", - "CardDetail.add-content": "Add content", - "CardDetail.add-icon": "Add icon", - "CardDetail.add-property": "+ Add a property", - "CardDetail.addCardText": "add card text", - "CardDetail.limited-body": "Upgrade to the Professional or Enterprise plan.", - "CardDetail.limited-button": "Upgrade", - "CardDetail.limited-title": "This card is hidden", - "CardDetail.moveContent": "Move card content", - "CardDetail.new-comment-placeholder": "Add a comment", - "CardDetailProperty.confirm-delete-heading": "Confirm property deletion", - "CardDetailProperty.confirm-delete-subtext": "Are you sure you want to delete the property '{propertyName}'? This will remove the property from all cards in this board.", - "CardDetailProperty.confirm-property-name-change-subtext": "Are you sure you want to change property '{propertyName}' {customText}? This will affect value(s) across {numOfCards} card(s) in this board, and may result in loss of data.", - "CardDetailProperty.confirm-property-type-change": "Confirm change of property type", - "CardDetailProperty.delete-action-button": "Delete", - "CardDetailProperty.property-change-action-button": "Change property", - "CardDetailProperty.property-changed": "Property changed successfully!", - "CardDetailProperty.property-deleted": "{propertyName} deleted successfully!", - "CardDetailProperty.property-name-change-subtext": "type from '{oldPropType}' to '{newPropType}'", - "CardDetial.limited-link": "Learn more about our plans.", - "CardDialog.delete-confirmation-dialog-attachment": "Confirm attachment deletion", - "CardDialog.delete-confirmation-dialog-button-text": "Delete", - "CardDialog.delete-confirmation-dialog-heading": "Confirm card deletion", - "CardDialog.editing-template": "You're editing a template.", - "CardDialog.nocard": "This card doesn't exist or is inaccessible.", - "Categories.CreateCategoryDialog.CancelText": "Cancel", - "Categories.CreateCategoryDialog.CreateText": "Create", - "Categories.CreateCategoryDialog.Placeholder": "Name your category", - "Categories.CreateCategoryDialog.UpdateText": "Update", - "CenterPanel.Login": "Login", - "CenterPanel.Share": "Share", - "ChannelIntro.CreateBoard": "Create a board", - "ColorOption.selectColor": "Select {color} Colour", - "Comment.delete": "Delete", - "CommentsList.send": "Send", - "ConfirmPerson.empty": "Empty", - "ConfirmPerson.search": "Search...", - "ConfirmationDialog.cancel-action": "Cancel", - "ConfirmationDialog.confirm-action": "Confirm", - "ContentBlock.Delete": "Delete", - "ContentBlock.DeleteAction": "delete", - "ContentBlock.addElement": "add {type}", - "ContentBlock.checkbox": "checkbox", - "ContentBlock.divider": "divider", - "ContentBlock.editCardCheckbox": "toggled-checkbox", - "ContentBlock.editCardCheckboxText": "edit card text", - "ContentBlock.editCardText": "edit card text", - "ContentBlock.editText": "Edit text", - "ContentBlock.image": "image", - "ContentBlock.insertAbove": "Insert above", - "ContentBlock.moveBlock": "move card content", - "ContentBlock.moveDown": "Move down", - "ContentBlock.moveUp": "Move up", - "ContentBlock.text": "text", - "DateFilter.empty": "Empty", - "DateRange.clear": "Clear", - "DateRange.empty": "Empty", - "DateRange.endDate": "End date", - "DateRange.today": "Today", - "DeleteBoardDialog.confirm-cancel": "Cancel", - "DeleteBoardDialog.confirm-delete": "Delete", - "DeleteBoardDialog.confirm-info": "Are you sure you want to delete the board '{boardTitle}'? This will remove all cards in the board.", - "DeleteBoardDialog.confirm-info-template": "Are you sure you want to delete the board template ‘{boardTitle}’?", - "DeleteBoardDialog.confirm-tite": "Confirm board deletion", - "DeleteBoardDialog.confirm-tite-template": "Confirm deletion of board template", - "Dialog.closeDialog": "Close dialog", - "EditableDayPicker.today": "Today", - "Error.mobileweb": "Mobile web support is currently in early beta. Not all functionality may be present.", - "Error.websocket-closed": "Websocket connection closed, connection interrupted. If this persists, check your server or web proxy configuration.", - "Filter.contains": "contains", - "Filter.ends-with": "ends with", - "Filter.includes": "includes", - "Filter.is": "is", - "Filter.is-after": "is after", - "Filter.is-before": "is before", - "Filter.is-empty": "is empty", - "Filter.is-not-empty": "is not empty", - "Filter.is-not-set": "is not set", - "Filter.is-set": "is set", - "Filter.isafter": "is after", - "Filter.isbefore": "is before", - "Filter.not-contains": "does not contain", - "Filter.not-ends-with": "does not end with", - "Filter.not-includes": "doesn't include", - "Filter.not-starts-with": "does not start with", - "Filter.starts-with": "starts with", - "FilterByText.placeholder": "filter text", - "FilterComponent.add-filter": "+ Add filter", - "FilterComponent.delete": "Delete", - "FilterValue.empty": "(empty)", - "FindBoardsDialog.IntroText": "Search for boards", - "FindBoardsDialog.NoResultsFor": "No results for '\\{searchQuery}'\\", - "FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.", - "FindBoardsDialog.SubTitle": "Type to find a board. Use UP/DOWN to browse. ENTER to select, ESC to dismiss", - "FindBoardsDialog.Title": "Find Boards", - "GroupBy.hideEmptyGroups": "Hide {count} empty groups", - "GroupBy.showHiddenGroups": "Show {count} hidden groups", - "GroupBy.ungroup": "Ungroup", - "HideBoard.MenuOption": "Hide board", - "KanbanCard.untitled": "Untitled", - "MentionSuggestion.is-not-board-member": "(not board member)", - "Mutator.new-board-from-template": "new board from template", - "Mutator.new-card-from-template": "new card from template", - "Mutator.new-template-from-card": "new template from card", - "OnboardingTour.AddComments.Body": "You can comment on issues, and even @mention your fellow Mattermost users to get their attention.", - "OnboardingTour.AddComments.Title": "Add comments", - "OnboardingTour.AddDescription.Body": "Add a description to your card so your teammates know what the card is about.", - "OnboardingTour.AddDescription.Title": "Add description", - "OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful.", - "OnboardingTour.AddProperties.Title": "Add properties", - "OnboardingTour.AddView.Body": "Go here to create a new view to organise your board using different layouts.", - "OnboardingTour.AddView.Title": "Add a new view", - "OnboardingTour.CopyLink.Body": "You can share your cards with teammates by copying the link and pasting it in a channel, direct message or group message.", - "OnboardingTour.CopyLink.Title": "Copy link", - "OnboardingTour.OpenACard.Body": "Open a card to explore the powerful ways that Boards can help you organise your work.", - "OnboardingTour.OpenACard.Title": "Open a card", - "OnboardingTour.ShareBoard.Body": "You can share your board internally, within your team or publish it publicly for visibility outside of your organisation.", - "OnboardingTour.ShareBoard.Title": "Share board", - "PersonProperty.board-members": "Board members", - "PersonProperty.me": "Me", - "PersonProperty.non-board-members": "Not board members", - "PropertyMenu.Delete": "Delete", - "PropertyMenu.changeType": "Change property type", - "PropertyMenu.selectType": "Select property type", - "PropertyMenu.typeTitle": "Type", - "PropertyType.Checkbox": "Checkbox", - "PropertyType.CreatedBy": "Created by", - "PropertyType.CreatedTime": "Time created", - "PropertyType.Date": "Date", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Multi person", - "PropertyType.MultiSelect": "Multi select", - "PropertyType.Number": "Number", - "PropertyType.Person": "Person", - "PropertyType.Phone": "Phone", - "PropertyType.Select": "Select", - "PropertyType.Text": "Text", - "PropertyType.Unknown": "Unknown", - "PropertyType.UpdatedBy": "Last updated by", - "PropertyType.UpdatedTime": "Time last updated", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Empty", - "RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", - "RegistrationLink.copiedLink": "Copied!", - "RegistrationLink.copyLink": "Copy link", - "RegistrationLink.description": "Share this link for others to create accounts:", - "RegistrationLink.regenerateToken": "Regenerate token", - "RegistrationLink.tokenRegenerated": "Registration link regenerated", - "ShareBoard.PublishDescription": "Publish and share a read-only link with everyone on the web.", - "ShareBoard.PublishTitle": "Publish to the web", - "ShareBoard.ShareInternal": "Share internally", - "ShareBoard.ShareInternalDescription": "Users who have permissions will be able to use this link.", - "ShareBoard.Title": "Share Board", - "ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", - "ShareBoard.copiedLink": "Copied!", - "ShareBoard.copyLink": "Copy link", - "ShareBoard.regenerate": "Regenerate token", - "ShareBoard.searchPlaceholder": "Search for people and channels", - "ShareBoard.teamPermissionsText": "Everyone at {teamName} team", - "ShareBoard.tokenRegenrated": "Token regenerated", - "ShareBoard.userPermissionsRemoveMemberText": "Remove member", - "ShareBoard.userPermissionsYouText": "(You)", - "ShareTemplate.Title": "Share template", - "ShareTemplate.searchPlaceholder": "Search for people", - "Sidebar.about": "About Focalboard", - "Sidebar.add-board": "+ Add board", - "Sidebar.changePassword": "Change password", - "Sidebar.delete-board": "Delete board", - "Sidebar.duplicate-board": "Duplicate board", - "Sidebar.export-archive": "Export archive", - "Sidebar.import": "Import", - "Sidebar.import-archive": "Import archive", - "Sidebar.invite-users": "Invite users", - "Sidebar.logout": "Log out", - "Sidebar.new-category.badge": "New", - "Sidebar.new-category.drag-boards-cta": "Drag boards here...", - "Sidebar.no-boards-in-category": "No boards inside", - "Sidebar.product-tour": "Product tour", - "Sidebar.random-icons": "Random icons", - "Sidebar.set-language": "Set language", - "Sidebar.set-theme": "Set theme", - "Sidebar.settings": "Settings", - "Sidebar.template-from-board": "New template from board", - "Sidebar.untitled-board": "(Untitled Board)", - "Sidebar.untitled-view": "(Untitled View)", - "SidebarCategories.BlocksMenu.Move": "Move To...", - "SidebarCategories.CategoryMenu.CreateNew": "Create New Category", - "SidebarCategories.CategoryMenu.Delete": "Delete Category", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in {categoryName} will move back to the Boards categories. You're not removed from any boards.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?", - "SidebarCategories.CategoryMenu.Update": "Rename Category", - "SidebarTour.ManageCategories.Body": "Create and manage custom categories. Categories are user-specific, so moving a board to your category won’t impact other members using the same board.", - "SidebarTour.ManageCategories.Title": "Manage categories", - "SidebarTour.SearchForBoards.Body": "Open the board switcher (Cmd/Ctrl + K) to quickly search and add boards to your sidebar.", - "SidebarTour.SearchForBoards.Title": "Search for boards", - "SidebarTour.SidebarCategories.Body": "All your boards are now organised under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.", - "SidebarTour.SidebarCategories.Link": "Learn more", - "SidebarTour.SidebarCategories.Title": "Sidebar categories", - "SiteStats.total_boards": "Total boards", - "SiteStats.total_cards": "Total cards", - "TableComponent.add-icon": "Add icon", - "TableComponent.name": "Name", - "TableComponent.plus-new": "+ New", - "TableHeaderMenu.delete": "Delete", - "TableHeaderMenu.duplicate": "Duplicate", - "TableHeaderMenu.hide": "Hide", - "TableHeaderMenu.insert-left": "Insert left", - "TableHeaderMenu.insert-right": "Insert right", - "TableHeaderMenu.sort-ascending": "Sort ascending", - "TableHeaderMenu.sort-descending": "Sort descending", - "TableRow.DuplicateCard": "duplicate card", - "TableRow.MoreOption": "More actions", - "TableRow.open": "Open", - "TopBar.give-feedback": "Give feedback", - "URLProperty.copiedLink": "Copied", - "URLProperty.copy": "Copy", - "URLProperty.edit": "Edit", - "UndoRedoHotKeys.canRedo": "Redo", - "UndoRedoHotKeys.canRedo-with-description": "Redo {description}", - "UndoRedoHotKeys.canUndo": "Undo", - "UndoRedoHotKeys.canUndo-with-description": "Undo {description}", - "UndoRedoHotKeys.cannotRedo": "Nothing to Redo", - "UndoRedoHotKeys.cannotUndo": "Nothing to Undo", - "ValueSelector.noOptions": "No options. Start typing to add the first one!", - "ValueSelector.valueSelector": "Value selector", - "ValueSelectorLabel.openMenu": "Open menu", - "VersionMessage.help": "Check out what's new in this version.", - "VersionMessage.learn-more": "Learn more", - "View.AddView": "Add view", - "View.Board": "Board", - "View.DeleteView": "Delete view", - "View.DuplicateView": "Duplicate view", - "View.Gallery": "Gallery", - "View.NewBoardTitle": "Board view", - "View.NewCalendarTitle": "Calendar view", - "View.NewGalleryTitle": "Gallery view", - "View.NewTableTitle": "Table view", - "View.NewTemplateDefaultTitle": "Untitled Template", - "View.NewTemplateTitle": "Untitled", - "View.Table": "Table", - "ViewHeader.add-template": "New template", - "ViewHeader.delete-template": "Delete", - "ViewHeader.display-by": "Display by: {property}", - "ViewHeader.edit-template": "Edit", - "ViewHeader.empty-card": "Empty card", - "ViewHeader.export-board-archive": "Export board archive", - "ViewHeader.export-complete": "Export complete!", - "ViewHeader.export-csv": "Export to CSV", - "ViewHeader.export-failed": "Export failed", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Group by: {property}", - "ViewHeader.new": "New", - "ViewHeader.properties": "Properties", - "ViewHeader.properties-menu": "Properties menu", - "ViewHeader.search-text": "Search cards", - "ViewHeader.select-a-template": "Select a template", - "ViewHeader.set-default-template": "Set as default", - "ViewHeader.sort": "Sort", - "ViewHeader.untitled": "Untitled", - "ViewHeader.view-header-menu": "View header menu", - "ViewHeader.view-menu": "View menu", - "ViewLimitDialog.Heading": "Views per board limit reached", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Upgrade", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notify Admin", - "ViewLimitDialog.Subtext.Admin": "Upgrade to the Professional or Enterprise plan.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Learn more about our plans.", - "ViewLimitDialog.Subtext.RegularUser": "Ask your Admin to upgrade to the Professional or Enterprise plan.", - "ViewLimitDialog.UpgradeImg.AltText": "upgrade image", - "ViewLimitDialog.notifyAdmin.Success": "Your admin has been contacted", - "ViewTitle.hide-description": "hide description", - "ViewTitle.pick-icon": "Pick icon", - "ViewTitle.random-icon": "Random", - "ViewTitle.remove-icon": "Remove icon", - "ViewTitle.show-description": "show description", - "ViewTitle.untitled-board": "Untitled board", - "WelcomePage.Description": "Boards is a project management tool that helps define, organise, track and manage work across teams using a familiar Kanban board view.", - "WelcomePage.Explore.Button": "Take a tour", - "WelcomePage.Heading": "Welcome To Boards", - "WelcomePage.NoThanks.Text": "No thanks, I'll figure it out myself", - "WelcomePage.StartUsingIt.Text": "Start using it", - "Workspace.editing-board-template": "You're editing a board template.", - "badge.guest": "Guest", - "boardPage.confirm-join-button": "Join", - "boardPage.confirm-join-text": "You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?", - "boardPage.confirm-join-title": "Join private board", - "boardSelector.confirm-link-board": "Link board to channel", - "boardSelector.confirm-link-board-button": "Link board", - "boardSelector.confirm-link-board-subtext": "When you link '\\{boardName}'\\ to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests. You can unlink a board from a channel at any time.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "When you link '\\{boardName}'\\ to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.{lineBreak} This board is currently linked to another channel. It will be unlinked if you choose to link it here.", - "boardSelector.create-a-board": "Create a board", - "boardSelector.link": "Link", - "boardSelector.search-for-boards": "Search for boards", - "boardSelector.title": "Link boards", - "boardSelector.unlink": "Unlink", - "calendar.month": "Month", - "calendar.today": "TODAY", - "calendar.week": "Week", - "centerPanel.undefined": "No {propertyName}", - "centerPanel.unknown-user": "Unknown user", - "cloudMessage.learn-more": "Learn more", - "createImageBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.", - "default-properties.badges": "Comments and description", - "default-properties.title": "Title", - "error.back-to-home": "Back to home", - "error.back-to-team": "Back to team", - "error.board-not-found": "Board not found.", - "error.go-login": "Log in", - "error.invalid-read-only-board": "You don't have access to this board. Log in to access Boards.", - "error.not-logged-in": "Your session may have expired or you're not logged in. Log in again to access Boards.", - "error.page.title": "An error occurred", - "error.team-undefined": "Invalid team.", - "error.unknown": "An error occurred.", - "generic.previous": "Previous", - "guest-no-board.subtitle": "You don't have access to any board in this team yet, please wait until somebody adds you to any board.", - "guest-no-board.title": "No boards yet", - "imagePaste.upload-failed": "Some files weren't uploaded because the file size limit has been reached.", - "limitedCard.title": "Cards hidden", - "login.log-in-button": "Log in", - "login.log-in-title": "Log in", - "login.register-button": "or create an account if you don't have one", - "new_channel_modal.create_board.empty_board_description": "Create a new empty board", - "new_channel_modal.create_board.empty_board_title": "Empty board", - "new_channel_modal.create_board.select_template_placeholder": "Select a template", - "new_channel_modal.create_board.title": "Create a board for this channel", - "notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days", - "notification-box-card-limit-reached.contact-link": "Contact your adminstrator", - "notification-box-card-limit-reached.link": "Upgrade to a paid plan", - "notification-box-card-limit-reached.title": "{cards} cards hidden from board", - "notification-box-cards-hidden.title": "This action has hidden another card", - "notification-box.card-limit-reached.not-admin.text": "To access archived cards, you can {contactLink} to upgrade to a paid plan.", - "notification-box.card-limit-reached.text": "Card limit reached, to view older cards, {link}", - "person.add-user-to-board": "Add {username} to board", - "person.add-user-to-board-confirm-button": "Add to board", - "person.add-user-to-board-permissions": "Permissions", - "person.add-user-to-board-question": "Do you want to add {username} to the board?", - "person.add-user-to-board-warning": "{username} isn't a member of the board and won't receive any notifications for it.", - "register.login-button": "or log in if you already have an account", - "register.signup-title": "Sign up for your account", - "rhs-board-non-admin-msg": "You're not an admin of the board", - "rhs-boards.add": "Add", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "this direct message", - "rhs-boards.header.gm": "this group message", - "rhs-boards.last-update-at": "Last update at: {datetime}", - "rhs-boards.link-boards-to-channel": "Link boards to {channelName}", - "rhs-boards.linked-boards": "Linked boards", - "rhs-boards.no-boards-linked-to-channel": "No boards are linked to {channelName} yet", - "rhs-boards.no-boards-linked-to-channel-description": "Boards is a project management tool that helps define, organise, track and manage work across teams using a familiar kanban board view.", - "rhs-boards.unlink-board": "Unlink board", - "rhs-boards.unlink-board1": "Unlink board", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Publish", - "share-board.share": "Share", - "shareBoard.channels-select-group": "Channels", - "shareBoard.confirm-change-team-role.body": "Everyone on this board with a lower permission than the '\\{role}'\\ role will now be promoted to {role}. Are you sure you want to change the minimum role for the board?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Change minimum board role", - "shareBoard.confirm-change-team-role.title": "Change minimum board role", - "shareBoard.confirm-link-channel": "Link board to channel", - "shareBoard.confirm-link-channel-button": "Link channel", - "shareBoard.confirm-link-channel-button-with-other-channel": "Unlink and link here", - "shareBoard.confirm-link-channel-subtext": "When you link a channel to a board, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "When you link a channel to a board, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests.{lineBreak}This board is currently linked to another channel. It will be unlinked if you choose to link it here.", - "shareBoard.confirm-unlink.body": "When you unlink a channel from a board, all members of the channel (existing and new) will lose access to it unless they're given permission separately.", - "shareBoard.confirm-unlink.confirmBtnText": "Unlink channel", - "shareBoard.confirm-unlink.title": "Unlink channel from board", - "shareBoard.lastAdmin": "Boards must have at least one Administrator", - "shareBoard.members-select-group": "Members", - "shareBoard.unknown-channel-display-name": "Unknown channel", - "tutorial_tip.finish_tour": "Done", - "tutorial_tip.got_it": "Got it", - "tutorial_tip.ok": "Next", - "tutorial_tip.out": "Opt out of these tips.", - "tutorial_tip.seen": "Have you seen this before?" -} diff --git a/webapp/boards/i18n/es.json b/webapp/boards/i18n/es.json deleted file mode 100644 index 9de79aac92..0000000000 --- a/webapp/boards/i18n/es.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "AppBar.Tooltip": "Alternar tableros vinculados", - "Attachment.Attachment-title": "Archivos adjuntos", - "AttachmentBlock.DeleteAction": "borrar", - "AttachmentBlock.addElement": "agregar {type}", - "AttachmentBlock.delete": "Archivo adjunto eliminado.", - "AttachmentBlock.failed": "Este archivo no puede subirse debido a que excede el límite de tamaño de archivo.", - "AttachmentBlock.upload": "Subiendo archivo adjunto.", - "AttachmentBlock.uploadSuccess": "Archivo adjunto subido.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Borrar", - "AttachmentElement.download": "Descargar", - "AttachmentElement.upload-percentage": "Subiendo...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Añadir un grupo", - "BoardComponent.delete": "Borrar", - "BoardComponent.hidden-columns": "Columnas Ocultas", - "BoardComponent.hide": "Ocultar", - "BoardComponent.new": "+ Nuevo", - "BoardComponent.no-property": "Sin {property}", - "BoardComponent.no-property-title": "Elementos sin la propiedad {property} irán aquí. Esta columna no se puede eliminar.", - "BoardComponent.show": "Mostrar", - "BoardMember.schemeAdmin": "Administrador", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "Ninguno", - "BoardMember.schemeViewer": "Visualizador", - "BoardMember.unlinkChannel": "Desvincular", - "BoardPage.newVersion": "Una nueva versión de Boards está disponible, haz clic aquí para recargar.", - "BoardPage.syncFailed": "El tablero puede haber sido eliminado o el acceso revocado.", - "BoardTemplateSelector.add-template": "Crear nueva plantilla", - "BoardTemplateSelector.create-empty-board": "Crear un tablero vacío", - "BoardTemplateSelector.delete-template": "Eliminar", - "BoardTemplateSelector.description": "Agregar un tablero a la barra lateral usando alguna de las plantillas definidas a continuación o empezar desde cero.", - "BoardTemplateSelector.edit-template": "Editar", - "BoardTemplateSelector.plugin.no-content-description": "Agregar un tablero a la barra lateral usando alguna de las plantillas definidas a continuación o empezar desde cero.", - "BoardTemplateSelector.plugin.no-content-title": "Crear un tablero", - "BoardTemplateSelector.title": "Crear un tablero", - "BoardTemplateSelector.use-this-template": "Utiliza esta plantilla", - "BoardsSwitcher.Title": "Encontrar tableros", - "BoardsUnfurl.Limited": "Los detalles adicionales están ocultos debido a que la tarjeta ha sido archivada", - "BoardsUnfurl.Updated": "Actualizado {time}", - "Calculations.Options.average.displayName": "Promedio", - "Calculations.Options.average.label": "Promedio", - "Calculations.Options.count.displayName": "Contar", - "Calculations.Options.count.label": "Contar", - "Calculations.Options.countChecked.displayName": "Marcado", - "Calculations.Options.countChecked.label": "Contar marcados", - "Calculations.Options.countUnchecked.displayName": "Deseleccionado", - "Calculations.Options.countUnchecked.label": "Contar no marcados", - "Calculations.Options.countUniqueValue.displayName": "Único", - "Calculations.Options.countUniqueValue.label": "Contar valores únicos", - "Calculations.Options.countValue.displayName": "Valores", - "Calculations.Options.dateRange.displayName": "Rango", - "Calculations.Options.dateRange.label": "Rango", - "Calculations.Options.earliest.displayName": "Más antiguo", - "Calculations.Options.earliest.label": "Más antiguo", - "Calculations.Options.latest.displayName": "Último", - "Calculations.Options.latest.label": "Último", - "Calculations.Options.max.displayName": "Máx", - "Calculations.Options.max.label": "Máx", - "Calculations.Options.median.displayName": "Mediana", - "Calculations.Options.median.label": "Mediana", - "Calculations.Options.min.displayName": "Mín", - "Calculations.Options.min.label": "Mín", - "Calculations.Options.none.displayName": "Calcular", - "Calculations.Options.none.label": "Ninguna", - "Calculations.Options.percentChecked.displayName": "Marcado", - "Calculations.Options.percentChecked.label": "Porcentaje marcado", - "Calculations.Options.percentUnchecked.displayName": "Desmarcado", - "Calculations.Options.percentUnchecked.label": "Porcentaje desmarcado", - "Calculations.Options.range.displayName": "Rango", - "Calculations.Options.range.label": "Rango", - "Calculations.Options.sum.displayName": "Suma", - "Calculations.Options.sum.label": "Suma", - "CalendarCard.untitled": "Sin título", - "CardActionsMenu.copiedLink": "¡Copiado!", - "CardActionsMenu.copyLink": "Copiar hipervínculo", - "CardActionsMenu.delete": "Eliminar", - "CardActionsMenu.duplicate": "Duplicar", - "CardBadges.title-checkboxes": "Casillas de verificación", - "CardBadges.title-comments": "Comentarios", - "CardBadges.title-description": "Esta tarjeta tiene una descripción", - "CardDetail.Attach": "Adjuntar", - "CardDetail.Follow": "Seguir", - "CardDetail.Following": "Siguiendo", - "CardDetail.add-content": "Añadir contenido", - "CardDetail.add-icon": "Añadir icono", - "CardDetail.add-property": "+ Añadir propiedad", - "CardDetail.addCardText": "agregar texto a la tarjeta", - "CardDetail.limited-body": "Mejorar a nuestro plan Professional o Enterprise.", - "CardDetail.limited-button": "Mejorar", - "CardDetail.limited-title": "Esta tarjeta está oculta", - "CardDetail.moveContent": "Mover contenido de la tarjeta", - "CardDetail.new-comment-placeholder": "Añadir un comentario...", - "CardDetailProperty.confirm-delete-heading": "Confirmar eliminación de la propiedad", - "CardDetailProperty.confirm-delete-subtext": "¿Estás seguro de que quieres eliminar la propiedad \"{propertyName}\"? Al eliminarla también se removerá la propiedad en todas las tarjetas de este tablero.", - "CardDetailProperty.confirm-property-name-change-subtext": "¿Estás seguro de que quieres cambiar la propiedad \"{propertyName}\" {customText}? Esto puede afectar a los valores en {numOfCards} tarjeta(s) en este tablero, lo que puede resultar en una pérdida de datos.", - "CardDetailProperty.confirm-property-type-change": "Confirmar cambio de tipo de la propiedad", - "CardDetailProperty.delete-action-button": "Eliminar", - "CardDetailProperty.property-change-action-button": "Modificar propiedad", - "CardDetailProperty.property-changed": "¡Propiedad modificada exitosamente!", - "CardDetailProperty.property-deleted": "¡La propiedad {propertyName} ha sido eliminada exitosamente!", - "CardDetial.limited-link": "Aprende más sobre nuestros planes.", - "CardDialog.delete-confirmation-dialog-attachment": "Confirmar eliminación del archivo adjunto", - "CardDialog.delete-confirmation-dialog-button-text": "Eliminar", - "CardDialog.delete-confirmation-dialog-heading": "Confirmar eliminación de la tarjeta", - "CardDialog.editing-template": "Estás editando una plantilla.", - "CardDialog.nocard": "Esta tarjeta no existe o es inaccesible.", - "Categories.CreateCategoryDialog.CancelText": "Cancelar", - "Categories.CreateCategoryDialog.CreateText": "Crear", - "Categories.CreateCategoryDialog.Placeholder": "Pon nombre a la categoría", - "Categories.CreateCategoryDialog.UpdateText": "Actualizar", - "CenterPanel.Login": "Ingresar", - "CenterPanel.Share": "Compartir", - "ChannelIntro.CreateBoard": "Crear un tablero", - "ColorOption.selectColor": "Seleccionar {color} Color", - "Comment.delete": "Borrar", - "CommentsList.send": "Enviar", - "ContentBlock.Delete": "Borrar", - "ContentBlock.DeleteAction": "borrar", - "ContentBlock.addElement": "añadir {type}", - "ContentBlock.checkbox": "casilla de selección", - "ContentBlock.divider": "divisor", - "ContentBlock.editCardCheckbox": "casilla de verificación conmutada", - "ContentBlock.editCardCheckboxText": "editar texto de la tarjeta", - "ContentBlock.editCardText": "editar texto de la tarjeta", - "ContentBlock.editText": "Editar texto...", - "ContentBlock.image": "imagen", - "ContentBlock.insertAbove": "Insertar encima", - "ContentBlock.moveDown": "Mover hacia abajo", - "ContentBlock.moveUp": "Mover hacia arriba", - "ContentBlock.text": "texto", - "Dialog.closeDialog": "Cerrar diálogo", - "EditableDayPicker.today": "Hoy", - "Error.websocket-closed": "Conexión de Websocket cerrada, conexión interrumpida. Si esto persiste, verifique la configuración de su servidor o proxy web.", - "Filter.includes": "incluye", - "Filter.is-empty": "está vacío", - "Filter.is-not-empty": "no está vacío", - "Filter.not-includes": "no incluye", - "FilterComponent.add-filter": "+ Añadir filtro", - "FilterComponent.delete": "Borrar", - "GroupBy.ungroup": "Desagrupar", - "KanbanCard.untitled": "Sin título", - "Mutator.new-card-from-template": "nueva tarjeta desde una plantilla", - "Mutator.new-template-from-card": "nueva plantilla desde una tarjeta", - "PropertyMenu.Delete": "Borrar", - "PropertyMenu.changeType": "Cambiar el tipo de propiedad", - "PropertyMenu.typeTitle": "Tipo", - "PropertyType.Checkbox": "Casilla de selección", - "PropertyType.CreatedBy": "Creado por", - "PropertyType.CreatedTime": "Hora de creación", - "PropertyType.Date": "Fecha", - "PropertyType.Email": "Email", - "PropertyType.MultiSelect": "Selección Múltiple", - "PropertyType.Number": "Número", - "PropertyType.Person": "Persona", - "PropertyType.Phone": "Teléfono", - "PropertyType.Select": "Selector", - "PropertyType.Text": "Texto", - "PropertyType.UpdatedBy": "Última actualización por", - "PropertyType.UpdatedTime": "Hora de última actualización", - "RegistrationLink.confirmRegenerateToken": "Esto invalidará los enlaces compartidos previos. ¿Continuar?", - "RegistrationLink.copiedLink": "¡Copiado!", - "RegistrationLink.copyLink": "Copiar enlace", - "RegistrationLink.description": "Comparte este enlace para que otros se creen sus cuentas:", - "RegistrationLink.regenerateToken": "Regenerar token", - "RegistrationLink.tokenRegenerated": "Enlace de registro regenerado", - "ShareBoard.confirmRegenerateToken": "Esto invalidará los enlaces compartidos previos. ¿Continuar?", - "ShareBoard.copiedLink": "¡Copiado!", - "ShareBoard.copyLink": "Copiar enlace", - "ShareBoard.tokenRegenrated": "Token regenerado", - "Sidebar.about": "Sobre Focalboard", - "Sidebar.add-board": "+ Añadir panel", - "Sidebar.changePassword": "Cambiar contraseña", - "Sidebar.delete-board": "Borrar Panel", - "Sidebar.export-archive": "Exportar Archivo", - "Sidebar.import-archive": "Importar Archivo", - "Sidebar.invite-users": "Invitar usuarios", - "Sidebar.logout": "Cerrar sesión", - "Sidebar.random-icons": "Íconos random", - "Sidebar.set-language": "Establecer idioma", - "Sidebar.set-theme": "Establecer apariencia", - "Sidebar.settings": "Configuración", - "Sidebar.untitled-board": "(Panel sin titulo)", - "TableComponent.add-icon": "Añadir Icono", - "TableComponent.name": "Nombre", - "TableComponent.plus-new": "+ Nueva", - "TableHeaderMenu.delete": "Borrar", - "TableHeaderMenu.duplicate": "Duplicar", - "TableHeaderMenu.hide": "Ocultar", - "TableHeaderMenu.insert-left": "Insertar a la izquierda", - "TableHeaderMenu.insert-right": "Insertar a la derecha", - "TableHeaderMenu.sort-ascending": "Orden ascendente", - "TableHeaderMenu.sort-descending": "Orden descendente", - "TableRow.open": "Abrir", - "TopBar.give-feedback": "Dar feedback", - "ValueSelector.valueSelector": "Valorar el selector", - "ValueSelectorLabel.openMenu": "Abrir menú", - "View.AddView": "Añadir vista", - "View.Board": "Panel", - "View.DeleteView": "Eliminar vista", - "View.DuplicateView": "Duplicar vista", - "View.NewBoardTitle": "Vista de panel", - "View.NewGalleryTitle": "Vista de galería", - "View.NewTableTitle": "Vista de tabla", - "View.Table": "Tabla", - "ViewHeader.add-template": "+ Nueva plantilla", - "ViewHeader.delete-template": "Borrar", - "ViewHeader.edit-template": "Editar", - "ViewHeader.empty-card": "Tarjeta vacía", - "ViewHeader.export-board-archive": "Exportar archivo de tablero", - "ViewHeader.export-complete": "¡Se ha completado la exportación!", - "ViewHeader.export-csv": "Exportar a CSV", - "ViewHeader.export-failed": "¡Ha fallado la exportación!", - "ViewHeader.filter": "Filtrar", - "ViewHeader.group-by": "Agrupar por: {property}", - "ViewHeader.new": "Nueva", - "ViewHeader.properties": "Propiedades", - "ViewHeader.search-text": "Texto de búsqueda", - "ViewHeader.select-a-template": "Seleccionar una plantilla", - "ViewHeader.sort": "Ordenar", - "ViewHeader.untitled": "Sin título", - "ViewTitle.hide-description": "ocultar descripción", - "ViewTitle.pick-icon": "Escoger Icono", - "ViewTitle.random-icon": "Aleatorio", - "ViewTitle.remove-icon": "Quitar Icono", - "ViewTitle.show-description": "mostrar descripción", - "ViewTitle.untitled-board": "Panel sin título", - "default-properties.title": "Título" -} diff --git a/webapp/boards/i18n/et.json b/webapp/boards/i18n/et.json deleted file mode 100644 index 67eac23028..0000000000 --- a/webapp/boards/i18n/et.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Lisa grupp", - "BoardComponent.delete": "Kustuta", - "BoardComponent.hidden-columns": "Peidetud veerud", - "BoardComponent.hide": "Peida", - "BoardComponent.new": "+ Uus", - "BoardComponent.no-property": "{property} pole", - "BoardComponent.show": "Näita", - "BoardsUnfurl.Updated": "Uuendatud {time}", - "Calculations.Options.average.displayName": "Keskmine", - "Calculations.Options.average.label": "Keskmine", - "Calculations.Options.count.displayName": "Arv", - "Calculations.Options.count.label": "Arv", - "Calculations.Options.countUniqueValue.displayName": "Unikaalne", - "Calculations.Options.countValue.displayName": "Väärtused", - "Calculations.Options.dateRange.displayName": "Vahemik", - "Calculations.Options.dateRange.label": "Vahemik", - "Calculations.Options.earliest.displayName": "Varaseim", - "Calculations.Options.earliest.label": "Varaseim", - "Calculations.Options.latest.displayName": "Viimased", - "Calculations.Options.latest.label": "Viimased", - "Calculations.Options.max.displayName": "Maks", - "Calculations.Options.max.label": "Maks", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Arvuta", - "Calculations.Options.none.label": "Pole", - "Calculations.Options.range.displayName": "Vahemik", - "Calculations.Options.range.label": "Vahemik", - "Calculations.Options.sum.displayName": "Sum", - "Calculations.Options.sum.label": "Sum", - "CardDetail.Follow": "Jälgi", - "CardDetail.Following": "Jälgimisel", - "CardDetail.add-content": "Lisa sisu", - "CardDetail.add-icon": "Lisa ikoon", - "CardDetail.add-property": "+ Lisa omadus", - "CardDetail.addCardText": "lisa kaardi tekst", - "CardDetail.moveContent": "liiguta kaardi sisu", - "CardDetail.new-comment-placeholder": "Lisa kommentaar...", - "CardDetailProperty.confirm-delete-heading": "Kinnita omaduse kustutamine", - "CardDetailProperty.delete-action-button": "Kustuta", - "CardDetailProperty.property-change-action-button": "Muuda omadust", - "CardDialog.editing-template": "Sa muudad malli.", - "CardDialog.nocard": "Seda kaarti pole olemas või see pole ligipääsetav.", - "Comment.delete": "Kustuta", - "CommentsList.send": "Saada", - "ConfirmationDialog.cancel-action": "Loobu", - "ConfirmationDialog.confirm-action": "Kinnita", - "ContentBlock.Delete": "Kustuta", - "ContentBlock.DeleteAction": "Kustuta", - "ContentBlock.addElement": "lisa {type}", - "ContentBlock.divider": "eraldaja", - "ContentBlock.editCardCheckboxText": "lisa kaardi tekst", - "ContentBlock.editCardText": "muuda kaardi teksti", - "ContentBlock.editText": "Muuda teksti...", - "ContentBlock.image": "pilt", - "ContentBlock.insertAbove": "Sisesta üles", - "ContentBlock.moveDown": "Liiguta alla", - "ContentBlock.moveUp": "Liiguta üles", - "ContentBlock.text": "tekst", - "DeleteBoardDialog.confirm-cancel": "Loobu", - "DeleteBoardDialog.confirm-delete": "Kustuta", - "EditableDayPicker.today": "Täna", - "Filter.includes": "sisaldab", - "Filter.is-empty": "on tühi", - "Filter.is-not-empty": "pole tühi", - "Filter.not-includes": "ei sisalda", - "FilterComponent.add-filter": "+ Lisa filter", - "FilterComponent.delete": "Kustuta", - "KanbanCard.untitled": "Nimetu", - "Mutator.new-card-from-template": "malli põhjal uus kaart", - "PropertyMenu.Delete": "Kustuta", - "PropertyMenu.typeTitle": "Liik", - "PropertyType.CreatedBy": "Lisas", - "PropertyType.CreatedTime": "Lisamise aeg", - "PropertyType.Date": "Kuupäev", - "PropertyType.Email": "E-post", - "PropertyType.MultiSelect": "Mitme valimine", - "PropertyType.Number": "Number", - "PropertyType.Person": "Isik", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Vali", - "PropertyType.Text": "Tekst", - "PropertyType.UpdatedBy": "Viimati uuendati", - "PropertyType.UpdatedTime": "Viimane uuendamise aeg", - "PropertyValueElement.empty": "Tühi", - "RegistrationLink.copiedLink": "Kopeeritud!", - "RegistrationLink.copyLink": "Kopeeri link", - "ShareBoard.copiedLink": "Kopeeritud!", - "ShareBoard.copyLink": "Kopeeri link", - "Sidebar.about": "Focalboardi info", - "Sidebar.changePassword": "Muuda parooli", - "Sidebar.invite-users": "Kutsu kasutajaid", - "Sidebar.logout": "Logi välja", - "Sidebar.random-icons": "Juhuslikud ikoonid", - "Sidebar.set-language": "Määra keel", - "Sidebar.set-theme": "Määra kujundus", - "Sidebar.settings": "Seaded", - "TableComponent.add-icon": "Lisa ikoon", - "TableComponent.name": "Nimi", - "TableComponent.plus-new": "+ Uus", - "TableHeaderMenu.delete": "Kustuta", - "TableHeaderMenu.duplicate": "Tee koopia", - "TableHeaderMenu.hide": "Peida", - "TableHeaderMenu.insert-left": "Sisesta vasakule", - "TableHeaderMenu.insert-right": "Sisesta paremale", - "TableHeaderMenu.sort-ascending": "Sorteeri kasvavalt", - "TableHeaderMenu.sort-descending": "Sorteeri kahanevalt", - "TableRow.open": "Ava", - "TopBar.give-feedback": "Anna tagasisidet", - "ValueSelector.valueSelector": "Väärtuse valija", - "ValueSelectorLabel.openMenu": "Ava menüü", - "View.AddView": "Lisa vaade", - "View.DeleteView": "Kustuta vaade", - "View.DuplicateView": "Tee vaatest koopia", - "View.Gallery": "Galerii", - "View.NewCalendarTitle": "Kalendri vaade", - "View.NewGalleryTitle": "Galerii vaade", - "View.NewTableTitle": "Tabeli vaade", - "View.Table": "Tabel", - "ViewHeader.add-template": "Uus mall", - "ViewHeader.delete-template": "Kustuta", - "ViewHeader.edit-template": "Muuda", - "ViewHeader.empty-card": "Tühi kaart", - "ViewHeader.export-complete": "Eksportimine on valmis!", - "ViewHeader.export-csv": "Ekspordi CSV-na", - "ViewHeader.export-failed": "Eksportimine ebaõnnestus!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Grupeeri: {property}", - "ViewHeader.new": "Uus", - "ViewHeader.properties": "Omadused", - "ViewHeader.search-text": "Otsi teksti", - "ViewHeader.select-a-template": "Vali mall", - "ViewHeader.set-default-template": "Määra vaikeväärtuseks", - "ViewHeader.sort": "Sorteeri", - "ViewHeader.untitled": "Nimetu", - "ViewTitle.hide-description": "peida kirjeldus", - "ViewTitle.pick-icon": "Vali ikoon", - "ViewTitle.random-icon": "Juhuslik", - "ViewTitle.remove-icon": "Eemalda ikoon", - "ViewTitle.show-description": "näita kirjeldust", - "calendar.month": "Kuu", - "calendar.today": "TÄNA", - "calendar.week": "Nädal", - "default-properties.title": "Pealkiri", - "login.log-in-button": "Logi sisse", - "login.log-in-title": "Logi sisse", - "login.register-button": "või loo konto, kui sul seda veel pole", - "register.login-button": "või logi sisse, kui sul juba on konto", - "register.signup-title": "Loo omale konto" -} diff --git a/webapp/boards/i18n/fa.json b/webapp/boards/i18n/fa.json deleted file mode 100644 index c875c9456a..0000000000 --- a/webapp/boards/i18n/fa.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "AppBar.Tooltip": "تغییر وضعیت تابلوهای مرتبط", - "Attachment.Attachment-title": "ضمیمه", - "AttachmentBlock.DeleteAction": "حذف", - "AttachmentBlock.addElement": "افزودن {type}", - "AttachmentBlock.delete": "ضمیمه حذف شد.", - "AttachmentBlock.failed": "به دلیل محدودیت حجم، این پرونده نمی‌تواند بارگذاری شود.", - "AttachmentBlock.upload": "بارگذاری ضمیمه.", - "AttachmentBlock.uploadSuccess": "ضمیمه بارگذاری شد.", - "AttachmentElement.delete-confirmation-dialog-button-text": "حذف", - "AttachmentElement.download": "بارگیری", - "AttachmentElement.upload-percentage": "بارگذاری...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ افزودن گروه", - "BoardComponent.delete": "حذف", - "BoardComponent.hidden-columns": "ستون های مخفی", - "BoardComponent.hide": "مخفی", - "BoardComponent.new": "+ جدید", - "BoardComponent.no-property": "بدون {property}", - "BoardComponent.no-property-title": "موارد با {property} خالی اینجا نمایش داده میشوند. این ستون قابل حذف نیست.", - "BoardComponent.show": "نمایش", - "BoardMember.schemeAdmin": "مدیر", - "BoardMember.schemeEditor": "ویرایشگر", - "BoardMember.schemeNone": "هیچکدام", - "BoardMember.schemeViewer": "بیننده", - "BoardPage.newVersion": "نسخه جدیدی از برنامه Boards موجود است، برای بارگیری مجدد اینجا را کلیک کنید.", - "BoardPage.syncFailed": "تابلو ممکن است حذف شود یا دسترسی آن لغو شود.", - "BoardTemplateSelector.add-template": "ایجاد قالب جدید", - "BoardTemplateSelector.create-empty-board": "ایجاد تابلو خالی", - "BoardTemplateSelector.delete-template": "حذف", - "BoardTemplateSelector.description": "با استفاده از الگوهای زیر، یک تابلو به نوار کناری اضافه کنید یا از اول شروع کنید.", - "BoardTemplateSelector.edit-template": "ویرایش", - "BoardTemplateSelector.plugin.no-content-description": "با استفاده از الگوهای زیر، یک تابلو به نوار کناری اضافه کنید یا از اول شروع کنید.", - "BoardTemplateSelector.plugin.no-content-title": "ایجاد یک تابلو", - "BoardTemplateSelector.title": "ایجاد یک تابلو", - "BoardTemplateSelector.use-this-template": "از این قالب استفاده کنید", - "BoardsSwitcher.Title": "جستجوی تابلوها", - "BoardsUnfurl.Remainder": "+{remainder} بیشتر", - "BoardsUnfurl.Updated": "به روز شد {time}", - "Calculations.Options.average.displayName": "میانگین", - "Calculations.Options.average.label": "میانگین", - "Calculations.Options.count.displayName": "تعداد", - "Calculations.Options.count.label": "تعداد", - "Calculations.Options.countChecked.displayName": "نشان‌دار", - "Calculations.Options.countChecked.label": "تعداد نشان‌دارها", - "Calculations.Options.countUnchecked.displayName": "بی‌نشان", - "Calculations.Options.countUnchecked.label": "تعداد نشان‌دارها", - "Calculations.Options.countUniqueValue.displayName": "یکتا", - "Calculations.Options.countUniqueValue.label": "تعداد مقادیر یکتا", - "Calculations.Options.countValue.displayName": "مقادیر", - "Calculations.Options.countValue.label": "تعداد مقادیر", - "Calculations.Options.dateRange.displayName": "دامنه", - "Calculations.Options.dateRange.label": "دامنه", - "Calculations.Options.earliest.displayName": "اولین", - "Calculations.Options.earliest.label": "اولین", - "Calculations.Options.latest.displayName": "آخرین", - "Calculations.Options.latest.label": "آخرین", - "Calculations.Options.max.displayName": "حداکثر", - "Calculations.Options.max.label": "حداکثر", - "Calculations.Options.median.displayName": "میانه", - "Calculations.Options.median.label": "میانه", - "Calculations.Options.min.displayName": "حداقل", - "Calculations.Options.min.label": "حداقل", - "Calculations.Options.none.displayName": "محاسبه", - "Calculations.Options.none.label": "هیچ یک", - "Calculations.Options.percentChecked.displayName": "بررسی شد", - "Calculations.Options.percentChecked.label": "درصد انتخاب‌شده‌ها", - "Calculations.Options.percentUnchecked.displayName": "بدون علامت", - "Calculations.Options.percentUnchecked.label": "درصد بی‌نشان", - "Calculations.Options.range.displayName": "بازه", - "Calculations.Options.range.label": "بازه", - "Calculations.Options.sum.displayName": "جمع", - "Calculations.Options.sum.label": "جمع", - "CardBadges.title-checkboxes": "چک باکس ها", - "CardBadges.title-comments": "نظرات", - "CardBadges.title-description": "این کارت دارای توضیحات است", - "CardDetail.Follow": "دنبال کردن", - "CardDetail.Following": "ذیل", - "CardDetail.add-content": "محتوا اضافه کنید", - "CardDetail.add-icon": "اضافه کردن نماد", - "CardDetail.add-property": "+ اضافه کردن یک ویژگی", - "CardDetail.addCardText": "متن کارت را اضافه کنید", - "CardDetail.moveContent": "انتقال محتوای کارت", - "CardDetail.new-comment-placeholder": "افزودن نظر...", - "CardDetailProperty.confirm-delete-heading": "تایید حذف ویژگی", - "CardDetailProperty.confirm-delete-subtext": "آیا مطمئن هستید که می خواهید ویژگی \"{propertyName}\" را حذف کنید؟ با حذف آن، اموال از تمام کارت های موجود در این تابلو حذف می شود.", - "CardDetailProperty.confirm-property-name-change-subtext": "آیا مطمئن هستید که می خواهید ویژگی \"{propertyName}\" {customText} را تغییر دهید؟ این روی مقدار(های) کارت(های) {numOfCards} در این برد تأثیر می گذارد و می تواند منجر به از دست رفتن داده شود.", - "CardDetailProperty.confirm-property-type-change": "تایید تغییر نوع ویژگی", - "CardDetailProperty.delete-action-button": "حذف", - "CardDetailProperty.property-change-action-button": "تغییر ویژگی", - "CardDetailProperty.property-changed": "تغییر ویژگی با موفقیت!", - "CardDetailProperty.property-deleted": "{propertyName} با موفقیت حذف شد!", - "CardDetailProperty.property-name-change-subtext": "از \"{oldPropType}\" به \"{newPropType}\" تایپ کنید", - "CardDialog.delete-confirmation-dialog-button-text": "حذف", - "CardDialog.delete-confirmation-dialog-heading": "تایید حذف کارت", - "CardDialog.editing-template": "شما در حال ویرایش یک الگو هستید.", - "CardDialog.nocard": "این کارت وجود ندارد یا غیرقابل دسترسی است.", - "Categories.CreateCategoryDialog.CancelText": "لغو کنید", - "Categories.CreateCategoryDialog.CreateText": "ايجاد كردن", - "Categories.CreateCategoryDialog.Placeholder": "دسته خود را نام ببرید", - "Categories.CreateCategoryDialog.UpdateText": "به روز رسانی", - "CenterPanel.Login": "وارد شدن", - "CenterPanel.Share": "اشتراک گذاری", - "ColorOption.selectColor": "رنگ {color} را انتخاب کنید", - "Comment.delete": "حذف", - "CommentsList.send": "ارسال", - "ConfirmationDialog.cancel-action": "لغو کنید", - "ConfirmationDialog.confirm-action": "تایید", - "ContentBlock.Delete": "حذف", - "ContentBlock.DeleteAction": "حذف", - "GroupBy.hideEmptyGroups": "پنهان‌کردن {count} گروه خالی", - "GroupBy.showHiddenGroups": "نمایش {count} گروه پنهان‌شده", - "ViewHeader.view-menu": "نمایش فهرست", - "ViewLimitDialog.Heading": "محدودیت تعداد نما برای تخته رسید", - "ViewLimitDialog.PrimaryButton.Title.Admin": "ارتقا", - "ViewLimitDialog.UpgradeImg.AltText": "ارتقا عکس", - "ViewLimitDialog.notifyAdmin.Success": "مدیر شما مطلع شد", - "ViewTitle.hide-description": "پنهان کردن توضیحات", - "ViewTitle.pick-icon": "انتخاب تصویرک", - "ViewTitle.random-icon": "تصادفی", - "ViewTitle.remove-icon": "پاک‌کردن تصویرک", - "ViewTitle.show-description": "نمایش توضیحات", - "ViewTitle.untitled-board": "تخته بدون عنوان", - "badge.guest": "مهمان" -} diff --git a/webapp/boards/i18n/fr.json b/webapp/boards/i18n/fr.json deleted file mode 100644 index a94f4529f5..0000000000 --- a/webapp/boards/i18n/fr.json +++ /dev/null @@ -1,457 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrateur", - "AdminBadge.TeamAdmin": "Administrateur d'équipe", - "AppBar.Tooltip": "Activer les panneaux liés", - "Attachment.Attachment-title": "Pièce jointe", - "AttachmentBlock.DeleteAction": "supprimer", - "AttachmentBlock.addElement": "ajouter {type}", - "AttachmentBlock.delete": "Pièce jointe supprimée.", - "AttachmentBlock.failed": "Impossible de télécharger le fichier. Limite de taille de fichier atteinte.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Supprimer", - "AttachmentElement.download": "Télécharger", - "BoardComponent.add-a-group": "+ Ajouter un groupe", - "BoardComponent.delete": "Supprimer", - "BoardComponent.hidden-columns": "Colonnes cachées", - "BoardComponent.hide": "Cacher", - "BoardComponent.new": "+ Nouveau", - "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.show": "Montrer", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Commentateur", - "BoardMember.schemeEditor": "Éditeur", - "BoardMember.schemeNone": "Aucun", - "BoardMember.schemeViewer": "Lecteur", - "BoardMember.unlinkChannel": "Détacher", - "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.", - "BoardTemplateSelector.add-template": "Nouveau modèle", - "BoardTemplateSelector.create-empty-board": "Créer un tableau vide", - "BoardTemplateSelector.delete-template": "Supprimer", - "BoardTemplateSelector.description": "Ajoutez un tableau à la barre latérale en utilisant l'un des modèles définis ci-dessous ou recommencez de zéro.", - "BoardTemplateSelector.edit-template": "Éditer", - "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.", - "BoardTemplateSelector.plugin.no-content-title": "Créer un tableau", - "BoardTemplateSelector.title": "Créer un tableau", - "BoardTemplateSelector.use-this-template": "Utiliser ce modèle", - "BoardsSwitcher.Title": "Rechercher des tableaux", - "BoardsUnfurl.Limited": "Les détails supplémentaires sont masqués car la carte est archivée", - "BoardsUnfurl.Remainder": "+{remainder} plus", - "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": "Calculer la 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.percentChecked.displayName": "Coché", - "Calculations.Options.percentChecked.label": "Pourcentage coché", - "Calculations.Options.percentUnchecked.displayName": "Décoché", - "Calculations.Options.percentUnchecked.label": "Pourcentage décoché", - "Calculations.Options.range.displayName": "Curseur", - "Calculations.Options.range.label": "Curseur", - "Calculations.Options.sum.displayName": "Somme", - "Calculations.Options.sum.label": "Somme", - "CalendarCard.untitled": "Sans titre", - "CardActionsMenu.copiedLink": "Copié !", - "CardActionsMenu.copyLink": "Copier le lien", - "CardActionsMenu.delete": "Supprimer", - "CardActionsMenu.duplicate": "Dupliquer", - "CardBadges.title-checkboxes": "Cases à cocher", - "CardBadges.title-comments": "Commentaires", - "CardBadges.title-description": "Cette carte a une description", - "CardDetail.Attach": "Joindre", - "CardDetail.Follow": "Suivre", - "CardDetail.Following": "Suivi", - "CardDetail.add-content": "Ajouter du contenu", - "CardDetail.add-icon": "Ajouter une icône", - "CardDetail.add-property": "+ Ajouter une propriété", - "CardDetail.addCardText": "ajouter une carte texte", - "CardDetail.limited-body": "Passez à notre offre Professionnel ou Entreprise pour afficher les cartes archivées, avoir des vues illimitées par tableau, des cartes illimitées et plus encore.", - "CardDetail.limited-button": "Mise à niveau", - "CardDetail.limited-title": "Cette carte est masquée", - "CardDetail.moveContent": "Déplacer le contenu de la carte", - "CardDetail.new-comment-placeholder": "Ajouter un commentaire...", - "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-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.delete-action-button": "Supprimer", - "CardDetailProperty.property-change-action-button": "Modifier la propriété", - "CardDetailProperty.property-changed": "Propriété modifiée avec succès !", - "CardDetailProperty.property-deleted": "{propertyName} supprimé avec succès !", - "CardDetailProperty.property-name-change-subtext": "de \"{oldPropType}\" à \"{newPropType}\"", - "CardDetial.limited-link": "En savoir plus sur nos offres.", - "CardDialog.delete-confirmation-dialog-attachment": "Confirmer la suppression de la pièce jointe", - "CardDialog.delete-confirmation-dialog-button-text": "Supprimer", - "CardDialog.delete-confirmation-dialog-heading": "Confirmer la suppression de la carte !", - "CardDialog.editing-template": "Vous éditez un modèle.", - "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.Login": "Connexion", - "CenterPanel.Share": "Partager", - "ChannelIntro.CreateBoard": "Créer un tableau", - "ColorOption.selectColor": "Choisir la couleur {color}", - "Comment.delete": "Supprimer", - "CommentsList.send": "Envoyer", - "ConfirmPerson.empty": "Vide", - "ConfirmPerson.search": "Recherche en cours…", - "ConfirmationDialog.cancel-action": "Annuler", - "ConfirmationDialog.confirm-action": "Confirmer", - "ContentBlock.Delete": "Supprimer", - "ContentBlock.DeleteAction": "supprimer", - "ContentBlock.addElement": "ajouter {type}", - "ContentBlock.checkbox": "case à cocher", - "ContentBlock.divider": "séparateur", - "ContentBlock.editCardCheckbox": "case cochée", - "ContentBlock.editCardCheckboxText": "éditer le texte de la carte", - "ContentBlock.editCardText": "éditer le texte de la carte", - "ContentBlock.editText": "Éditer le texte...", - "ContentBlock.image": "image", - "ContentBlock.insertAbove": "Insérer au-dessus", - "ContentBlock.moveBlock": "Déplacer le contenu de la carte", - "ContentBlock.moveDown": "Déplacer vers le bas", - "ContentBlock.moveUp": "Déplacer vers le haut", - "ContentBlock.text": "texte", - "DateFilter.empty": "Vide", - "DateRange.clear": "Supprimer", - "DateRange.empty": "Vide", - "DateRange.endDate": "Date de fin", - "DateRange.today": "Aujourd'hui", - "DeleteBoardDialog.confirm-cancel": "Annuler", - "DeleteBoardDialog.confirm-delete": "Supprimer", - "DeleteBoardDialog.confirm-info": "Êtes-vous sûr de vouloir supprimer le tableau «{boardTitle}» ? Cela supprimera toutes les cartes dans ce tableau.", - "DeleteBoardDialog.confirm-info-template": "Voulez-vous vraiment supprimer le modèle de tableau “{boardTitle}” ?", - "DeleteBoardDialog.confirm-tite": "Confirmer la suppression du tableau", - "DeleteBoardDialog.confirm-tite-template": "Confirmer la suppression du modèle", - "Dialog.closeDialog": "Fermer la boîte de dialogue", - "EditableDayPicker.today": "Aujourd'hui", - "Error.mobileweb": "La prise en charge de la version mobile est actuellement en version bêta. Certaines fonctionnalités peuvent manquer.", - "Error.websocket-closed": "Connexion au websocket fermé, connexion interrompue. Si cela persiste, vérifiez la configuration de votre serveur ou de votre proxy web.", - "Filter.contains": "contient", - "Filter.ends-with": "se termine par", - "Filter.includes": "inclus", - "Filter.is": "est", - "Filter.is-after": "est après", - "Filter.is-before": "est avant", - "Filter.is-empty": "est vide", - "Filter.is-not-empty": "n'est pas vide", - "Filter.is-not-set": "n'est pas renseigné", - "Filter.is-set": "est renseigné", - "Filter.isafter": "est après", - "Filter.isbefore": "est avant", - "Filter.not-contains": "ne contient pas", - "Filter.not-ends-with": "ne se termine pas par", - "Filter.not-includes": "n'inclut pas", - "Filter.not-starts-with": "ne commence pas par", - "Filter.starts-with": "commence par", - "FilterByText.placeholder": "filtre de texte", - "FilterComponent.add-filter": "+ Ajouter un filtre", - "FilterComponent.delete": "Supprimer", - "FilterValue.empty": "(vide)", - "FindBoardsDialog.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 HAUT/BAS pour naviguer. ENTRER pour sélectionner, ECHAP pour annuler", - "FindBoardsDialog.Title": "Rechercher des tableaux", - "GroupBy.hideEmptyGroups": "Masquer {count} groupes vides", - "GroupBy.showHiddenGroups": "Afficher les {count} groupes masqués", - "GroupBy.ungroup": "Dégrouper", - "HideBoard.MenuOption": "Cacher le tableau", - "KanbanCard.untitled": "Sans titre", - "MentionSuggestion.is-not-board-member": "(non membre du tableau)", - "Mutator.new-board-from-template": "nouveau tableau à partir du modèle", - "Mutator.new-card-from-template": "nouvelle carte depuis un modèle", - "Mutator.new-template-from-card": "nouveau modèle depuis une carte", - "OnboardingTour.AddComments.Body": "Vous pouvez commenter les bugs et même @mentionner les utilisateurs de Mattermost pour attirer leur attention.", - "OnboardingTour.AddComments.Title": "Ajouter des commentaires", - "OnboardingTour.AddDescription.Body": "Ajouter une description à votre carte pour que votre équipe sachent de quoi il s'agit.", - "OnboardingTour.AddDescription.Title": "Ajouter une description", - "OnboardingTour.AddProperties.Body": "Ajouter diverses propriétés aux cartes pour les rendre plus efficace !", - "OnboardingTour.AddProperties.Title": "Ajouter des propriétés", - "OnboardingTour.AddView.Body": "Allez ici pour créer une nouvelle vue pour organiser votre tableau en utilisant différentes mises en page.", - "OnboardingTour.AddView.Title": "Ajouter une nouvelle vue", - "OnboardingTour.CopyLink.Body": "Vous pouvez partager vos cartes avec votre équipe en copiant le lien et en le collant dans un canal, un message direct ou un message de groupe.", - "OnboardingTour.CopyLink.Title": "Copier le lien", - "OnboardingTour.OpenACard.Body": "Ouvrir une carte pour découvrir les façons dont les tableaux peuvent vous aider à organiser votre travail.", - "OnboardingTour.OpenACard.Title": "Ouvrir une carte", - "OnboardingTour.ShareBoard.Body": "Vous pouvez partager votre tableau en interne, au sein de votre équipe ou le publier publiquement pour une visibilité en dehors de votre organisation.", - "OnboardingTour.ShareBoard.Title": "Partager un tableau", - "PersonProperty.board-members": "Membres du tableau", - "PersonProperty.me": "Moi", - "PersonProperty.non-board-members": "Non membres du tableau", - "PropertyMenu.Delete": "Supprimer", - "PropertyMenu.changeType": "Changer le type de la propriété", - "PropertyMenu.selectType": "Sélectionner le type de propriété", - "PropertyMenu.typeTitle": "Type", - "PropertyType.Checkbox": "Case à cocher", - "PropertyType.CreatedBy": "Créé par", - "PropertyType.CreatedTime": "Date de création", - "PropertyType.Date": "Date", - "PropertyType.Email": "Adresse e-mail", - "PropertyType.MultiPerson": "Personne multiple", - "PropertyType.MultiSelect": "Sélection multiple", - "PropertyType.Number": "Nombre", - "PropertyType.Person": "Personne", - "PropertyType.Phone": "Téléphone", - "PropertyType.Select": "Liste", - "PropertyType.Text": "Texte", - "PropertyType.Unknown": "Inconnue", - "PropertyType.UpdatedBy": "Dernière mise à jour par", - "PropertyType.UpdatedTime": "Date de dernière mise à jour", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Vide", - "RegistrationLink.confirmRegenerateToken": "Ceci va désactiver les liens de partages existants. Continuer ?", - "RegistrationLink.copiedLink": "Copié !", - "RegistrationLink.copyLink": "Copier le lien", - "RegistrationLink.description": "Partagez ce lien avec des personnes pour leur permettre de créer un compte :", - "RegistrationLink.regenerateToken": "Générer un nouveau jeton", - "RegistrationLink.tokenRegenerated": "Un nouveau lien d'inscription a été créé", - "ShareBoard.PublishDescription": "Publiez et partagez un lien en lecture seule avec tout le monde sur le Web.", - "ShareBoard.PublishTitle": "Publier sur le web", - "ShareBoard.ShareInternal": "Partager en interne", - "ShareBoard.ShareInternalDescription": "Les utilisateurs qui ont des autorisations pourront utiliser ce lien.", - "ShareBoard.Title": "Partager le tableau", - "ShareBoard.confirmRegenerateToken": "Ceci va désactiver les liens de partages existants. Continuer ?", - "ShareBoard.copiedLink": "Copié !", - "ShareBoard.copyLink": "Copier le lien", - "ShareBoard.regenerate": "Régénérer le jeton", - "ShareBoard.searchPlaceholder": "Rechercher des membres et des canaux", - "ShareBoard.teamPermissionsText": "Tout le monde à l'équipe {teamName}", - "ShareBoard.tokenRegenrated": "Le jeton a été recréé", - "ShareBoard.userPermissionsRemoveMemberText": "Supprimer un membre", - "ShareBoard.userPermissionsYouText": "(Vous)", - "ShareTemplate.Title": "Partager un modèle", - "ShareTemplate.searchPlaceholder": "Recherche de personnes", - "Sidebar.about": "À propos de Focalboard", - "Sidebar.add-board": "+ Ajouter un tableau", - "Sidebar.changePassword": "Modifier le mot de passe", - "Sidebar.delete-board": "Supprimer le tableau", - "Sidebar.duplicate-board": "Dupliquer une carte", - "Sidebar.export-archive": "Exporter une archive", - "Sidebar.import": "Importer", - "Sidebar.import-archive": "Importer une archive", - "Sidebar.invite-users": "Inviter des utilisateurs", - "Sidebar.logout": "Se déconnecter", - "Sidebar.new-category.badge": "Nouveau", - "Sidebar.new-category.drag-boards-cta": "Déplacer les tableaux ici...", - "Sidebar.no-boards-in-category": "Aucun tableaux", - "Sidebar.product-tour": "Visite guidée", - "Sidebar.random-icons": "Icônes aléatoires", - "Sidebar.set-language": "Choisir la langue", - "Sidebar.set-theme": "Choisir le thème", - "Sidebar.settings": "Réglages", - "Sidebar.template-from-board": "Nouveau modèle de tableau", - "Sidebar.untitled-board": "(Tableau sans titre)", - "Sidebar.untitled-view": "(Vue 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 {categoryName} 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", - "SidebarTour.ManageCategories.Body": "Créez et gérez des catégories personnalisées. Les catégories sont spécifiques à l'utilisateur, donc déplacer un tableau vers votre catégorie n'aura pas d'impact sur les autres membres utilisant le même tableau.", - "SidebarTour.ManageCategories.Title": "Gérer les catégories", - "SidebarTour.SearchForBoards.Body": "Ouvrez le sélecteur de tableau (Cmd/Ctrl + K) pour rechercher et ajouter rapidement des tableaux à votre barre latérale.", - "SidebarTour.SearchForBoards.Title": "Rechercher des cartes", - "SidebarTour.SidebarCategories.Body": "Tous vos tableaux sont maintenant organisés sous votre nouvelle barre latérale. Plus besoin de basculer entre les espaces de travail. Des catégories personnalisées uniques basées sur vos espaces de travail précédents peuvent avoir été automatiquement créées pour vous dans le cadre de la mise à jour 7.2. Ceux-ci peuvent être supprimés ou modifiés selon vos préférences.", - "SidebarTour.SidebarCategories.Link": "En savoir plus", - "SidebarTour.SidebarCategories.Title": "Catégories de la barre latérale", - "SiteStats.total_boards": "Total des tableaux", - "SiteStats.total_cards": "Total des cartes", - "TableComponent.add-icon": "Ajouter une icône", - "TableComponent.name": "Nom", - "TableComponent.plus-new": "+ Nouveau", - "TableHeaderMenu.delete": "Supprimer", - "TableHeaderMenu.duplicate": "Dupliquer", - "TableHeaderMenu.hide": "Cacher", - "TableHeaderMenu.insert-left": "Insérer à gauche", - "TableHeaderMenu.insert-right": "Insérer à droite", - "TableHeaderMenu.sort-ascending": "Tri ascendant", - "TableHeaderMenu.sort-descending": "Tri descendant", - "TableRow.DuplicateCard": "Dupliquer une carte", - "TableRow.MoreOption": "Plus d'actions", - "TableRow.open": "Ouvrir", - "TopBar.give-feedback": "Donner un avis", - "URLProperty.copiedLink": "Copié !", - "URLProperty.copy": "Copie", - "URLProperty.edit": "Modifier", - "UndoRedoHotKeys.canRedo": "Refaire", - "UndoRedoHotKeys.canRedo-with-description": "Refaire {description}", - "UndoRedoHotKeys.canUndo": "Annuler", - "UndoRedoHotKeys.canUndo-with-description": "Annuler {description}", - "UndoRedoHotKeys.cannotRedo": "Rien à refaire", - "UndoRedoHotKeys.cannotUndo": "Rien à annuler", - "ValueSelector.noOptions": "Aucune option. Commencez à taper pour ajouter la première !", - "ValueSelector.valueSelector": "Sélecteur de value", - "ValueSelectorLabel.openMenu": "Ouvrir le menu", - "VersionMessage.help": "Découvrez les nouveautés de cette version.", - "VersionMessage.learn-more": "En savoir plus", - "View.AddView": "Ajouter une vue", - "View.Board": "Tableau", - "View.DeleteView": "Supprimer la vue", - "View.DuplicateView": "Dupliquer la vue", - "View.Gallery": "Galerie", - "View.NewBoardTitle": "Vue en tableau", - "View.NewCalendarTitle": "Vue calendrier", - "View.NewGalleryTitle": "Vue en galerie", - "View.NewTableTitle": "Vue en table", - "View.NewTemplateDefaultTitle": "Modèle sans titre", - "View.NewTemplateTitle": "Sans titre", - "View.Table": "Table", - "ViewHeader.add-template": "Nouveau modèle", - "ViewHeader.delete-template": "Supprimer", - "ViewHeader.display-by": "Afficher par : {property}", - "ViewHeader.edit-template": "Éditer", - "ViewHeader.empty-card": "Carte vide", - "ViewHeader.export-board-archive": "Exporter une archive du tableau", - "ViewHeader.export-complete": "Export terminé !", - "ViewHeader.export-csv": "Exporter au format CSV", - "ViewHeader.export-failed": "L'export a échoué !", - "ViewHeader.filter": "Filtre", - "ViewHeader.group-by": "Grouper par : {property}", - "ViewHeader.new": "Nouveau", - "ViewHeader.properties": "Propriétés", - "ViewHeader.properties-menu": "Menu propriétés", - "ViewHeader.search-text": "Rechercher des cartes", - "ViewHeader.select-a-template": "Sélectionner un modèle", - "ViewHeader.set-default-template": "Définir par défaut", - "ViewHeader.sort": "Trier", - "ViewHeader.untitled": "Sans titre", - "ViewHeader.view-header-menu": "Afficher le menu d'en-tête", - "ViewHeader.view-menu": "Afficher le menu", - "ViewLimitDialog.Heading": "Limite de vues par tableau atteinte", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Mise à niveau", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notifier l'Admin", - "ViewLimitDialog.Subtext.Admin": "Passez à notre offre Professionnel ou Entreprise pour afficher les cartes archivées, avoir des vues illimitées par tableau, des cartes illimitées et plus encore.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "En savoir plus sur nos offres.", - "ViewLimitDialog.Subtext.RegularUser": "Informez votre administrateur qu'il peut passer à notre offre professionnel ou d'entreprise pour avoir un nombre illimité de vues par tableau, un nombre illimité de cartes, et plus encore.", - "ViewLimitDialog.UpgradeImg.AltText": "mise à jour de l'image", - "ViewLimitDialog.notifyAdmin.Success": "Votre administrateur a été notifié", - "ViewTitle.hide-description": "cacher la description", - "ViewTitle.pick-icon": "Choisir une icône", - "ViewTitle.random-icon": "Aléatoire", - "ViewTitle.remove-icon": "Supprimer l'icône", - "ViewTitle.show-description": "montrer la description", - "ViewTitle.untitled-board": "Tableau sans titre", - "WelcomePage.Description": "Boards est un outil de gestion de projet qui permet d'organiser, de suivre et de gérer le travail entre équipes en utilisant des tableaux Kanban.", - "WelcomePage.Explore.Button": "Tutoriel", - "WelcomePage.Heading": "Bienvenue sur Boards", - "WelcomePage.NoThanks.Text": "Non merci, je vais me renseigner moi-même", - "WelcomePage.StartUsingIt.Text": "Commencez à l'utiliser", - "Workspace.editing-board-template": "Vous éditez un modèle de tableau.", - "badge.guest": "Invité", - "boardPage.confirm-join-button": "Rejoindre", - "boardPage.confirm-join-text": "Vous êtes sur le point de rejoindre un forum privé sans avoir été explicitement ajouté par l'administrateur du forum. Êtes-vous sûr de vouloir rejoindre ce forum privé ?", - "boardPage.confirm-join-title": "Rejoindre un tableau privé", - "boardSelector.confirm-link-board": "Lier la carte au canal", - "boardSelector.confirm-link-board-button": "Oui, lier ce tableau", - "boardSelector.confirm-link-board-subtext": "Lorsque vous liez \"{boardName}\" au canal, tous les membres du canal (existants et nouveaux) pourront le modifier. Vous pouvez dissocier un tableau d'un canal à tout moment.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Lorsque vous liez \"{boardName}\" au canal, tous les membres du canal (existants et nouveaux) pourront le modifier.{lineBreak} Ce tableau est actuellement lié à un autre canal. Il sera dissocié si vous choisissez de le lier ici.", - "boardSelector.create-a-board": "Créer un tableau", - "boardSelector.link": "Lien", - "boardSelector.search-for-boards": "Rechercher des tableaux", - "boardSelector.title": "Lier les tableaux", - "boardSelector.unlink": "Détacher", - "calendar.month": "Mois", - "calendar.today": "AUJOURD'HUI", - "calendar.week": "Semaine", - "centerPanel.undefined": "Pas de {propertyName}", - "centerPanel.unknown-user": "Utilisateur inconnu", - "cloudMessage.learn-more": "En savoir plus", - "createImageBlock.failed": "Impossible de télécharger le fichier. Limite de taille de fichier atteinte.", - "default-properties.badges": "Commentaires et description", - "default-properties.title": "Titre", - "error.back-to-home": "Retour à la page d'accueil", - "error.back-to-team": "Retour à l'équipe", - "error.board-not-found": "Tableau non trouvé.", - "error.go-login": "Connexion", - "error.invalid-read-only-board": "Vous n'avez pas accès à ce tableau. Connectez-vous pour y accéder.", - "error.not-logged-in": "Votre session a peut-être expiré ou vous n’êtes pas connecté. Connectez-vous à nouveau pour accéder aux tableaux d'administration.", - "error.page.title": "Désolé, quelque chose s'est mal passé", - "error.team-undefined": "Ce n'est pas une équipe valable.", - "error.unknown": "Une erreur s'est produite.", - "generic.previous": "Précédent", - "guest-no-board.subtitle": "Vous n'avez pas encore accès à un tableau dans cette équipe, veuillez patienter jusqu'à ce que quelqu'un vous ajoute à un tableau.", - "guest-no-board.title": "Pas encore de tableaux", - "imagePaste.upload-failed": "Certains fichiers n'ont pas été téléchargés. Limite de taille de fichier atteinte", - "limitedCard.title": "Cartes cachées", - "login.log-in-button": "Connexion", - "login.log-in-title": "Connexion", - "login.register-button": "ou créez un compte si vous n'en avez pas", - "new_channel_modal.create_board.empty_board_description": "Créer un nouveau tableau vide", - "new_channel_modal.create_board.empty_board_title": "Tableau vide", - "new_channel_modal.create_board.select_template_placeholder": "Sélectionner un modèle", - "new_channel_modal.create_board.title": "Créer un tableau pour ce canal", - "notification-box-card-limit-reached.close-tooltip": "Oublier pendant 10 jours", - "notification-box-card-limit-reached.contact-link": "informez votre administrateur", - "notification-box-card-limit-reached.link": "Passer à une offre payante", - "notification-box-card-limit-reached.title": "{cards} cartes cachées du tableau", - "notification-box-cards-hidden.title": "Cette action a caché une autre carte", - "notification-box.card-limit-reached.not-admin.text": "Pour accéder aux cartes archivées, vous pouvez {contactLink} pour passer à une offre payante.", - "notification-box.card-limit-reached.text": "Limite de cartes atteinte, pour afficher les anciennes cartes, {link}", - "person.add-user-to-board": "Ajouter {username} au tableau", - "person.add-user-to-board-confirm-button": "Ajouter au tableau", - "person.add-user-to-board-permissions": "Permissions", - "person.add-user-to-board-question": "Voulez-vous ajouter {username} au tableau ?", - "person.add-user-to-board-warning": "{username} n'est pas membre du conseil d'administration et ne recevra aucune notification à ce sujet.", - "register.login-button": "ou connectez-vous si vous avez déjà un compte", - "register.signup-title": "Inscrivez-vous pour créer un compte", - "rhs-board-non-admin-msg": "Vous n'êtes pas administrateur du forum", - "rhs-boards.add": "Ajouter", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "ce message direct", - "rhs-boards.header.gm": "ce message de groupe", - "rhs-boards.last-update-at": "Dernière mise à jour à : {datetime}", - "rhs-boards.link-boards-to-channel": "Lier les tableaux à {channelName}", - "rhs-boards.linked-boards": "Tableaux liés", - "rhs-boards.no-boards-linked-to-channel": "Aucun tableau n'est lié à {channelName} pour le moment", - "rhs-boards.no-boards-linked-to-channel-description": "Boards est un outil de gestion de projet qui permet d'organiser, de suivre et de gérer le travail entre équipes en utilisant des tableaux Kanban.", - "rhs-boards.unlink-board": "Dissocier le tableau", - "rhs-boards.unlink-board1": "Dissocier le tableau", - "rhs-channel-boards-header.title": "Tableaux", - "share-board.publish": "Publier", - "share-board.share": "Partager", - "shareBoard.channels-select-group": "Canaux", - "shareBoard.confirm-change-team-role.body": "Tous les membres de ce forum avec une autorisation inférieure au rôle \"{role}\" seront maintenant promus au {role}. Êtes-vous sûr de vouloir modifier le rôle minimum ?", - "shareBoard.confirm-link-channel": "Lier le tableau au canal", - "shareBoard.confirm-link-channel-button": "Lien canal", - "shareBoard.confirm-link-channel-button-with-other-channel": "Dissocier et lier ici", - "shareBoard.confirm-link-channel-subtext": "Lorsque vous liez une chaîne à un tableau, tous les membres de la chaîne (existants et nouveaux) pourront le modifier.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Lorsque vous liez un canal à un tableau, tous les membres du canal (existants et nouveaux) pourront le modifier.{lineBreak}Ce tableau est actuellement lié à un autre canal. Il sera dissocié si vous choisissez de le lier ici.", - "shareBoard.confirm-unlink.body": "Lorsque vous dissociez une chaîne d'un tableau, tous les membres de la chaîne (existants et nouveaux) n'y auront plus accès, sauf s'ils en ont reçu l'autorisation séparément.", - "shareBoard.confirm-unlink.confirmBtnText": "Dissocier le canal", - "shareBoard.confirm-unlink.title": "Dissocier le canal du tableau", - "shareBoard.lastAdmin": "Les conseils doivent avoir au moins un administrateur", - "shareBoard.members-select-group": "Membres", - "shareBoard.unknown-channel-display-name": "Canal inconnu", - "tutorial_tip.finish_tour": "Terminé", - "tutorial_tip.got_it": "J'ai compris", - "tutorial_tip.ok": "Suivant", - "tutorial_tip.out": "Désactiver les conseils.", - "tutorial_tip.seen": "Déjà vu ?" -} diff --git a/webapp/boards/i18n/he.json b/webapp/boards/i18n/he.json deleted file mode 100644 index 692ec27fb0..0000000000 --- a/webapp/boards/i18n/he.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ הוספת קבוצה", - "BoardComponent.delete": "מחיקה", - "BoardComponent.hidden-columns": "עמודות מוסתרות", - "BoardComponent.hide": "הסתרה", - "BoardComponent.new": "+ חדש", - "BoardComponent.no-property": "ללא {property}", - "BoardComponent.no-property-title": "יופיעו כאן פריטים עם מאפיין {property} ריק. עמודה זו אינה ניתנת להסרה.", - "BoardComponent.show": "הצגה", - "BoardMember.schemeAdmin": "מנהל מערכת", - "BoardMember.schemeCommenter": "משתמש מגיב", - "BoardMember.schemeEditor": "עורך", - "BoardMember.schemeNone": "ללא", - "BoardMember.schemeViewer": "צופה", - "BoardMember.unlinkChannel": "ניתוק", - "BoardPage.newVersion": "ישנה גרסה חדשה וזמינה של לוחות, לטעינה מחדש לחץ כאן.", - "BoardPage.syncFailed": "ייתכן שהלוח נמחק או שנשללה ממך גישה", - "BoardTemplateSelector.add-template": "יצירת תבנית חדשה", - "BoardTemplateSelector.create-empty-board": "יצירת לוח ריק", - "BoardTemplateSelector.delete-template": "מחיקה", - "BoardTemplateSelector.description": "הוספת לוח לתפריט הצד באמצעות אחת התבניות, או יצירת תבנית חדשה", - "BoardTemplateSelector.edit-template": "עריכה", - "BoardTemplateSelector.plugin.no-content-description": "הוספת לוח לתפריט הצד באמצעות אחת מן התבניות שהוגדרו או יצירת תבנית חדשה.", - "BoardTemplateSelector.plugin.no-content-title": "יצירת לוח", - "BoardTemplateSelector.title": "יצירת לוח", - "BoardTemplateSelector.use-this-template": "שימוש בתבנית זו", - "BoardsSwitcher.Title": "חיפוש לוחות", - "BoardsUnfurl.Limited": "פרטים נוספים מוסתרים מאחר והכרטיס הועבר לארכיון", - "BoardsUnfurl.Remainder": "עוד {remainder}", - "BoardsUnfurl.Updated": "{time} מעודכן", - "Calculations.Options.average.displayName": "ממוצע", - "Calculations.Options.average.label": "ממוצע", - "Calculations.Options.count.displayName": "כמות", - "Calculations.Options.count.label": "כמות", - "Calculations.Options.countChecked.displayName": "סומנו", - "Calculations.Options.countChecked.label": "כמות שנבחרו", - "Calculations.Options.countUnchecked.displayName": "הסרת סימון", - "Calculations.Options.countUnchecked.label": "כמות שלא סומנו", - "Calculations.Options.countUniqueValue.displayName": "ייחודי", - "Calculations.Options.countUniqueValue.label": "כמות ערכים יחידניים", - "Calculations.Options.countValue.displayName": "ערכים", - "Calculations.Options.countValue.label": "כמות ערכים", - "Calculations.Options.dateRange.displayName": "טווח", - "Calculations.Options.dateRange.label": "טווח", - "Calculations.Options.earliest.displayName": "המוקדם ביותר", - "Calculations.Options.earliest.label": "המוקדם ביותר", - "Calculations.Options.latest.displayName": "המאוחר ביותר", - "Calculations.Options.latest.label": "המאוחר ביותר", - "Calculations.Options.max.displayName": "מקסימום", - "Calculations.Options.max.label": "מקסימום", - "Calculations.Options.median.displayName": "חציון", - "Calculations.Options.median.label": "חציון", - "Calculations.Options.min.displayName": "מינימום", - "Calculations.Options.min.label": "מינימום", - "Calculations.Options.none.displayName": "חישוב", - "Calculations.Options.none.label": "ללא", - "Calculations.Options.percentChecked.displayName": "סומן", - "Calculations.Options.percentChecked.label": "אחוז ערכים שסומנו", - "Calculations.Options.percentUnchecked.displayName": "לא סומנו", - "Calculations.Options.percentUnchecked.label": "אחוזים שלא סומנו", - "Calculations.Options.range.displayName": "טווח", - "Calculations.Options.range.label": "טווח", - "Calculations.Options.sum.displayName": "סכום", - "Calculations.Options.sum.label": "סכום", - "CalendarCard.untitled": "ללא כותרת", - "CardActionsMenu.copiedLink": "הועתק!", - "CardActionsMenu.copyLink": "העתקת קישור", - "CardActionsMenu.delete": "מחיקה", - "CardActionsMenu.duplicate": "שיכפול", - "CardBadges.title-checkboxes": "תיבות סימון", - "CardBadges.title-comments": "הערות", - "CardBadges.title-description": "כרטיס זה מכיל תיאור", - "CardDetail.Follow": "מעקב", - "CardDetail.Following": "עוקב", - "CardDetail.add-content": "הוספת מידע", - "CardDetail.add-icon": "הוספת אייקון", - "CardDetail.add-property": "+ הוספת תכונה", - "CardDetail.addCardText": "הוספת מלל הכרטיס", - "CardDetail.limited-body": "שדרג למסלול Professional או למסלול Enterprise ותוכל לצפות בכרטיסיות שהועברו לארכיון, לצפות במבטים על לוחות ללא הגבלה, לפתוח כרטיסיות ללא הגבלה ועוד.", - "CardDetail.limited-button": "שדרוג", - "CardDetail.limited-title": "כרטיס זה מוסתר", - "CardDetail.moveContent": "הזזת תוכן הכרטיס", - "CardDetail.new-comment-placeholder": "הוספת הערה...", - "CardDetailProperty.confirm-delete-heading": "אישור מחיקת תכונה", - "CardDetailProperty.confirm-delete-subtext": "האם הינך בטוח שאתה מעוניין במחיקת התכונה \"{propertyName}\"? מחיקת התכונה תגרור את מחיקתה בכל הכרטיסים שבלוח זה.", - "CardDetailProperty.confirm-property-name-change-subtext": "האם הינך בטוח ברצונך לשנות את התכונה \"{propertyName}\"' \"{customText}\"? זה ישפיע על ערך.ים לאורך {numOfCards} כרטיס.ים בלוח זה, ויכול להוביל לאיבוד מידע.", - "CardDetailProperty.confirm-property-type-change": "אישור שינוי סוג תכונה", - "CardDetailProperty.delete-action-button": "מחיקה", - "CardDetailProperty.property-change-action-button": "שינוי תכונה", - "CardDetailProperty.property-changed": "תכונה השתנתה בהצלחה!", - "CardDetailProperty.property-deleted": "מחיקת {propertyName} בוצעה בהצלחה!", - "CardDetailProperty.property-name-change-subtext": "סוג מ \"{oldPropType}\" אל \"{newPropType}\"", - "CardDetial.limited-link": "מידע נוסף לגבי המסלולים שלנו.", - "CardDialog.delete-confirmation-dialog-button-text": "מחיקה", - "CardDialog.delete-confirmation-dialog-heading": "מחיקת כרטיס מאושרת!", - "CardDialog.editing-template": "הינך מעדכן תבנית.", - "CardDialog.nocard": "כרטיס זה אינו קיים או ללא גישה.", - "Categories.CreateCategoryDialog.CancelText": "ביטול", - "Categories.CreateCategoryDialog.CreateText": "יצירה", - "Categories.CreateCategoryDialog.Placeholder": "שם הקטגוריה שלך", - "Categories.CreateCategoryDialog.UpdateText": "עדכון", - "CenterPanel.Login": "כניסה", - "CenterPanel.Share": "שיתוף", - "ColorOption.selectColor": "בחירת צבע {color}", - "Comment.delete": "מחיקה", - "CommentsList.send": "שליחה", - "ConfirmationDialog.cancel-action": "ביטול", - "ConfirmationDialog.confirm-action": "אישור", - "ContentBlock.Delete": "מחיקה", - "ContentBlock.DeleteAction": "מחיקה", - "ContentBlock.addElement": "הוספת {type}", - "ContentBlock.checkbox": "תיבת סימון", - "ContentBlock.divider": "מפריד", - "ContentBlock.editCardCheckbox": "תיבת סימון מתחלפת", - "ContentBlock.editCardCheckboxText": "עריכת מלל כרטיס", - "ContentBlock.editCardText": "עריכת מלל כרטיס" -} diff --git a/webapp/boards/i18n/hr.json b/webapp/boards/i18n/hr.json deleted file mode 100644 index 23f7f35791..0000000000 --- a/webapp/boards/i18n/hr.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrator", - "AdminBadge.TeamAdmin": "Tim administratora", - "AppBar.Tooltip": "Uklj./Isklj. povezane ploče", - "Attachment.Attachment-title": "Prilog", - "AttachmentBlock.DeleteAction": "izbriši", - "AttachmentBlock.addElement": "dodaj {type}", - "AttachmentBlock.delete": "Prilog je izbrisan.", - "AttachmentBlock.failed": "Nije moguće prenijeti datoteku jer je dosegnuta granica veličine datoteke.", - "AttachmentBlock.upload": "Prijenos priloga.", - "AttachmentBlock.uploadSuccess": "Prilog je prenesen.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Izbriši", - "AttachmentElement.download": "Preuzmi", - "AttachmentElement.upload-percentage": "Prijenos … ({uploadPercent} %)", - "BoardComponent.add-a-group": "+ Dodaj grupu", - "BoardComponent.delete": "Izbriši", - "BoardComponent.hidden-columns": "Skriveni stupci", - "BoardComponent.hide": "Sakrij", - "BoardComponent.new": "+ Novo", - "BoardComponent.no-property": "Bez svojstva {property}", - "BoardComponent.no-property-title": "Elementi s praznim svojstvom {property} smjestit će se ovdje. Ovaj se stupac ne može ukloniti.", - "BoardComponent.show": "Prikaži", - "BoardMember.schemeAdmin": "Administrator", - "BoardMember.schemeCommenter": "Komentator", - "BoardMember.schemeEditor": "Urednik", - "BoardMember.schemeNone": "Ništa", - "BoardMember.schemeViewer": "Gledatelj", - "BoardMember.unlinkChannel": "Odspoji", - "BoardPage.newVersion": "Dostupna je nova verzija za „Ploče”. Pritisni ovdje za ponovno učitavanje.", - "BoardPage.syncFailed": "Ploča se može izbrisati ili pristup opozvati.", - "BoardTemplateSelector.add-template": "Stvori novi predložak", - "BoardTemplateSelector.create-empty-board": "Stvori praznu ploču", - "BoardTemplateSelector.delete-template": "Izbriši", - "BoardTemplateSelector.description": "Za početak odaberi predložak. Prilagodi predložak kako bi odgovarao tvojim potrebama ili stvori praznu ploču.", - "BoardTemplateSelector.edit-template": "Uredi", - "BoardTemplateSelector.plugin.no-content-description": "Dodaj ploču u bočnu traku koristeći bilo koji od niže dolje definiranih predložaka ili počni ispočetka.", - "BoardTemplateSelector.plugin.no-content-title": "Stvori ploču", - "BoardTemplateSelector.title": "Stvori ploču", - "BoardTemplateSelector.use-this-template": "Koristi ovaj predložak", - "BoardsSwitcher.Title": "Pronađi ploče", - "BoardsUnfurl.Limited": "Dodatni detalji su skriveni jer je kartica arhivirana", - "BoardsUnfurl.Remainder": "+ još {remainder}", - "BoardsUnfurl.Updated": "Aktulaizirano {time}", - "Calculations.Options.average.displayName": "Prosjek", - "Calculations.Options.average.label": "Prosjek", - "Calculations.Options.count.displayName": "Broji", - "Calculations.Options.count.label": "Broji", - "Calculations.Options.countChecked.displayName": "Označeno", - "Calculations.Options.countChecked.label": "Broji označene", - "Calculations.Options.countUnchecked.displayName": "Neoznačeno", - "Calculations.Options.countUnchecked.label": "Broji neoznačene", - "Calculations.Options.countUniqueValue.displayName": "Jedinstveno", - "Calculations.Options.countUniqueValue.label": "Broji jedinstvene vrijednosti", - "Calculations.Options.countValue.displayName": "Vrijednosti", - "Calculations.Options.countValue.label": "Broji vrijednost", - "Calculations.Options.dateRange.displayName": "Raspon", - "Calculations.Options.dateRange.label": "Raspon", - "Calculations.Options.earliest.displayName": "Najraniji", - "Calculations.Options.earliest.label": "Najraniji", - "Calculations.Options.latest.displayName": "Najnoviji", - "Calculations.Options.latest.label": "Najnoviji", - "Calculations.Options.max.displayName": "Maks.", - "Calculations.Options.max.label": "Maks.", - "Calculations.Options.median.displayName": "Medijan", - "Calculations.Options.median.label": "Medijan", - "Calculations.Options.min.displayName": "Min.", - "Calculations.Options.min.label": "Min.", - "Calculations.Options.none.displayName": "Izračunaj", - "Calculations.Options.none.label": "Ništa", - "Calculations.Options.percentChecked.displayName": "Provjereno", - "Calculations.Options.percentChecked.label": "Provjeren postotak", - "Calculations.Options.percentUnchecked.displayName": "Neprovjereno", - "Calculations.Options.percentUnchecked.label": "Neprovjeren postotak", - "Calculations.Options.range.displayName": "Raspon", - "Calculations.Options.range.label": "Raspon", - "Calculations.Options.sum.displayName": "Zbroj", - "Calculations.Options.sum.label": "Zbroj", - "CalendarCard.untitled": "Bez naslova", - "CardActionsMenu.copiedLink": "Kopirano!", - "CardActionsMenu.copyLink": "Kopiraj poveznicu", - "CardActionsMenu.delete": "Izbriši", - "CardActionsMenu.duplicate": "Dupliciraj", - "CardBadges.title-checkboxes": "Označiva polja", - "CardBadges.title-comments": "Komentari", - "CardBadges.title-description": "Ova kartica ima opis", - "CardDetail.Attach": "Priloži", - "CardDetail.Follow": "Prati", - "CardDetail.Following": "Pratiš", - "CardDetail.add-content": "Dodaj sadržaj", - "CardDetail.add-icon": "Dodaj ikonu", - "CardDetail.add-property": "+ Dodaj svojstvo", - "CardDetail.addCardText": "dodaj tekst kartice", - "CardDetail.limited-body": "Nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.", - "CardDetail.limited-button": "Nadogradi", - "CardDetail.limited-title": "Ova je kartica skrivena", - "CardDetail.moveContent": "Pomakni sadržaj kartice", - "CardDetail.new-comment-placeholder": "Dodaj komentar …", - "CardDetailProperty.confirm-delete-heading": "Potvrdi brisanje svojstva", - "CardDetailProperty.confirm-delete-subtext": "Stvarno želiš izbrisati svojstvo „{propertyName}”? Brisanjem će se izbrisati svojstvo sa svih kartica na ovoj ploči.", - "CardDetailProperty.confirm-property-name-change-subtext": "Stvarno želiš promijeniti svojstvo „{propertyName}” {customText}? To će utjecati na vrijednosti na {numOfCards} kartica na ovoj ploči i može prouzročiti gubitak podataka.", - "CardDetailProperty.confirm-property-type-change": "Potvrdi promjenu vrste svojstva", - "CardDetailProperty.delete-action-button": "Izbriši", - "CardDetailProperty.property-change-action-button": "Promijeni svojstvo", - "CardDetailProperty.property-changed": "Promjena svojstva uspjela!", - "CardDetailProperty.property-deleted": "Svojstvo {propertyName} uspješno izbrisano!", - "CardDetailProperty.property-name-change-subtext": "vrste „{oldPropType}” u „{newPropType}”", - "CardDetial.limited-link": "Saznaj više o našim tarifma.", - "CardDialog.delete-confirmation-dialog-attachment": "Potvrdi brisanje priloga", - "CardDialog.delete-confirmation-dialog-button-text": "Izbriši", - "CardDialog.delete-confirmation-dialog-heading": "Potvrdi brisanje kartice", - "CardDialog.editing-template": "Uređuješ predložak.", - "CardDialog.nocard": "Ova kartica ne postoji ili je nedostupna.", - "Categories.CreateCategoryDialog.CancelText": "Odustani", - "Categories.CreateCategoryDialog.CreateText": "Stvori", - "Categories.CreateCategoryDialog.Placeholder": "Zadaj ime kategoriji", - "Categories.CreateCategoryDialog.UpdateText": "Aktualiziraj", - "CenterPanel.Login": "Prijava", - "CenterPanel.Share": "Dijeli", - "ChannelIntro.CreateBoard": "Stvori ploču", - "ColorOption.selectColor": "Odaberi boju {color}", - "Comment.delete": "Izbriši", - "CommentsList.send": "Pošalji", - "ConfirmPerson.empty": "Prazno", - "ConfirmPerson.search": "Traži …", - "ConfirmationDialog.cancel-action": "Odustani", - "ConfirmationDialog.confirm-action": "Potvrdi", - "ContentBlock.Delete": "Izbriši", - "ContentBlock.DeleteAction": "izbriši", - "ContentBlock.addElement": "dodaj {type}", - "ContentBlock.checkbox": "označivo polje", - "ContentBlock.divider": "razdjeljivač", - "ContentBlock.editCardCheckbox": "uklj./isklj. označivo polje", - "ContentBlock.editCardCheckboxText": "uredi tekst kartice", - "ContentBlock.editCardText": "uredi tekst kartice", - "ContentBlock.editText": "Uredi tekst …", - "ContentBlock.image": "slika", - "ContentBlock.insertAbove": "Umetni iznad", - "ContentBlock.moveBlock": "premjesti sadržaj kartice", - "ContentBlock.moveDown": "Pomakni dolje", - "ContentBlock.moveUp": "Pomakni gore", - "ContentBlock.text": "tekst", - "DateFilter.empty": "Prazno", - "DateRange.clear": "Isprazni", - "DateRange.empty": "Prazno", - "DateRange.endDate": "Datum kraja", - "DateRange.today": "Danas", - "DeleteBoardDialog.confirm-cancel": "Odustani", - "DeleteBoardDialog.confirm-delete": "Izbriši", - "DeleteBoardDialog.confirm-info": "Stvarno želiš izbrisati ploču „{boardTitle}”? Brisanjem će se izbrisati sve kartice na ploči.", - "DeleteBoardDialog.confirm-info-template": "Stvrano želiš izbrisati predložak ploče „{boardTitle}”?", - "DeleteBoardDialog.confirm-tite": "Potvrdi brisanje ploče", - "DeleteBoardDialog.confirm-tite-template": "Potvrdi brisanje predloška za ploče", - "Dialog.closeDialog": "Zatvori dijalog", - "EditableDayPicker.today": "Danas", - "Error.mobileweb": "Web podrška za mobilne uređaje trenutačno se nalazi u ranoj beta verziji. Nekih funkcionalsnosti možda još nema.", - "Error.websocket-closed": "Veza s websocketom je zatvorena, veza je prekinuta. Ako problem ustraje, provjeri konfiguraciju poslužitelja ili web proxyja.", - "Filter.contains": "sadrži", - "Filter.ends-with": "završava sa", - "Filter.includes": "uključuje", - "Filter.is": "je", - "Filter.is-after": "je nakon", - "Filter.is-before": "je prije", - "Filter.is-empty": "je prazno", - "Filter.is-not-empty": "nije prazno", - "Filter.is-not-set": "nije postavljeno", - "Filter.is-set": "je postavljeno", - "Filter.isafter": "je nakon", - "Filter.isbefore": "je prije", - "Filter.not-contains": "ne sadrži", - "Filter.not-ends-with": "ne završava sa", - "Filter.not-includes": "ne uključuje", - "Filter.not-starts-with": "ne počinje sa", - "Filter.starts-with": "počinje sa", - "FilterByText.placeholder": "filtriraj tekst", - "FilterComponent.add-filter": "+ Dodaj filtar", - "FilterComponent.delete": "Izbriši", - "FilterValue.empty": "(prazno)", - "FindBoardsDialog.IntroText": "Traži ploče", - "FindBoardsDialog.NoResultsFor": "Nema rezultata za „{searchQuery}”", - "FindBoardsDialog.NoResultsSubtext": "Provjeri pravopis ili pretraži s jednim drugim pojmom.", - "FindBoardsDialog.SubTitle": "Utipkaj ime za pronalaženje ploče. Koristi GORE/DOLJE za pretraživanje. ENTER za odabiranje, ESC za prekid", - "FindBoardsDialog.Title": "Pronađi ploče", - "GroupBy.hideEmptyGroups": "Sakrij {count} prazne grupe", - "GroupBy.showHiddenGroups": "Prikaži {count} skrivene grupe", - "GroupBy.ungroup": "Razgrupiraj", - "HideBoard.MenuOption": "Sakrij ploču", - "KanbanCard.untitled": "Bez naslova", - "MentionSuggestion.is-not-board-member": "(nije član u ploči)", - "Mutator.new-board-from-template": "nova ploča iz predloška", - "Mutator.new-card-from-template": "nova kartica iz predloška", - "Mutator.new-template-from-card": "novi predložak iz kartice", - "OnboardingTour.AddComments.Body": "Probleme možeš komentirati. Možeš čak i @spomenuti svoje Mattermost kolege za privlačenje njihove pozornosti.", - "OnboardingTour.AddComments.Title": "Dodaj komentare", - "OnboardingTour.AddDescription.Body": "Dodaj opis za tvoju karticu kako bi tvoji članovi tima znali o čemu se radi.", - "OnboardingTour.AddDescription.Title": "Dodaj opis", - "OnboardingTour.AddProperties.Body": "Dodaj razna svojstva karticama kako bi bile još snažnije.", - "OnboardingTour.AddProperties.Title": "Dodaj svojstva", - "OnboardingTour.AddView.Body": "Prijeđi ovamo za stvaranje novog prikaza za organiziranje tvoje ploče koristeći različite rasporede.", - "OnboardingTour.AddView.Title": "Dodaj novi prikaz", - "OnboardingTour.CopyLink.Body": "Svoje kartice možeš dijeliti s članovima tima pomuću kopiranja i umetanja poveznice u kanal, izravnu poruku ili grupnu poruku.", - "OnboardingTour.CopyLink.Title": "Kopiraj poveznicu", - "OnboardingTour.OpenACard.Body": "Otvori jednu karticu i istraži načine kako ti Ploče mogu pomoći organizirati tvoj rad.", - "OnboardingTour.OpenACard.Title": "Otvori jednu karticu", - "OnboardingTour.ShareBoard.Body": "Tvoju ploču možeš dijeliti interno, unutar tvog tima ili je javno objaviti radi vidljivosti izvan tvoje organizacije.", - "OnboardingTour.ShareBoard.Title": "Dijeli ploču", - "PersonProperty.board-members": "Članovi ploče", - "PersonProperty.me": "Ja", - "PersonProperty.non-board-members": "Ne članovi ploče", - "PropertyMenu.Delete": "Izbriši", - "PropertyMenu.changeType": "Promijei vrstu svojstva", - "PropertyMenu.selectType": "Odaberi vrstu svojstva", - "PropertyMenu.typeTitle": "Vrsta", - "PropertyType.Checkbox": "Označivo polje", - "PropertyType.CreatedBy": "Stvoreno od", - "PropertyType.CreatedTime": "Vrijeme stvaranja", - "PropertyType.Date": "Datum", - "PropertyType.Email": "E-mail adresa", - "PropertyType.MultiPerson": "Više osoba", - "PropertyType.MultiSelect": "Višestruki odabir", - "PropertyType.Number": "Broj", - "PropertyType.Person": "Osoba", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Odaberi", - "PropertyType.Text": "Tekst", - "PropertyType.Unknown": "Nepoznato", - "PropertyType.UpdatedBy": "Autor zadnjeg aktualiziranja", - "PropertyType.UpdatedTime": "Vrijme zadnjeg aktualiziranja", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Prazno", - "RegistrationLink.confirmRegenerateToken": "Ovo će poništiti prethodno dijeljene poveznice. Nastaviti?", - "RegistrationLink.copiedLink": "Kopirano!", - "RegistrationLink.copyLink": "Kopiraj poveznicu", - "RegistrationLink.description": "Dijeli ovu poveznicu kako bi drugi mogli stvoriti račune:", - "RegistrationLink.regenerateToken": "Ponovo generiraj token", - "RegistrationLink.tokenRegenerated": "Poveznica za registraciju je ponovo generirana", - "ShareBoard.PublishDescription": "Objavi i dijeli poveznicu „samo za čitanje” sa svima na webu.", - "ShareBoard.PublishTitle": "Objavi na webu", - "ShareBoard.ShareInternal": "Dijeli interno", - "ShareBoard.ShareInternalDescription": "Korisnici koji imaju prava moći će koristiti ovu poveznicu.", - "ShareBoard.Title": "Dijeli ploču", - "ShareBoard.confirmRegenerateToken": "Ovo će poništiti prethodno dijeljene poveznice. Nastaviti?", - "ShareBoard.copiedLink": "Kopirano!", - "ShareBoard.copyLink": "Kopiraj poveznicu", - "ShareBoard.regenerate": "Ponovo generiraj token", - "ShareBoard.searchPlaceholder": "Traži ljude", - "ShareBoard.teamPermissionsText": "Svatko u timu {teamName}", - "ShareBoard.tokenRegenrated": "Token je ponovo generiran", - "ShareBoard.userPermissionsRemoveMemberText": "Ukloni člana", - "ShareBoard.userPermissionsYouText": "(Ti)", - "ShareTemplate.Title": "Dijeli predložak", - "ShareTemplate.searchPlaceholder": "Traži osobe", - "Sidebar.about": "O programu Focalboard", - "Sidebar.add-board": "+ Dodaj ploču", - "Sidebar.changePassword": "Promijeni lozinku", - "Sidebar.delete-board": "Izbriši ploču", - "Sidebar.duplicate-board": "Dupliciraj ploču", - "Sidebar.export-archive": "Izvezi arhivu", - "Sidebar.import": "Uvezi", - "Sidebar.import-archive": "Uvezi arhivu", - "Sidebar.invite-users": "Pozovi korisnika", - "Sidebar.logout": "Odjavi se", - "Sidebar.new-category.badge": "Nova", - "Sidebar.new-category.drag-boards-cta": "Povuci ploče ovamo …", - "Sidebar.no-boards-in-category": "Nema ploča u kategoriji", - "Sidebar.product-tour": "Pregled proizvoda", - "Sidebar.random-icons": "Slučajne ikone", - "Sidebar.set-language": "Postavi jezik", - "Sidebar.set-theme": "Postavi temu", - "Sidebar.settings": "Postavke", - "Sidebar.template-from-board": "Novi predložak iz ploče", - "Sidebar.untitled-board": "(Ploča bez naslova)", - "Sidebar.untitled-view": "(Neimenovani prikaz)", - "SidebarCategories.BlocksMenu.Move": "Premjesti u …", - "SidebarCategories.CategoryMenu.CreateNew": "Stvori novu kategoriju", - "SidebarCategories.CategoryMenu.Delete": "Izbriši kategoriju", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Ploče u kategoriji {categoryName} vratit će se u kategorije ploča. Nećeš biti uklonjen/a s nijedne ploče.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Izbrisati ovu kategoriju?", - "SidebarCategories.CategoryMenu.Update": "Preimenuj kategoriju", - "SidebarTour.ManageCategories.Body": "Stvori vlastite kategorije i upravljaj njima. Kategorije se spremaju za svakog korisnika zasebno, tako da premještanje ploče u tvoju kategoriju neće utjecati na druge članove koji koriste istu ploču.", - "SidebarTour.ManageCategories.Title": "Upravljaj kategorijama", - "SidebarTour.SearchForBoards.Body": "Otvori sklopku ploča (Cmd/Ctrl + K) za brzo pretraživanje i dodavanje ploča u bočnu traku.", - "SidebarTour.SearchForBoards.Title": "Traži ploče", - "SidebarTour.SidebarCategories.Body": "Sve tvoje ploče se sada nalaze u tvojoj novoj bočnoj traci. Nema više prebacivanja između radnih prostora. Jednokratne prilagođene kategorije temeljene na tvojim prethodnim radnim prostorima su možda automatski stvorene tijekom nadogradnje na v7.2. Ako želiš, možeš ih ukloniti ili urediti.", - "SidebarTour.SidebarCategories.Link": "Saznaj više", - "SidebarTour.SidebarCategories.Title": "Kategorije u bočnoj traci", - "SiteStats.total_boards": "Ukupno ploča", - "SiteStats.total_cards": "Ukupno kartica", - "TableComponent.add-icon": "Dodaj ikonu", - "TableComponent.name": "Ime", - "TableComponent.plus-new": "+ Novo", - "TableHeaderMenu.delete": "Izbriši", - "TableHeaderMenu.duplicate": "Dupliciraj", - "TableHeaderMenu.hide": "Sakrij", - "TableHeaderMenu.insert-left": "Umetni lijevo", - "TableHeaderMenu.insert-right": "Umetni desno", - "TableHeaderMenu.sort-ascending": "Razvrstaj uzlazno", - "TableHeaderMenu.sort-descending": "Razvrstaj silazno", - "TableRow.DuplicateCard": "dupliciraj karticu", - "TableRow.MoreOption": "Daljnje radnje", - "TableRow.open": "Otvori", - "TopBar.give-feedback": "Pošalji povratne informacije", - "URLProperty.copiedLink": "Kopirano!", - "URLProperty.copy": "Kopiraj", - "URLProperty.edit": "Uredi", - "UndoRedoHotKeys.canRedo": "Ponovi", - "UndoRedoHotKeys.canRedo-with-description": "Ponovi {description}", - "UndoRedoHotKeys.canUndo": "Poništi", - "UndoRedoHotKeys.canUndo-with-description": "Poništi {description}", - "UndoRedoHotKeys.cannotRedo": "Ništa se ne može ponoviti", - "UndoRedoHotKeys.cannotUndo": "Ništa se ne može poništiti", - "ValueSelector.noOptions": "Nema opcija. Za dodavanje prve opcije počni tipkati!", - "ValueSelector.valueSelector": "Selektor vrijednosti", - "ValueSelectorLabel.openMenu": "Otvori izbornik", - "VersionMessage.help": "Provjeri što je novo u ovoj verziji.", - "VersionMessage.learn-more": "Saznaj više", - "View.AddView": "Dodaj prikaz", - "View.Board": "Ploča", - "View.DeleteView": "Izbriši prikaz", - "View.DuplicateView": "Dupliciraj prikaz", - "View.Gallery": "Galerija", - "View.NewBoardTitle": "Prikaz ploče", - "View.NewCalendarTitle": "Prikaz kalendara", - "View.NewGalleryTitle": "Prikaz galerije", - "View.NewTableTitle": "Prikaz tablice", - "View.NewTemplateDefaultTitle": "Predložak bez naslova", - "View.NewTemplateTitle": "Bez naslova", - "View.Table": "Tablica", - "ViewHeader.add-template": "Novi predložak", - "ViewHeader.delete-template": "Izbriši", - "ViewHeader.display-by": "Prikaži prema svojstvu: {property}", - "ViewHeader.edit-template": "Uredi", - "ViewHeader.empty-card": "Prazna kartica", - "ViewHeader.export-board-archive": "Izvezi arhivu ploče", - "ViewHeader.export-complete": "Izvoz je završen!", - "ViewHeader.export-csv": "Izvezi u CSV", - "ViewHeader.export-failed": "Izvoz neuspio!", - "ViewHeader.filter": "Filtar", - "ViewHeader.group-by": "Grupiraj prema svojstvu: {property}", - "ViewHeader.new": "Novo", - "ViewHeader.properties": "Svojstva", - "ViewHeader.properties-menu": "Izbornik svojstava", - "ViewHeader.search-text": "Traži kartice", - "ViewHeader.select-a-template": "Odaberi predložak", - "ViewHeader.set-default-template": "Postavi kao zadano", - "ViewHeader.sort": "Razvrstaj", - "ViewHeader.untitled": "Bez naslova", - "ViewHeader.view-header-menu": "Prikaz izbornika zaglavlja", - "ViewHeader.view-menu": "Prikaz izbornika", - "ViewLimitDialog.Heading": "Ograničenje prikaza po ploči dosegnuta", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Nadogradi", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Obavijesti aministratora", - "ViewLimitDialog.Subtext.Admin": "Nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Saznaj više o našim tarifama.", - "ViewLimitDialog.Subtext.RegularUser": "Obavijesti svog administratora da nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.", - "ViewLimitDialog.UpgradeImg.AltText": "nadogradi sliku", - "ViewLimitDialog.notifyAdmin.Success": "Tvoj je administrator obaviješten", - "ViewTitle.hide-description": "sakrij opis", - "ViewTitle.pick-icon": "Odaberi ikonu", - "ViewTitle.random-icon": "Slučajno", - "ViewTitle.remove-icon": "Ukloni ikonu", - "ViewTitle.show-description": "prikaži opis", - "ViewTitle.untitled-board": "Bezimena ploča", - "WelcomePage.Description": "„Ploče” je alat za upravljanje projektima koji pomaže definirati, organizirati, pratiti i upravljati radovima timova, koristeći poznati Kanban prikaz ploče.", - "WelcomePage.Explore.Button": "Uvod u rad programa", - "WelcomePage.Heading": "Dobro došao, dobro došla u „Ploče”", - "WelcomePage.NoThanks.Text": "Ne hvala", - "WelcomePage.StartUsingIt.Text": "Počni ga koristiti", - "Workspace.editing-board-template": "Uređuješ predložak ploče.", - "badge.guest": "Gost", - "boardPage.confirm-join-button": "Pridruži se", - "boardPage.confirm-join-text": "Pridružit ćeš se privatnoj ploči bez da te je administrator ploče izričito dodao. Stvarno se želiš pridružiti ovoj privatnoj ploči?", - "boardPage.confirm-join-title": "Pridruži se privatnoj ploči", - "boardSelector.confirm-link-board": "Poveži ploču s kanalom", - "boardSelector.confirm-link-board-button": "Da, poveži ploču", - "boardSelector.confirm-link-board-subtext": "Kad povežeš ploču „{boardName}” s kanalom, svi članovi kanala (postojeći i novi) moći će je uređivati. To ne vrijedi za članove koji su gosti. Vezu između ploče i kanala možeš raskinuti u bilo kojem trenutku.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Kad povežeš ploču „{boardName}” s kanalom, svi članovi kanala (postojeći i novi) moći će ga uređivati. To ne vrijedi za članove koji su gosti.{lineBreak}Ova je ploča trenutačno povezana s drugim kanalom. Veza će se raskinuti ako odlučiš je ovdje povezati.", - "boardSelector.create-a-board": "Stvori ploču", - "boardSelector.link": "Poveži", - "boardSelector.search-for-boards": "Traži ploče", - "boardSelector.title": "Poveži ploče", - "boardSelector.unlink": "Odspoji", - "calendar.month": "Mjesec", - "calendar.today": "DANAS", - "calendar.week": "Tjedan", - "centerPanel.undefined": "Bez {propertyName}", - "centerPanel.unknown-user": "Nepoznat korisnik", - "cloudMessage.learn-more": "Saznaj više", - "createImageBlock.failed": "Nije moguće prenijeti ovu datoteku jer je dosegnuta granica veličine datoteke.", - "default-properties.badges": "Komentari i opis", - "default-properties.title": "Naslov", - "error.back-to-home": "Natrag na početnu stranicu", - "error.back-to-team": "Natrag u tim", - "error.board-not-found": "Ploča nije pronađena.", - "error.go-login": "Prijavi se", - "error.invalid-read-only-board": "Nemaš pristup ovoj ploči. Prijavi se za pristup pločama.", - "error.not-logged-in": "Tvoja sesija je možda istekla ili nisi prijavljen/a. Ponovo se prijavi za pristup pločama.", - "error.page.title": "Oprosti, dogodila se greška", - "error.team-undefined": "Nije valjani tim.", - "error.unknown": "Dogodila se greška.", - "generic.previous": "Prethodno", - "guest-no-board.subtitle": "Još nemaš pristup nijednoj ploči u ovom timu, pričekaj dok te netko ne doda u bilo koju ploču.", - "guest-no-board.title": "Još nema ploča", - "imagePaste.upload-failed": "Neke datoteke nisu prenesene jer je dosegnuta granica veličine datoteke.", - "limitedCard.title": "Skrivene kartice", - "login.log-in-button": "Prijavi se", - "login.log-in-title": "Prijavi se", - "login.register-button": "ili stvori račun, ako ga još nemaš", - "new_channel_modal.create_board.empty_board_description": "Stvori novu praznu ploču", - "new_channel_modal.create_board.empty_board_title": "Prazna ploča", - "new_channel_modal.create_board.select_template_placeholder": "Odaberi predložak", - "new_channel_modal.create_board.title": "Stvori ploču za ovaj kanal", - "notification-box-card-limit-reached.close-tooltip": "Postavi pripravno stanje na 10 dana", - "notification-box-card-limit-reached.contact-link": "obavijesti svog administratora", - "notification-box-card-limit-reached.link": "Nadogradi na plaćenu tarifu", - "notification-box-card-limit-reached.title": "Skrivene kartice iz ploče: {cards}", - "notification-box-cards-hidden.title": "Ova radnja je sakrlia jednu drugu karticu", - "notification-box.card-limit-reached.not-admin.text": "Za pristupanje arhiviranim karticama, možeš {contactLink} za nadogradnju na plaćenu tarifu.", - "notification-box.card-limit-reached.text": "Dosegnuto je ograničenje broja kartica. Za pregled starijih kartica, {link}", - "person.add-user-to-board": "Dodaj korisničko ime {username} u ploču", - "person.add-user-to-board-confirm-button": "Dodaj u ploču", - "person.add-user-to-board-permissions": "Prava", - "person.add-user-to-board-question": "Želiš li dodati korisničko ime {username} u ploču?", - "person.add-user-to-board-warning": "{username} nije član ploče i neće primati obavijesti o njoj.", - "register.login-button": "ili se prijavi ako već imaš račun", - "register.signup-title": "Prijavi se na svoj račun", - "rhs-board-non-admin-msg": "Nisi administrator ploče", - "rhs-boards.add": "Dodaj", - "rhs-boards.dm": "DP", - "rhs-boards.gm": "GP", - "rhs-boards.header.dm": "ovu izravnu poruku", - "rhs-boards.header.gm": "ovu grupnu poruku", - "rhs-boards.last-update-at": "Zadnje aktualiziranje: {datetime}", - "rhs-boards.link-boards-to-channel": "Poveži ploče s kanalom {channelName}", - "rhs-boards.linked-boards": "Povezane ploče", - "rhs-boards.no-boards-linked-to-channel": "Do sada nije povezana nijedna ploča s kanalom {channelName}", - "rhs-boards.no-boards-linked-to-channel-description": "Ploče su alat za upravljanje projektima koji pomaže definirati, organizirati, pratiti i upravljati rad timova, koristeći poznati kanban prikaz ploče.", - "rhs-boards.unlink-board": "Odspoji ploču", - "rhs-boards.unlink-board1": "Odspoji ploču", - "rhs-channel-boards-header.title": "Ploče", - "share-board.publish": "Objavi", - "share-board.share": "Dijeli", - "shareBoard.channels-select-group": "Kanali", - "shareBoard.confirm-change-team-role.body": "Svatko na ovoj ploči s manje prava od uloge „{role}” će sada biti promaknut u {role}. Stvarno želiš promijeniti najmanju ulogu za ploču?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Promijeni najmanju ulogu ploče", - "shareBoard.confirm-change-team-role.title": "Promijeni najmanju ulogu ploče", - "shareBoard.confirm-link-channel": "Poveži ploču s kanalom", - "shareBoard.confirm-link-channel-button": "Poveži kanal", - "shareBoard.confirm-link-channel-button-with-other-channel": "Odspoji i poveži ovamo", - "shareBoard.confirm-link-channel-subtext": "Kad povežeš kanal s pločom, svi članovi kanala (postojeći i novi) moći će ga uređivati. To ne vrijedi za članove koji su gosti.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Kad povežeš kanal s pločom, svi članovi kanala (postojeći i novi) moći će ga uređivati. To ne vrijedi za članove koji su gosti.{lineBreak}Ova je ploča trenutačno povezana s drugim kanalom. Veza će se raskinuti ako odlučiš je ovdje povezati.", - "shareBoard.confirm-unlink.body": "Kad odspojiš kanal od ploče, svi članovi kanala (postojeći i novi) izgubit će pristup kanalu, osim ako su im se prava dala zasebno.", - "shareBoard.confirm-unlink.confirmBtnText": "Odspoji kanal", - "shareBoard.confirm-unlink.title": "Odspoji kanal od ploče", - "shareBoard.lastAdmin": "Ploče moraju imati barem jednog administratora", - "shareBoard.members-select-group": "Članovi", - "shareBoard.unknown-channel-display-name": "Nepoznat kanal", - "tutorial_tip.finish_tour": "Gotovo", - "tutorial_tip.got_it": "Razumijem", - "tutorial_tip.ok": "Dalje", - "tutorial_tip.out": "Deaktiviraj ove savjete.", - "tutorial_tip.seen": "Ovaj savjet već poznaš?" -} diff --git a/webapp/boards/i18n/hu.json b/webapp/boards/i18n/hu.json deleted file mode 100644 index e835144850..0000000000 --- a/webapp/boards/i18n/hu.json +++ /dev/null @@ -1,439 +0,0 @@ -{ - "AppBar.Tooltip": "Kapcsolt táblák kapcsolása", - "Attachment.Attachment-title": "Melléklet", - "AttachmentBlock.DeleteAction": "törlés", - "AttachmentBlock.addElement": "{type} hozzáadása", - "AttachmentBlock.delete": "Melléklet sikeresen törölve.", - "AttachmentBlock.failed": "Nem sikerült feltölteni a fájlt. A csatolmány mérete túl nagy.", - "AttachmentBlock.upload": "Melléklet feltöltése.", - "AttachmentBlock.uploadSuccess": "A melléklet feltöltése sikeres.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Törlés", - "AttachmentElement.download": "Letöltés", - "AttachmentElement.upload-percentage": "Feltöltés...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Csoport hozzáadása", - "BoardComponent.delete": "Törlés", - "BoardComponent.hidden-columns": "Rejtett oszlopok", - "BoardComponent.hide": "Elrejtés", - "BoardComponent.new": "+ Új", - "BoardComponent.no-property": "Nincs {property}", - "BoardComponent.no-property-title": "Elemek üres {property} tulajdonsággal kerülnek ide. Ez az oszlop nem eltávolítható.", - "BoardComponent.show": "Mutat", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Véleményező", - "BoardMember.schemeEditor": "Szerkesztő", - "BoardMember.schemeNone": "Nincs", - "BoardMember.schemeViewer": "Megtekintő", - "BoardMember.unlinkChannel": "Leválasztás", - "BoardPage.newVersion": "Elérhető a Táblák egy új verziója, kattintson ide az újratöltéshez.", - "BoardPage.syncFailed": "A tábla törölve lett vagy hozzáférés vissza lett vonva.", - "BoardTemplateSelector.add-template": "Új sablon létrehozása", - "BoardTemplateSelector.create-empty-board": "Üres tábla készítése", - "BoardTemplateSelector.delete-template": "Törlés", - "BoardTemplateSelector.description": "Adjon hozzá egy táblát az oldalsávhoz az alább meghatározott sablonok bármelyikével, vagy kezdje elölről.", - "BoardTemplateSelector.edit-template": "Szerkesztés", - "BoardTemplateSelector.plugin.no-content-description": "Adjon hozzá egy táblát az oldalsávhoz az alább megadott sablonok bármelyikével, vagy kezdje elölről.", - "BoardTemplateSelector.plugin.no-content-title": "Tábla létrehozása", - "BoardTemplateSelector.title": "Tábla létrehozása", - "BoardTemplateSelector.use-this-template": "Használja ezt a sablont", - "BoardsSwitcher.Title": "Táblák keresése", - "BoardsUnfurl.Limited": "A kártya archiválása miatt a további részletek rejtve vannak", - "BoardsUnfurl.Remainder": "+{remainder} további", - "BoardsUnfurl.Updated": "Frissítve {time}", - "Calculations.Options.average.displayName": "Átlag", - "Calculations.Options.average.label": "Átlag", - "Calculations.Options.count.displayName": "Mennyiség", - "Calculations.Options.count.label": "Mennyiség", - "Calculations.Options.countChecked.displayName": "Kijelölt", - "Calculations.Options.countChecked.label": "Kijelöltek száma", - "Calculations.Options.countUnchecked.displayName": "Nem kijelölt", - "Calculations.Options.countUnchecked.label": "Nem kijelöltek száma", - "Calculations.Options.countUniqueValue.displayName": "Egyedi", - "Calculations.Options.countUniqueValue.label": "Egyedi értékek száma", - "Calculations.Options.countValue.displayName": "Értékek", - "Calculations.Options.countValue.label": "Értékek száma", - "Calculations.Options.dateRange.displayName": "Tartomány", - "Calculations.Options.dateRange.label": "Tartomány", - "Calculations.Options.earliest.displayName": "Korábbi", - "Calculations.Options.earliest.label": "Korábbi", - "Calculations.Options.latest.displayName": "Későbbi", - "Calculations.Options.latest.label": "Későbbi", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Közép", - "Calculations.Options.median.label": "Közép", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Kiszámítás", - "Calculations.Options.none.label": "Nincs", - "Calculations.Options.percentChecked.displayName": "Kijelölt", - "Calculations.Options.percentChecked.label": "Kijelöltek aránya", - "Calculations.Options.percentUnchecked.displayName": "Nem kijelölt", - "Calculations.Options.percentUnchecked.label": "Nem kijelöltek aránya", - "Calculations.Options.range.displayName": "Tartomány", - "Calculations.Options.range.label": "Tartomány", - "Calculations.Options.sum.displayName": "Összeg", - "Calculations.Options.sum.label": "Összeg", - "CalendarCard.untitled": "Névtelen", - "CardActionsMenu.copiedLink": "Másolva!", - "CardActionsMenu.copyLink": "Link másolása", - "CardActionsMenu.delete": "Törlés", - "CardActionsMenu.duplicate": "Duplikálás", - "CardBadges.title-checkboxes": "Teendők", - "CardBadges.title-comments": "Megjegyzések", - "CardBadges.title-description": "Ennek a kártyának van leírása", - "CardDetail.Attach": "Csatolás", - "CardDetail.Follow": "Követés", - "CardDetail.Following": "Követés", - "CardDetail.add-content": "Tartalom hozzáadása", - "CardDetail.add-icon": "Ikon hozzáadása", - "CardDetail.add-property": "+ Tulajdonság hozzáadása", - "CardDetail.addCardText": "kártya szövegének hozzáadása", - "CardDetail.limited-body": "Az archivált kártyák megtekintéséhez, a táblánkénti korlátlan megtekintéshez, korlátlan számú kártyához és még sok máshoz váltson a Professional vagy Enterprise csomagra.", - "CardDetail.limited-button": "Kiadás váltása", - "CardDetail.limited-title": "Ez a kártya rejtett", - "CardDetail.moveContent": "Kártya tartalmának mozgatása", - "CardDetail.new-comment-placeholder": "Megjegyzés hozzáadása...", - "CardDetailProperty.confirm-delete-heading": "Tulajdonság törlésének jóváhagyása", - "CardDetailProperty.confirm-delete-subtext": "Biztos benne, hogy törölni szeretné a \"{propertyName}\" tulajdonságot? A törléssel a tulajdonság minden kártyáról el lesz távolítva.", - "CardDetailProperty.confirm-property-name-change-subtext": "Biztosan szeretné megváltoztatni a \"{propertyName}\" tulajdonság {customText}? Ez érint {numOfCards} kártya adatát ebben a táblában, és akár adatvesztéssel is járhat.", - "CardDetailProperty.confirm-property-type-change": "Hagyja jóvá a tulajdonság típusának módosítását", - "CardDetailProperty.delete-action-button": "Törlés", - "CardDetailProperty.property-change-action-button": "Tulajdonság módosítása", - "CardDetailProperty.property-changed": "Tulajdonság sikeresen módosult!", - "CardDetailProperty.property-deleted": "{propertyName} törlése sikeres!", - "CardDetailProperty.property-name-change-subtext": "típus erről: \"{oldPropType}\" erre: \"{newPropType}\"", - "CardDetial.limited-link": "Tudjon meg többet csomagjainkról.", - "CardDialog.delete-confirmation-dialog-attachment": "Erősítse meg a csatolmány törlését!", - "CardDialog.delete-confirmation-dialog-button-text": "Törlés", - "CardDialog.delete-confirmation-dialog-heading": "Hagyja jóvá a kártya törlését!", - "CardDialog.editing-template": "Ön egy sablont szerkeszt.", - "CardDialog.nocard": "Ez a kártya nem létezik vagy elérhetetlen.", - "Categories.CreateCategoryDialog.CancelText": "Mégsem", - "Categories.CreateCategoryDialog.CreateText": "Létrehozás", - "Categories.CreateCategoryDialog.Placeholder": "Nevezze el a kategóriáját", - "Categories.CreateCategoryDialog.UpdateText": "Frissítés", - "CenterPanel.Login": "Bejelentkezés", - "CenterPanel.Share": "Megosztás", - "ColorOption.selectColor": "{color} szín kiválasztása", - "Comment.delete": "Törlés", - "CommentsList.send": "Küldés", - "ConfirmationDialog.cancel-action": "Mégsem", - "ConfirmationDialog.confirm-action": "Jóváhagyás", - "ContentBlock.Delete": "Törlés", - "ContentBlock.DeleteAction": "törlés", - "ContentBlock.addElement": "{type} hozzáadása", - "ContentBlock.checkbox": "jelölőnégyzet", - "ContentBlock.divider": "elválasztó", - "ContentBlock.editCardCheckbox": "három állású jelölőnégyzet", - "ContentBlock.editCardCheckboxText": "kártya szövegének szerkesztése", - "ContentBlock.editCardText": "kártya szövegének szerkesztése", - "ContentBlock.editText": "Szöveg szerkesztése...", - "ContentBlock.image": "kép", - "ContentBlock.insertAbove": "Beszúrás fölé", - "ContentBlock.moveBlock": "kártya tartalmának áthelyezése", - "ContentBlock.moveDown": "Mozgatás le", - "ContentBlock.moveUp": "Mozgatás fel", - "ContentBlock.text": "szöveg", - "DateRange.clear": "Törlés", - "DateRange.empty": "Üres", - "DateRange.endDate": "Vége dátum", - "DateRange.today": "Ma", - "DeleteBoardDialog.confirm-cancel": "Mégsem", - "DeleteBoardDialog.confirm-delete": "Törlés", - "DeleteBoardDialog.confirm-info": "Biztos benne, hogy törölni szeretné a “{boardTitle}” táblát? A törlésével az összes benne lévő kártya is törlődni fog.", - "DeleteBoardDialog.confirm-info-template": "Biztos, hogy törölni szeretné a \"{boardTitle}\" tábla sablont?", - "DeleteBoardDialog.confirm-tite": "Tábla törlésének jóváhagyása", - "DeleteBoardDialog.confirm-tite-template": "Tábla sablon törlésének jóváhagyása", - "Dialog.closeDialog": "Ablak bezárása", - "EditableDayPicker.today": "Ma", - "Error.mobileweb": "Mobil web támogatás jelenleg előzetes béta állapotban van. Nem minden funkcionalitás érhető el.", - "Error.websocket-closed": "Websocket kapcsolat bezárult, kapcsolat megszakadt, Ha ez továbbra is fennáll, akkor ellenőrizze le a kiszolgáló vagy web proxy beállítását.", - "Filter.contains": "tartalmazza", - "Filter.ends-with": "végződik", - "Filter.includes": "tartalmazza", - "Filter.is": "egy", - "Filter.is-empty": "üres", - "Filter.is-not-empty": "nem üres", - "Filter.is-not-set": "nincs megadva", - "Filter.is-set": "meg van adva", - "Filter.not-contains": "nem tartalmazza", - "Filter.not-ends-with": "nem végződik", - "Filter.not-includes": "nem tartalmazza", - "Filter.not-starts-with": "nem kezdődik", - "Filter.starts-with": "kezdődik", - "FilterByText.placeholder": "szöveg szűrése", - "FilterComponent.add-filter": "+ Szűrő hozzáadása", - "FilterComponent.delete": "Törlés", - "FindBoardsDialog.IntroText": "Táblák keresése", - "FindBoardsDialog.NoResultsFor": "Nincs találat a \"{searchQuery}\" kereséshez", - "FindBoardsDialog.NoResultsSubtext": "Ellenőrizze az elgépelést vagy próbáljon egy új keresést.", - "FindBoardsDialog.SubTitle": "Gépeljen, hogy megtalálja a táblát. Használja a FEL/LE gombokat a böngészéshez. ENTER gombot a kiválasztáshoz és ESC gombot az eldobáshoz", - "FindBoardsDialog.Title": "Táblák keresése", - "GroupBy.hideEmptyGroups": "{count} üres csoport elrejtése", - "GroupBy.showHiddenGroups": "{count} rejtett csoport megjelenítése", - "GroupBy.ungroup": "Csoportosítás megszüntetése", - "HideBoard.MenuOption": "Tábla elrejtése", - "KanbanCard.untitled": "Névtelen", - "MentionSuggestion.is-not-board-member": "(nem tagja a táblának)", - "Mutator.new-board-from-template": "új tábla sablon alapján", - "Mutator.new-card-from-template": "új kártya sablonból", - "Mutator.new-template-from-card": "új sablon kártyából", - "OnboardingTour.AddComments.Body": "Hozzászólhat a témákhoz, sőt, más Mattermost felhasználó társát is @megemlítheti, hogy felhívja a figyelmüket.", - "OnboardingTour.AddComments.Title": "Megjegyzés hozzáadása", - "OnboardingTour.AddDescription.Body": "Adjon leírást a kártyájához, hogy csapattársai tudják, miről szól a kártya.", - "OnboardingTour.AddDescription.Title": "Leírás hozzáadása", - "OnboardingTour.AddProperties.Body": "Adjon hozzá különböző tulajdonságokat a kártyákhoz, hogy hatékonyabbá tegye őket!", - "OnboardingTour.AddProperties.Title": "Tulajdonságok hozzáadása", - "OnboardingTour.AddView.Body": "Menjen ide, és hozzon létre új nézetet, hogy különböző elrendezésekkel rendszerezze a tábláját.", - "OnboardingTour.AddView.Title": "Új nézet hozzáadása", - "OnboardingTour.CopyLink.Body": "Megoszthatja kártyáit csapattársaival a link másolásával és beillesztésével egy csatornán, közvetlen üzenetben vagy csoportos üzenetben.", - "OnboardingTour.CopyLink.Title": "Link másolása", - "OnboardingTour.OpenACard.Body": "Nyisson meg egy kártyát, hogy felfedezze, milyen hatékony módon segíthetnek a táblák a munkája megszervezésében.", - "OnboardingTour.OpenACard.Title": "Kártya megnyitása", - "OnboardingTour.ShareBoard.Body": "A táblát megoszthatja belsőleg, a csapatán belül, vagy nyilvánosan is közzéteheti, hogy a szervezeten kívül is látható legyen.", - "OnboardingTour.ShareBoard.Title": "Tábla megosztása", - "PersonProperty.board-members": "Tábla tagjai", - "PersonProperty.non-board-members": "Nem tábla tagok", - "PropertyMenu.Delete": "Törlés", - "PropertyMenu.changeType": "Tulajdonság típusának módosítása", - "PropertyMenu.selectType": "Tulajdonság típusának kiválasztása", - "PropertyMenu.typeTitle": "Típus", - "PropertyType.Checkbox": "Jelölőnégyzet", - "PropertyType.CreatedBy": "Létrehozta", - "PropertyType.CreatedTime": "Létrehozás ideje", - "PropertyType.Date": "Dátum", - "PropertyType.Email": "E-mail", - "PropertyType.MultiPerson": "Több személy", - "PropertyType.MultiSelect": "Több kiválasztós", - "PropertyType.Number": "Szám", - "PropertyType.Person": "Személy", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Kiválasztás", - "PropertyType.Text": "Szöveg", - "PropertyType.Unknown": "Ismeretlen", - "PropertyType.UpdatedBy": "Utoljára frissítette", - "PropertyType.UpdatedTime": "Utolsó frissítés ideje", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Üres", - "RegistrationLink.confirmRegenerateToken": "Ez érvényteleníteni fogja a korábban megosztott linkeket. Folytassuk?", - "RegistrationLink.copiedLink": "Másolt!", - "RegistrationLink.copyLink": "Link másolása", - "RegistrationLink.description": "Ossza meg ezt a linket másokkat a fiók létrehozásához:", - "RegistrationLink.regenerateToken": "Token újragenerálása", - "RegistrationLink.tokenRegenerated": "Regisztrációs link újragenerálva", - "ShareBoard.PublishDescription": "Csak olvasható link közzététele és megosztása mindenkivel a weben.", - "ShareBoard.PublishTitle": "Közzététel a webre", - "ShareBoard.ShareInternal": "Megosztás belsőleg", - "ShareBoard.ShareInternalDescription": "A jogosultságokkal rendelkező felhasználók használhatják ezt a linket.", - "ShareBoard.Title": "Tábla megosztása", - "ShareBoard.confirmRegenerateToken": "Ez érvényteleníteni fogja a korábban megosztott linkeket. Folytassuk?", - "ShareBoard.copiedLink": "Másolt!", - "ShareBoard.copyLink": "Link másolása", - "ShareBoard.regenerate": "Token újragenerálása", - "ShareBoard.searchPlaceholder": "Személyek és csatornák keresése", - "ShareBoard.teamPermissionsText": "Mindenki a {teamName} Csapatban", - "ShareBoard.tokenRegenrated": "Token újragenerálva", - "ShareBoard.userPermissionsRemoveMemberText": "Tag eltávolítása", - "ShareBoard.userPermissionsYouText": "(Ön)", - "ShareTemplate.Title": "Sablon megosztása", - "ShareTemplate.searchPlaceholder": "Személyek keresése", - "Sidebar.about": "Focalboard névjegye", - "Sidebar.add-board": "+ Tábla hozzáadása", - "Sidebar.changePassword": "Jelszó módosítása", - "Sidebar.delete-board": "Tábla törlése", - "Sidebar.duplicate-board": "Tábla duplikálása", - "Sidebar.export-archive": "Archiváltak exportálása", - "Sidebar.import": "Importálás", - "Sidebar.import-archive": "Archiváltak importálása", - "Sidebar.invite-users": "Felhasználók meghívása", - "Sidebar.logout": "Kijelentkezés", - "Sidebar.new-category.badge": "Új", - "Sidebar.new-category.drag-boards-cta": "Húzza a táblákat ide...", - "Sidebar.no-boards-in-category": "Nincsennek bent táblák", - "Sidebar.product-tour": "Termék bemutató", - "Sidebar.random-icons": "Véletlen ikonok", - "Sidebar.set-language": "Nyelv megadása", - "Sidebar.set-theme": "Téma megadása", - "Sidebar.settings": "Beállítások", - "Sidebar.template-from-board": "Új sablon a táblából", - "Sidebar.untitled-board": "(Névtelen tábla)", - "Sidebar.untitled-view": "(Névtelen nézet)", - "SidebarCategories.BlocksMenu.Move": "Áthelyezés...", - "SidebarCategories.CategoryMenu.CreateNew": "Új kategória létrehozása", - "SidebarCategories.CategoryMenu.Delete": "Kategória törlése", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "A {categoryName} kategóriában lévő táblák visszakerülnek a Táblák kategóriákba. Ön egyik táblából sem lesz eltávolítva.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Törli ezt a kategóriát?", - "SidebarCategories.CategoryMenu.Update": "Kategória átnevezése", - "SidebarTour.ManageCategories.Body": "Egyéni kategóriák létrehozása és kezelése. A kategóriák felhasználó-specifikusak, így egy tábla áthelyezése a kategóriájába nem befolyásolja az ugyanazt a táblát használó többi tagot.", - "SidebarTour.ManageCategories.Title": "Kategóriák kezelése", - "SidebarTour.SearchForBoards.Body": "A táblaváltó megnyitásával (Cmd/Ctrl + K) gyorsan kereshet és adhat hozzá táblákat az oldalsávjához.", - "SidebarTour.SearchForBoards.Title": "Tábla keresése", - "SidebarTour.SidebarCategories.Body": "Az összes tábláját mostantól az új oldalsávja alá rendezi. Nincs többé váltás a munkaterületek között. A v7.2 frissítés részeként automatikusan létrejöhettek az Ön számára a korábbi munkaterületek alapján létrehozott egyszeri egyéni kategóriák. Ezeket eltávolíthatja vagy szerkesztheti tetszése szerint.", - "SidebarTour.SidebarCategories.Link": "Tudjon meg többet", - "SidebarTour.SidebarCategories.Title": "Oldalsáv kategóriák", - "SiteStats.total_boards": "Összes tábla", - "SiteStats.total_cards": "Összes kártya", - "TableComponent.add-icon": "Ikon hozzáadása", - "TableComponent.name": "Név", - "TableComponent.plus-new": "+ Új", - "TableHeaderMenu.delete": "Törlés", - "TableHeaderMenu.duplicate": "Duplikálás", - "TableHeaderMenu.hide": "Elrejtés", - "TableHeaderMenu.insert-left": "Beillesztés balra", - "TableHeaderMenu.insert-right": "Beillesztés jobbra", - "TableHeaderMenu.sort-ascending": "Rendezés növekvő sorrendben", - "TableHeaderMenu.sort-descending": "Rendezés csökkenő sorrendben", - "TableRow.MoreOption": "További műveletek", - "TableRow.open": "Megnyitás", - "TopBar.give-feedback": "Visszajelzés", - "URLProperty.copiedLink": "Másolva!", - "URLProperty.copy": "Másolás", - "URLProperty.edit": "Szerkesztés", - "UndoRedoHotKeys.canRedo": "Újracsinálás", - "UndoRedoHotKeys.canRedo-with-description": "Újracsinálás {description}", - "UndoRedoHotKeys.canUndo": "Visszavonás", - "UndoRedoHotKeys.canUndo-with-description": "Visszavonás {description}", - "UndoRedoHotKeys.cannotRedo": "Nincs mit újracsinálni", - "UndoRedoHotKeys.cannotUndo": "Nincs mit visszavonni", - "ValueSelector.noOptions": "Nincsenek lehetőségek. Kezdjen el gépelni, hogy hozzáadja az elsőt!", - "ValueSelector.valueSelector": "Érték kiválasztó", - "ValueSelectorLabel.openMenu": "Menü megnyitása", - "VersionMessage.help": "Tekintse meg ezen verzió újdonságait.", - "View.AddView": "Nézet hozzáadása", - "View.Board": "Tábla", - "View.DeleteView": "Nézet törlése", - "View.DuplicateView": "Nézet duplikálása", - "View.Gallery": "Galéria", - "View.NewBoardTitle": "Tábla nézet", - "View.NewCalendarTitle": "Naptár nézet", - "View.NewGalleryTitle": "Galéria nézet", - "View.NewTableTitle": "Táblázat nézet", - "View.NewTemplateDefaultTitle": "Névtelen sablon", - "View.NewTemplateTitle": "Névtelen", - "View.Table": "Táblázat", - "ViewHeader.add-template": "Új sablon", - "ViewHeader.delete-template": "Törlés", - "ViewHeader.display-by": "Rendezés: {property}", - "ViewHeader.edit-template": "Szerkesztés", - "ViewHeader.empty-card": "Üres kártya", - "ViewHeader.export-board-archive": "Archivált tábla exportálása", - "ViewHeader.export-complete": "Exportálás kész!", - "ViewHeader.export-csv": "Exportálás CSV-be", - "ViewHeader.export-failed": "Exportálás meghiúsult!", - "ViewHeader.filter": "Szűrő", - "ViewHeader.group-by": "Csoportosítás: {property}", - "ViewHeader.new": "Új", - "ViewHeader.properties": "Tulajdonságok", - "ViewHeader.properties-menu": "Tulajdonságok menü", - "ViewHeader.search-text": "Kártya keresése", - "ViewHeader.select-a-template": "Sablon kiválasztása", - "ViewHeader.set-default-template": "Beállítás alapértelmezettnek", - "ViewHeader.sort": "Rendezés", - "ViewHeader.untitled": "Névtelen", - "ViewHeader.view-header-menu": "Fejléc menü megjelenítése", - "ViewHeader.view-menu": "Megtekintés menü", - "ViewLimitDialog.Heading": "Táblánkénti megtekintések korlátja elérve", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Előfizetés váltása", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Admin értesítése", - "ViewLimitDialog.Subtext.Admin": "A Professional vagy Enterprise csomagra való váltással korlátlan számú megtekintést kaphat táblánként, korlátlan számú kártyát és még sok mást.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Tudjon meg többet a csomagjainkról.", - "ViewLimitDialog.Subtext.RegularUser": "Értesítse a rendszergazdát, hogy frissíthessen a Professional vagy Enterprise csomagra, hogy korlátlan megtekintést kapjon táblánként, korlátlan számú kártyát és még többet.", - "ViewLimitDialog.UpgradeImg.AltText": "előfizetés váltás kép", - "ViewLimitDialog.notifyAdmin.Success": "A rendszergazdája értesítve lett", - "ViewTitle.hide-description": "leírás elrejtése", - "ViewTitle.pick-icon": "Válasszon ikont", - "ViewTitle.random-icon": "Véletlen", - "ViewTitle.remove-icon": "Ikon eltávolítása", - "ViewTitle.show-description": "leírás mutatása", - "ViewTitle.untitled-board": "Névtelen tábla", - "WelcomePage.Description": "A Táblák egy projekt kezelő segédeszköz ami segít azonosítani, rendezni, követni és vezetni a munkát csapatok között, egy ismerős kanban táblás nézet segítségével.", - "WelcomePage.Explore.Button": "Nézze meg a bemutatót", - "WelcomePage.Heading": "Üdvözöljük a Táblákban", - "WelcomePage.NoThanks.Text": "Nem, köszönöm, majd kitalálom magam", - "WelcomePage.StartUsingIt.Text": "Kezdjük el használni", - "Workspace.editing-board-template": "Ön egy sablon táblát szerkeszt.", - "badge.guest": "Vendég", - "boardSelector.confirm-link-board": "Kösse össze a táblát csatornával", - "boardSelector.confirm-link-board-button": "Igen, kösse össze a táblát", - "boardSelector.confirm-link-board-subtext": "Ha a \"{boardName}\" táblát összekapcsolja a csatornával, a csatorna minden tagja (meglévő és új) képes lesz szerkeszteni azt. Ez nem vonatkozik a vendég tagokra. A táblát bármikor leválaszthatja a csatornáról.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Amikor a \"{boardName}\" táblát összekapcsolja a csatornával, a csatorna minden tagja (meglévő és új) képes lesz szerkeszteni azt. Ez nem vonatkozik a vendég tagokra.{lineBreak} A tábla jelenleg egy másik csatornához van kapcsolva. Le lesz választva, amennyiben úgy dönt, hogy ide kapcsolja.", - "boardSelector.create-a-board": "Tábla létrehozása", - "boardSelector.link": "Összekapcsolás", - "boardSelector.search-for-boards": "Táblák keresése", - "boardSelector.title": "Táblák összekapcsolása", - "boardSelector.unlink": "Leválasztás", - "calendar.month": "Hónap", - "calendar.today": "MA", - "calendar.week": "Hét", - "cloudMessage.learn-more": "Tudjon meg többet", - "createImageBlock.failed": "Nem sikerült feltölteni a fájlt. Fájlméret korlát elérve.", - "default-properties.badges": "Megjegyzések és leírás", - "default-properties.title": "Cím", - "error.back-to-home": "Vissza a kezdőlapra", - "error.back-to-team": "Vissza a csapatba", - "error.board-not-found": "Tábla nem található.", - "error.go-login": "Bejelentkezés", - "error.invalid-read-only-board": "Önnek nincs hozzáférése ehhez a táblához. Jelentkezz be a Táblák eléréséhez.", - "error.not-logged-in": "Lehet, hogy lejárt a munkamenete, vagy nincs bejelentkezve. Jelentkezzen be újra a Táblákhoz való hozzáféréshez.", - "error.page.title": "Sajnálom, valami rosszul sikerült", - "error.team-undefined": "Nem egy érvényes csapat.", - "error.unknown": "Hiba lépett fel.", - "generic.previous": "Előző", - "guest-no-board.subtitle": "Ebben a csapatban még nincs hozzáférése egyik táblához sem, kérjük, várjon, amíg valaki felveszi Önt bármelyik táblához.", - "guest-no-board.title": "Még nincsenek táblák", - "imagePaste.upload-failed": "Néhány fájl nem került feltöltésre. Fájlméret korlát elérve", - "limitedCard.title": "Rejtett kártyák", - "login.log-in-button": "Bejelentkezés", - "login.log-in-title": "Bejelentkezés", - "login.register-button": "vagy hozzon létre egy fiókot ha még nincs", - "notification-box-card-limit-reached.close-tooltip": "Altatás 10 napig", - "notification-box-card-limit-reached.contact-link": "értesítheti a rendszergazdát", - "notification-box-card-limit-reached.link": "Váltson fizetős csomagra", - "notification-box-card-limit-reached.title": "{cards} kártya rejtve a táblán", - "notification-box-cards-hidden.title": "Ez a művelet elrejtett egy másik kártyát", - "notification-box.card-limit-reached.not-admin.text": "Az archivált kártyák eléréséhez {contactLink}, hogy váltson fizetős csomagra.", - "notification-box.card-limit-reached.text": "A kártyák limitjét elérte, a régebbi kártyák megtekintéséhez {link}", - "person.add-user-to-board": "{username} hozzáadása a táblához", - "person.add-user-to-board-confirm-button": "Hozzáadás a táblához", - "person.add-user-to-board-permissions": "Jogosultságok", - "person.add-user-to-board-question": "Biztosan hozzá szeretné adni {username} felhasználót a táblához?", - "person.add-user-to-board-warning": "{username} nem tagja a táblának, és nem kap értesítést róla.", - "register.login-button": "vagy jelentkezzen be ha már van fiókja", - "register.signup-title": "Regisztráljon fiókjáért", - "rhs-board-non-admin-msg": "Ön nem rendszergazdája a táblának", - "rhs-boards.add": "Hozzáadás", - "rhs-boards.dm": "KÜ", - "rhs-boards.gm": "CSÜ", - "rhs-boards.header.dm": "ez egy Közvetlen Üzenet", - "rhs-boards.header.gm": "ez egy Csoportos Üzenet", - "rhs-boards.last-update-at": "Utolsó frissítés: {datetime}", - "rhs-boards.link-boards-to-channel": "Táblák összekapcsolása a {channelName} csatornával", - "rhs-boards.linked-boards": "Összekapcsolt kártyák", - "rhs-boards.no-boards-linked-to-channel": "Még nincs {channelName} csatornához kapcsolódó tábla", - "rhs-boards.no-boards-linked-to-channel-description": "A Boards egy projekt kezelő eszköz, amely segít meghatározni, szervezni, nyomon követni és kezelni a munkát a csapatokon belül, egy ismerős kanban tábla nézet segítségével.", - "rhs-boards.unlink-board": "Tábla leválasztása", - "rhs-boards.unlink-board1": "Tábla leválasztása", - "rhs-channel-boards-header.title": "Táblák", - "share-board.publish": "Közzététel", - "share-board.share": "Megosztás", - "shareBoard.channels-select-group": "Csatornák", - "shareBoard.confirm-change-team-role.body": "Mindenki ebben a táblában, akinek alacsonyabb jogosultsága van, mint a \"{role}\" szerepkör, mostantól {role} lesz. Biztos, hogy meg akarja változtatni a tábla minimális szerepkörét?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Minimális szerepkör módosítása", - "shareBoard.confirm-change-team-role.title": "Minimális szerepkör módosítása", - "shareBoard.confirm-link-channel": "Tábla összekapcsolása csatornával", - "shareBoard.confirm-link-channel-button": "Csatorna összekapcsolása", - "shareBoard.confirm-link-channel-button-with-other-channel": "Leválasztás és ide kapcsolás", - "shareBoard.confirm-link-channel-subtext": "Ha egy csatornát összekapcsol egy táblával, a csatorna minden tagja (meglévő és új) képes lesz szerkeszteni azt. Ez nem vonatkozik a vendég tagokra.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Ha egy csatornát összekapcsol egy táblával, a csatorna minden tagja (meglévő és új) képes lesz szerkeszteni azt. Ez nem vonatkozik a vendég tagokra.{lineBreak}A tábla jelenleg egy másik csatornához van kapcsolva. Le lesz választva, amennyiben úgy dönt, hogy ide kapcsolja.", - "shareBoard.confirm-unlink.body": "Ha egy csatornát leválaszt egy tábláról, a csatorna minden tagja (meglévő és új) elveszíti a hozzáférést, kivéve, ha külön engedélyt kapott rá.", - "shareBoard.confirm-unlink.confirmBtnText": "Csatorna leválasztása", - "shareBoard.confirm-unlink.title": "Csatorna leválasztása a tábláról", - "shareBoard.lastAdmin": "A tábláknak legalább egy Adminisztárorral kell rendelkezniük", - "shareBoard.members-select-group": "Tagok", - "shareBoard.unknown-channel-display-name": "Ismeretlen csatorna", - "tutorial_tip.finish_tour": "Kész", - "tutorial_tip.got_it": "Értettem", - "tutorial_tip.ok": "Következő", - "tutorial_tip.out": "Ezen tippek kikapcsolása.", - "tutorial_tip.seen": "Ezt látta már?" -} diff --git a/webapp/boards/i18n/id.json b/webapp/boards/i18n/id.json deleted file mode 100644 index 519c553940..0000000000 --- a/webapp/boards/i18n/id.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Tambahkan kelompok", - "BoardComponent.delete": "Hapus", - "BoardComponent.hidden-columns": "Kolom-kolom yang disembunyikan", - "BoardComponent.hide": "Sembunyikan", - "BoardComponent.new": "+ Buat", - "BoardComponent.no-property": "Tidak ada {property}", - "BoardComponent.no-property-title": "Item dengan properti {property} yang kosong akan berpindah ke sini. Kolom ini tidak dapat dihapus.", - "BoardComponent.show": "Tampilkan", - "BoardPage.newVersion": "Versi baru Board tersedia, klik di sini untuk memuat ulang.", - "BoardPage.syncFailed": "Papan mungkin terhapus atau akses Anda ke papan ditolak.", - "BoardTemplateSelector.add-template": "Template baru", - "BoardTemplateSelector.create-empty-board": "Buat board kosong", - "BoardTemplateSelector.delete-template": "Hapus", - "BoardTemplateSelector.description": "Pilih template untuk membantu Anda memulai. Sesuaikan template dengan mudah agar sesuai dengan kebutuhan Anda, atau buat papan kosong untuk memulai dari awal.", - "BoardTemplateSelector.edit-template": "Ubah", - "BoardTemplateSelector.plugin.no-content-description": "Tambahkan papan ke bilah sisi menggunakan salah satu kerangka yang ditentukan di bawah atau mulai dari awal.{lineBreak} Anggota \"{workspaceName}\" akan memiliki akses ke papan yang dibuat di sini.", - "BoardTemplateSelector.plugin.no-content-title": "Buat Papan di {workspaceName}", - "BoardTemplateSelector.title": "Buat Papan", - "CardDetail.add-content": "Tambahkan isi", - "CardDetail.add-icon": "Tambahkan ikon", - "CardDetail.add-property": "+ Tambahkan properti", - "CardDetail.addCardText": "tambahkan teks kartu", - "CardDetail.moveContent": "pindahkan isi kartu", - "CardDetail.new-comment-placeholder": "Tambahkan komentar...", - "CardDialog.editing-template": "Anda sedang menyunting sebuah template.", - "CardDialog.nocard": "Kartu ini tidak ada atau tidak dapat diakses.", - "ColorOption.selectColor": "Pilih Warna {color}", - "Comment.delete": "Hapus", - "CommentsList.send": "Kirim", - "ContentBlock.Delete": "Hapus", - "ContentBlock.DeleteAction": "hapus", - "ContentBlock.addElement": "tambahkan {type}", - "ContentBlock.checkbox": "kotak centang", - "ContentBlock.divider": "pembagi", - "ContentBlock.editCardCheckbox": "kotakpilihan-beralih", - "ContentBlock.editCardCheckboxText": "sunting teks kartu", - "ContentBlock.editCardText": "sunting teks kartu", - "ContentBlock.editText": "Sunting teks...", - "ContentBlock.image": "gambar", - "ContentBlock.insertAbove": "Masukkan di atas", - "ContentBlock.moveDown": "Turunkan", - "ContentBlock.moveUp": "Naikkan", - "ContentBlock.text": "teks", - "Dialog.closeDialog": "Tutup dialog", - "EditableDayPicker.today": "Hari Ini", - "Error.websocket-closed": "Koneksi ke soket web tertutup, koneksi terganggu. Jika hal ini terus berlanjut, periksa konfigurasi server atau proxy web Anda.", - "Filter.includes": "termasuk", - "Filter.is-empty": "kosong", - "Filter.is-not-empty": "tidak kosong", - "Filter.not-includes": "tidak termasuk", - "FilterComponent.add-filter": "+ Tambahkan saringan", - "FilterComponent.delete": "Hapus", - "GroupBy.ungroup": "Pisahkan grup", - "KanbanCard.untitled": "Tidak berjudul", - "Mutator.new-card-from-template": "kartu baru dari template", - "Mutator.new-template-from-card": "template baru dari kartu", - "PropertyMenu.Delete": "Hapus", - "PropertyMenu.changeType": "Ubah jenis properti", - "PropertyMenu.typeTitle": "Jenis", - "PropertyType.Checkbox": "Kotak Centang", - "PropertyType.CreatedBy": "Dibuat oleh", - "PropertyType.CreatedTime": "Waktu dibuat", - "PropertyType.Date": "Tanggal", - "PropertyType.Email": "Surel", - "PropertyType.MultiSelect": "Banyak Pilihan", - "PropertyType.Number": "Angka", - "PropertyType.Person": "Orang", - "PropertyType.Phone": "Telepon", - "PropertyType.Select": "Pilihan", - "PropertyType.Text": "Teks", - "PropertyType.UpdatedBy": "Diperbarui oleh", - "PropertyType.UpdatedTime": "Waktu diperbarui", - "RegistrationLink.confirmRegenerateToken": "Ini akan membuat tautan yang sebelumnya dibagikan tidak valid. Lanjutkan?", - "RegistrationLink.copiedLink": "Disalin!", - "RegistrationLink.copyLink": "Salin tautan", - "RegistrationLink.description": "Bagikan tautan ini untuk membuat akun lainnya:", - "RegistrationLink.regenerateToken": "Buat ulang token", - "RegistrationLink.tokenRegenerated": "Tautan pendaftaran dibuat ulang", - "ShareBoard.confirmRegenerateToken": "Ini akan membuat tautan yang sebelumnya dibagikan tidak valid. Lanjutkan?", - "ShareBoard.copiedLink": "Disalin!", - "ShareBoard.copyLink": "Salin tautan", - "ShareBoard.tokenRegenrated": "Token dibuat ulang", - "Sidebar.about": "Tentang Focalboard", - "Sidebar.add-board": "+ Tambahkan papan", - "Sidebar.changePassword": "Ubah kata sandi", - "Sidebar.delete-board": "Hapus papan", - "Sidebar.export-archive": "Ekpor arsip", - "Sidebar.import-archive": "Impor arsip", - "Sidebar.invite-users": "Undang pengguna", - "Sidebar.logout": "Keluar", - "Sidebar.random-icons": "Ikon acak", - "Sidebar.set-language": "Tetapkan bahasa", - "Sidebar.set-theme": "Tetapkan tema", - "Sidebar.settings": "Pengaturan", - "Sidebar.untitled-board": "(Papan Tak Berjudul)", - "TableComponent.add-icon": "Tambahkan ikon", - "TableComponent.name": "Nama", - "TableComponent.plus-new": "+ Buat", - "TableHeaderMenu.delete": "Hapus", - "TableHeaderMenu.duplicate": "Duplikasikan", - "TableHeaderMenu.hide": "Sembunyikan", - "TableHeaderMenu.insert-left": "Masukkan di kiri", - "TableHeaderMenu.insert-right": "Masukkan di kanan", - "TableHeaderMenu.sort-ascending": "Urutkan ke atas", - "TableHeaderMenu.sort-descending": "Urutkan ke bawah", - "TableRow.open": "Buka", - "TopBar.give-feedback": "Beri Masukan", - "ValueSelector.valueSelector": "Pilihan nilai", - "ValueSelectorLabel.openMenu": "Menu terbuka", - "View.AddView": "Tambahkan tampilan", - "View.Board": "Papan", - "View.DeleteView": "Hapus tampilan", - "View.DuplicateView": "Duplikasikan tampilan", - "View.NewBoardTitle": "Tampilan papan", - "View.NewGalleryTitle": "Tampilan galeri", - "View.NewTableTitle": "Tampilan tabel", - "View.Table": "Tabel", - "ViewHeader.add-template": "Template baru", - "ViewHeader.delete-template": "Hapus", - "ViewHeader.edit-template": "Sunting", - "ViewHeader.empty-card": "Kartu kosong", - "ViewHeader.export-board-archive": "Ekspor arsip papan", - "ViewHeader.export-complete": "Ekspor selesai!", - "ViewHeader.export-csv": "Ekspor ke CSV", - "ViewHeader.export-failed": "Ekspor gagal!", - "ViewHeader.filter": "Penyaringan", - "ViewHeader.group-by": "Kelompokkan berdasarkan: {property}", - "ViewHeader.new": "Buat", - "ViewHeader.properties": "Properti-properti", - "ViewHeader.search-text": "Cari teks", - "ViewHeader.select-a-template": "Pilih sebuah template", - "ViewHeader.sort": "Pengurutan", - "ViewHeader.untitled": "Tak berjudul", - "ViewTitle.hide-description": "sembunyikan deskripsi", - "ViewTitle.pick-icon": "Pilih ikon", - "ViewTitle.random-icon": "Acak", - "ViewTitle.remove-icon": "Hapus ikon", - "ViewTitle.show-description": "tampilkan deskripsi", - "ViewTitle.untitled-board": "Papan tak berjudul", - "WelcomePage.Description": "Papan adalah alat manajemen proyek yang membantu Anda untuk mendefinisikan, mengatur, memantau, dan mengelola pekerjaan dalam tim, menggunakan tampilan kanban yang mudah dipahami", - "WelcomePage.Explore.Button": "Jelajahi", - "WelcomePage.Heading": "Selamat Datang di Papan", - "Workspace.editing-board-template": "Anda sedang menyunting sebuah template papan.", - "default-properties.title": "Judul", - "login.log-in-button": "Masuk", - "login.log-in-title": "Masuk", - "login.register-button": "atau buat sebuah akun jika Anda belum memilikinya", - "register.login-button": "atau masuk jika Anda sudah memiliki akun", - "register.signup-title": "Daftar untuk mendapatkan akun Anda" -} diff --git a/webapp/boards/i18n/it.json b/webapp/boards/i18n/it.json deleted file mode 100644 index ce133950b4..0000000000 --- a/webapp/boards/i18n/it.json +++ /dev/null @@ -1,311 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Aggiungi un gruppo", - "BoardComponent.delete": "Elimina", - "BoardComponent.hidden-columns": "Campi nascosti", - "BoardComponent.hide": "Nascondi", - "BoardComponent.new": "+ Nuovo", - "BoardComponent.no-property": "No {property}", - "BoardComponent.no-property-title": "Gli oggetti senza alcuna proprietà {property} andranno qui. Questo campo non può essere rimosso.", - "BoardComponent.show": "Mostra", - "BoardMember.schemeAdmin": "Amministratore", - "BoardMember.schemeCommenter": "Commentatore", - "BoardMember.schemeEditor": "Editore", - "BoardMember.schemeNone": "Niente", - "BoardMember.schemeViewer": "Vista", - "BoardMember.unlinkChannel": "Rimuovi collegamento", - "BoardPage.newVersion": "Una nuova versione di Board è disponibile, clicca qui per ricaricare.", - "BoardPage.syncFailed": "La board potrebbe essere cancellata o l'accesso revocato.", - "BoardTemplateSelector.add-template": "Nuovo modello", - "BoardTemplateSelector.create-empty-board": "Crea una board vuota", - "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.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 \"{teamName}\" avranno accesso alle bacheche create qui.", - "BoardTemplateSelector.plugin.no-content-title": "Crea una bacheca", - "BoardTemplateSelector.title": "Crea una bacheca", - "BoardTemplateSelector.use-this-template": "Usa questo modello", - "BoardsSwitcher.Title": "Trova bacheche", - "BoardsUnfurl.Limited": "Altri dettagli non sono nascosti in quanto la scheda è archiviata", - "BoardsUnfurl.Remainder": "+{remainder} di più", - "BoardsUnfurl.Updated": "Aggiornato {time}", - "Calculations.Options.average.displayName": "Media", - "Calculations.Options.average.label": "Media", - "Calculations.Options.count.displayName": "Conta", - "Calculations.Options.count.label": "Conta", - "Calculations.Options.countChecked.displayName": "Controllato", - "Calculations.Options.countChecked.label": "Conteggio selezionati", - "Calculations.Options.countUnchecked.displayName": "Non selezionato", - "Calculations.Options.countUnchecked.label": "Conteggio non selezionato", - "Calculations.Options.countUniqueValue.displayName": "Unico", - "Calculations.Options.countUniqueValue.label": "Conta i valori unici", - "Calculations.Options.countValue.displayName": "Valori", - "Calculations.Options.countValue.label": "Conteggio valori", - "Calculations.Options.dateRange.displayName": "Intervallo", - "Calculations.Options.dateRange.label": "Intervallo", - "Calculations.Options.earliest.displayName": "Primo", - "Calculations.Options.earliest.label": "Primo", - "Calculations.Options.latest.displayName": "Ultimo", - "Calculations.Options.latest.label": "Ultimo", - "Calculations.Options.max.displayName": "Massimo", - "Calculations.Options.max.label": "Massimo", - "Calculations.Options.median.displayName": "Mediana", - "Calculations.Options.median.label": "Mediana", - "Calculations.Options.min.displayName": "Minimo", - "Calculations.Options.min.label": "Minimo", - "Calculations.Options.none.displayName": "Calcola", - "Calculations.Options.none.label": "Nulla", - "Calculations.Options.percentChecked.displayName": "Controllato", - "Calculations.Options.percentChecked.label": "Percentuale selezionata", - "Calculations.Options.percentUnchecked.displayName": "Non selezionato", - "Calculations.Options.percentUnchecked.label": "Percentuale non selezionata", - "Calculations.Options.range.displayName": "Intervallo", - "Calculations.Options.range.label": "Intervallo", - "Calculations.Options.sum.displayName": "Somma", - "Calculations.Options.sum.label": "Somma", - "CalendarCard.untitled": "Senza titolo", - "CardActionsMenu.copiedLink": "Copiato!", - "CardActionsMenu.copyLink": "Copia collegamento", - "CardActionsMenu.delete": "Elimina", - "CardActionsMenu.duplicate": "Duplica", - "CardBadges.title-checkboxes": "Checkboxes", - "CardBadges.title-comments": "Commenti", - "CardBadges.title-description": "Questa scheda ha una descrizione", - "CardDetail.Follow": "Segui", - "CardDetail.Following": "Prossimo", - "CardDetail.add-content": "Aggiungi contenuto", - "CardDetail.add-icon": "Aggiungi icona", - "CardDetail.add-property": "+ Aggiungi una proprietà", - "CardDetail.addCardText": "aggiungi testo alla scheda", - "CardDetail.limited-button": "Aggiorna", - "CardDetail.limited-title": "Questa scheda è nascosta", - "CardDetail.moveContent": "Sposta il contenuto della scheda", - "CardDetail.new-comment-placeholder": "Aggiungi un commento...", - "CardDetailProperty.confirm-delete-heading": "Conferma l'eliminazione della proprietà", - "CardDetailProperty.confirm-delete-subtext": "Sei sicuro di voler eliminare la proprietà \"{propertyName}\"? Rimuovendola, verranno eliminate le proprietà da tutte le carte in questa board.", - "CardDetailProperty.confirm-property-name-change-subtext": "Sei sicuro di voler cambiare la proprietà \"{propertyName}\" {customText}? Questo influirà su {numOfCards} schede in questa bacheca e alcuni dati potrebbero andare persi.", - "CardDetailProperty.confirm-property-type-change": "Conferma la modifica del tipo di proprietà", - "CardDetailProperty.delete-action-button": "Rimuovi", - "CardDetailProperty.property-change-action-button": "Cambia proprietà", - "CardDetailProperty.property-changed": "Cambiata proprietà con successo!", - "CardDetailProperty.property-deleted": "{propertyName} rimossa con successo!", - "CardDetailProperty.property-name-change-subtext": "tipo da \"{oldPropType}\" a \"{newPropType}\"", - "CardDetial.limited-link": "Scopri di più sui nostri piani.", - "CardDialog.delete-confirmation-dialog-button-text": "Elimina", - "CardDialog.delete-confirmation-dialog-heading": "Conferma l'eliminazione della scheda!", - "CardDialog.editing-template": "Stai modificando un template.", - "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.Login": "Login", - "CenterPanel.Share": "Condividi", - "ColorOption.selectColor": "Seleziona{color} Colore", - "Comment.delete": "Elimina", - "CommentsList.send": "Invia", - "ConfirmationDialog.cancel-action": "Annulla", - "ConfirmationDialog.confirm-action": "Conferma", - "ContentBlock.Delete": "Elimina", - "ContentBlock.DeleteAction": "elimina", - "ContentBlock.addElement": "aggiungi {type}", - "ContentBlock.checkbox": "casella di controllo", - "ContentBlock.divider": "divisore", - "ContentBlock.editCardCheckbox": "casella di controllo spuntata", - "ContentBlock.editCardCheckboxText": "modifica il testo della scheda", - "ContentBlock.editCardText": "modifica il testo della scheda", - "ContentBlock.editText": "Modifica il testo...", - "ContentBlock.image": "immagine", - "ContentBlock.insertAbove": "Inserisci sopra", - "ContentBlock.moveDown": "Sposta giù", - "ContentBlock.moveUp": "Sposta su", - "ContentBlock.text": "testo", - "DateRange.clear": "Pulisci", - "DateRange.empty": "Vuoto", - "DateRange.endDate": "Data di scadenza", - "DateRange.today": "Oggi", - "DeleteBoardDialog.confirm-cancel": "Annulla", - "DeleteBoardDialog.confirm-delete": "Elimina", - "DeleteBoardDialog.confirm-info": "Sei sicuro di voler eliminare la bacheca \"{boardTitle}\"? Eliminandola, rimuoverai tutte le schede in bacheca.", - "DeleteBoardDialog.confirm-tite": "Conferma la rimozione della bacheca", - "DeleteBoardDialog.confirm-tite-template": "Confermi di eliminare il template della bacheca", - "Dialog.closeDialog": "Chiudi finestra di dialogo", - "EditableDayPicker.today": "Oggi", - "Error.mobileweb": "Il supporto web mobile è attualmente in fase di beta iniziale. Non tutte le funzionalità potrebbero essere presenti.", - "Error.websocket-closed": "Connessione interrotta col Websocket. Se il problema persiste, controlla la configurazione del tuo server o del proxy web.", - "Filter.contains": "contiene", - "Filter.ends-with": "termina con", - "Filter.includes": "include", - "Filter.is": "è", - "Filter.is-empty": "è vuoto", - "Filter.is-not-empty": "non è vuoto", - "Filter.is-not-set": "non configurato", - "Filter.is-set": "configurato", - "Filter.not-contains": "non contiene", - "Filter.not-ends-with": "non termina con", - "Filter.not-includes": "non include", - "Filter.not-starts-with": "non inizia con", - "Filter.starts-with": "inizia con", - "FilterComponent.add-filter": "+ Aggiungi un filtro", - "FilterComponent.delete": "Elimina", - "FindBoardsDialog.IntroText": "Cerca per bacheca", - "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 SU/GIÙ per navigare. INVIO per selezionare, ESC per annullare", - "FindBoardsDialog.Title": "Trova bacheche", - "GroupBy.hideEmptyGroups": "Nascondi {count} gruppi vuoti", - "GroupBy.showHiddenGroups": "Mostra {count} gruppi nascosti", - "GroupBy.ungroup": "Dividi", - "HideBoard.MenuOption": "Nascondi bacheca", - "KanbanCard.untitled": "Senza titolo", - "Mutator.new-board-from-template": "nuova bacheca da template", - "Mutator.new-card-from-template": "nuova scheda da modello", - "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", - "PersonProperty.board-members": "Membri bacheca", - "PropertyMenu.Delete": "Elimina", - "PropertyMenu.changeType": "Cambia il tipo di proprietà", - "PropertyMenu.selectType": "Seleziona il tipo di proprietà", - "PropertyMenu.typeTitle": "Tipo", - "PropertyType.Checkbox": "Casella di controllo", - "PropertyType.CreatedBy": "Creato da", - "PropertyType.CreatedTime": "Orario di creazione", - "PropertyType.Date": "Data", - "PropertyType.Email": "Email", - "PropertyType.MultiSelect": "Selezione Multipla", - "PropertyType.Number": "Numero", - "PropertyType.Person": "Persona", - "PropertyType.Phone": "Telefono", - "PropertyType.Select": "Seleziona", - "PropertyType.Text": "Testo", - "PropertyType.UpdatedBy": "Aggiornato da", - "PropertyType.UpdatedTime": "Ora di aggiornamento", - "PropertyValueElement.empty": "Vuoto", - "RegistrationLink.confirmRegenerateToken": "Questo invaliderà i link condivisi in precedenza. Continuare?", - "RegistrationLink.copiedLink": "Copiato!", - "RegistrationLink.copyLink": "Copia link", - "RegistrationLink.description": "Condividi questo link per creare nuovi account:", - "RegistrationLink.regenerateToken": "Rigenera il token", - "RegistrationLink.tokenRegenerated": "Link di registrazione ricreato", - "ShareBoard.PublishDescription": "Pubblica e condividi un link di sola lettura con chiunque", - "ShareBoard.PublishTitle": "Pubblica", - "ShareBoard.Title": "Condividi bacheca", - "ShareBoard.confirmRegenerateToken": "Questo invaliderà i link condivisi in precedenza. Continuare?", - "ShareBoard.copiedLink": "Copiato!", - "ShareBoard.copyLink": "Copia link", - "ShareBoard.regenerate": "Rigenera token", - "ShareBoard.teamPermissionsText": "Tutti nel team {teamName}", - "ShareBoard.tokenRegenrated": "Token rigenerato", - "ShareBoard.userPermissionsRemoveMemberText": "Rimuovi utente", - "ShareBoard.userPermissionsYouText": "(Tu)", - "ShareTemplate.Title": "Condividi template", - "Sidebar.about": "Informazioni su Focalboard", - "Sidebar.add-board": "+ Aggiungi Contenitore", - "Sidebar.changePassword": "Cambia password", - "Sidebar.delete-board": "Elimina contenitore", - "Sidebar.duplicate-board": "Duplica bacheca", - "Sidebar.export-archive": "Esporta archivio", - "Sidebar.import": "Importa", - "Sidebar.import-archive": "Importa archivio", - "Sidebar.invite-users": "Invita utenti", - "Sidebar.logout": "Logout", - "Sidebar.no-boards-in-category": "Nessuna bacheca all'interno", - "Sidebar.random-icons": "Icone casuali", - "Sidebar.set-language": "Imposta la lingua", - "Sidebar.set-theme": "Imposta il tema", - "Sidebar.settings": "Impostazioni", - "Sidebar.template-from-board": "Nuovo template dalla bacheca", - "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 {categoryName} 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.name": "Nome", - "TableComponent.plus-new": "+ Nuovo", - "TableHeaderMenu.delete": "Elimina", - "TableHeaderMenu.duplicate": "Duplica", - "TableHeaderMenu.hide": "Nascondi", - "TableHeaderMenu.insert-left": "Inserisci a sinistra", - "TableHeaderMenu.insert-right": "Inserisci a destra", - "TableHeaderMenu.sort-ascending": "Ordine crescente", - "TableHeaderMenu.sort-descending": "Ordine decrescente", - "TableRow.open": "Apri", - "TopBar.give-feedback": "Dai un feedback", - "ValueSelector.noOptions": "Nessuna opzione. Inizia a digitare per aggiungere la prima!", - "ValueSelector.valueSelector": "Seleziona valore", - "ValueSelectorLabel.openMenu": "Apri menu", - "View.AddView": "Aggiungi Vista", - "View.Board": "Contenitore", - "View.DeleteView": "Elimina Vista", - "View.DuplicateView": "Duplica Vista", - "View.Gallery": "Galleria", - "View.NewBoardTitle": "Vista Contenitore", - "View.NewCalendarTitle": "Vista calendario", - "View.NewGalleryTitle": "Vista gallery", - "View.NewTableTitle": "Vista tabella", - "View.Table": "Tabella", - "ViewHeader.add-template": "+ Nuovo modello", - "ViewHeader.delete-template": "Elimina", - "ViewHeader.display-by": "Visualizzato da: {property}", - "ViewHeader.edit-template": "Modifica", - "ViewHeader.empty-card": "Scheda vuota", - "ViewHeader.export-board-archive": "Esporta archivio board", - "ViewHeader.export-complete": "Esportazione completata!", - "ViewHeader.export-csv": "Esporta in formato CSV", - "ViewHeader.export-failed": "Esportazione fallita!", - "ViewHeader.filter": "Filtro", - "ViewHeader.group-by": "Raggruppa per: {property}", - "ViewHeader.new": "Nuovo", - "ViewHeader.properties": "Proprietà", - "ViewHeader.properties-menu": "Menù delle proprietà", - "ViewHeader.search-text": "Cerca testo", - "ViewHeader.select-a-template": "Seleziona un modello", - "ViewHeader.set-default-template": "Imposta come predefinito", - "ViewHeader.sort": "Ordina", - "ViewHeader.untitled": "Senza titolo", - "ViewHeader.view-header-menu": "Vedi menù header", - "ViewHeader.view-menu": "Visualizza menù", - "ViewTitle.hide-description": "nascondi descrizione", - "ViewTitle.pick-icon": "Scegli un'icona", - "ViewTitle.random-icon": "Casuale", - "ViewTitle.remove-icon": "Rimuovi icona", - "ViewTitle.show-description": "mostra descrizione", - "ViewTitle.untitled-board": "Contenitore senza titolo", - "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.Heading": "Benvenuto in Boards", - "WelcomePage.NoThanks.Text": "No grazie, farò da solo", - "Workspace.editing-board-template": "Stai modificando un modello di una bacheca.", - "calendar.month": "Mese", - "calendar.today": "OGGI", - "calendar.week": "Settimana", - "default-properties.badges": "Commenti e Descrizione", - "default-properties.title": "Titolo", - "error.page.title": "Mi dispiace, qualcosa è andato storto", - "generic.previous": "Precedente", - "login.log-in-button": "Login", - "login.log-in-title": "Login", - "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.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?" -} diff --git a/webapp/boards/i18n/ja.json b/webapp/boards/i18n/ja.json deleted file mode 100644 index 9d5b6fc20e..0000000000 --- a/webapp/boards/i18n/ja.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "管理者", - "AdminBadge.TeamAdmin": "チーム管理者", - "AppBar.Tooltip": "リンク先Boardsの切替え", - "Attachment.Attachment-title": "添付する", - "AttachmentBlock.DeleteAction": "削除", - "AttachmentBlock.addElement": "{type} を追加", - "AttachmentBlock.delete": "添付ファイルを削除しました。", - "AttachmentBlock.failed": "ファイルサイズの制限に達したため、ファイルをアップロードできませんでした。", - "AttachmentBlock.upload": "添付ファイルをアップロードしています。", - "AttachmentBlock.uploadSuccess": "添付ファイルをアップロードしました。", - "AttachmentElement.delete-confirmation-dialog-button-text": "削除", - "AttachmentElement.download": "ダウンロード", - "AttachmentElement.upload-percentage": "アップロード中...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ グループを追加する", - "BoardComponent.delete": "削除", - "BoardComponent.hidden-columns": "非表示", - "BoardComponent.hide": "非表示", - "BoardComponent.new": "+ 新規", - "BoardComponent.no-property": "{property} 無し", - "BoardComponent.no-property-title": "{property}が空のアイテムがここに表示されます。このカラムは削除できません。", - "BoardComponent.show": "表示", - "BoardMember.schemeAdmin": "管理者", - "BoardMember.schemeCommenter": "コメンター", - "BoardMember.schemeEditor": "編集者", - "BoardMember.schemeNone": "なし", - "BoardMember.schemeViewer": "閲覧者", - "BoardMember.unlinkChannel": "リンク解除", - "BoardPage.newVersion": "Boardsの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。", - "BoardPage.syncFailed": "Boardが削除されたか、アクセスが取り消されました。", - "BoardTemplateSelector.add-template": "テンプレート新規作成", - "BoardTemplateSelector.create-empty-board": "空のBoardを作成", - "BoardTemplateSelector.delete-template": "削除する", - "BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。", - "BoardTemplateSelector.edit-template": "編集", - "BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。", - "BoardTemplateSelector.plugin.no-content-title": "Boardを作成する", - "BoardTemplateSelector.title": "Boardを作成する", - "BoardTemplateSelector.use-this-template": "このテンプレートを使う", - "BoardsSwitcher.Title": "Board検索", - "BoardsUnfurl.Limited": "カードがアーカイブされているため詳細は表示されません", - "BoardsUnfurl.Remainder": "残り +{remainder}", - "BoardsUnfurl.Updated": "更新日時 {time}", - "Calculations.Options.average.displayName": "平均", - "Calculations.Options.average.label": "平均", - "Calculations.Options.count.displayName": "カウント", - "Calculations.Options.count.label": "カウント", - "Calculations.Options.countChecked.displayName": "チェック済み", - "Calculations.Options.countChecked.label": "チェック済みの数", - "Calculations.Options.countUnchecked.displayName": "未チェック", - "Calculations.Options.countUnchecked.label": "未チェックの数", - "Calculations.Options.countUniqueValue.displayName": "ユニーク", - "Calculations.Options.countUniqueValue.label": "ユニーク値の数", - "Calculations.Options.countValue.displayName": "値", - "Calculations.Options.countValue.label": "値の数", - "Calculations.Options.dateRange.displayName": "範囲", - "Calculations.Options.dateRange.label": "範囲", - "Calculations.Options.earliest.displayName": "最初", - "Calculations.Options.earliest.label": "最初", - "Calculations.Options.latest.displayName": "最新", - "Calculations.Options.latest.label": "最新", - "Calculations.Options.max.displayName": "最大", - "Calculations.Options.max.label": "最大", - "Calculations.Options.median.displayName": "中央値", - "Calculations.Options.median.label": "中央値", - "Calculations.Options.min.displayName": "最小", - "Calculations.Options.min.label": "最小", - "Calculations.Options.none.displayName": "計算", - "Calculations.Options.none.label": "なし", - "Calculations.Options.percentChecked.displayName": "チェック済み", - "Calculations.Options.percentChecked.label": "チェック済みの割合", - "Calculations.Options.percentUnchecked.displayName": "未チェック", - "Calculations.Options.percentUnchecked.label": "未チェックの割合", - "Calculations.Options.range.displayName": "範囲", - "Calculations.Options.range.label": "範囲", - "Calculations.Options.sum.displayName": "合計", - "Calculations.Options.sum.label": "合計", - "CalendarCard.untitled": "無題", - "CardActionsMenu.copiedLink": "コピーしました!", - "CardActionsMenu.copyLink": "リンクをコピー", - "CardActionsMenu.delete": "削除", - "CardActionsMenu.duplicate": "複製", - "CardBadges.title-checkboxes": "チェックボックス", - "CardBadges.title-comments": "コメント", - "CardBadges.title-description": "このカードには説明があります", - "CardDetail.Attach": "添付", - "CardDetail.Follow": "フォローする", - "CardDetail.Following": "フォロー中", - "CardDetail.add-content": "内容を追加する", - "CardDetail.add-icon": "アイコンを追加する", - "CardDetail.add-property": "+ プロパティを追加", - "CardDetail.addCardText": "カードテキストを追加する", - "CardDetail.limited-body": "ProfessionalプランまたはEnterpriseプランにアップグレードしてください。", - "CardDetail.limited-button": "アップグレード", - "CardDetail.limited-title": "このカードは表示できません", - "CardDetail.moveContent": "カード内容の移動", - "CardDetail.new-comment-placeholder": "コメントを追加する...", - "CardDetailProperty.confirm-delete-heading": "プロパティの削除を確定する", - "CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このBoardのすべてのカードからそのプロパティが削除されます。", - "CardDetailProperty.confirm-property-name-change-subtext": "本当にプロパティ \"{propertyName}\" の \"{customText}\" に変更しますか? これは、このBoardの{numOfCards}カード全体の値に影響し、データの損失につながる恐れがあります。", - "CardDetailProperty.confirm-property-type-change": "プロパティ種別の変更を確定する", - "CardDetailProperty.delete-action-button": "削除", - "CardDetailProperty.property-change-action-button": "プロパティの変更", - "CardDetailProperty.property-changed": "プロパティが変更されました!", - "CardDetailProperty.property-deleted": "{propertyName} が正常に削除されました!", - "CardDetailProperty.property-name-change-subtext": "種別を \"{oldPropType}\" から\"{newPropType}\" に", - "CardDetial.limited-link": "各プランの詳細についてはこちらをご覧ください。", - "CardDialog.delete-confirmation-dialog-attachment": "添付ファイルを削除する", - "CardDialog.delete-confirmation-dialog-button-text": "削除", - "CardDialog.delete-confirmation-dialog-heading": "カード削除の確認", - "CardDialog.editing-template": "テンプレートを編集しています。", - "CardDialog.nocard": "このカードは存在しないか、アクセスできません。", - "Categories.CreateCategoryDialog.CancelText": "キャンセル", - "Categories.CreateCategoryDialog.CreateText": "作成", - "Categories.CreateCategoryDialog.Placeholder": "カテゴリ名を入力してください", - "Categories.CreateCategoryDialog.UpdateText": "更新", - "CenterPanel.Login": "ログイン", - "CenterPanel.Share": "共有", - "ChannelIntro.CreateBoard": "Boardを作成する", - "ColorOption.selectColor": "{color} 色を選択", - "Comment.delete": "削除", - "CommentsList.send": "送信", - "ConfirmPerson.empty": "空", - "ConfirmPerson.search": "検索中...", - "ConfirmationDialog.cancel-action": "キャンセル", - "ConfirmationDialog.confirm-action": "確認", - "ContentBlock.Delete": "削除", - "ContentBlock.DeleteAction": "削除する", - "ContentBlock.addElement": "{type} を追加する", - "ContentBlock.checkbox": "チェックボックス", - "ContentBlock.divider": "仕切り", - "ContentBlock.editCardCheckbox": "切替えられたチェックボックス", - "ContentBlock.editCardCheckboxText": "カードテキストの編集", - "ContentBlock.editCardText": "カードテキストの編集", - "ContentBlock.editText": "テキストを編集する...", - "ContentBlock.image": "画像", - "ContentBlock.insertAbove": "上に挿入する", - "ContentBlock.moveBlock": "カード内容の移動", - "ContentBlock.moveDown": "下へ移動する", - "ContentBlock.moveUp": "上へ移動する", - "ContentBlock.text": "テキスト", - "DateFilter.empty": "空", - "DateRange.clear": "クリア", - "DateRange.empty": "空", - "DateRange.endDate": "終了日", - "DateRange.today": "今日", - "DeleteBoardDialog.confirm-cancel": "キャンセル", - "DeleteBoardDialog.confirm-delete": "削除", - "DeleteBoardDialog.confirm-info": "本当にBoard \"{boardTitle}\" を削除しますか? 削除すると、このBoardのすべてのカードが削除されます。", - "DeleteBoardDialog.confirm-info-template": "Boardテンプレート \"{boardTitle}\" を本当に削除しますか?", - "DeleteBoardDialog.confirm-tite": "Boardの削除を確定する", - "DeleteBoardDialog.confirm-tite-template": "Boardテンプレートの削除を確定する", - "Dialog.closeDialog": "ダイアログを閉じる", - "EditableDayPicker.today": "今日", - "Error.mobileweb": "モバイルウェブのサポートは現在、初期ベータ版です。一部の機能が利用できない場合があります。", - "Error.websocket-closed": "ウェブソケット接続が閉じられ、接続が中断されました。この問題が解決しない場合は、サーバーまたはウェブプロキシの設定を確認してください。", - "Filter.contains": "を含む", - "Filter.ends-with": "で終わる", - "Filter.includes": "を含む", - "Filter.is": "と一致する", - "Filter.is-after": "が次の日付以降", - "Filter.is-before": "が次の日付以前", - "Filter.is-empty": "が空である", - "Filter.is-not-empty": "が空でない", - "Filter.is-not-set": "が未設定", - "Filter.is-set": "が設定済み", - "Filter.isafter": "が次の日付以降", - "Filter.isbefore": "が次の日付以前", - "Filter.not-contains": "を含まない", - "Filter.not-ends-with": "で終わらない", - "Filter.not-includes": "を含まない", - "Filter.not-starts-with": "で始まらない", - "Filter.starts-with": "で始まる", - "FilterByText.placeholder": "フィルター文字列", - "FilterComponent.add-filter": "+ フィルターを追加する", - "FilterComponent.delete": "削除", - "FilterValue.empty": "(空)", - "FindBoardsDialog.IntroText": "Boardを検索", - "FindBoardsDialog.NoResultsFor": "\"{searchQuery}\"に対する結果はありません", - "FindBoardsDialog.NoResultsSubtext": "スペルを確認し、再度検索してください。", - "FindBoardsDialog.SubTitle": "Boardを検索するために文字を入力してください。UP/DOWNで閲覧、ENTERで選択、ESCでキャンセル", - "FindBoardsDialog.Title": "Boardを探す", - "GroupBy.hideEmptyGroups": "{count} 個の空のグループを隠す", - "GroupBy.showHiddenGroups": "{count} 個の非表示グループを表示する", - "GroupBy.ungroup": "グループ解除", - "HideBoard.MenuOption": "Boardを隠す", - "KanbanCard.untitled": "無題", - "MentionSuggestion.is-not-board-member": "(not board member)", - "Mutator.new-board-from-template": "テンプレートからの新しいBoard", - "Mutator.new-card-from-template": "テンプレートから新しいカードを作成", - "Mutator.new-template-from-card": "カードから新しいテンプレートを作成", - "OnboardingTour.AddComments.Body": "問題にコメントしたり、仲間のMattermostユーザーの注意を引くために@メンションすることもできます。", - "OnboardingTour.AddComments.Title": "コメントを追加する", - "OnboardingTour.AddDescription.Body": "カードに説明を追加し、チームメイトに何のカードかわかるようにしましょう。", - "OnboardingTour.AddDescription.Title": "説明を追加する", - "OnboardingTour.AddProperties.Body": "カードに様々なプロパティを追加することで、より便利になります。", - "OnboardingTour.AddProperties.Title": "プロパティを追加する", - "OnboardingTour.AddView.Body": "異なるレイアウトでBoardを整理するための新しいビューを作成するには、ここに移動します。", - "OnboardingTour.AddView.Title": "新しいビューを追加する", - "OnboardingTour.CopyLink.Body": "リンクをコピーしてチャンネル、ダイレクトメッセージ、グループメッセージに貼り付けることで、カードをチームメイトと共有することができます。", - "OnboardingTour.CopyLink.Title": "リンクをコピー", - "OnboardingTour.OpenACard.Body": "カードを開き、あなたの仕事を整理するのに役立つBoardの便利な使い方を探ってみてください。", - "OnboardingTour.OpenACard.Title": "カードを開く", - "OnboardingTour.ShareBoard.Body": "作成したBoardは、社内やチーム内で共有することも、組織外から見えるように公開することも可能です。", - "OnboardingTour.ShareBoard.Title": "Boardを共有", - "PersonProperty.board-members": "Board members", - "PersonProperty.me": "私", - "PersonProperty.non-board-members": "Not board members", - "PropertyMenu.Delete": "削除", - "PropertyMenu.changeType": "プロパティのタイプを変更する", - "PropertyMenu.selectType": "プロパティタイプの選択", - "PropertyMenu.typeTitle": "タイプ", - "PropertyType.Checkbox": "チェックボックス", - "PropertyType.CreatedBy": "作成者", - "PropertyType.CreatedTime": "作成日時", - "PropertyType.Date": "日付", - "PropertyType.Email": "メールアドレス", - "PropertyType.MultiPerson": "複数人", - "PropertyType.MultiSelect": "マルチセレクト", - "PropertyType.Number": "数字", - "PropertyType.Person": "人物", - "PropertyType.Phone": "電話番号", - "PropertyType.Select": "セレクト", - "PropertyType.Text": "テキスト", - "PropertyType.Unknown": "不明", - "PropertyType.UpdatedBy": "更新者", - "PropertyType.UpdatedTime": "更新日時", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "空", - "RegistrationLink.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?", - "RegistrationLink.copiedLink": "コピーしました!", - "RegistrationLink.copyLink": "リンクをコピー", - "RegistrationLink.description": "アカウントを作成には、このリンクを共有してください:", - "RegistrationLink.regenerateToken": "トークンを再生成する", - "RegistrationLink.tokenRegenerated": "登録リンクが再生成されました", - "ShareBoard.PublishDescription": "Web上の全員へ \"読み取り専用\" のリンクを公開および共有する。", - "ShareBoard.PublishTitle": "Web上へ公開する", - "ShareBoard.ShareInternal": "内部で共有する", - "ShareBoard.ShareInternalDescription": "権限のあるユーザーは、このリンクを使用することができます。", - "ShareBoard.Title": "Boardを共有", - "ShareBoard.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?", - "ShareBoard.copiedLink": "コピーしました!", - "ShareBoard.copyLink": "リンクをコピー", - "ShareBoard.regenerate": "トークンを再生成する", - "ShareBoard.searchPlaceholder": "人とチャンネルを検索", - "ShareBoard.teamPermissionsText": "{teamName}チームの全員", - "ShareBoard.tokenRegenrated": "トークンが再生成されました", - "ShareBoard.userPermissionsRemoveMemberText": "メンバーを削除する", - "ShareBoard.userPermissionsYouText": "(あなた)", - "ShareTemplate.Title": "テンプレートを共有する", - "ShareTemplate.searchPlaceholder": "人を検索", - "Sidebar.about": "Focalboardについて", - "Sidebar.add-board": "+ Boardを追加", - "Sidebar.changePassword": "パスワードを変更する", - "Sidebar.delete-board": "Boardを削除", - "Sidebar.duplicate-board": "Boardを複製する", - "Sidebar.export-archive": "エクスポート", - "Sidebar.import": "インポート", - "Sidebar.import-archive": "インポート", - "Sidebar.invite-users": "ユーザーを招待する", - "Sidebar.logout": "ログアウト", - "Sidebar.new-category.badge": "新規", - "Sidebar.new-category.drag-boards-cta": "ここにBoardをドラッグ...", - "Sidebar.no-boards-in-category": "カテゴリ内にBoardがありません", - "Sidebar.product-tour": "プロダクトツアー", - "Sidebar.random-icons": "ランダムアイコン", - "Sidebar.set-language": "言語設定", - "Sidebar.set-theme": "テーマ設定", - "Sidebar.settings": "設定", - "Sidebar.template-from-board": "Boardからの新しいテンプレート", - "Sidebar.untitled-board": "(無題のBoard)", - "Sidebar.untitled-view": "(無題のビュー)", - "SidebarCategories.BlocksMenu.Move": "移動...", - "SidebarCategories.CategoryMenu.CreateNew": "新しいカテゴリを作成する", - "SidebarCategories.CategoryMenu.Delete": "カテゴリを削除する", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} にあるBoardは、Boards カテゴリに戻されます。どのBoardからも削除されることはありません。", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "このカテゴリを削除しますか?", - "SidebarCategories.CategoryMenu.Update": "カテゴリ名を変更する", - "SidebarTour.ManageCategories.Body": "カスタムカテゴリーを作成し、管理することができます。カテゴリはユーザーごとに設定されるため、Boardを自分のカテゴリに移動しても、同じBoardを使用している他のメンバーには影響がありません。", - "SidebarTour.ManageCategories.Title": "カテゴリー管理", - "SidebarTour.SearchForBoards.Body": "Board切替(Cmd/Ctrl + K)により、素早くBoardを検索し、サイドバーに追加することができます。", - "SidebarTour.SearchForBoards.Title": "Boardを検索", - "SidebarTour.SidebarCategories.Body": "すべてのBoardが新しいサイドバーの下に整理されました。もう、ワークスペースを切り替える必要はありません。v7.2へのアップグレードに伴い、以前のワークスペースに基づいたカスタムカテゴリーが自動的に作成されている場合があります。これらは、お好みで削除したり編集することができます。", - "SidebarTour.SidebarCategories.Link": "詳細", - "SidebarTour.SidebarCategories.Title": "サイドバーカテゴリー", - "SiteStats.total_boards": "Board総数", - "SiteStats.total_cards": "カード数", - "TableComponent.add-icon": "アイコンを追加する", - "TableComponent.name": "名前", - "TableComponent.plus-new": "+ 新規", - "TableHeaderMenu.delete": "削除", - "TableHeaderMenu.duplicate": "複製", - "TableHeaderMenu.hide": "非表示", - "TableHeaderMenu.insert-left": "左に挿入", - "TableHeaderMenu.insert-right": "右に挿入", - "TableHeaderMenu.sort-ascending": "昇順でソート", - "TableHeaderMenu.sort-descending": "降順でソート", - "TableRow.DuplicateCard": "カードを複製する", - "TableRow.MoreOption": "その他のアクション", - "TableRow.open": "開く", - "TopBar.give-feedback": "フィードバックを送る", - "URLProperty.copiedLink": "コピーしました!", - "URLProperty.copy": "コピー", - "URLProperty.edit": "編集", - "UndoRedoHotKeys.canRedo": "やり直す", - "UndoRedoHotKeys.canRedo-with-description": "{description} をやり直す", - "UndoRedoHotKeys.canUndo": "元に戻す", - "UndoRedoHotKeys.canUndo-with-description": "{description} を元に戻す", - "UndoRedoHotKeys.cannotRedo": "やり直しする操作がありません", - "UndoRedoHotKeys.cannotUndo": "元に戻す操作がありません", - "ValueSelector.noOptions": "オプションがありません。最初の一つを追加するために入力を開始してください!", - "ValueSelector.valueSelector": "値選択", - "ValueSelectorLabel.openMenu": "メニューを開く", - "VersionMessage.help": "このバージョンの新機能を確認する。", - "VersionMessage.learn-more": "詳しく", - "View.AddView": "ビューを追加", - "View.Board": "Board", - "View.DeleteView": "ビューを削除", - "View.DuplicateView": "ビューを複製", - "View.Gallery": "ギャラリー", - "View.NewBoardTitle": "Board表示", - "View.NewCalendarTitle": "カレンダー表示", - "View.NewGalleryTitle": "ギャラリービュー", - "View.NewTableTitle": "テーブル表示", - "View.NewTemplateDefaultTitle": "無題のテンプレート", - "View.NewTemplateTitle": "無題", - "View.Table": "テーブル", - "ViewHeader.add-template": "新しいテンプレート", - "ViewHeader.delete-template": "削除", - "ViewHeader.display-by": "表示対象: {property}", - "ViewHeader.edit-template": "編集", - "ViewHeader.empty-card": "空のカード", - "ViewHeader.export-board-archive": "Boardアーカイブのエクスポート", - "ViewHeader.export-complete": "エクスポートが完了しました!", - "ViewHeader.export-csv": "CSVエクスポート", - "ViewHeader.export-failed": "エクスポートが失敗しました!", - "ViewHeader.filter": "フィルター", - "ViewHeader.group-by": "{property} でグループ化", - "ViewHeader.new": "新規", - "ViewHeader.properties": "プロパティ", - "ViewHeader.properties-menu": "プロパティメニュー", - "ViewHeader.search-text": "カード検索", - "ViewHeader.select-a-template": "テンプレート選択", - "ViewHeader.set-default-template": "デフォルトとして設定", - "ViewHeader.sort": "ソート", - "ViewHeader.untitled": "無題", - "ViewHeader.view-header-menu": "ヘッダーメニューを見る", - "ViewHeader.view-menu": "メニューを見る", - "ViewLimitDialog.Heading": "Boardごとのビュー数制限に達しました", - "ViewLimitDialog.PrimaryButton.Title.Admin": "アップグレード", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "管理者に通知する", - "ViewLimitDialog.Subtext.Admin": "ProfessionalプランまたはEnterpriseプランにアップグレードしてください。", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "各プランの詳細についてはこちらをご覧ください。", - "ViewLimitDialog.Subtext.RegularUser": "ProfessionalプランまたはEnterpriseプランへアップグレードするよう管理者に連絡してください。", - "ViewLimitDialog.UpgradeImg.AltText": "アップグレードイメージ", - "ViewLimitDialog.notifyAdmin.Success": "管理者に通知されました", - "ViewTitle.hide-description": "説明を非表示", - "ViewTitle.pick-icon": "アイコンを選ぶ", - "ViewTitle.random-icon": "ランダム", - "ViewTitle.remove-icon": "アイコンを削除する", - "ViewTitle.show-description": "説明を表示", - "ViewTitle.untitled-board": "無題のBoard", - "WelcomePage.Description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、整理、追跡、管理するためのプロジェクト管理ツールです。", - "WelcomePage.Explore.Button": "ツアーに参加する", - "WelcomePage.Heading": "Boardへようこそ", - "WelcomePage.NoThanks.Text": "いいえ、自分で調べます", - "WelcomePage.StartUsingIt.Text": "利用を開始する", - "Workspace.editing-board-template": "Boardのテンプレートを編集しています。", - "badge.guest": "ゲスト", - "boardPage.confirm-join-button": "参加", - "boardPage.confirm-join-text": "あなたは、ボード管理者によって明示的に追加されることなく、非公開のボードに参加しようとしています。本当にこの非公開ボードに参加しますか?", - "boardPage.confirm-join-title": "非公開ボードに参加", - "boardSelector.confirm-link-board": "Boardをチャンネルへリンク", - "boardSelector.confirm-link-board-button": "はい、Boardをリンクします", - "boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。Boardとチャンネルのリンク解除はいつでも可能です。", - "boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", - "boardSelector.create-a-board": "Boardを作成", - "boardSelector.link": "リンク", - "boardSelector.search-for-boards": "Boardを検索", - "boardSelector.title": "Boardをリンク", - "boardSelector.unlink": "リンク解除", - "calendar.month": "月", - "calendar.today": "今日", - "calendar.week": "週", - "centerPanel.undefined": "{propertyName} 無し", - "centerPanel.unknown-user": "不明なユーザー", - "cloudMessage.learn-more": "さらに詳しく", - "createImageBlock.failed": "ファイルサイズの上限に達しているため、ファイルをアップロードできませんでした。", - "default-properties.badges": "コメントと説明", - "default-properties.title": "タイトル", - "error.back-to-home": "ホームへ戻る", - "error.back-to-team": "チームに戻る", - "error.board-not-found": "Boardが見つかりませんでした。", - "error.go-login": "ログイン", - "error.invalid-read-only-board": "このBoardにアクセスできません。アクセスするにはBoardsにログインしてください。", - "error.not-logged-in": "セッションの有効期限が切れているか、ログインしていない可能性があります。Boardsにアクセスするには再度ログインしてください。", - "error.page.title": "申し訳ありませんが、何か問題が発生しました", - "error.team-undefined": "有効なチームではありません。", - "error.unknown": "エラーが発生しました。", - "generic.previous": "前へ", - "guest-no-board.subtitle": "あなたはまだこのチームのどのBoardにもアクセスできません。誰かがあなたをBoardに追加するまでお待ちください。", - "guest-no-board.title": "まだBoardsはありません", - "imagePaste.upload-failed": "ファイルサイズの制限に達しているため、一部のファイルをアップロードできませんでした。", - "limitedCard.title": "非表示カード", - "login.log-in-button": "ログイン", - "login.log-in-title": "ログイン", - "login.register-button": "アカウントをお持ちでない方はアカウントを作成してください", - "new_channel_modal.create_board.empty_board_description": "空のBoardを新規作成する", - "new_channel_modal.create_board.empty_board_title": "空のBoard", - "new_channel_modal.create_board.select_template_placeholder": "テンプレートを選択", - "new_channel_modal.create_board.title": "このチャンネル用のBoardを作成する", - "notification-box-card-limit-reached.close-tooltip": "10日間のスヌーズ", - "notification-box-card-limit-reached.contact-link": "管理者に通知する", - "notification-box-card-limit-reached.link": "有料プランへのアップグレード", - "notification-box-card-limit-reached.title": "Boardから {cards} カードが非表示になっています", - "notification-box-cards-hidden.title": "このアクションにより他のカードが非表示になります", - "notification-box.card-limit-reached.not-admin.text": "アーカイブされたカードにアクセスするには、{contactLink}から有料プランにアップグレードしてください。", - "notification-box.card-limit-reached.text": "カード数の制限に達しました。古いカードを閲覧するには、{link}", - "person.add-user-to-board": "{username} をBoardに追加", - "person.add-user-to-board-confirm-button": "Boardに追加", - "person.add-user-to-board-permissions": "権限", - "person.add-user-to-board-question": "{username} をBoardに追加しますか?", - "person.add-user-to-board-warning": "{username} はBoardのメンバーではないので、それに関する通知を受け取ることはありません。", - "register.login-button": "または、すでにアカウントをお持ちの方はログインしてください", - "register.signup-title": "アカウント登録", - "rhs-board-non-admin-msg": "あなたはBoardの管理者ではありません", - "rhs-boards.add": "追加", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "このダイレクトメッセージ", - "rhs-boards.header.gm": "このグループメッセージ", - "rhs-boards.last-update-at": "最終更新: {datetime}", - "rhs-boards.link-boards-to-channel": "Boardsを{channelName}へリンクする", - "rhs-boards.linked-boards": "リンク済みBoards", - "rhs-boards.no-boards-linked-to-channel": "{channelName}にリンクされたBoardsはまだありません", - "rhs-boards.no-boards-linked-to-channel-description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、生理、追跡、管理するためのプロジェクト管理ツールです。", - "rhs-boards.unlink-board": "Boardのリンクを解除", - "rhs-boards.unlink-board1": "Boardのリンクを解除", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "公開", - "share-board.share": "共有", - "shareBoard.channels-select-group": "Channels", - "shareBoard.confirm-change-team-role.body": "このBoardで \"{role}\" より弱い権限のユーザー全員が {role} に昇格します。本当にBoardの最低限のロールを変更しますか?", - "shareBoard.confirm-change-team-role.confirmBtnText": "最低限のロールを変更", - "shareBoard.confirm-change-team-role.title": "最低限のロールを変更", - "shareBoard.confirm-link-channel": "Boardをチャンネルへリンク", - "shareBoard.confirm-link-channel-button": "チャンネルにリンク", - "shareBoard.confirm-link-channel-button-with-other-channel": "リンク解除とリンクはこちら", - "shareBoard.confirm-link-channel-subtext": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。", - "shareBoard.confirm-unlink.body": "Boardからチャンネルへのリンクを解除すると、別途権限を付与されない限り、チャンネルの(既存/新規)メンバー全員がBoardへアクセスできなくなります。", - "shareBoard.confirm-unlink.confirmBtnText": "チャンネルとのリンクを解除", - "shareBoard.confirm-unlink.title": "Boardからチャンネルへのリンクを解除する", - "shareBoard.lastAdmin": "Boardsには少なくとも1名の管理者が必要です", - "shareBoard.members-select-group": "メンバー", - "shareBoard.unknown-channel-display-name": "不明なチャンネル", - "tutorial_tip.finish_tour": "完了", - "tutorial_tip.got_it": "了解", - "tutorial_tip.ok": "次へ", - "tutorial_tip.out": "これらのコツを表示しません。", - "tutorial_tip.seen": "以前に見たことがありますか?" -} diff --git a/webapp/boards/i18n/ka.json b/webapp/boards/i18n/ka.json deleted file mode 100644 index 9d3b5d3553..0000000000 --- a/webapp/boards/i18n/ka.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "AppBar.Tooltip": "დაკავშირებული დაფების გადართვა", - "BoardComponent.add-a-group": "+ ჯგუფის დამატება", - "BoardComponent.delete": "წაშლა", - "BoardComponent.hidden-columns": "დამალული სვეტები", - "BoardComponent.hide": "დამალვა", - "BoardComponent.new": "+ ახალი", - "BoardComponent.no-property": "არ არის {property}", - "BoardComponent.no-property-title": "ცარიელი {property} საკუთრების მქონე ელემენტები აქ წავა. ამ სვეტის წაშლა შეუძლებელია.", - "BoardComponent.show": "ჩვენება", - "BoardMember.schemeAdmin": "ადმინისტრატორი", - "BoardMember.schemeCommenter": "კომენტატორი", - "BoardMember.schemeEditor": "რედაქტორი", - "BoardMember.schemeNone": "არცერთი", - "BoardMember.schemeViewer": "მაყურებელი", - "BoardMember.unlinkChannel": "კავშირის გაუქმება", - "BoardPage.newVersion": "დაფების ახალი ვერსია ხელმისაწვდომია, დააწკაპუნეთ აქ გადასატვირთად.", - "BoardPage.syncFailed": "დაფა შეიძლება წაშლილია ან გაუქმებულია წვდომა.", - "BoardTemplateSelector.add-template": "ახალი შაბლონი", - "BoardTemplateSelector.create-empty-board": "შექმენით ცარიელი დაფა", - "BoardTemplateSelector.delete-template": "წაშლა", - "BoardTemplateSelector.description": "დაამატეთ დაფა გვერდითა ზოლში ქვემოთ განსაზღვრული რომელიმე შაბლონის გამოყენებით ან დაიწყეთ ნულიდან.", - "BoardTemplateSelector.edit-template": "რედაქტირება", - "BoardTemplateSelector.plugin.no-content-description": "დაამატეთ დაფა გვერდითა ზოლში ქვემოთ განსაზღვრული რომელიმე შაბლონის გამოყენებით ან დაიწყეთ ნულიდან.", - "BoardTemplateSelector.plugin.no-content-title": "შექმენით დაფა", - "BoardTemplateSelector.title": "შექმენით დაფა", - "BoardTemplateSelector.use-this-template": "გამოიყენეთ ეს შაბლონი", - "BoardsSwitcher.Title": "დაფების ძებნა", - "BoardsUnfurl.Limited": "დამატებითი დეტალები დამალულია ბარათის დაარქივების გამო", - "BoardsUnfurl.Remainder": "+{remainder} მეტი", - "BoardsUnfurl.Updated": "განახლებული {time}", - "Calculations.Options.average.displayName": "საშუალო", - "Calculations.Options.average.label": "საშუალო", - "Calculations.Options.count.displayName": "დათვლა", - "Calculations.Options.count.label": "დათვლა", - "Calculations.Options.countChecked.displayName": "შემოწმებული", - "Calculations.Options.countChecked.label": "რაოდენობა შემოწმებულია", - "Calculations.Options.countUnchecked.displayName": "შეუმოწმებელი", - "Calculations.Options.countUnchecked.label": "რაოდენობა შეუმოწმებელია", - "Calculations.Options.countUniqueValue.displayName": "უნიკალური", - "Calculations.Options.countUniqueValue.label": "უნიკალური ღირებულების დათვლა", - "Calculations.Options.countValue.displayName": "ღირებულებები", - "Calculations.Options.countValue.label": "დათვალეთ მნიშვნელობა", - "Calculations.Options.dateRange.displayName": "დიაპაზონი", - "Calculations.Options.dateRange.label": "დიაპაზონი", - "Calculations.Options.earliest.displayName": "ყველაზე ადრეული", - "Calculations.Options.earliest.label": "ყველაზე ადრეული", - "Calculations.Options.latest.displayName": "უახლესი", - "Calculations.Options.latest.label": "უახლესი", - "Calculations.Options.max.displayName": "მაქსიმალური", - "Calculations.Options.max.label": "მაქსიმალური", - "Calculations.Options.median.displayName": "საშუალო", - "Calculations.Options.median.label": "საშუალო", - "Calculations.Options.min.displayName": "მინიმალური", - "Calculations.Options.min.label": "მინიმალური", - "Calculations.Options.none.displayName": "გამოთვლა", - "Calculations.Options.none.label": "გამოთვლა", - "Calculations.Options.percentChecked.displayName": "შემოწმებული", - "Calculations.Options.percentChecked.label": "პროცენტი შემოწმებულია", - "Calculations.Options.percentUnchecked.displayName": "შეუმოწმებელი", - "Calculations.Options.percentUnchecked.label": "პროცენტი შეუმოწმებელია", - "Calculations.Options.range.displayName": "დიაპაზონი", - "Calculations.Options.range.label": "დიაპაზონი", - "Calculations.Options.sum.displayName": "ჯამი", - "Calculations.Options.sum.label": "ჯამი", - "CalendarCard.untitled": "უსათაურო", - "CardActionsMenu.copiedLink": "დაკოპირებულია!", - "CardActionsMenu.copyLink": "Ბმულის კოპირება", - "CardActionsMenu.delete": "წაშლა", - "CardActionsMenu.duplicate": "დუბლიკატი", - "CardBadges.title-checkboxes": "მოსანიშნი ველები", - "CardBadges.title-comments": "კომენტარები", - "CardBadges.title-description": "ამ ბარათს აქვს აღწერა", - "CardDetail.Follow": "გაყოლა", - "CardDetail.Following": "მომდევნო", - "CardDetail.add-content": "დაამატეთ შინაარსი", - "CardDetail.add-icon": "ხატულის დამატება", - "CardDetail.add-property": "+ დაამატეთ Property", - "CardDetail.addCardText": "ბარათის ტექსტის დამატება", - "CardDetail.limited-body": "ბარათის ტექსტის დამატება განაახლეთ ჩვენს პროფესიონალურ ან საწარმოს გეგმაში დაარქივებული ბარათების სანახავად, თითო დაფაზე ულიმიტო ნახვები, ულიმიტო ბარათები და სხვა.", - "CardDetail.limited-button": "განახლება", - "CardDetail.limited-title": "ეს ბარათი დამალულია", - "CardDetail.moveContent": "ბარათის შინაარსის გადატანა", - "CardDetail.new-comment-placeholder": "კომენტარის დამატება...", - "CardDetailProperty.confirm-delete-heading": "დაადასტურეთ Property-ის წაშლა", - "CardDetailProperty.confirm-delete-subtext": "დარწმუნებული ხართ, რომ გსურთ წაშალოთ Property „{propertyName}“? მისი წაშლა წაშლის Property-ის ამ დაფის ყველა ბარათიდან.", - "CardDetailProperty.confirm-property-name-change-subtext": "დარწმუნებული ხართ, რომ გსურთ შეცვალოთ თვისება „{propertyName}“ {customText}? ეს გავლენას მოახდენს მნიშვნელობა(ებ)ზე {numOfCards} ბარათ(ებ)ში ამ დაფაზე და შეიძლება გამოიწვიოს მონაცემთა დაკარგვა.", - "CardDetailProperty.confirm-property-type-change": "დაადასტურეთ Property-ის ტიპის ცვლილება", - "CardDetailProperty.delete-action-button": "წაშლა", - "CardDetailProperty.property-change-action-button": "Property-ის შეცვლა", - "CardDetailProperty.property-changed": "Property წარმატებით შეიცვალა!", - "CardDetailProperty.property-deleted": "{propertyName} წარმატებით წაიშალა!", - "CardDetailProperty.property-name-change-subtext": "აკრიფეთ „{oldPropType}“-დან „{newPropType}“-მდე", - "CardDetial.limited-link": "შეიტყვეთ მეტი ჩვენი გეგმების შესახებ.", - "CardDialog.delete-confirmation-dialog-button-text": "წაშლა", - "CardDialog.delete-confirmation-dialog-heading": "დაადასტურეთ ბარათის წაშლა!", - "CardDialog.editing-template": "თქვენ არედაქტირებთ შაბლონს.", - "CardDialog.nocard": "ეს ბარათი არ არსებობს ან მიუწვდომელია.", - "Categories.CreateCategoryDialog.CancelText": "გაუქმება", - "Categories.CreateCategoryDialog.CreateText": "Შექმნა", - "Categories.CreateCategoryDialog.Placeholder": "მიეცით სახელი თქვენს კატეგორიას", - "Categories.CreateCategoryDialog.UpdateText": "განახლება", - "CenterPanel.Login": "შესვლა", - "CenterPanel.Share": "გაზიარება", - "ColorOption.selectColor": "აირჩიეთ {color} ფერი", - "Comment.delete": "წაშლა", - "CommentsList.send": "გაგზავნა", - "ConfirmationDialog.cancel-action": "გაუქმება", - "ConfirmationDialog.confirm-action": "დადასტურება", - "ContentBlock.Delete": "წაშლა", - "ContentBlock.DeleteAction": "წაშლა", - "ContentBlock.addElement": "დაამატეთ {type}", - "ContentBlock.checkbox": "მონიშვნის ველი", - "ContentBlock.divider": "გამყოფი", - "ContentBlock.editCardCheckbox": "მონიშნული-მონიშვნის ველი", - "ContentBlock.editCardCheckboxText": "ბარათის ტექსტის რედაქტირება", - "ContentBlock.editCardText": "ბარათის ტექსტის რედაქტირება", - "ContentBlock.editText": "ტექსტის რედაქტირება...", - "ContentBlock.image": "გამოსახულება" -} diff --git a/webapp/boards/i18n/kab.json b/webapp/boards/i18n/kab.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/webapp/boards/i18n/kab.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/webapp/boards/i18n/kk.json b/webapp/boards/i18n/kk.json deleted file mode 100644 index 9b4e6d61dc..0000000000 --- a/webapp/boards/i18n/kk.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Гіруп қосу", - "BoardComponent.delete": "Жою", - "BoardComponent.hidden-columns": "Жасырын бағандар", - "BoardComponent.hide": "Жасыру", - "BoardComponent.new": "+ Жаңа", - "BoardComponent.no-property": "{property} жоқ", - "BoardComponent.no-property-title": "Бос {property} сипаты бар Item'дер осында болады. Бұл бағанды жою мүмкін емес.", - "BoardComponent.show": "Көрсету", - "BoardPage.newVersion": "Boards'тың жаңа нұсқасы қолжетімді, қайта жүктеу үшін осы жерді басыңыз.", - "BoardPage.syncFailed": "Тақта жойылуы немесе кіруге тыйым салынуы мүмкін.", - "BoardsUnfurl.Remainder": "+{remainder} көбірек", - "BoardsUnfurl.Updated": "Жүктелді {time}", - "Calculations.Options.average.displayName": "Орташа", - "Calculations.Options.average.label": "Орташа", - "Calculations.Options.count.displayName": "Санау", - "Calculations.Options.count.label": "Санау", - "Calculations.Options.countChecked.displayName": "Тексерілді", - "Calculations.Options.countChecked.label": "Санақ Тексерілді", - "Calculations.Options.countUnchecked.displayName": "Тексерілмеген", - "Calculations.Options.countUnchecked.label": "Санақ Тексерілмеген", - "Calculations.Options.countUniqueValue.displayName": "Бірегей", - "Calculations.Options.countUniqueValue.label": "Бірегей Мәндерді Санау", - "Calculations.Options.countValue.displayName": "Мәндер", - "Calculations.Options.countValue.label": "Мәнін Есептеу", - "Calculations.Options.dateRange.displayName": "Ранжы", - "Calculations.Options.dateRange.label": "Ранжы", - "Calculations.Options.earliest.displayName": "Бұрынғысы", - "Calculations.Options.earliest.label": "Бұрынғысы", - "Calculations.Options.latest.displayName": "Соңғы", - "Calculations.Options.latest.label": "Соңғы", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Есептеу", - "Calculations.Options.none.label": "Жоқ", - "Calculations.Options.percentChecked.displayName": "Тексерілді", - "Calculations.Options.percentChecked.label": "Пайыз Тексерілді", - "Calculations.Options.percentUnchecked.displayName": "Тексерілмеген", - "Calculations.Options.percentUnchecked.label": "Пайыз Тексерілмеген", - "Calculations.Options.range.displayName": "Ранжы", - "Calculations.Options.range.label": "Ранжы", - "Calculations.Options.sum.displayName": "Сома", - "Calculations.Options.sum.label": "Сома", - "CardDetail.Follow": "Follow", - "CardDetail.Following": "Following", - "CardDetail.add-content": "Кәнтен қосу", - "CardDetail.add-icon": "Икон қосу", - "CardDetail.add-property": "+ Қасиет қосу", - "CardDetail.addCardText": "Кәрте мәтінін қосу", - "CardDetail.moveContent": "Кәрте кәнтенін жылжыту", - "CardDetail.new-comment-placeholder": "Пікір қосу...", - "CardDetailProperty.confirm-delete-heading": "Сипатты Жоюды Растаңыз", - "CardDetailProperty.confirm-delete-subtext": "\"{propertyName}\" сипатын шынымен жойғыныз келе ме? Оны жойсаныз бұл сипат осы тақтадағы барлық кәртелерден жойылады.", - "CardDetailProperty.confirm-property-name-change-subtext": "\"{propertyName}\" {customText} сипатты шынымен өзгерткініз келе ме? Бұл осы тақтадағы {numOfCards} кәрте(лердің) мән(дер)іне әсер етеді және деректердің жағалуына әкелуі мүмкін.", - "CardDetailProperty.confirm-property-type-change": "Сипат түрін өзгертуді растаңыз!", - "CardDetailProperty.delete-action-button": "Жою", - "CardDetailProperty.property-change-action-button": "Property'ді Өзгерту", - "CardDetailProperty.property-changed": "Property сәтті өзгертілді!", - "CardDetailProperty.property-deleted": "{propertyName} Сәтті Жойылды!", - "CardDetailProperty.property-name-change-subtext": "\"{oldPropType}\" тен \"{newPropType}\"'қа дейін теріңіз", - "CardDialog.editing-template": "Сіз үлгіні өзгертудесіз.", - "CardDialog.nocard": "Бұл кәрте жоқ немесе қолжетімсіз.", - "ColorOption.selectColor": "{color} Түсті Танданыз", - "Comment.delete": "Жою", - "CommentsList.send": "Жіберу", - "ConfirmationDialog.cancel-action": "Болдырмау", - "ConfirmationDialog.confirm-action": "Растау", - "ContentBlock.Delete": "Жою", - "ContentBlock.DeleteAction": "жою", - "ContentBlock.addElement": "{type} қосу", - "ContentBlock.checkbox": "құсбелгі", - "ContentBlock.divider": "бөлгіш", - "ContentBlock.editCardCheckbox": "тандалған құсбелгі", - "ContentBlock.editCardCheckboxText": "кәрте мәтінін өзгерту", - "ContentBlock.editCardText": "кәрте мәтінін өзгерту", - "ContentBlock.editText": "Мәтінді өзгерту...", - "ContentBlock.image": "сурет", - "ContentBlock.insertAbove": "Жоғарға еңгізу", - "ContentBlock.moveDown": "Түсіру", - "ContentBlock.moveUp": "Көтеру", - "ContentBlock.text": "мәтін", - "DeleteBoardDialog.confirm-cancel": "Болдырмау", - "DeleteBoardDialog.confirm-delete": "Жою", - "DeleteBoardDialog.confirm-info": "\"{boardTitle}\" тақтасын шынымен жойғыңыз келе ме? Оны жою тақтадағы барлық кәртелерді жояды.", - "DeleteBoardDialog.confirm-tite": "Тақтаны Жоюды Растаңыз", - "Dialog.closeDialog": "Диалогты жабу", - "EditableDayPicker.today": "Бүгін", - "Error.mobileweb": "Mobile web қолдау қазір бастапқы бетада. Барлық мүмкіншілктер болмауы мүмкін.", - "Error.websocket-closed": "Websocket қосылымы жабылды, байланыс үзілді. Бұл әлі сақталса, серверді немесе web proxy кәнфиғуратенін тексеріңіз.", - "Filter.includes": "кірістіреді", - "Filter.is-empty": "бос", - "Filter.is-not-empty": "іші бос емес", - "Filter.not-includes": "кірістірмейді", - "FilterComponent.add-filter": "+ Филтір қосу", - "FilterComponent.delete": "Жою", - "GroupBy.ungroup": "Гірупсіздендіру", - "KanbanCard.untitled": "Атаусыз", - "Mutator.new-card-from-template": "үлгіден жаңа кәрте жасау", - "Mutator.new-template-from-card": "кәртеден жаңа үлгі", - "PropertyMenu.Delete": "Жою", - "PropertyMenu.changeType": "Property түрін өзгерту", - "PropertyMenu.selectType": "Property түрін тандау", - "PropertyMenu.typeTitle": "Түрі", - "PropertyType.Checkbox": "Құсбелгі", - "PropertyType.CreatedBy": "Жасаған", - "PropertyType.CreatedTime": "Жасалған уақыты", - "PropertyType.Date": "Даты", - "PropertyType.Email": "Email", - "PropertyType.MultiSelect": "Multi таңдау", - "PropertyType.Number": "Нөмір", - "PropertyType.Person": "Тұлға", - "PropertyType.Phone": "Телефон", - "PropertyType.Select": "Таңдау", - "PropertyType.Text": "Мәтін", - "PropertyType.UpdatedBy": "Соңғы өзгерткен", - "PropertyType.UpdatedTime": "Соңғы өзгертілген уақыты", - "PropertyValueElement.empty": "Бос", - "RegistrationLink.confirmRegenerateToken": "Бұл бұрын таратылған сілтемелерді жарамсыз етеді. Жалғастырасыз ба?", - "RegistrationLink.copiedLink": "Көшірілді!", - "RegistrationLink.copyLink": "Сілтемені көшіру", - "RegistrationLink.description": "Басқалар аққаунт жасау үшін осы сілтемені тарату:", - "RegistrationLink.regenerateToken": "Токенді регенераттау", - "RegistrationLink.tokenRegenerated": "Тіркелу сілтемесі регенератталды", - "ShareBoard.confirmRegenerateToken": "Бұл бұрын таратылған сілтемелерді жарамсыз етеді. Жалғастырасыз ба?", - "ShareBoard.copiedLink": "Көшірілді!", - "ShareBoard.copyLink": "Сілтемені көшіру", - "ShareBoard.tokenRegenrated": "Токен регенератталды", - "Sidebar.about": "Focalboard туралы", - "Sidebar.add-board": "+ Тақта қосу", - "Sidebar.changePassword": "Кілтсөзді өзгерту", - "Sidebar.delete-board": "Тақтаны жою", - "Sidebar.export-archive": "Мұрағатты экспорттау", - "Sidebar.import-archive": "Мұрағатты импорттау", - "Sidebar.invite-users": "Қолданушыларды шақыру", - "Sidebar.logout": "Шығу", - "Sidebar.random-icons": "Рандом икондар", - "Sidebar.set-language": "Тілді таңдау", - "Sidebar.set-theme": "Теміні орнату", - "Sidebar.settings": "Баптаулар", - "Sidebar.untitled-board": "(Атаусыз Тақта)", - "TableComponent.add-icon": "Иконды қосу", - "TableComponent.name": "Атауы", - "TableComponent.plus-new": "+ Қосу", - "TableHeaderMenu.delete": "Жою", - "TableHeaderMenu.duplicate": "Көшірмесін жасау", - "TableHeaderMenu.hide": "Жасыру", - "TableHeaderMenu.insert-left": "Солға еңгізу", - "TableHeaderMenu.insert-right": "Оңға еңгізу", - "TableHeaderMenu.sort-ascending": "Өсуі бойынша сұрыптау", - "TableHeaderMenu.sort-descending": "Кему бойынша сұрыптау", - "TableRow.open": "Ашу", - "TopBar.give-feedback": "Feedback беру", - "ValueSelector.noOptions": "Оптендер жоқ. Біріншісін қосу үшін теруді бастаныз!", - "ValueSelector.valueSelector": "Мән селектірі", - "ValueSelectorLabel.openMenu": "Мәзірді ашу", - "View.AddView": "Көріністі қосу", - "View.Board": "Тақта", - "View.DeleteView": "Көріністі жою", - "View.DuplicateView": "Көріністің көшірмесін жасау", - "View.Gallery": "Гәлері", - "View.NewBoardTitle": "Тақта көрінісі", - "View.NewCalendarTitle": "Күнтізбе Көрінісі", - "View.NewGalleryTitle": "Гәлері көрінісі", - "View.NewTableTitle": "Тақта көрінісі", - "View.Table": "Кесте", - "ViewHeader.add-template": "Жаңа үлгі", - "ViewHeader.delete-template": "Жою", - "ViewHeader.display-by": "{property} бойынша көрсету", - "ViewHeader.edit-template": "Өзгерту", - "ViewHeader.empty-card": "Кәртені тазарту", - "ViewHeader.export-board-archive": "Тақта мұрағатын экспорттау", - "ViewHeader.export-complete": "Экспорт аяқталды!", - "ViewHeader.export-csv": "CSV'ге экспорттау", - "ViewHeader.export-failed": "Экспорт сәтсіз аяқталды!", - "ViewHeader.filter": "Филтір", - "ViewHeader.group-by": "{property} бойынша гіруптеу", - "ViewHeader.new": "Жаңа", - "ViewHeader.properties": "Property'лер", - "ViewHeader.search-text": "Мәтінді іздеу", - "ViewHeader.select-a-template": "Үлгіні таңдау", - "ViewHeader.set-default-template": "Әдепкі ретінде орнату", - "ViewHeader.sort": "Сұрыптау", - "ViewHeader.untitled": "Атаусыз", - "ViewTitle.hide-description": "сипаттаманы жасыру", - "ViewTitle.pick-icon": "Иконды таңдау", - "ViewTitle.random-icon": "Рандом", - "ViewTitle.remove-icon": "Иконды кетіру", - "ViewTitle.show-description": "Сипаттаманы көрсету", - "ViewTitle.untitled-board": "Атаусыз тақта", - "WelcomePage.Description": "Тақталар дегеніміз танымал қанбан (kanban) тақта көрінісін қолданып, тимдер арасындағы жұмысты анықтауға, ұйымдастыруға, қадағалауға және басқаруға көмектесетін жобаны басқару құралы", - "WelcomePage.Explore.Button": "Зерттеу", - "WelcomePage.Heading": "Тақталарға Қош Келдініз", - "Workspace.editing-board-template": "Сіз тақта үлгісін өзгертудесіз.", - "calendar.month": "Ай", - "calendar.today": "БҮГІН", - "calendar.week": "Апта", - "default-properties.title": "Атау", - "login.log-in-button": "Кіру", - "login.log-in-title": "Кіру", - "login.register-button": "немесе сізде жоқ болса аққаунт жасаныз", - "register.login-button": "немесе аққаунтыныз болса кірініз", - "register.signup-title": "Аққаунт жасау үшін тіркелініз" -} diff --git a/webapp/boards/i18n/ko.json b/webapp/boards/i18n/ko.json deleted file mode 100644 index 08582fc0df..0000000000 --- a/webapp/boards/i18n/ko.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "시스템 관리자", - "AdminBadge.TeamAdmin": "팀 관리자", - "AppBar.Tooltip": "연결된 보드 토글", - "Attachment.Attachment-title": "첨부", - "AttachmentBlock.DeleteAction": "삭제", - "AttachmentBlock.addElement": "{type} 추가", - "AttachmentBlock.delete": "첨부 파일을 삭제했습니다.", - "AttachmentBlock.failed": "첨부 파일 크기 제한을 초과하기 때문에 업로드할 수 없습니다.", - "AttachmentBlock.upload": "첨부 파일을 업로드 중입니다.", - "AttachmentBlock.uploadSuccess": "첨부 파일을 업로드했습니다.", - "AttachmentElement.delete-confirmation-dialog-button-text": "삭제", - "AttachmentElement.download": "다운로드", - "AttachmentElement.upload-percentage": "업로드 중...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ 그룹 추가", - "BoardComponent.delete": "삭제", - "BoardComponent.hidden-columns": "숨겨진 열", - "BoardComponent.hide": "숨기기", - "BoardComponent.new": "+ 신규", - "BoardComponent.no-property": "{property} 속성 없음", - "BoardComponent.no-property-title": "{property} 속성이 빈 항목은 여기에 배치됩니다. 이 열은 제거할 수 없습니다.", - "BoardComponent.show": "보이기", - "BoardMember.schemeAdmin": "관리자", - "BoardMember.schemeCommenter": "댓글 작성자", - "BoardMember.schemeEditor": "편집자", - "BoardMember.schemeNone": "없음", - "BoardMember.schemeViewer": "열람자", - "BoardMember.unlinkChannel": "링크 해제", - "BoardPage.newVersion": "새 버전의 보드가 존재합니다, 여기를 눌러 다시 불러오세요.", - "BoardPage.syncFailed": "보드가 삭제되었거나 권한이 거부되었습니다.", - "BoardTemplateSelector.add-template": "새 템플릿 만들기", - "BoardTemplateSelector.create-empty-board": "빈 보드 만들기", - "BoardTemplateSelector.delete-template": "삭제", - "BoardTemplateSelector.description": "아래에 정의된 템플릿을 사용하여 사이드바에 보드를 추가하거나 처음부터 시작하세요.", - "BoardTemplateSelector.edit-template": "편집", - "BoardTemplateSelector.plugin.no-content-description": "아래에 정의된 템플릿을 사용하여 사이드바에 보드를 추가하거나 처음부터 시작하세요.", - "BoardTemplateSelector.plugin.no-content-title": "보드 만들기", - "BoardTemplateSelector.title": "보드 만들기", - "BoardTemplateSelector.use-this-template": "이 템플릿 사용", - "BoardsSwitcher.Title": "보드 찾기", - "BoardsUnfurl.Limited": "보관 중인 카드로 인해 추가 세부정보가 숨겨져 있습니다", - "BoardsUnfurl.Remainder": "+{remainder} 추가", - "BoardsUnfurl.Updated": "{time}에 수정됨", - "Calculations.Options.average.displayName": "평균", - "Calculations.Options.average.label": "평균", - "Calculations.Options.count.displayName": "개수", - "Calculations.Options.count.label": "개수", - "Calculations.Options.countChecked.displayName": "확인됨", - "Calculations.Options.countChecked.label": "확인된 수", - "Calculations.Options.countUnchecked.displayName": "확인되지 않음", - "Calculations.Options.countUnchecked.label": "확인되지 않은 개수", - "Calculations.Options.countUniqueValue.displayName": "고윳값", - "Calculations.Options.countUniqueValue.label": "고유값 계산", - "Calculations.Options.countValue.displayName": "값", - "Calculations.Options.countValue.label": "계산 값", - "Calculations.Options.dateRange.displayName": "범위", - "Calculations.Options.dateRange.label": "범위", - "Calculations.Options.earliest.displayName": "이른 순으로", - "Calculations.Options.earliest.label": "이른 순으로", - "Calculations.Options.latest.displayName": "늦은 순으로", - "Calculations.Options.latest.label": "늦은 순으로", - "Calculations.Options.max.displayName": "최대", - "Calculations.Options.max.label": "최대", - "Calculations.Options.median.displayName": "중간", - "Calculations.Options.median.label": "중간", - "Calculations.Options.min.displayName": "최소", - "Calculations.Options.min.label": "최소", - "Calculations.Options.none.displayName": "계산", - "Calculations.Options.none.label": "없음", - "Calculations.Options.percentChecked.displayName": "확인됨", - "Calculations.Options.percentChecked.label": "확인된 비율", - "Calculations.Options.percentUnchecked.displayName": "미확인", - "Calculations.Options.percentUnchecked.label": "미확인 비율", - "Calculations.Options.range.displayName": "범위", - "Calculations.Options.range.label": "범위", - "Calculations.Options.sum.displayName": "합계", - "Calculations.Options.sum.label": "합계", - "CalendarCard.untitled": "제목 없음", - "CardActionsMenu.copiedLink": "복사됨!", - "CardActionsMenu.copyLink": "링크 복사", - "CardActionsMenu.delete": "삭제", - "CardActionsMenu.duplicate": "복제하기", - "CardBadges.title-checkboxes": "체크박스", - "CardBadges.title-comments": "댓글", - "CardBadges.title-description": "이 카드에는 설명이 있습니다", - "CardDetail.Attach": "첨부", - "CardDetail.Follow": "팔로우", - "CardDetail.Following": "팔로잉", - "CardDetail.add-content": "콘텐츠 추가", - "CardDetail.add-icon": "아이콘 추가", - "CardDetail.add-property": "+ 속성 추가", - "CardDetail.addCardText": "카드 텍스트 추가", - "CardDetail.limited-body": "Professional 또는 Enterprise Plan으로 업그레이드하여 보관된 카드를 보거나 보드당 무제한 보기, 카드 무제한 보기 등을 할 수 있습니다.", - "CardDetail.limited-button": "업그레이드", - "CardDetail.limited-title": "숨겨진 카드입니다", - "CardDetail.moveContent": "카드 내용 이동", - "CardDetail.new-comment-placeholder": "댓글 추가...", - "CardDetailProperty.confirm-delete-heading": "속성 삭제 확인", - "CardDetailProperty.confirm-delete-subtext": "정말로 \"{propertyName}\"속성을 삭제할까요? 보드의 모든 카드에서 이 속성이 삭제됩니다.", - "CardDetailProperty.confirm-property-name-change-subtext": "정말로 \"{propertyName}\" 속성을 {customText}(으)로 바꾸시겠습니까? 이 보드에 있는 {numOfCards}개의 카드가 수정되며, 데이터가 손실될 수 있습니다.", - "CardDetailProperty.confirm-property-type-change": "속성 유형 변경 확인", - "CardDetailProperty.delete-action-button": "삭제", - "CardDetailProperty.property-change-action-button": "속성 변경", - "CardDetailProperty.property-changed": "성공적으로 속성이 변경되었습니다!", - "CardDetailProperty.property-deleted": "{propertyName}을(를) 성공적으로 삭제했습니다!", - "CardDetailProperty.property-name-change-subtext": "유형을 \"{oldPropType}\"에서 \"{newPropType}\"(으)로", - "CardDetial.limited-link": "Plan에 대해 자세히 알아보기.", - "CardDialog.delete-confirmation-dialog-attachment": "첨부 파일 삭제 확인", - "CardDialog.delete-confirmation-dialog-button-text": "삭제", - "CardDialog.delete-confirmation-dialog-heading": "카드 삭제 확인", - "CardDialog.editing-template": "템플릿을 수정하는 중입니다.", - "CardDialog.nocard": "이 카드는 존재하지 않거나 사용할 수 없습니다.", - "Categories.CreateCategoryDialog.CancelText": "취소", - "Categories.CreateCategoryDialog.CreateText": "생성", - "Categories.CreateCategoryDialog.Placeholder": "카테고리 이름 지정", - "Categories.CreateCategoryDialog.UpdateText": "업데이트", - "CenterPanel.Login": "로그인", - "CenterPanel.Share": "공유", - "ChannelIntro.CreateBoard": "보드 생성", - "ColorOption.selectColor": "{color} 색 선택하기", - "Comment.delete": "삭제", - "CommentsList.send": "보내기", - "ConfirmPerson.empty": "비우기", - "ConfirmPerson.search": "검색...", - "ConfirmationDialog.cancel-action": "취소", - "ConfirmationDialog.confirm-action": "확인", - "ContentBlock.Delete": "삭제", - "ContentBlock.DeleteAction": "삭제", - "ContentBlock.addElement": "{type} 추가", - "ContentBlock.checkbox": "체크박스", - "ContentBlock.divider": "구분선", - "ContentBlock.editCardCheckbox": "토글 체크박스", - "ContentBlock.editCardCheckboxText": "카드 텍스트 수정", - "ContentBlock.editCardText": "카드 텍스트 수정", - "ContentBlock.editText": "텍스트 수정...", - "ContentBlock.image": "이미지", - "ContentBlock.insertAbove": "위에 삽입", - "ContentBlock.moveBlock": "카드 내용 이동", - "ContentBlock.moveDown": "아래로 이동", - "ContentBlock.moveUp": "위로 이동", - "ContentBlock.text": "텍스트", - "DateFilter.empty": "비우기", - "DateRange.clear": "지우기", - "DateRange.empty": "비우기", - "DateRange.endDate": "종료일자", - "DateRange.today": "오늘", - "DeleteBoardDialog.confirm-cancel": "취소", - "DeleteBoardDialog.confirm-delete": "삭제", - "DeleteBoardDialog.confirm-info": "“{boardTitle}” 보드를 삭제할까요? 이 보드에 있는 모든 카드들이 삭제됩니다.", - "DeleteBoardDialog.confirm-info-template": "{boardTitle} 보드 템플릿을 삭제할까요?", - "DeleteBoardDialog.confirm-tite": "보드 삭제 확인", - "DeleteBoardDialog.confirm-tite-template": "보드 템플릿 삭제 확인", - "Dialog.closeDialog": "대화창 닫기", - "EditableDayPicker.today": "오늘", - "Error.mobileweb": "모바일 웹 지원은 현재 초기 베타 버전입니다. 모든 기능이 있는 것은 아닙니다.", - "Error.websocket-closed": "웹 소켓 연결이 닫혀서 연결이 중단되었습니다. 이 문제가 지속되면 서버 또는 웹 프록시 구성을 확인하세요.", - "Filter.contains": "~을 포함", - "Filter.ends-with": "~로 끝남", - "Filter.includes": "~을 포함", - "Filter.is": "~이다", - "Filter.is-after": "~ 이후", - "Filter.is-before": "~ 이전", - "Filter.is-empty": "비어있음", - "Filter.is-not-empty": "비어 있지 않음", - "Filter.is-not-set": "설정되지 않음", - "Filter.is-set": "설정된", - "Filter.isafter": "~ 이후", - "Filter.isbefore": "~ 이전", - "Filter.not-contains": "~을 포함하지 않음", - "Filter.not-ends-with": "~로 끝나지 않음", - "Filter.not-includes": "~을 포함하지 않음", - "Filter.not-starts-with": "~로 시작하지 않음", - "Filter.starts-with": "~로 시작함", - "FilterByText.placeholder": "필터 텍스트", - "FilterComponent.add-filter": "+ 필터 추가", - "FilterComponent.delete": "삭제", - "FilterValue.empty": "(비었음)", - "FindBoardsDialog.IntroText": "보드에서 검색", - "FindBoardsDialog.NoResultsFor": "\"{searchQuery}\" 에 대한 검색 결과가 없습니다", - "FindBoardsDialog.NoResultsSubtext": "오타를 확인하거나 또는 다른 단어로 검색해보세요.", - "FindBoardsDialog.SubTitle": "보드를 찾으려면 입력하십시오. 탐색은 UP/DOWN 버튼으로, 선택은 ENTER , 해제는 ESC입니다", - "FindBoardsDialog.Title": "보드 찾기", - "GroupBy.hideEmptyGroups": "{count}개의 빈 그룹 숨기기", - "GroupBy.showHiddenGroups": "{count}개의 숨김 그룹 보기", - "GroupBy.ungroup": "그룹 해제", - "HideBoard.MenuOption": "보드 숨기기", - "KanbanCard.untitled": "제목 없음", - "MentionSuggestion.is-not-board-member": "(보드 멤버가 아님)", - "Mutator.new-board-from-template": "템플릿에서 새 보드 만들기", - "Mutator.new-card-from-template": "템플릿에서 새 카드 만들기", - "Mutator.new-template-from-card": "카드에서 새 템플릿 만들기", - "OnboardingTour.AddComments.Body": "이슈에 댓글을 달거나, 동료 Mattermost 사용자를 @멘션하여 관심을 끌 수도 있습니다.", - "OnboardingTour.AddComments.Title": "댓글 추가", - "OnboardingTour.AddDescription.Body": "팀원들이 카드에 대해 알 수 있도록 설명을 추가하세요.", - "OnboardingTour.AddDescription.Title": "설명 추가", - "OnboardingTour.AddProperties.Body": "카드에 다양한 속성을 추가하여 더욱 강력해 질 수 있습니다.", - "OnboardingTour.AddProperties.Title": "속성 추가", - "OnboardingTour.AddView.Body": "다양한 레이아웃을 사용하여 보드를 구성하는 새 보기를 만들려면 여기로 이동하세요.", - "OnboardingTour.AddView.Title": "새 보기 추가", - "OnboardingTour.CopyLink.Body": "링크를 복사한 후 채널, 쪽지 또는 그룹 메시지에 붙여넣어서 팀원들과 카드를 공유할 수 있습니다.", - "OnboardingTour.CopyLink.Title": "링크 복사", - "OnboardingTour.OpenACard.Body": "카드를 열어 보드로 어떻게 작업을 정리할 수 있는지 효과적인 방법을 살펴보세요.", - "OnboardingTour.OpenACard.Title": "카드 열기", - "OnboardingTour.ShareBoard.Body": "보드를 팀 내에서 내부적으로 공유하거나 조직 외부에서 볼 수 있도록 공개적으로 발행할 수 있습니다.", - "OnboardingTour.ShareBoard.Title": "보드 공유", - "PersonProperty.board-members": "보드 멤버", - "PersonProperty.me": "나", - "PersonProperty.non-board-members": "보드 멤버가 아닙니다", - "PropertyMenu.Delete": "삭제", - "PropertyMenu.changeType": "속성 유형 변경", - "PropertyMenu.selectType": "속성 유형 선택", - "PropertyMenu.typeTitle": "유형", - "PropertyType.Checkbox": "체크박스", - "PropertyType.CreatedBy": "만든 사람", - "PropertyType.CreatedTime": "생성 시간", - "PropertyType.Date": "날짜", - "PropertyType.Email": "이메일", - "PropertyType.MultiPerson": "다중 사용자", - "PropertyType.MultiSelect": "다중 선택", - "PropertyType.Number": "숫자", - "PropertyType.Person": "사람", - "PropertyType.Phone": "전화번호", - "PropertyType.Select": "선택", - "PropertyType.Text": "텍스트", - "PropertyType.Unknown": "알 수 없음", - "PropertyType.UpdatedBy": "마지막으로 업데이트한 사람", - "PropertyType.UpdatedTime": "마지막 업데이트 시간", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "비었음", - "RegistrationLink.confirmRegenerateToken": "이전에 공유된 링크가 무효화됩니다. 계속할까요?", - "RegistrationLink.copiedLink": "복사됨!", - "RegistrationLink.copyLink": "링크 복사", - "RegistrationLink.description": "다른 구성원이 계정을 만들 수 있도록 이 링크를 공유하세요:", - "RegistrationLink.regenerateToken": "토큰 재성성", - "RegistrationLink.tokenRegenerated": "등록 링크를 다시 생성했습니다", - "ShareBoard.PublishDescription": "읽기 전용 링크를 발행하고 웹 상의 모든 사용자와 공유합니다.", - "ShareBoard.PublishTitle": "웹에 발행", - "ShareBoard.ShareInternal": "내부 공유", - "ShareBoard.ShareInternalDescription": "권한이 있는 사용자는 이 링크를 사용할 수 있습니다.", - "ShareBoard.Title": "보드 공유", - "ShareBoard.confirmRegenerateToken": "이전에 공유된 링크가 무효화됩니다. 계속하시겠습니까?", - "ShareBoard.copiedLink": "복사됨!", - "ShareBoard.copyLink": "링크 복사", - "ShareBoard.regenerate": "토큰 재생성", - "ShareBoard.searchPlaceholder": "사용자 및 채널 검색", - "ShareBoard.teamPermissionsText": "{teamName}팀의 모든 사용자", - "ShareBoard.tokenRegenrated": "토큰이 재성생되었음", - "ShareBoard.userPermissionsRemoveMemberText": "멤버 제거", - "ShareBoard.userPermissionsYouText": "(당신)", - "ShareTemplate.Title": "템플릿 공유", - "ShareTemplate.searchPlaceholder": "사용자 검색", - "Sidebar.about": "Focalboard에 대하여", - "Sidebar.add-board": "+ 보드 추가", - "Sidebar.changePassword": "패스워드 변경", - "Sidebar.delete-board": "보드 삭제", - "Sidebar.duplicate-board": "보드 복제", - "Sidebar.export-archive": "보관 데이터 내보내기", - "Sidebar.import": "가져오기", - "Sidebar.import-archive": "보관데이터 가져오기", - "Sidebar.invite-users": "사용자 초대", - "Sidebar.logout": "로그아웃", - "Sidebar.new-category.badge": "신규", - "Sidebar.new-category.drag-boards-cta": "보드를 여기에 드래그하세요...", - "Sidebar.no-boards-in-category": "해당 카테고리에 보드가 존재하지 않음", - "Sidebar.product-tour": "제품 둘러보기", - "Sidebar.random-icons": "임의 아이콘", - "Sidebar.set-language": "언어 설정", - "Sidebar.set-theme": "테마 설정", - "Sidebar.settings": "설정", - "Sidebar.template-from-board": "보드의 새 템플릿 추가", - "Sidebar.untitled-board": "(제목 없는 보드)", - "Sidebar.untitled-view": "(제목 없는 뷰)", - "SidebarCategories.BlocksMenu.Move": "이동 ...", - "SidebarCategories.CategoryMenu.CreateNew": "새 카테고리 생성", - "SidebarCategories.CategoryMenu.Delete": "카테고리 삭제", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName}의 다시 보드 카테고리로 이동합니다. 당신은 어떤 보드에서도 제거되지 않았습니다.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "해당 카테고리를 삭제하시겠습니까?", - "SidebarCategories.CategoryMenu.Update": "카테고리 이름 수정하기", - "SidebarTour.ManageCategories.Body": "사용자 정의 카테고리를 만들고 관리합니다. 카테고리는 사용자별로 다르므로 보드를 사용자의 카레고리로 이동해도 동일한 보드를 사용하는 다른 구성원에게는 영향을 주지 않습니다.", - "SidebarTour.ManageCategories.Title": "카테고리 관리하기", - "SidebarTour.SearchForBoards.Body": "(Cmd/Ctrl + K)를 열어 보드를 빠르게 검색하고 사이드바에 추가합니다.", - "SidebarTour.SearchForBoards.Title": "보드 검색하기", - "SidebarTour.SidebarCategories.Body": "이제 모든 보드가 새 사이드바 아래에 정리됩니다. 워크스페이스 간 전환은 더 이상 필요 없습니다. v7.2 업그레이드의 일부로 이전 작업 공간을 기반으로 한 일회성 사용자 지정 카테고리가 자동으로 생성될 수 있습니다. 해당 기능을 통해 원하는 대로 카테고리를 제거하거나 편집할 수 있습니다.", - "SidebarTour.SidebarCategories.Link": "더 배우기", - "SidebarTour.SidebarCategories.Title": "사이드바 카테고리", - "SiteStats.total_boards": "모든 보드", - "SiteStats.total_cards": "모든 카드", - "TableComponent.add-icon": "아이콘 추가", - "TableComponent.name": "이름", - "TableComponent.plus-new": "+ 생성", - "TableHeaderMenu.delete": "삭제", - "TableHeaderMenu.duplicate": "복제", - "TableHeaderMenu.hide": "숨김", - "TableHeaderMenu.insert-left": "왼쪽에 삽입", - "TableHeaderMenu.insert-right": "오른쪽에 삽입", - "TableHeaderMenu.sort-ascending": "오름차순 정렬", - "TableHeaderMenu.sort-descending": "내림차순 정렬", - "TableRow.DuplicateCard": "카드 복제", - "TableRow.MoreOption": "더 많은 행동", - "TableRow.open": "열기", - "TopBar.give-feedback": "피드백 하기", - "URLProperty.copiedLink": "복사되었습니다!", - "URLProperty.copy": "복사", - "URLProperty.edit": "수정", - "UndoRedoHotKeys.canRedo": "다시 실행하기", - "UndoRedoHotKeys.canRedo-with-description": "{description} 다시 실행하기", - "UndoRedoHotKeys.canUndo": "실행 취소하기", - "UndoRedoHotKeys.canUndo-with-description": "{description} 실행 취소하기", - "UndoRedoHotKeys.cannotRedo": "다시 실행하지 않기", - "UndoRedoHotKeys.cannotUndo": "실행취소 하지 않기", - "ValueSelector.noOptions": "옵션이 없습니다. 새로 추가하려면 입력을 시작하세요!", - "ValueSelector.valueSelector": "값 선택", - "ValueSelectorLabel.openMenu": "메뉴 열기", - "VersionMessage.help": "이 버전의 새로운 기능을 확인하십시오.", - "VersionMessage.learn-more": "더 알아보기", - "View.AddView": "뷰 추가", - "View.Board": "보드", - "View.DeleteView": "뷰 삭제", - "View.DuplicateView": "뷰 복제", - "View.Gallery": "갤러리", - "View.NewBoardTitle": "보드 형태로 보기", - "View.NewCalendarTitle": "달력 형태로 보기", - "View.NewGalleryTitle": "갤리리 형태로 보기", - "View.NewTableTitle": "표 형태로 보기", - "View.NewTemplateDefaultTitle": "무제 템플릿", - "View.NewTemplateTitle": "제목 없음", - "View.Table": "표", - "ViewHeader.add-template": "새 템플릿", - "ViewHeader.delete-template": "삭제", - "ViewHeader.display-by": "표시 대상: {property}", - "ViewHeader.edit-template": "수정", - "ViewHeader.empty-card": "빈 카드", - "ViewHeader.export-board-archive": "보드 아카이브 내보내기", - "ViewHeader.export-complete": "내보내기가 완료되었습니다!", - "ViewHeader.export-csv": "CSV로 내보내기", - "ViewHeader.export-failed": "내보내기가 실패했습니다!", - "ViewHeader.filter": "필터", - "ViewHeader.group-by": "{property}로 그룹화", - "ViewHeader.new": "생성", - "ViewHeader.properties": "속성", - "ViewHeader.properties-menu": "속성 메뉴", - "ViewHeader.search-text": "카드 검색하기", - "ViewHeader.select-a-template": "템플릿 선택", - "ViewHeader.set-default-template": "기본으로 설정", - "ViewHeader.sort": "정렬", - "ViewHeader.untitled": "제목 없음", - "ViewHeader.view-header-menu": "머리글 메뉴 보기", - "ViewHeader.view-menu": "뷰 메뉴", - "ViewLimitDialog.Heading": "보드 당 조회 수 제한에 도달했습니다", - "ViewLimitDialog.PrimaryButton.Title.Admin": "업그레이드", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "관리자에게 알리기", - "ViewLimitDialog.Subtext.Admin": "Professional 또는 Enterprise 요금제로 업그레이드합니다.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "요금제 더 알아보기.", - "ViewLimitDialog.Subtext.RegularUser": "관리자에게 Professional 또는 Enterprise 요금제로 업그레이드를 요청하는 알림을 보냈습니다.", - "ViewLimitDialog.UpgradeImg.AltText": "이미지 업그레이드", - "ViewLimitDialog.notifyAdmin.Success": "관리자에게 알림을 보냈습니다", - "ViewTitle.hide-description": "설명 숨기기", - "ViewTitle.pick-icon": "아이콘 선택", - "ViewTitle.random-icon": "무작위", - "ViewTitle.remove-icon": "아이콘 제거", - "ViewTitle.show-description": "설명 보기", - "ViewTitle.untitled-board": "제목 없는 보드", - "WelcomePage.Description": "보드는 친숙한 칸반 보드 형태를 사용하여 팀간의 업무를 정의, 구성 및 추적하고 관리하는 프로젝트 관리 도구입니다.", - "WelcomePage.Explore.Button": "둘러보기", - "WelcomePage.Heading": "보드 사용을 환영합니다", - "WelcomePage.NoThanks.Text": "아니오, 스스로 찾아보겠습니다", - "WelcomePage.StartUsingIt.Text": "사용 시작하기", - "Workspace.editing-board-template": "보드 템플릿을 수정하는 중입니다.", - "badge.guest": "게스트", - "boardPage.confirm-join-button": "가입", - "boardPage.confirm-join-text": "보드 관리자가 명시적으로 추가하지 않은 비공개 보드에 가입하려고 합니다. 이 비공개 보드에 가입하시겠습니까?", - "boardPage.confirm-join-title": "비공개 보드 가입", - "boardSelector.confirm-link-board": "채널에 보드 연결", - "boardSelector.confirm-link-board-button": "예, 보드에 연결합니다", - "boardSelector.confirm-link-board-subtext": "{boardName}을(를) 채널에 연결하면 게스트를 제외한 채널의 모든 구성원(기존 및 새)이 해당 채널을 편집할 수 있습니다. 언제든지 채널에서 보드의 연결을 해제할 수 있습니다.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "{boardName}을(를) 채널에 연결하면 게스트를 제외한 채널의 모든 구성원(기존 및 새)이 해당 채널을 편집할 수 있습니다.{lineBreak} 이 보드는 현재 다른 채널에 연결되어 있습니다. 여기에 연결을 선택하면 연결이 해제됩니다.", - "boardSelector.create-a-board": "보드 생성", - "boardSelector.link": "연결", - "boardSelector.search-for-boards": "보드 검색", - "boardSelector.title": "보드 연결", - "boardSelector.unlink": "연결 해제", - "calendar.month": "월", - "calendar.today": "오늘", - "calendar.week": "주", - "centerPanel.undefined": "{propertyName} 없음", - "centerPanel.unknown-user": "알 수 없는 사용자", - "cloudMessage.learn-more": "더 배우기", - "createImageBlock.failed": "파일 크기 제한에 도달했기 때문에 이 파일을 업로드 할 수 없습니다.", - "default-properties.badges": "댓글 및 설명", - "default-properties.title": "제목", - "error.back-to-home": "홈으로 돌아가기", - "error.back-to-team": "팀으로 돌아가기", - "error.board-not-found": "보드를 찾을 수 없습니다.", - "error.go-login": "로그인", - "error.invalid-read-only-board": "이 보드에 액세스할 수 없습니다. 보드에 액세스하려면 로그인하십시오.", - "error.not-logged-in": "세션이 만료되었거나 로그인 하지 않았습니다. 보드에 액세스하려면 다시 로그인하십시오.", - "error.page.title": "죄송합니다, 뭔가 잘못되었습니다", - "error.team-undefined": "유효한 팀이 아닙니다.", - "error.unknown": "오류가 발생했습니다.", - "generic.previous": "이전", - "guest-no-board.subtitle": "이 팀의 어느 보드에도 접속할 수 없습니다. 누군가 보드에 추가해 줄 때까지 기다려주세요.", - "guest-no-board.title": "아직 참여 중인 보드가 없습니다", - "imagePaste.upload-failed": "파일 크기 제한으로 일부 파일을 업로드 할 수 없습니다.", - "limitedCard.title": "숨겨진 카드", - "login.log-in-button": "로그인", - "login.log-in-title": "로그인", - "login.register-button": "계정이 없다면 계정을 만드세요", - "new_channel_modal.create_board.empty_board_description": "비어있는 새 보드 생성", - "new_channel_modal.create_board.empty_board_title": "비어있는 보드", - "new_channel_modal.create_board.select_template_placeholder": "템플릿 선택", - "new_channel_modal.create_board.title": "이 채널용 보드 생성", - "notification-box-card-limit-reached.close-tooltip": "10일 동안 알림 중단", - "notification-box-card-limit-reached.contact-link": "관리자에게 알리기", - "notification-box-card-limit-reached.link": "유료 요금제로 업그레이드하기", - "notification-box-card-limit-reached.title": "{cards}개의 숨겨진 카드가 보드에 있습니다", - "notification-box-cards-hidden.title": "이 작업으로 인해 다른 카드가 숨겨졌습니다", - "notification-box.card-limit-reached.not-admin.text": "보관된 카드에 액세스하려면 {contactLink}에서 유료 요금제로 업그레이드하십시오.", - "notification-box.card-limit-reached.text": "카드 제한에 도달했습니다,이전 카드를 보려면 {link}를 눌러주세요", - "person.add-user-to-board": "{username}을(를) 보드에 추가", - "person.add-user-to-board-confirm-button": "보드에 추가", - "person.add-user-to-board-permissions": "권한", - "person.add-user-to-board-question": "{username}을(을) 보드에 추가하시겠습니까?", - "person.add-user-to-board-warning": "{username}은(는) 보드멤버가 아니므로 관련 알림을 수신하지 않습니다.", - "register.login-button": "이미 계정이 있다면 로그인하세요", - "register.signup-title": "계정 등록", - "rhs-board-non-admin-msg": "당신은 보드의 관리자가 아닙니다", - "rhs-boards.add": "추가하기", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "그룹 메시지", - "rhs-boards.header.dm": "이 DM", - "rhs-boards.header.gm": "이 그룹 메시지", - "rhs-boards.last-update-at": "마지막 업데이트 시간: {datetime}", - "rhs-boards.link-boards-to-channel": "{channelName}에 보드 연결", - "rhs-boards.linked-boards": "연결된 보드", - "rhs-boards.no-boards-linked-to-channel": "{channelName}에 아직 연결된 보드가 없습니다", - "rhs-boards.no-boards-linked-to-channel-description": "보드는 익숙한 Kanban 보드를 사용하여 팀 전체의 작업을 정의, 구성, 추적 및 관리하는 데 도움이 되는 프로젝트 관리 도구입니다.", - "rhs-boards.unlink-board": "보드 연결 해제", - "rhs-boards.unlink-board1": "보드 연결 해제", - "rhs-channel-boards-header.title": "보드", - "share-board.publish": "발행", - "share-board.share": "공유", - "shareBoard.channels-select-group": "채널", - "shareBoard.confirm-change-team-role.body": "이 보드 사용자 중 {role}보다 낮은 권한을 가진 사용자들은 {role}권한으로 승급합니다. 이 보드의 최소 권한을 변경하시겠습니까?", - "shareBoard.confirm-change-team-role.confirmBtnText": "최소 보드 권한 변경", - "shareBoard.confirm-change-team-role.title": "최소 보드 권한 변경", - "shareBoard.confirm-link-channel": "채널에 보드 연결", - "shareBoard.confirm-link-channel-button": "채널 연결", - "shareBoard.confirm-link-channel-button-with-other-channel": "연결 및 연결해제는 여기로", - "shareBoard.confirm-link-channel-subtext": "채널을 보드에 연결하면 게스트를 제외한 채널의 모든 구성원(기존 및 신규)이 해당 채널을 편집할 수 있습니다.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "채널을 보드에 연결하면 게스트를 제외한 채널의 모든 구성원(기존 및 신규)이 해당 채널을 편집할 수 있습니다.{lineBreak}이 보드는 현재 다른 채널에 연결되어 있습니다. 여기에 연결을 선택하면 연결이 해제됩니다.", - "shareBoard.confirm-unlink.body": "보드에서 채널 연결을 해제하면 채널의 모든 구성원(기존 및 신규)이 개별적으로 권한이 부여되지 않는 한 해당 채널에 대한 액세스 권한을 잃게 됩니다.", - "shareBoard.confirm-unlink.confirmBtnText": "채널 연결 해제", - "shareBoard.confirm-unlink.title": "보드에서 채널 연결 해제", - "shareBoard.lastAdmin": "보드에는 최소한 한명 이상의 관리자가 있어야 합니다", - "shareBoard.members-select-group": "멤버", - "shareBoard.unknown-channel-display-name": "알 수 없는 채널", - "tutorial_tip.finish_tour": "완료", - "tutorial_tip.got_it": "알겠습니다", - "tutorial_tip.ok": "다음", - "tutorial_tip.out": "이 도움말을 선택 해제합니다.", - "tutorial_tip.seen": "전에 본 적 있나요?" -} diff --git a/webapp/boards/i18n/lt.json b/webapp/boards/i18n/lt.json deleted file mode 100644 index 27c6f80214..0000000000 --- a/webapp/boards/i18n/lt.json +++ /dev/null @@ -1,456 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administratorius", - "AdminBadge.TeamAdmin": "Komandos administratorius", - "AppBar.Tooltip": "Perjungti susietas lentas", - "Attachment.Attachment-title": "Priedas", - "AttachmentBlock.DeleteAction": "Ištrinti", - "AttachmentBlock.addElement": "pridėti {type}", - "AttachmentBlock.delete": "Priedas ištrintas.", - "AttachmentBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.", - "AttachmentBlock.upload": "Priedo įkėlimas.", - "AttachmentBlock.uploadSuccess": "Priedas įkeltas.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Ištrinti", - "AttachmentElement.download": "Parsisiųsti", - "AttachmentElement.upload-percentage": "Įkeliama...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Pridėti grupę", - "BoardComponent.delete": "Ištrinti", - "BoardComponent.hidden-columns": "Paslėpti stulpeliai", - "BoardComponent.hide": "Slėpti", - "BoardComponent.new": "+ Naujas", - "BoardComponent.no-property": "Nėra {property}", - "BoardComponent.no-property-title": "Elementai su tuščia nuosavybe {property} pateks čia. Šio stulpelio pašalinti negalima.", - "BoardComponent.show": "Rodyti", - "BoardMember.schemeAdmin": "Administratorius", - "BoardMember.schemeCommenter": "Komentuotojas", - "BoardMember.schemeEditor": "Redaktorius", - "BoardMember.schemeNone": "Joks", - "BoardMember.schemeViewer": "Žiūriklis", - "BoardMember.unlinkChannel": "Atsieti", - "BoardPage.newVersion": "Yra nauja Boards versija. Spustelėkite čia, kad įkeltumėte iš naujo.", - "BoardPage.syncFailed": "Lenta gali būti ištrinta arba jos prieiga gali būti atšaukta.", - "BoardTemplateSelector.add-template": "Sukurti naują šabloną", - "BoardTemplateSelector.create-empty-board": "Sukurti tuščią lentą", - "BoardTemplateSelector.delete-template": "Ištrinti", - "BoardTemplateSelector.description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.", - "BoardTemplateSelector.edit-template": "Redaguoti", - "BoardTemplateSelector.plugin.no-content-description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.", - "BoardTemplateSelector.plugin.no-content-title": "Sukurti lentą", - "BoardTemplateSelector.title": "Sukurti lentą", - "BoardTemplateSelector.use-this-template": "Naudoti šį šabloną", - "BoardsSwitcher.Title": "Rasti lentas", - "BoardsUnfurl.Limited": "Papildoma informacija yra paslėpta, nes kortelė yra archyvuojama", - "BoardsUnfurl.Remainder": "+{remainder} dar", - "BoardsUnfurl.Updated": "Atnaujinta {time}", - "Calculations.Options.average.displayName": "Vidutinis", - "Calculations.Options.average.label": "Vidutinis", - "Calculations.Options.count.displayName": "Skaičius", - "Calculations.Options.count.label": "Skaičius", - "Calculations.Options.countChecked.displayName": "Pažymėta", - "Calculations.Options.countChecked.label": "Skaičius patikrintas", - "Calculations.Options.countUnchecked.displayName": "Nepažymėta", - "Calculations.Options.countUnchecked.label": "Skaičiaus žymėjimas panaikintas", - "Calculations.Options.countUniqueValue.displayName": "Unikalus", - "Calculations.Options.countUniqueValue.label": "Suskaičiuoti unikalias reikšmes", - "Calculations.Options.countValue.displayName": "Reikšmės", - "Calculations.Options.countValue.label": "Skaičius", - "Calculations.Options.dateRange.displayName": "Diapazonas", - "Calculations.Options.dateRange.label": "Diapazonas", - "Calculations.Options.earliest.displayName": "Anksčiausiai", - "Calculations.Options.earliest.label": "Anksčiausiai", - "Calculations.Options.latest.displayName": "Naujausias", - "Calculations.Options.latest.label": "Naujausias", - "Calculations.Options.max.displayName": "Maks.", - "Calculations.Options.max.label": "Maks.", - "Calculations.Options.median.displayName": "Mediana", - "Calculations.Options.median.label": "Mediana", - "Calculations.Options.min.displayName": "Min.", - "Calculations.Options.min.label": "Min.", - "Calculations.Options.none.displayName": "Apskaičiuoti", - "Calculations.Options.none.label": "Joks", - "Calculations.Options.percentChecked.displayName": "Pažymėta", - "Calculations.Options.percentChecked.label": "Patikrinta procentais", - "Calculations.Options.percentUnchecked.displayName": "Nepažymėta", - "Calculations.Options.percentUnchecked.label": "Nepažymėtas procentas", - "Calculations.Options.range.displayName": "Diapazonas", - "Calculations.Options.range.label": "Diapazonas", - "Calculations.Options.sum.displayName": "Iš viso", - "Calculations.Options.sum.label": "Iš viso", - "CalendarCard.untitled": "Be pavadinimo", - "CardActionsMenu.copiedLink": "Nukopijuota!", - "CardActionsMenu.copyLink": "Kopijuoti nuorodą", - "CardActionsMenu.delete": "Ištrinti", - "CardActionsMenu.duplicate": "Pasikartojantis", - "CardBadges.title-checkboxes": "Žymimieji langeliai", - "CardBadges.title-comments": "Komentarai", - "CardBadges.title-description": "Ši kortelė turi aprašymą", - "CardDetail.Attach": "Prisegti", - "CardDetail.Follow": "Sekti", - "CardDetail.Following": "Sekama", - "CardDetail.add-content": "Pridėti turinį", - "CardDetail.add-icon": "Pridėti piktogramą", - "CardDetail.add-property": "+ Pridėti savybę", - "CardDetail.addCardText": "pridėti kortelės tekstą", - "CardDetail.limited-body": "Naujovinkite į mūsų Professional arba Enterprise planą.", - "CardDetail.limited-button": "Patobulinti", - "CardDetail.limited-title": "Ši kortelė paslėpta", - "CardDetail.moveContent": "Perkelti kortelės turinį", - "CardDetail.new-comment-placeholder": "Pridėti komentarą...", - "CardDetailProperty.confirm-delete-heading": "Patvirtinkite nuosavybės ištrynimą", - "CardDetailProperty.confirm-delete-subtext": "Ar tikrai norite ištrinti savybę „{propertyName}“? Ją ištrynus, savybė bus ištrinta iš visų šios lentos kortelių.", - "CardDetailProperty.confirm-property-name-change-subtext": "Ar tikrai norite pakeisti savybę „{propertyName}“ {customText}? Tai turės įtakos {numOfCards} kortelės reikšmei (-ėms) šioje lentoje ir duomenys gali būti prarasti.", - "CardDetailProperty.confirm-property-type-change": "Patvirtinkite nuosavybės tipo pakeitimą", - "CardDetailProperty.delete-action-button": "Ištrinti", - "CardDetailProperty.property-change-action-button": "Keisti savybę", - "CardDetailProperty.property-changed": "Savybė sėkmingai pakeista!", - "CardDetailProperty.property-deleted": "{propertyName} sėkmingai ištrinta!", - "CardDetailProperty.property-name-change-subtext": "įveskite iš „{oldPropType}“ į „{newPropType}“", - "CardDetial.limited-link": "Sužinokite daugiau apie mūsų planus.", - "CardDialog.delete-confirmation-dialog-attachment": "Patvirtinkite priedo ištrynimą", - "CardDialog.delete-confirmation-dialog-button-text": "Ištrinti", - "CardDialog.delete-confirmation-dialog-heading": "Patvirtinkite kortelės ištrynimą", - "CardDialog.editing-template": "Jūs redaguojate šabloną.", - "CardDialog.nocard": "Šios kortelės nėra arba ji nepasiekiama.", - "Categories.CreateCategoryDialog.CancelText": "Atšaukti", - "Categories.CreateCategoryDialog.CreateText": "Sukurti", - "Categories.CreateCategoryDialog.Placeholder": "Pavadinkite savo kategoriją", - "Categories.CreateCategoryDialog.UpdateText": "Atnaujinti", - "CenterPanel.Login": "Prisijungti", - "CenterPanel.Share": "Bendrinti", - "ChannelIntro.CreateBoard": "Sukurti lentą", - "ColorOption.selectColor": "Pasirinkti {color} spalvą", - "Comment.delete": "Ištrinti", - "CommentsList.send": "Siųsti", - "ConfirmPerson.empty": "Tuščias", - "ConfirmPerson.search": "Ieškoti...", - "ConfirmationDialog.cancel-action": "Atšaukti", - "ConfirmationDialog.confirm-action": "Patvirtinti", - "ContentBlock.Delete": "Ištrinti", - "ContentBlock.DeleteAction": "Ištrinti", - "ContentBlock.addElement": "pridėti {type}", - "ContentBlock.checkbox": "žymimasis langelis", - "ContentBlock.divider": "skirtukas", - "ContentBlock.editCardCheckbox": "perjungtas žymimasis laukelis", - "ContentBlock.editCardCheckboxText": "redaguoti kortelės tekstą", - "ContentBlock.editCardText": "redaguoti kortelės tekstą", - "ContentBlock.editText": "Keisti tekstą...", - "ContentBlock.image": "Paveikslėlis", - "ContentBlock.insertAbove": "Įdėti aukščiau", - "ContentBlock.moveBlock": "perkelti kortelės turinį", - "ContentBlock.moveDown": "Perkelti žemyn", - "ContentBlock.moveUp": "Perkelti aukštyn", - "ContentBlock.text": "tekstas", - "DateFilter.empty": "Tuščias", - "DateRange.clear": "Išvalyti", - "DateRange.empty": "Tuščias", - "DateRange.endDate": "Pabaigos data", - "DateRange.today": "Šiandien", - "DeleteBoardDialog.confirm-cancel": "Atšaukti", - "DeleteBoardDialog.confirm-delete": "Ištrinti", - "DeleteBoardDialog.confirm-info": "Ar tikrai norite ištrinti lentą „{boardTitle}“? Ją ištrynus, bus ištrintos visos lentos kortelės.", - "DeleteBoardDialog.confirm-info-template": "Ar tikrai norite ištrinti lentos šabloną „{boardTitle}“?", - "DeleteBoardDialog.confirm-tite": "Patvirtinkite lentos ištrynimą", - "DeleteBoardDialog.confirm-tite-template": "Patvirtinkite lentos šablono ištrynimą", - "Dialog.closeDialog": "Uždaryti dialogo langą", - "EditableDayPicker.today": "Šiandien", - "Error.mobileweb": "Mobiliojo žiniatinklio palaikymas šiuo metu yra ankstyvoje beta versijoje. Gali būti ne visos funkcijos.", - "Error.websocket-closed": "Interneto lizdo ryšys uždarytas, ryšys nutrauktas. Jei tai išlieka, patikrinkite savo serverio arba žiniatinklio tarpinio serverio konfigūraciją.", - "Filter.contains": "turi", - "Filter.ends-with": "baigiasi", - "Filter.includes": "apima", - "Filter.is": "yra", - "Filter.is-after": "yra po", - "Filter.is-before": "yra prieš", - "Filter.is-empty": "yra tuščias", - "Filter.is-not-empty": "nėra tuščias", - "Filter.is-not-set": "nėra nustatytas", - "Filter.is-set": "yra nustatytas", - "Filter.isafter": "yra po", - "Filter.isbefore": "yra prieš", - "Filter.not-contains": "nėra", - "Filter.not-ends-with": "nesibaigia", - "Filter.not-includes": "neapima", - "Filter.not-starts-with": "neprasideda", - "Filter.starts-with": "prasideda", - "FilterByText.placeholder": "filtruoti tekstą", - "FilterComponent.add-filter": "+ Pridėti filtrą", - "FilterComponent.delete": "Ištrinti", - "FilterValue.empty": "(tuščia)", - "FindBoardsDialog.IntroText": "Ieškoti lentų", - "FindBoardsDialog.NoResultsFor": "Nėra rezultatų pagal „{searchQuery}“", - "FindBoardsDialog.NoResultsSubtext": "Patikrinkite rašybą arba pabandykite atlikti kitą paiešką.", - "FindBoardsDialog.SubTitle": "Įveskite, kad rastumėte lentą. Norėdami naršyti, naudokite AUKŠTYN / ŽEMYN. ENTER , kad pasirinktumėte, ESC , kad atsisakytumėte", - "FindBoardsDialog.Title": "Raskite lentas", - "GroupBy.hideEmptyGroups": "Slėpti {count} tuščias grupes", - "GroupBy.showHiddenGroups": "Rodyti paslėptas grupes: {count}", - "GroupBy.ungroup": "Išgrupuoti", - "HideBoard.MenuOption": "Paslėpti lentą", - "KanbanCard.untitled": "Be pavadinimo", - "MentionSuggestion.is-not-board-member": "(ne lentos narys)", - "Mutator.new-board-from-template": "nauja lenta pagal šabloną", - "Mutator.new-card-from-template": "nauja kortelė pagal šabloną", - "Mutator.new-template-from-card": "naujas šablonas pagal kortelę", - "OnboardingTour.AddComments.Body": "Galite komentuoti problemas ir net @paminėti kitus Mattermost naudotojus, kad atkreiptumėte jų dėmesį.", - "OnboardingTour.AddComments.Title": "Pridėti komentarų", - "OnboardingTour.AddDescription.Body": "Pridėkite aprašymą prie savo kortelės, kad komandos draugai žinotų, kam ji skirta.", - "OnboardingTour.AddDescription.Title": "Pridėti aprašymą", - "OnboardingTour.AddProperties.Body": "Pridėkite prie kortelių įvairių savybių, kad jos būtų dar galingesnės.", - "OnboardingTour.AddProperties.Title": "Pridėti savybių", - "OnboardingTour.AddView.Body": "Eikite čia, kad sukurtumėte naują rodinį ir galėtumėte tvarkyti lentą naudodami skirtingus išdėstymus.", - "OnboardingTour.AddView.Title": "Pridėti naują rodinį", - "OnboardingTour.CopyLink.Body": "Galite bendrinti savo korteles su komandos draugais nukopijuodami nuorodą ir įklijuodami ją į kanalą, tiesioginį pranešimą ar grupės pranešimą.", - "OnboardingTour.CopyLink.Title": "Kopijuoti nuorodą", - "OnboardingTour.OpenACard.Body": "Atidarykite kortelę, kad sužinotumėte, kokiais galingais būdais Boards gali padėti organizuoti darbą.", - "OnboardingTour.OpenACard.Title": "Atidaryti kortelę", - "OnboardingTour.ShareBoard.Body": "Galite bendrinti lentą viduje, savo komandoje arba paskelbti ją viešai, kad būtų matoma už organizacijos ribų.", - "OnboardingTour.ShareBoard.Title": "Bendrinti lentą", - "PersonProperty.board-members": "Lentos nariai", - "PersonProperty.me": "aš", - "PersonProperty.non-board-members": "Ne lentos nariai", - "PropertyMenu.Delete": "Ištrinti", - "PropertyMenu.changeType": "Pakeisti savybės tipą", - "PropertyMenu.selectType": "Pasirinkti savybės tipą", - "PropertyMenu.typeTitle": "Tipas", - "PropertyType.Checkbox": "Žymimasis langelis", - "PropertyType.CreatedBy": "Sukurta", - "PropertyType.CreatedTime": "Sukūrimo laikas", - "PropertyType.Date": "Data", - "PropertyType.Email": "El. paštas", - "PropertyType.MultiPerson": "Daugelio žmonių", - "PropertyType.MultiSelect": "Keletas pasirinkimų", - "PropertyType.Number": "Skaičius", - "PropertyType.Person": "Asmuo", - "PropertyType.Phone": "Telefonas", - "PropertyType.Select": "Pasirinkite", - "PropertyType.Text": "Tekstas", - "PropertyType.Unknown": "Nežinoma", - "PropertyType.UpdatedBy": "Paskutinį kartą atnaujino", - "PropertyType.UpdatedTime": "Paskutinio atnaujinimo laikas", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Tuščias", - "RegistrationLink.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?", - "RegistrationLink.copiedLink": "Nukopijuota!", - "RegistrationLink.copyLink": "Kopijuoti nuorodą", - "RegistrationLink.description": "Pasidalykite šia nuoroda, kad kiti galėtų susikurti paskyras:", - "RegistrationLink.regenerateToken": "Atkurti prieigos raktą", - "RegistrationLink.tokenRegenerated": "Registracijos nuoroda sugeneruota iš naujo", - "ShareBoard.PublishDescription": "Paskelbti ir bendrinti tik skaitomą nuorodą su visais žiniatinklio naudotojais.", - "ShareBoard.PublishTitle": "Paskelbti žiniatinklyje", - "ShareBoard.ShareInternal": "Bendrinti viduje", - "ShareBoard.ShareInternalDescription": "Leidimus turintys naudotojai galės naudotis šia nuoroda.", - "ShareBoard.Title": "Bendrinti lentą", - "ShareBoard.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?", - "ShareBoard.copiedLink": "Nukopijuota!", - "ShareBoard.copyLink": "Kopijuoti nuorodą", - "ShareBoard.regenerate": "Atkurti prieigos raktą", - "ShareBoard.searchPlaceholder": "Ieškoti žmonių ir kanalų", - "ShareBoard.teamPermissionsText": "Visi {teamName} komandos nariai", - "ShareBoard.tokenRegenrated": "Prieigos raktas atkurtas", - "ShareBoard.userPermissionsRemoveMemberText": "Pašalinti narį", - "ShareBoard.userPermissionsYouText": "(Jūs)", - "ShareTemplate.Title": "Bendrinti šabloną", - "ShareTemplate.searchPlaceholder": "Ieškoti žmonių", - "Sidebar.delete-board": "Ištrinti lentą", - "Sidebar.duplicate-board": "Pasikartojanti lenta", - "Sidebar.export-archive": "Eksportuoti archyvą", - "Sidebar.import": "Importuoti", - "Sidebar.import-archive": "Importuoti archyvą", - "Sidebar.new-category.badge": "Naujas", - "Sidebar.new-category.drag-boards-cta": "Vilkite lentas čia...", - "Sidebar.no-boards-in-category": "Viduje nėra lentų", - "Sidebar.product-tour": "Produkto apžvalga", - "Sidebar.random-icons": "Atsitiktinės piktogramos", - "Sidebar.set-language": "Nustatyti kalbą", - "Sidebar.set-theme": "Nustatyti temą", - "Sidebar.settings": "Nustatymai", - "Sidebar.template-from-board": "Naujas šablonas pagal lentą", - "Sidebar.untitled-board": "(Lenta be pavadinimo)", - "Sidebar.untitled-view": "(Rodinys be pavadinimo)", - "SidebarCategories.BlocksMenu.Move": "Perkelti į...", - "SidebarCategories.CategoryMenu.CreateNew": "Sukurti naują kategoriją", - "SidebarCategories.CategoryMenu.Delete": "Ištrinti kategoriją", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} lentos bus perkeltos atgal į lentų kategorijas. Jūs nesate pašalintas iš jokių lentų.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Ištrinti šią kategoriją?", - "SidebarCategories.CategoryMenu.Update": "Pervadinti kategoriją", - "SidebarTour.ManageCategories.Body": "Kurkite ir tvarkykite pasirinktines kategorijas. Kategorijos priklauso nuo naudotojo, todėl lentos perkėlimas į kategoriją neturės įtakos kitiems nariams, naudojantiems tą pačią lentą.", - "SidebarTour.ManageCategories.Title": "Tvarkyti kategorijas", - "SidebarTour.SearchForBoards.Body": "Atidarykite lentos perjungiklį (Cmd / Ctrl + K), kad galėtumėte greitai ieškoti ir pridėti lentų šoninėje juostoje.", - "SidebarTour.SearchForBoards.Title": "Ieškoti lentų", - "SidebarTour.SidebarCategories.Body": "Visos Jūsų lentos dabar sutvarkytos naujoje šoninėje juostoje. Nebereikia perjungti darbo erdvių. Vienkartinės pasirinktinės kategorijos, pagrįstos ankstesnėmis darbo sritimis, gali būti automatiškai sukurtos Jums atnaujinant į 7.2 versiją. Jas galima pašalinti arba redaguoti pagal savo pageidavimus.", - "SidebarTour.SidebarCategories.Link": "Sužinoti daugiau", - "SidebarTour.SidebarCategories.Title": "Šoninės juostos kategorijos", - "SiteStats.total_boards": "Iš viso lentų", - "SiteStats.total_cards": "Iš viso kortelių", - "TableComponent.add-icon": "Pridėti piktogramą", - "TableComponent.name": "Pavadinimas", - "TableComponent.plus-new": "+ Naujas", - "TableHeaderMenu.delete": "Ištrinti", - "TableHeaderMenu.duplicate": "Pasikartojantis", - "TableHeaderMenu.hide": "Slėpti", - "TableHeaderMenu.insert-left": "Įdėkite kairėje", - "TableHeaderMenu.insert-right": "Įdėkite dešinėje", - "TableHeaderMenu.sort-ascending": "Rūšiuoti didėjančia tvarka", - "TableHeaderMenu.sort-descending": "Rūšiuoti mažėjančia tvarka", - "TableRow.DuplicateCard": "kortelės dublikatas", - "TableRow.MoreOption": "Daugiau veiksmų", - "TableRow.open": "Atverti", - "TopBar.give-feedback": "Palikti atsiliepimą", - "URLProperty.copiedLink": "Nukopijuota!", - "URLProperty.copy": "Kopijuoti", - "URLProperty.edit": "Redaguoti", - "UndoRedoHotKeys.canRedo": "Grąžinti", - "UndoRedoHotKeys.canRedo-with-description": "Perdaryti {description}", - "UndoRedoHotKeys.canUndo": "Anuliuoti", - "UndoRedoHotKeys.canUndo-with-description": "Anuliuoti {description}", - "UndoRedoHotKeys.cannotRedo": "Nėra ką perdaryti", - "UndoRedoHotKeys.cannotUndo": "Nėra ką anuliuoti", - "ValueSelector.noOptions": "Jokių parinkčių. Pradėkite rašyti, kad pridėtumėte pirmąją!", - "ValueSelector.valueSelector": "Reikšmės parinkiklis", - "ValueSelectorLabel.openMenu": "Atidaryti meniu", - "VersionMessage.help": "Patikrinkite, kas naujo šioje versijoje.", - "VersionMessage.learn-more": "Sužinoti daugiau", - "View.AddView": "Pridėti rodinį", - "View.Board": "Lenta", - "View.DeleteView": "Ištrinti rodinį", - "View.DuplicateView": "Pasikartojantis rodinys", - "View.Gallery": "Galerija", - "View.NewBoardTitle": "Lentos rodinys", - "View.NewCalendarTitle": "Kalendoriaus rodinys", - "View.NewGalleryTitle": "Galerijos vaizdas", - "View.NewTableTitle": "Lentelės rodinys", - "View.NewTemplateDefaultTitle": "Šablonas be pavadinimo", - "View.NewTemplateTitle": "Be pavadinimo", - "View.Table": "Lentelė", - "ViewHeader.add-template": "Naujas šablonas", - "ViewHeader.delete-template": "Ištrinti", - "ViewHeader.display-by": "Pateikė: {property}", - "ViewHeader.edit-template": "Redaguoti", - "ViewHeader.empty-card": "Tuščia kortelė", - "ViewHeader.export-board-archive": "Eksportuoti lentos archyvą", - "ViewHeader.export-complete": "Eksportas baigtas!", - "ViewHeader.export-csv": "Eksportuoti į CSV", - "ViewHeader.export-failed": "Eksportuoti nepavyko!", - "ViewHeader.filter": "Filtras", - "ViewHeader.group-by": "Grupuoti pagal: {property}", - "ViewHeader.new": "Naujas", - "ViewHeader.properties": "Savybės", - "ViewHeader.properties-menu": "Savybių meniu", - "ViewHeader.search-text": "Ieškoti kortelių", - "ViewHeader.select-a-template": "Pasirinkti šabloną", - "ViewHeader.set-default-template": "Nustatyti kaip numatytąjį", - "ViewHeader.sort": "Rūšiuoti", - "ViewHeader.untitled": "Be pavadinimo", - "ViewHeader.view-header-menu": "Žiūrėti antraštės meniu", - "ViewHeader.view-menu": "Žiūrėti meniu", - "ViewLimitDialog.Heading": "Pasiektas peržiūrų skaičius vienoje lentoje", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Patobulinti", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Pranešti administratoriui", - "ViewLimitDialog.Subtext.Admin": "Naujovinkite į mūsų Professional arba Enterprise planą.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Sužinokite daugiau apie mūsų planus.", - "ViewLimitDialog.Subtext.RegularUser": "Praneškite savo administratoriui, kad jis naujovintų į mūsų Professional arba Enterprise planą.", - "ViewLimitDialog.UpgradeImg.AltText": "atnaujinti vaizdą", - "ViewLimitDialog.notifyAdmin.Success": "Jūsų administratoriui buvo pranešta", - "ViewTitle.hide-description": "slėpti aprašymą", - "ViewTitle.pick-icon": "Pasirinkti piktogramą", - "ViewTitle.random-icon": "Atsitiktinis", - "ViewTitle.remove-icon": "Pašalinti piktogramą", - "ViewTitle.show-description": "rodyti aprašymą", - "ViewTitle.untitled-board": "Lenta be pavadinimo", - "WelcomePage.Description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.", - "WelcomePage.Explore.Button": "Apžiūrėti", - "WelcomePage.Heading": "Sveiki atvykę į Boards", - "WelcomePage.NoThanks.Text": "Ne, ačiū, išsiaiškinsiu pats", - "WelcomePage.StartUsingIt.Text": "Pradėkite jį naudoti", - "Workspace.editing-board-template": "Redaguojate lentos šabloną.", - "badge.guest": "Svečias", - "boardPage.confirm-join-button": "Prisijungti", - "boardPage.confirm-join-text": "Jūs ketinate prisijungti prie privačios lentos, tačiau lentos administratorius Jūsų nepridėjo. Ar tikrai norite prisijungti prie šios privačios lentos?", - "boardPage.confirm-join-title": "Prisijunti prie privačios lentos", - "boardSelector.confirm-link-board": "Susieti lentą su kanalu", - "boardSelector.confirm-link-board-button": "Taip, susieti lentą", - "boardSelector.confirm-link-board-subtext": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai. Galite bet kada atsieti lentą nuo kanalo.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak} Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.", - "boardSelector.create-a-board": "Sukurti lentą", - "boardSelector.link": "nuoroda", - "boardSelector.search-for-boards": "Ieškoti lentų", - "boardSelector.title": "Susieti lentas", - "boardSelector.unlink": "Atsieti", - "calendar.month": "Mėnuo", - "calendar.today": "Šiandien", - "calendar.week": "Savaitė", - "centerPanel.undefined": "Nėra {propertyName}", - "centerPanel.unknown-user": "Nežinomas naudotojas", - "createImageBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.", - "default-properties.badges": "Komentarai ir aprašymas", - "default-properties.title": "Pavadinimas", - "error.back-to-home": "Grįžti namo", - "error.back-to-team": "Atgal į komandą", - "error.board-not-found": "Lenta nerasta.", - "error.go-login": "Prisijungti", - "error.invalid-read-only-board": "Neturite prieigos prie šios lentos. Prisijunkite, kad pasiektumėte lentas.", - "error.not-logged-in": "Jūsų sesija gali būti pasibaigusi arba nesate prisijungę. Prisijunkite dar kartą, kad pasiektumėte lentas.", - "error.page.title": "Atsiprašome, kažkas nutiko", - "error.team-undefined": "Netinkama komanda.", - "error.unknown": "Įvyko klaida.", - "generic.previous": "Ankstesnis", - "guest-no-board.subtitle": "Dar neturite prieigos prie jokios šios komandos lentos, palaukite, kol kas nors jus įtrauks į bet kurią lentą.", - "guest-no-board.title": "Dar nėra jokių lentų", - "imagePaste.upload-failed": "Kai kurie failai nebuvo įkelti, nes pasiektas failo dydžio limitas.", - "limitedCard.title": "Paslėptos kortelės", - "login.log-in-button": "Prisijungti", - "login.log-in-title": "Prisijungti", - "login.register-button": "arba susikurkite paskyrą, jei jos neturite", - "new_channel_modal.create_board.empty_board_description": "Sukurti naują tuščią lentą", - "new_channel_modal.create_board.empty_board_title": "Tuščia lenta", - "new_channel_modal.create_board.select_template_placeholder": "Pasirinkti šabloną", - "new_channel_modal.create_board.title": "Sukurti šio kanalo lentą", - "notification-box-card-limit-reached.close-tooltip": "Snausti 10 dienų", - "notification-box-card-limit-reached.contact-link": "praneškite savo administratoriui", - "notification-box-card-limit-reached.link": "Atnaujinti į mokamą planą", - "notification-box-card-limit-reached.title": "{cards} kortelės paslėptos", - "notification-box-cards-hidden.title": "Šis veiksmas paslėpė kitą kortelę", - "notification-box.card-limit-reached.not-admin.text": "Norėdami pasiekti archyvuotas korteles, galite {contactLink} naujovinti į mokamą planą.", - "notification-box.card-limit-reached.text": "Pasiektas kortelių limitas, jei norite peržiūrėti senesnes korteles, {link}", - "person.add-user-to-board": "Pridėti {username} prie lentos", - "person.add-user-to-board-confirm-button": "Pridėti prie lentos", - "person.add-user-to-board-permissions": "Leidimai", - "person.add-user-to-board-question": "Ar norite pridėti {username} prie lentos?", - "person.add-user-to-board-warning": "{username} nėra lentos narys ir negaus apie tai jokių pranešimų.", - "register.login-button": "arba prisijunkite, jei jau turite paskyrą", - "register.signup-title": "Prisiregistruokite prie paskyros", - "rhs-board-non-admin-msg": "Jūs nesate lentos administratorius", - "rhs-boards.add": "Pridėti", - "rhs-boards.dm": "AŽ", - "rhs-boards.gm": "GŽ", - "rhs-boards.header.dm": "ši tiesioginė žinutė", - "rhs-boards.header.gm": "ši grupės žinutė", - "rhs-boards.last-update-at": "Paskutinį kartą atnaujinta: {datetime}", - "rhs-boards.link-boards-to-channel": "Susieti lentas su {channelName}", - "rhs-boards.linked-boards": "Susietos lentos", - "rhs-boards.no-boards-linked-to-channel": "Su {channelName} dar nėra susietų lentų", - "rhs-boards.no-boards-linked-to-channel-description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.", - "rhs-boards.unlink-board": "Atsieti lentą", - "rhs-boards.unlink-board1": "Atsieti lentą", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Paskelbti", - "share-board.share": "Bendrinti", - "shareBoard.channels-select-group": "Kanalai", - "shareBoard.confirm-change-team-role.body": "Visi šioje lentoje esantys asmenys, turintys žemesnį nei „{role}“ vaidmenį , dabar bus paaukštinti į {role} . Ar tikrai norite pakeisti minimalų lentos vaidmenį?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Pakeiskite minimalų lentos vaidmenį", - "shareBoard.confirm-change-team-role.title": "Pakeiskite minimalų lentos vaidmenį", - "shareBoard.confirm-link-channel": "Susieti lentą su kanalu", - "shareBoard.confirm-link-channel-button": "Susieti kanalą", - "shareBoard.confirm-link-channel-button-with-other-channel": "Atsieti ir susieti čia", - "shareBoard.confirm-link-channel-subtext": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės jį redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak}Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.", - "shareBoard.confirm-unlink.body": "Kai atsiesite kanalą nuo lentos, visi kanalo nariai (esami ir nauji) praras prieigą prie jo, nebent jiems bus suteiktas atskiras leidimas.", - "shareBoard.confirm-unlink.confirmBtnText": "Atsieti kanalą", - "shareBoard.confirm-unlink.title": "Atsieti kanalą nuo lentos", - "shareBoard.lastAdmin": "Lentose turi būti bent vienas administratorius", - "shareBoard.members-select-group": "Nariai", - "shareBoard.unknown-channel-display-name": "Nežinomas kanalas", - "tutorial_tip.finish_tour": "Atlikta", - "tutorial_tip.got_it": "Supratau", - "tutorial_tip.ok": "Kitas", - "tutorial_tip.out": "Atsisakyti šių patarimų.", - "tutorial_tip.seen": "Matėte tai anksčiau?" -} diff --git a/webapp/boards/i18n/ml.json b/webapp/boards/i18n/ml.json deleted file mode 100644 index d6ced2f9ac..0000000000 --- a/webapp/boards/i18n/ml.json +++ /dev/null @@ -1,300 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ ഒരു ഗ്രൂപ്പ് ചേർക്കുക", - "BoardComponent.delete": "നീക്കം ചെയ്യുക", - "BoardComponent.hidden-columns": "മറഞ്ഞിരിക്കുന്ന നിരകൾ", - "BoardComponent.hide": "മറയ്ക്കുക", - "BoardComponent.new": "+ പുതിയത്", - "BoardComponent.no-property": "ഇല്ല {property}", - "BoardComponent.no-property-title": "ശൂന്യമായ {property} പ്രോപ്പർട്ടി ഉള്ള ഇനങ്ങൾ ഇവിടെ പോകും. ഈ കോളം നീക്കം ചെയ്യാൻ കഴിയില്ല.", - "BoardComponent.show": "കാണിക്കുക", - "BoardMember.schemeAdmin": "അഡ്മിൻ", - "BoardMember.schemeCommenter": "കമന്റേറ്റർ", - "BoardMember.schemeEditor": "എഡിറ്റർ", - "BoardMember.schemeNone": "ഒന്നുമില്ല", - "BoardMember.schemeViewer": "കാഴ്ചക്കാരൻ", - "BoardPage.newVersion": "ബോർഡുകളുടെ ഒരു പുതിയ പതിപ്പ് ലഭ്യമാണ്, റീലോഡ് ചെയ്യാൻ ഇവിടെ ക്ലിക്ക് ചെയ്യുക.", - "BoardPage.syncFailed": "ബോർഡ് ഇല്ലാതാക്കുകയോ ആക്സസ് റദ്ദാക്കുകയോ ചെയ്യാം.", - "BoardTemplateSelector.add-template": "പുതിയ ടെംപ്ലേറ്റ്", - "BoardTemplateSelector.create-empty-board": "ശൂന്യമായ ബോർഡ് സൃഷ്ടിക്കുക", - "BoardTemplateSelector.delete-template": "ഇല്ലാതാക്കുക", - "BoardTemplateSelector.description": "ആരംഭിക്കാൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ടെംപ്ലേറ്റ് തിരഞ്ഞെടുക്കുക. നിങ്ങളുടെ ആവശ്യങ്ങൾക്ക് അനുയോജ്യമായ രീതിയിൽ ടെംപ്ലേറ്റ് എളുപ്പത്തിൽ ഇച്ഛാനുസൃതമാക്കുക, അല്ലെങ്കിൽ ആദ്യം മുതൽ ആരംഭിക്കാൻ ഒരു ശൂന്യമായ ബോർഡ് സൃഷ്ടിക്കുക.", - "BoardTemplateSelector.edit-template": "എഡിറ്റ് ചെയ്യുക", - "BoardTemplateSelector.plugin.no-content-description": "ചുവടെ നിർവചിച്ചിരിക്കുന്ന ഏതെങ്കിലും ടെംപ്ലേറ്റുകൾ ഉപയോഗിച്ച് സൈഡ്ബാറിലേക്ക് ഒരു ബോർഡ് ചേർക്കുക അല്ലെങ്കിൽ ആദ്യം മുതൽ ആരംഭിക്കുക.", - "BoardTemplateSelector.plugin.no-content-title": "ഒരു ബോർഡ് ഉണ്ടാക്കുക", - "BoardTemplateSelector.title": "ഒരു ബോർഡ് ഉണ്ടാക്കുക", - "BoardTemplateSelector.use-this-template": "ഈ ടെംപ്ലേറ്റ് ഉപയോഗിക്കുക", - "BoardsSwitcher.Title": "ബോർഡുകൾ കണ്ടെത്തുക", - "BoardsUnfurl.Remainder": "+{remainder} കൂടുതൽ", - "BoardsUnfurl.Updated": "പുതുക്കിയത് {time}", - "Calculations.Options.average.displayName": "ശരാശരി", - "Calculations.Options.average.label": "ശരാശരി", - "Calculations.Options.count.displayName": "എണ്ണുക", - "Calculations.Options.count.label": "എണ്ണുക", - "Calculations.Options.countChecked.displayName": "പരിശോധിച്ചു", - "Calculations.Options.countChecked.label": "എണ്ണം പരിശോധിച്ചു", - "Calculations.Options.countUnchecked.displayName": "പരിശോധിക്കാത്തത്", - "Calculations.Options.countUnchecked.label": "പരിശോധിക്കാത്തതിന്റെ എണ്ണം", - "Calculations.Options.countUniqueValue.displayName": "അതുല്യമായ", - "Calculations.Options.countUniqueValue.label": "അദ്വിതീയ മൂല്യങ്ങൾ എണ്ണുക", - "Calculations.Options.countValue.displayName": "മൂല്യങ്ങൾ", - "Calculations.Options.countValue.label": "മൂല്യം എണ്ണുക", - "Calculations.Options.dateRange.displayName": "പരിധി", - "Calculations.Options.dateRange.label": "പരിധി", - "Calculations.Options.earliest.displayName": "നേരത്തെ", - "Calculations.Options.earliest.label": "നേരത്തെ", - "Calculations.Options.latest.displayName": "ഏറ്റവും പുതിയ", - "Calculations.Options.latest.label": "ഏറ്റവും പുതിയ", - "Calculations.Options.max.displayName": "പരമാവധി", - "Calculations.Options.max.label": "പരമാവധി", - "Calculations.Options.median.displayName": "മധ്യമം", - "Calculations.Options.median.label": "മധ്യമം", - "Calculations.Options.min.displayName": "കുറവ്", - "Calculations.Options.min.label": "കുറവ്", - "Calculations.Options.none.displayName": "കണക്കാക്കുക", - "Calculations.Options.none.label": "ഒന്നുമില്ല", - "Calculations.Options.percentChecked.displayName": "പരിശോധിച്ചു", - "Calculations.Options.percentChecked.label": "ശതമാനം പരിശോധിച്ചു", - "Calculations.Options.percentUnchecked.displayName": "പരിശോധിക്കാത്തത്", - "Calculations.Options.percentUnchecked.label": "ശതമാനം പരിശോധിക്കാത്തത്", - "Calculations.Options.range.displayName": "പരിധി", - "Calculations.Options.range.label": "പരിധി", - "Calculations.Options.sum.displayName": "തുക", - "Calculations.Options.sum.label": "തുക", - "CardActionsMenu.copiedLink": "പകർത്തി!", - "CardActionsMenu.copyLink": "ലിങ്ക് പകർത്തുക", - "CardActionsMenu.delete": "ഡിലീറ്റ് ചെയ്യുക", - "CardActionsMenu.duplicate": "പകർപ്പ്", - "CardBadges.title-checkboxes": "ചെക്ക്ബോക്സുകൾ", - "CardBadges.title-comments": "അഭിപ്രായങ്ങൾ", - "CardBadges.title-description": "ഈ കാർഡിന് ഒരു വിവരണമുണ്ട്", - "CardDetail.Follow": "പിന്തുടരുക", - "CardDetail.Following": "പിന്തുടരുന്നു", - "CardDetail.add-content": "ഉള്ളടക്കം ചേർക്കുക", - "CardDetail.add-icon": "ഐക്കൺ ചേർക്കുക", - "CardDetail.add-property": "+ ഒരു വിശേഷണം ചേർക്കുക", - "CardDetail.addCardText": "കാർഡിൽ വാക്യം ചേർക്കുക", - "CardDetail.limited-body": "ആർക്കൈവ് ചെയ്‌ത കാർഡുകൾ കാണുന്നതിനും ഓരോ ബോർഡുകൾക്കും പരിധിയില്ലാത്ത കാഴ്‌ചകൾ നേടുന്നതിനും പരിധിയില്ലാത്ത കാർഡുകൾക്കും മറ്റും ഞങ്ങളുടെ പ്രൊഫഷണൽ അല്ലെങ്കിൽ എന്റർപ്രൈസ് പ്ലാനിലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക.", - "CardDetail.limited-button": "അപ്ഗ്രേഡ്", - "CardDetail.moveContent": "കാർഡ് ഉള്ളടക്കം നീക്കുക", - "CardDetail.new-comment-placeholder": "ഒരു അഭിപ്രായം ചേർക്കുക...", - "CardDetailProperty.confirm-delete-heading": "പ്രോപ്പർട്ടി ഇല്ലാതാക്കുന്നത് സ്ഥിരീകരിക്കുക", - "CardDetailProperty.confirm-delete-subtext": "പ്രോപ്പർട്ടി ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ \"{propertyName}\"? ഇത് ഇല്ലാതാക്കുന്നത് ഈ ബോർഡിലെ എല്ലാ കാർഡുകളിൽ നിന്നും പ്രോപ്പർട്ടി ഇല്ലാതാക്കും.", - "CardDetailProperty.confirm-property-name-change-subtext": "\"{propertyName}\" {customText} പ്രോപ്പർട്ടി മാറ്റണമെന്ന് തീർച്ചയാണോ? ഇത് ഈ ബോർഡിലെ {numOfCards} കാർഡുകളിലുടനീളമുള്ള മൂല്യങ്ങളെ(കളെ) ബാധിക്കുകയും ഡാറ്റ നഷ്‌ടത്തിന് കാരണമാവുകയും ചെയ്യും.", - "CardDetailProperty.confirm-property-type-change": "പ്രോപ്പർട്ടി തരം മാറ്റം സ്ഥിരീകരിക്കുക!", - "CardDetailProperty.delete-action-button": "നീക്കം ചെയ്യുക", - "CardDetailProperty.property-change-action-button": "പ്രോപ്പർട്ടി മാറ്റുക", - "CardDetailProperty.property-changed": "പ്രോപ്പർട്ടി വിജയകരമായി മാറ്റി!", - "CardDetailProperty.property-deleted": "വിജയകരമായി {propertyName} ഇല്ലാതാക്കിയിരിക്കുന്നു!", - "CardDetailProperty.property-name-change-subtext": "\"{oldPropType}\" മുതൽ \"{newPropType}\" വരെ ടൈപ്പ് ചെയ്യുക", - "CardDialog.delete-confirmation-dialog-button-text": "ഇല്ലാതാക്കുക", - "CardDialog.delete-confirmation-dialog-heading": "കാർഡ് ഇല്ലാതാക്കൽ സ്ഥിരീകരിക്കുക!", - "CardDialog.editing-template": "നിങ്ങൾ ഒരു ടെംപ്ലേറ്റ് എഡിറ്റ് ചെയ്യുകയാണ്.", - "CardDialog.nocard": "ഈ കാർഡ് നിലവിലില്ല അല്ലെങ്കിൽ ആക്സസ് ചെയ്യാനാകുന്നില്ല.", - "Categories.CreateCategoryDialog.CancelText": "റദ്ദാക്കുക", - "Categories.CreateCategoryDialog.CreateText": "സൃഷ്ടിക്കുക", - "Categories.CreateCategoryDialog.Placeholder": "നിങ്ങളുടെ വിഭാഗത്തിന് പേര് നൽകുക", - "Categories.CreateCategoryDialog.UpdateText": "അപ്ഡേറ്റ് ചെയ്യുക", - "CenterPanel.Login": "ലോഗിൻ", - "CenterPanel.Share": "പങ്കിടുക", - "ColorOption.selectColor": "നിറം {color} തിരഞ്ഞെടുക്കുക", - "Comment.delete": "ഇല്ലാതാക്കുക", - "CommentsList.send": "അയക്കുക", - "ConfirmationDialog.cancel-action": "റദ്ദാക്കുക", - "ConfirmationDialog.confirm-action": "സ്ഥിരീകരിക്കുക", - "ContentBlock.Delete": "ഇല്ലാതാക്കുക", - "ContentBlock.DeleteAction": "ഇല്ലാതാക്കുക", - "ContentBlock.addElement": "ചേർക്കുക {type}", - "ContentBlock.checkbox": "ചെക്ക്ബോക്സ്", - "ContentBlock.divider": "ഡിവൈഡർ", - "ContentBlock.editCardCheckbox": "ടുഗേൾഡ് -ചെക്ക്ബോക്സ്", - "ContentBlock.editCardCheckboxText": "കാർഡിലെ വാചകം തിരുത്തുക", - "ContentBlock.editCardText": "കാർഡിലെ വാചകം തിരുത്തുക", - "ContentBlock.editText": "വാചകം തിരുത്തുക...", - "ContentBlock.image": "ചിത്രം", - "ContentBlock.insertAbove": "മുകളിൽ തിരുകുക", - "ContentBlock.moveDown": "താഴേക്ക് നീക്കുക", - "ContentBlock.moveUp": "മുകളിലേക്കു നീക്കുക", - "ContentBlock.text": "വാചകം", - "DateRange.endDate": "അവസാന തീയതി", - "DateRange.today": "ഇന്ന്", - "DeleteBoardDialog.confirm-cancel": "നിര്‍ത്തലാക്കുക", - "DeleteBoardDialog.confirm-delete": "നീക്കം ചെയ്യുക", - "DeleteBoardDialog.confirm-info": "\"{boardTitle}\" എന്ന ബോർഡ് ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഇത് ഇല്ലാതാക്കുന്നത് ബോർഡിലെ എല്ലാ കാർഡുകളും ഇല്ലാതാക്കും.", - "DeleteBoardDialog.confirm-tite": "ബോർഡ് നീക്കം ചെയ്യുന്നത് സ്ഥിരീകരിക്കുക", - "DeleteBoardDialog.confirm-tite-template": "ബോർഡ് ടെംപ്ലേറ്റ് ഇല്ലാതാക്കുന്നത് സ്ഥിരീകരിക്കുക", - "Dialog.closeDialog": "ഡയലോഗ് അവസാനിപ്പിക്കുക", - "EditableDayPicker.today": "ഇന്ന്", - "Error.mobileweb": "മൊബൈൽ വെബ് പിന്തുണ നിലവിൽ ആദ്യകാല ബീറ്റയിലാണ്. എല്ലാ പ്രവർത്തനങ്ങളും ഉണ്ടായിരിക്കണമെന്നില്ല.", - "Error.websocket-closed": "വെബ്‌സോക്കറ്റ് കണക്ഷൻ അടച്ചു, കണക്ഷൻ തടസ്സപ്പെട്ടു. ഇത് നിലനിൽക്കുകയാണെങ്കിൽ, നിങ്ങളുടെ സെർവർ അല്ലെങ്കിൽ വെബ് പ്രോക്സി കോൺഫിഗറേഷൻ പരിശോധിക്കുക.", - "Filter.includes": "ഉൾപ്പെടുന്നു", - "Filter.is-empty": "ശൂന്യമാണ്", - "Filter.is-not-empty": "ശൂന്യമല്ല", - "Filter.not-includes": "ഉൾപ്പെടുന്നില്ല", - "FilterComponent.add-filter": "+ ഫിൽട്ടർ ചേർക്കുക", - "FilterComponent.delete": "ഇല്ലാതാക്കുക", - "FindBoardsDialog.NoResultsFor": "\"{searchQuery}\" എന്നതിന് ഫലങ്ങളൊന്നുമില്ല", - "FindBoardsDialog.NoResultsSubtext": "അക്ഷരവിന്യാസം പരിശോധിക്കുക അല്ലെങ്കിൽ മറ്റൊരു തിരയൽ പരീക്ഷിക്കുക.", - "FindBoardsDialog.SubTitle": "ഒരു ബോർഡ് കണ്ടെത്താൻ ടൈപ്പ് ചെയ്യുക. ബ്രൗസ് ചെയ്യാൻ UP/DOWN ഉപയോഗിക്കുക. തിരഞ്ഞെടുക്കാൻ ENTER, ഡിസ്മിസ് ചെയ്യാൻ ESC", - "FindBoardsDialog.Title": "ബോർഡുകൾ കണ്ടെത്തുക", - "GroupBy.hideEmptyGroups": "{count} ശൂന്യമായ ഗ്രൂപ്പുകൾ മറയ്ക്കുക", - "GroupBy.showHiddenGroups": "മറഞ്ഞിരിക്കുന്ന {count} ഗ്രൂപ്പുകൾ കാണിക്കുക", - "GroupBy.ungroup": "ഗ്രൂപ്പിൽ നിന്നും മാറ്റുക", - "KanbanCard.untitled": "ശീർഷകമില്ലാത്തത്", - "Mutator.new-card-from-template": "ടെംപ്ലേറ്റിൽ നിന്നുള്ള പുതിയ കാർഡ്", - "Mutator.new-template-from-card": "കാർഡിൽ നിന്നുള്ള പുതിയ ടെംപ്ലേറ്റ്", - "OnboardingTour.AddComments.Body": "നിങ്ങൾക്ക് പ്രശ്‌നങ്ങളിൽ അഭിപ്രായമിടാം, ഒപ്പം നിങ്ങളുടെ സഹ മാറ്റർമോസ് ഉപയോക്താക്കളെ അവരുടെ ശ്രദ്ധ ആകർഷിക്കാൻ അവരെ @പരാമർശിക്കുകയും ചെയ്യാം.", - "OnboardingTour.AddComments.Title": "അഭിപ്രായങ്ങൾ ചേർക്കുക", - "OnboardingTour.AddDescription.Body": "നിങ്ങളുടെ കാർഡിലേക്ക് ഒരു വിവരണം ചേർക്കുക, അതുവഴി കാർഡ് എന്താണെന്ന് നിങ്ങളുടെ ടീമംഗങ്ങൾക്ക് അറിയാം.", - "OnboardingTour.AddDescription.Title": "വിവരണം ചേർക്കുക", - "OnboardingTour.AddProperties.Body": "കാർഡുകളെ കൂടുതൽ ശക്തമാക്കുന്നതിന് അവയിൽ വിവിധ പ്രോപ്പർട്ടികൾ ചേർക്കുക!", - "OnboardingTour.AddProperties.Title": "പ്രോപ്പർട്ടികൾ ചേർക്കുക", - "OnboardingTour.AddView.Body": "വ്യത്യസ്‌ത ലേഔട്ടുകൾ ഉപയോഗിച്ച് നിങ്ങളുടെ ബോർഡ് ഓർഗനൈസുചെയ്യുന്നതിന് ഒരു പുതിയ കാഴ്‌ച സൃഷ്‌ടിക്കുന്നതിന് ഇവിടെ പോകുക.", - "OnboardingTour.AddView.Title": "ഒരു പുതിയ കാഴ്ച ചേർക്കുക", - "OnboardingTour.CopyLink.Body": "ലിങ്ക് പകർത്തി ഒരു ചാനലിലോ ഡയറക്ട് മെസേജിലോ ഗ്രൂപ്പ് മെസേജിലോ പകർത്തികൊണ്ട് നിങ്ങൾക്ക് ടീമംഗങ്ങളുമായി കാർഡുകൾ പങ്കിടാം.", - "OnboardingTour.CopyLink.Title": "ലിങ്ക് പകർത്തുക", - "OnboardingTour.OpenACard.Body": "നിങ്ങളുടെ ജോലി ഓർഗനൈസുചെയ്യാൻ ബോർഡുകൾക്ക് നിങ്ങളെ സഹായിക്കുന്ന ശക്തമായ വഴികൾ പര്യവേക്ഷണം ചെയ്യാൻ ഒരു കാർഡ് തുറക്കുക.", - "OnboardingTour.OpenACard.Title": "ഒരു കാർഡ് തുറക്കുക", - "OnboardingTour.ShareBoard.Body": "നിങ്ങളുടെ ബോർഡ് ടീമിനുള്ളിലും പങ്കിടാം അല്ലെങ്കിൽ നിങ്ങളുടെ സ്ഥാപനത്തിന് പുറത്തുള്ള ദൃശ്യപരതയ്ക്കായി അത് പരസ്യമായി പ്രസിദ്ധീകരിക്കാം.", - "OnboardingTour.ShareBoard.Title": "ഷെയർ ബോർഡ്", - "PropertyMenu.Delete": "ഇല്ലാതാക്കുക", - "PropertyMenu.changeType": "പ്രോപ്പർട്ടി തരം മാറ്റുക", - "PropertyMenu.selectType": "പ്രോപ്പർട്ടി തരം തിരഞ്ഞെടുക്കുക", - "PropertyMenu.typeTitle": "തരം", - "PropertyType.Checkbox": "ചെക്ക്ബോക്സ്", - "PropertyType.CreatedBy": "ഉണ്ടാക്കിയത്", - "PropertyType.CreatedTime": "സൃഷ്ടിച്ച സമയം", - "PropertyType.Date": "തീയതി", - "PropertyType.Email": "ഇമെയിൽ", - "PropertyType.MultiSelect": "മൾട്ടി സെലക്ട്", - "PropertyType.Number": "നമ്പർ", - "PropertyType.Person": "വ്യക്തി", - "PropertyType.Phone": "ഫോൺ", - "PropertyType.Select": "തിരഞ്ഞെടുക്കുക", - "PropertyType.Text": "വാചകം", - "PropertyType.UpdatedBy": "അവസാനം അപ്ഡേറ്റ് ചെയ്തത്", - "PropertyType.UpdatedTime": "അവസാനം പുതുക്കിയ സമയം", - "PropertyValueElement.empty": "ശൂന്യം", - "RegistrationLink.confirmRegenerateToken": "ഇത് മുമ്പ് പങ്കിട്ട ലിങ്കുകളെ അസാധുവാക്കും. തുടരുക?", - "RegistrationLink.copiedLink": "പകർത്തി!", - "RegistrationLink.copyLink": "ലിങ്ക് പകർത്തുക", - "RegistrationLink.description": "മറ്റുള്ളവർക്ക് അക്കൗണ്ടുകൾ സൃഷ്ടിക്കാൻ ഈ ലിങ്ക് പങ്കിടുക:", - "RegistrationLink.regenerateToken": "ടോക്കൺ പുനർനിർമ്മിക്കുക", - "RegistrationLink.tokenRegenerated": "രജിസ്ട്രേഷൻ ലിങ്ക് പുനഃസൃഷ്ടിച്ചു", - "ShareBoard.PublishDescription": "വെബിലെ എല്ലാവരുമായും \"വായന മാത്രം\" എന്ന ലിങ്ക് പ്രസിദ്ധീകരിക്കുകയും പങ്കിടുകയും ചെയ്യുക", - "ShareBoard.PublishTitle": "വെബിൽ പ്രസിദ്ധീകരിക്കുക", - "ShareBoard.ShareInternalDescription": "അനുമതിയുള്ള ഉപയോക്താക്കൾക്ക് ഈ ലിങ്ക് ഉപയോഗിക്കാനാകും", - "ShareBoard.Title": "ഷെയർ ബോർഡ്", - "ShareBoard.confirmRegenerateToken": "ഇത് മുമ്പ് പങ്കിട്ട ലിങ്കുകളെ അസാധുവാക്കും. തുടരുക?", - "ShareBoard.copiedLink": "പകർത്തി!", - "ShareBoard.copyLink": "ലിങ്ക് പകർത്തുക", - "ShareBoard.regenerate": "ടോക്കൺ പുനർനിർമ്മിക്കുക", - "ShareBoard.teamPermissionsText": "{teamName} ടീമിലെ എല്ലാവരും", - "ShareBoard.tokenRegenrated": "ടോക്കൺ പുനർനിർമ്മിച്ചു", - "ShareBoard.userPermissionsRemoveMemberText": "അംഗത്തെ നീക്കം ചെയ്യുക", - "ShareBoard.userPermissionsYouText": "(നിങ്ങൾ)", - "ShareTemplate.Title": "ടെംപ്ലേറ്റ് പങ്കിടുക", - "Sidebar.about": "ഫോക്കൽബോർഡിനെക്കുറിച്ച്", - "Sidebar.add-board": "+ ബോർഡ് ചേർക്കുക", - "Sidebar.changePassword": "പാസ്സ്‌വേഡ്‌ മാറ്റുക", - "Sidebar.delete-board": "ബോർഡ് നീക്കം ചെയ്യുക", - "Sidebar.duplicate-board": "ഡ്യൂപ്ലിക്കേറ്റ് ബോർഡ്", - "Sidebar.export-archive": "ആർക്കൈവ് എക്സ്പോർട്ട് ചെയ്യുക", - "Sidebar.import": "ഇറക്കുമതി ചെയ്യുക", - "Sidebar.import-archive": "ആർക്കൈവ് ഇമ്പോർട്ട് ചെയ്യുക", - "Sidebar.invite-users": "ഉപയോക്താക്കളെ ക്ഷണിക്കുക", - "Sidebar.logout": "ലോഗ്ഔട്ട്", - "Sidebar.no-boards-in-category": "അകത്ത് ബോർഡുകളില്ല", - "Sidebar.random-icons": "ക്രമരഹിതമായ ഐക്കണുകൾ", - "Sidebar.set-language": "ഭാഷ സജ്ജമാക്കുക", - "Sidebar.set-theme": "തീം സജ്ജമാക്കുക", - "Sidebar.settings": "ക്രമീകരണങ്ങൾ", - "Sidebar.template-from-board": "ബോർഡിൽ നിന്നുള്ള പുതിയ ടെംപ്ലേറ്റ്", - "Sidebar.untitled-board": "(പേരില്ലാത്ത ബോർഡ്)", - "SidebarCategories.BlocksMenu.Move": "ഇതിലേക്ക് നീക്കുക...", - "SidebarCategories.CategoryMenu.CreateNew": "പുതിയ വിഭാഗം സൃഷ്ടിക്കുക", - "SidebarCategories.CategoryMenu.Delete": "വിഭാഗം ഇല്ലാതാക്കുക", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} എന്നതിലെ ബോർഡുകൾ ബോർഡുകളുടെ വിഭാഗങ്ങളിലേക്ക് തിരികെ നീങ്ങും. നിങ്ങളെ ഒരു ബോർഡിൽ നിന്നും നീക്കം ചെയ്‌തിട്ടില്ല.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "ഈ കാറ്റഗറി ഇല്ലാതാക്കണോ?", - "SidebarCategories.CategoryMenu.Update": "കാറ്റഗറിയുടെ പേര് മാറ്റുക", - "TableComponent.add-icon": "ഐക്കൺ ചേർക്കുക", - "TableComponent.name": "പേര്", - "TableComponent.plus-new": "+ പുതിയത്", - "TableHeaderMenu.delete": "ഇല്ലാതാക്കുക", - "TableHeaderMenu.duplicate": "തനിപ്പകർപ്പ്", - "TableHeaderMenu.hide": "മറയ്ക്കുക", - "TableHeaderMenu.insert-left": "ഇടത്തേക്ക് തിരുകുക", - "TableHeaderMenu.insert-right": "വലത്തേക്ക് തിരുകുക", - "TableHeaderMenu.sort-ascending": "ആരോഹണക്രമത്തിൽ അടുക്കുക", - "TableHeaderMenu.sort-descending": "അവരോഹണക്രമം അടുക്കുക", - "TableRow.open": "തുറക്കുക", - "TopBar.give-feedback": "അഭിപ്രായം അറിയിക്കുക", - "URLProperty.copiedLink": "പകർത്തി!", - "URLProperty.copy": "പകർത്തുക", - "ValueSelector.noOptions": "ഓപ്ഷനുകളൊന്നുമില്ല. ആദ്യത്തേത് ചേർക്കാൻ ടൈപ്പുചെയ്യാൻ ആരംഭിക്കുക!", - "ValueSelector.valueSelector": "മൂല്യ0 തിരഞ്ഞെടുക്കുക", - "ValueSelectorLabel.openMenu": "മെനു തുറക്കുക", - "View.AddView": "വ്യൂ ചേർക്കുക", - "View.Board": "ബോർഡ്", - "View.DeleteView": "വ്യൂ നീക്കം ചെയ്യുക", - "View.DuplicateView": "വ്യൂവിന്റെ തനിപ്പകര്‍പ്പ്", - "View.Gallery": "ചിത്രശാല", - "View.NewBoardTitle": "ബോർഡ് വ്യൂ", - "View.NewCalendarTitle": "കലണ്ടർ വ്യൂ", - "View.NewGalleryTitle": "ചിത്രശാല വ്യൂ", - "View.NewTableTitle": "പട്ടിക വ്യൂ", - "View.Table": "പട്ടിക", - "ViewHeader.add-template": "പുതിയ ടെംപ്ലേറ്റ്", - "ViewHeader.delete-template": "നീക്കം ചെയ്യുക", - "ViewHeader.display-by": "പ്രദർശിപ്പിക്കുന്നത്: {property}", - "ViewHeader.edit-template": "തിരുത്തുക", - "ViewHeader.empty-card": "ശൂന്യമായ കാർഡ്", - "ViewHeader.export-board-archive": "ബോർഡ് ആർക്കൈവ് എക്സ്പോർട്ട് ചെയ്യുക", - "ViewHeader.export-complete": "എക്സ്പോർട്ട് പൂർത്തിയായി!", - "ViewHeader.export-csv": "CSV-ലേക്ക് എക്സ്പോർട്ട് ചെയ്യുക", - "ViewHeader.export-failed": "എക്സ്പോർട്ട് പരാജയപ്പെട്ടു!", - "ViewHeader.filter": "ഫിൽട്ടർ", - "ViewHeader.group-by": "ഗ്രൂപ്പ്:{property}", - "ViewHeader.new": "പുതിയത്", - "ViewHeader.properties": "സവിശേഷതകള്‍", - "ViewHeader.properties-menu": "പ്രോപ്പർട്ടീസ് മെനു", - "ViewHeader.search-text": "കാർഡുകൾ തിരയുക", - "ViewHeader.select-a-template": "ഒരു ടെംപ്ലേറ്റ് തിരഞ്ഞെടുക്കുക", - "ViewHeader.set-default-template": "സ്ഥിരസ്ഥിതിയായി സജ്ജമാക്കാൻ", - "ViewHeader.sort": "അടുക്കുക", - "ViewHeader.untitled": "ശീർഷകമില്ലാത്തത്", - "ViewHeader.view-header-menu": "തലക്കെട്ട് മെനു കാണുക", - "ViewHeader.view-menu": "മെനു കാണുക", - "ViewTitle.hide-description": "വിവരണം മറയ്ക്കുക", - "ViewTitle.pick-icon": "ഐക്കൺ തിരഞ്ഞെടുക്കുക", - "ViewTitle.random-icon": "ക്രമരഹിതം", - "ViewTitle.remove-icon": "ഐക്കൺ നീക്കം ചെയ്യുക", - "ViewTitle.show-description": "വിവരണം കാണിക്കുക", - "ViewTitle.untitled-board": "ശീർഷകമില്ലാത്ത ബോർഡ്", - "WelcomePage.Description": "പരിചിതമായ കാൻബൻ ബോർഡ് വ്യൂ ഉപയോഗിച്ച് ടീമുകളിലുടനീളമുള്ള ജോലി നിർവചിക്കാനും ഓർഗനൈസുചെയ്യാനും ട്രാക്കുചെയ്യാനും നിയന്ത്രിക്കാനും സഹായിക്കുന്ന ഒരു പ്രോജക്റ്റ് മാനേജ്‌മെന്റ് ടൂളാണ് ബോർഡുകൾ", - "WelcomePage.Explore.Button": "ഒരു ടൂർ നടത്തുക", - "WelcomePage.Heading": "ബോർഡുകളിലേക്ക് സ്വാഗതം", - "WelcomePage.NoThanks.Text": "വേണ്ട നന്ദി, ഞാനത് സ്വയം കണ്ടുപിടിക്കാം", - "Workspace.editing-board-template": "നിങ്ങൾ ഒരു ബോർഡ് ടെംപ്ലേറ്റ് എഡിറ്റ് ചെയ്യുകയാണ്.", - "calendar.month": "മാസം", - "calendar.today": "ഇന്ന്", - "calendar.week": "ആഴ്ച", - "createImageBlock.failed": "ഫയൽ അപ്‌ലോഡ് ചെയ്യാൻ കഴിയുന്നില്ല. ഫയൽ വലുപ്പ പരിധി എത്തി.", - "default-properties.badges": "അഭിപ്രായങ്ങളും വിവരണവും", - "default-properties.title": "തലക്കെട്ട്", - "error.page.title": "ക്ഷമിക്കണം, എന്തോ കുഴപ്പം സംഭവിച്ചു", - "generic.previous": "മുൻപിലേക്ക്", - "imagePaste.upload-failed": "ചില ഫയലുകൾ അപ്‌ലോഡ് ചെയ്തിട്ടില്ല. ഫയൽ വലുപ്പ പരിധി എത്തി", - "login.log-in-button": "ലോഗിൻ", - "login.log-in-title": "ലോഗിൻ", - "login.register-button": "അല്ലെങ്കിൽ നിങ്ങൾക്ക് അക്കൗണ്ട് ഇല്ലെങ്കിൽ ഒരു അക്കൗണ്ട് സൃഷ്ടിക്കുക", - "register.login-button": "അല്ലെങ്കിൽ നിങ്ങൾക്ക് ഇതിനകം ഒരു അക്കൗണ്ട് ഉണ്ടെങ്കിൽ ലോഗിൻ ചെയ്യുക", - "register.signup-title": "നിങ്ങളുടെ അക്കൗണ്ടിനായി സൈൻ അപ്പ് ചെയ്യുക", - "share-board.publish": "പ്രസിദ്ധീകരിക്കുക", - "share-board.share": "പങ്കിടുക", - "shareBoard.lastAdmin": "ബോർഡുകളിൽ കുറഞ്ഞത് ഒരു അഡ്മിനിസ്ട്രേറ്റർ ഉണ്ടായിരിക്കണം", - "tutorial_tip.finish_tour": "ചെയ്തു", - "tutorial_tip.got_it": "മനസ്സിലായി", - "tutorial_tip.ok": "അടുത്തത്", - "tutorial_tip.out": "ഈ നുറുങ്ങുകളിൽ നിന്നും തിരഞ്ഞെടുക്കുക.", - "tutorial_tip.seen": "ഇത് മുമ്പ് കണ്ടിട്ടുണ്ടോ?" -} diff --git a/webapp/boards/i18n/nb_NO.json b/webapp/boards/i18n/nb_NO.json deleted file mode 100644 index 3dc682e13a..0000000000 --- a/webapp/boards/i18n/nb_NO.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "AppBar.Tooltip": "Veksle lenkede tavler", - "Attachment.Attachment-title": "Vedlegg", - "AttachmentBlock.DeleteAction": "slett", - "AttachmentBlock.addElement": "legg til {type}", - "AttachmentBlock.delete": "Vedlegg slettet.", - "AttachmentBlock.failed": "Denne filen kunne ikke lastes opp fordi størrelsesgrensen er nådd.", - "AttachmentBlock.upload": "Vedlegg lastes opp.", - "AttachmentBlock.uploadSuccess": "Vedlegg lastet opp.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Slett", - "AttachmentElement.download": "Last ned", - "AttachmentElement.upload-percentage": "Laster opp ...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Legg til gruppe", - "BoardComponent.delete": "Slett", - "BoardComponent.hidden-columns": "Skjulte kolonner", - "BoardComponent.hide": "Skjul", - "BoardComponent.new": "+ Ny", - "BoardComponent.no-property": "Ingen {property}", - "BoardComponent.no-property-title": "Elementer med tom {property} atributt legges her. Denne kolonnen kan ikke fjernes.", - "BoardComponent.show": "Vis", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Kommentator", - "BoardMember.schemeEditor": "Redaktør", - "BoardMember.schemeNone": "Ingen", - "BoardMember.schemeViewer": "Viser", - "BoardMember.unlinkChannel": "Fjern lenke", - "BoardPage.newVersion": "En ny versjon av Boards er tilgjengelig, klikk her for å laste inn på nytt.", - "BoardPage.syncFailed": "Tavle kan slettes eller adgangen trekkes tilbake.", - "BoardTemplateSelector.add-template": "Lag ny mal", - "BoardTemplateSelector.create-empty-board": "Opprett tom tavle", - "BoardTemplateSelector.delete-template": "Slett", - "BoardTemplateSelector.description": "Legg til en tavle til sidestolpen med hvilken mal du vil fra listen under, eller start med en helt tom tavle.", - "BoardTemplateSelector.edit-template": "Rediger", - "BoardTemplateSelector.plugin.no-content-description": "Legg til en tavle i sidestolpen med hvilken mal du vil, eller start med en tom tavle.", - "BoardTemplateSelector.plugin.no-content-title": "Lag ny tavle", - "BoardTemplateSelector.title": "Lag ny tavle", - "BoardTemplateSelector.use-this-template": "Bruk denne malen", - "BoardsSwitcher.Title": "Finn tavle", - "BoardsUnfurl.Limited": "Flere detaljer er skjult fordi kortet er arkivert", - "BoardsUnfurl.Remainder": "+{remainder} mer", - "BoardsUnfurl.Updated": "Oppdatert {time}", - "Calculations.Options.average.displayName": "Gjennomsnitt", - "Calculations.Options.average.label": "Average", - "Calculations.Options.count.displayName": "Antall", - "Calculations.Options.count.label": "Antall", - "Calculations.Options.countChecked.displayName": "Avkrysset", - "Calculations.Options.countChecked.label": "Antall valgt", - "Calculations.Options.countUnchecked.displayName": "Ikke avmerket", - "Calculations.Options.countUnchecked.label": "Antall ikke valgt", - "Calculations.Options.countUniqueValue.displayName": "Unik", - "Calculations.Options.countUniqueValue.label": "Antall unike verdier", - "Calculations.Options.countValue.displayName": "Verdier", - "Calculations.Options.countValue.label": "Antall verdier", - "Calculations.Options.dateRange.displayName": "Tidsrom", - "Calculations.Options.dateRange.label": "Tidsrom", - "Calculations.Options.earliest.displayName": "Tiligst", - "Calculations.Options.earliest.label": "Tiligst", - "Calculations.Options.latest.displayName": "Senest", - "Calculations.Options.latest.label": "Senest", - "Calculations.Options.max.displayName": "Maks", - "Calculations.Options.max.label": "Maks", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Kalkulèr", - "Calculations.Options.none.label": "Ingen", - "Calculations.Options.percentChecked.displayName": "Valgt", - "Calculations.Options.percentChecked.label": "Prosent valgt", - "Calculations.Options.percentUnchecked.displayName": "Ikke valgt", - "Calculations.Options.percentUnchecked.label": "Prosent ikke valgt", - "Calculations.Options.range.displayName": "Tidsrom", - "Calculations.Options.range.label": "Tidsrom", - "Calculations.Options.sum.displayName": "Sum", - "Calculations.Options.sum.label": "Sum", - "CalendarCard.untitled": "Uten navn", - "CardActionsMenu.copiedLink": "Kopiert!", - "CardActionsMenu.copyLink": "Kopier lenke", - "CardActionsMenu.delete": "Slett", - "CardActionsMenu.duplicate": "Dupliser", - "CardBadges.title-checkboxes": "Avkrysningsbokser", - "CardBadges.title-comments": "Kommentarer", - "CardBadges.title-description": "Dette kortet har en beskrivelsestekst", - "CardDetail.Attach": "Legg ved", - "CardDetail.Follow": "Følg", - "CardDetail.Following": "Følger", - "CardDetail.add-content": "Legg til innhold", - "CardDetail.add-icon": "Legg til ikon", - "CardDetail.add-property": "+ Legg til en verdi", - "CardDetail.addCardText": "legg inn tekst i kortet", - "CardDetail.limited-body": "Oppgrader til vår profesjonelle eller bedriftsplan.", - "CardDetail.limited-button": "Oppgrader", - "CardDetail.limited-title": "Dette kortet er skjult", - "CardDetail.moveContent": "Flytt innholdet", - "CardDetail.new-comment-placeholder": "Legg til kommentar ...", - "CardDetailProperty.confirm-delete-heading": "Bekreft sletting av verdi", - "CardDetailProperty.confirm-delete-subtext": "Er du sikker på at du vil slette verdien \"{propertyName}\"? Dette vil fjerne verdien fra alle kortene på denne tavlen.", - "CardDetailProperty.confirm-property-name-change-subtext": "Er du sikker på at du vil endre verdien \"{propertyName}\" {customText}? Dette vil påvirke verdien på {numOfCards} kort på denne tavlen, og kan forårsake at du mister informasjon.", - "CardDetailProperty.confirm-property-type-change": "Bekreft endring av verditype", - "CardDetailProperty.delete-action-button": "Slett", - "CardDetailProperty.property-change-action-button": "Endre verdi", - "CardDetailProperty.property-changed": "Verdi endret!", - "CardDetailProperty.property-deleted": "Fjernet {propertyName}!", - "CardDetailProperty.property-name-change-subtext": "type fra \"{oldPropType}\" til \"{newPropType}\"", - "CardDetial.limited-link": "Lær mer om våre planer.", - "CardDialog.delete-confirmation-dialog-attachment": "Bekreft sletting av vedlegg", - "CardDialog.delete-confirmation-dialog-button-text": "Slett", - "CardDialog.delete-confirmation-dialog-heading": "Bekreft sletting av kort", - "CardDialog.editing-template": "Du redigerer en mal.", - "CardDialog.nocard": "Dette kortet eksisterer ikke eller du har ikke tilgang.", - "Categories.CreateCategoryDialog.CancelText": "Avbryt", - "Categories.CreateCategoryDialog.CreateText": "Opprett", - "Categories.CreateCategoryDialog.Placeholder": "Navngi kategorien", - "Categories.CreateCategoryDialog.UpdateText": "Oppdater", - "CenterPanel.Login": "Logg inn", - "CenterPanel.Share": "Del", - "ChannelIntro.CreateBoard": "Opprett tavle", - "ColorOption.selectColor": "Velg {color} farge", - "Comment.delete": "Slett", - "CommentsList.send": "Send", - "ConfirmPerson.empty": "Tom", - "ConfirmPerson.search": "Søk ...", - "ConfirmationDialog.cancel-action": "Avbryt", - "ConfirmationDialog.confirm-action": "Bekreft", - "ContentBlock.Delete": "Slett", - "ContentBlock.DeleteAction": "slett", - "ContentBlock.addElement": "legg til {type}", - "ContentBlock.checkbox": "avkrysningsboks", - "ContentBlock.divider": "avdeler", - "ContentBlock.editCardCheckbox": "krysset-boks", - "ContentBlock.editCardCheckboxText": "rediger kort tekst", - "ContentBlock.editCardText": "rediger kort tekst", - "ContentBlock.editText": "Rediger tekst ...", - "ContentBlock.image": "bilde", - "ContentBlock.insertAbove": "Sett inn over", - "ContentBlock.moveBlock": "flytt kort innhold", - "ContentBlock.moveDown": "Flytt ned", - "ContentBlock.moveUp": "Flytt opp", - "ContentBlock.text": "tekst", - "DateRange.clear": "Tøm", - "DateRange.empty": "Tom", - "DateRange.endDate": "Sluttdato", - "DateRange.today": "I dag", - "DeleteBoardDialog.confirm-cancel": "Avbryt", - "DeleteBoardDialog.confirm-delete": "Slett", - "DeleteBoardDialog.confirm-info": "Er du sikker på at du vil slette tavlen \"{boardTitle}\"? Dette vil slette alle kortene på tavlen.", - "DeleteBoardDialog.confirm-info-template": "Er du sikker på at du vil slette tavlemalen \"{boardTitle}\"?", - "DeleteBoardDialog.confirm-tite": "Bekreft sletting av tavle", - "DeleteBoardDialog.confirm-tite-template": "Bekreft sletting av tavlemal", - "Dialog.closeDialog": "Lukk", - "EditableDayPicker.today": "I dag", - "Error.mobileweb": "Støtte for bruk i nettleser på mobil er i tidlig beta. Alt vil ikke fungere.", - "Error.websocket-closed": "Problemer med kobling til tjeneren. Sjekk konfigurasjonen hvis problemet vedvarer.", - "Filter.contains": "inneholder", - "Filter.ends-with": "ender med", - "Filter.includes": "inkluderer", - "Filter.is": "er", - "Filter.is-empty": "er tom", - "Filter.is-not-empty": "er ikke tom", - "Filter.is-not-set": "er ikke satt", - "Filter.is-set": "er satt", - "Filter.not-contains": "inkluderer ikke", - "Filter.not-ends-with": "ender ikke med", - "Filter.not-includes": "inkluderer ikke", - "Filter.not-starts-with": "starter ikke med", - "Filter.starts-with": "starter med", - "FilterByText.placeholder": "filtrer tekst", - "FilterComponent.add-filter": "+ Nytt filter", - "FilterComponent.delete": "Slett", - "FilterValue.empty": "(tom)", - "FindBoardsDialog.IntroText": "Søk etter tavle", - "FindBoardsDialog.NoResultsFor": "Ingen resultat for \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Sjekk stavingen eller søk på noe annet.", - "FindBoardsDialog.SubTitle": "Skriv for å finne en tavle. Bruk opp/ned for å navigere. Enter for å velge, eller Esc for å avbryte", - "FindBoardsDialog.Title": "Finn tavle", - "GroupBy.hideEmptyGroups": "Skjul {count} tomme grupper", - "GroupBy.showHiddenGroups": "Vis {count} tomme grupper", - "GroupBy.ungroup": "Fjern fra gruppe", - "HideBoard.MenuOption": "Skjul tavlen", - "KanbanCard.untitled": "Uten navn", - "Mutator.new-board-from-template": "ny tavle fra mal", - "Mutator.new-card-from-template": "nytt kort fra mal", - "Mutator.new-template-from-card": "ny mal fra kort" -} diff --git a/webapp/boards/i18n/nl.json b/webapp/boards/i18n/nl.json deleted file mode 100644 index 67c60a9a46..0000000000 --- a/webapp/boards/i18n/nl.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Beheerder", - "AdminBadge.TeamAdmin": "Teambeheerder", - "AppBar.Tooltip": "Gekoppelde borden weergeven", - "Attachment.Attachment-title": "Bijlage", - "AttachmentBlock.DeleteAction": "verwijderen", - "AttachmentBlock.addElement": "voeg {type} toe", - "AttachmentBlock.delete": "Bijlage verwijderd.", - "AttachmentBlock.failed": "Dit bestand kon niet worden geüpload omdat de bestandslimiet wordt overschreden.", - "AttachmentBlock.upload": "Bijlage aan het uploaden.", - "AttachmentBlock.uploadSuccess": "Bijlage geüpload.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Verwijderen", - "AttachmentElement.download": "Downloaden", - "AttachmentElement.upload-percentage": "Uploaden...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Een groep toevoegen", - "BoardComponent.delete": "Verwijderen", - "BoardComponent.hidden-columns": "Verborgen kolommen", - "BoardComponent.hide": "Verberg", - "BoardComponent.new": "+ Nieuw", - "BoardComponent.no-property": "Geen {property}", - "BoardComponent.no-property-title": "Items met een lege {property} eigenschap komen hier te staan. Deze kolom kan niet worden verwijderd.", - "BoardComponent.show": "Toon", - "BoardMember.schemeAdmin": "Beheerder", - "BoardMember.schemeCommenter": "Commentator", - "BoardMember.schemeEditor": "Bewerker", - "BoardMember.schemeNone": "Geen", - "BoardMember.schemeViewer": "Toeschouwer", - "BoardMember.unlinkChannel": "Losmaken", - "BoardPage.newVersion": "Er is een nieuwe versie van Boards, klik hier om te herladen.", - "BoardPage.syncFailed": "Het bord kan worden verwijderd of de toegang kan worden ingetrokken.", - "BoardTemplateSelector.add-template": "Nieuwe sjabloon aanmaken", - "BoardTemplateSelector.create-empty-board": "Maak een leeg bord", - "BoardTemplateSelector.delete-template": "Verwijderen", - "BoardTemplateSelector.description": "Voeg een bord aan de zijbalk door één van onderstaande sjabloon te gebruiken of start helemaal vanaf nul.", - "BoardTemplateSelector.edit-template": "Bewerken", - "BoardTemplateSelector.plugin.no-content-description": "Voeg een bord aan de zijbalk door één van onderstaande sjabloon te gebruiken of start helemaal vanaf nul.", - "BoardTemplateSelector.plugin.no-content-title": "Een bord aanmaken", - "BoardTemplateSelector.title": "Maak een board", - "BoardTemplateSelector.use-this-template": "Gebruik dit sjabloon", - "BoardsSwitcher.Title": "Boards vinden", - "BoardsUnfurl.Limited": "Extra details zijn verborgen omdat de kaart gearchiveerd is", - "BoardsUnfurl.Remainder": "+{remainder} meer", - "BoardsUnfurl.Updated": "Bijgewerkt {time}", - "Calculations.Options.average.displayName": "Gemiddeld", - "Calculations.Options.average.label": "Gemiddeld", - "Calculations.Options.count.displayName": "Aantal", - "Calculations.Options.count.label": "Aantal", - "Calculations.Options.countChecked.displayName": "Aangevinkt", - "Calculations.Options.countChecked.label": "Aantal aangevinkt", - "Calculations.Options.countUnchecked.displayName": "Niet aangevinkt", - "Calculations.Options.countUnchecked.label": "Aantal niet aangevinkt", - "Calculations.Options.countUniqueValue.displayName": "Uniek", - "Calculations.Options.countUniqueValue.label": "Tel unieke waarden", - "Calculations.Options.countValue.displayName": "Waarden", - "Calculations.Options.countValue.label": "Aantal waarden", - "Calculations.Options.dateRange.displayName": "Bereik", - "Calculations.Options.dateRange.label": "Bereik", - "Calculations.Options.earliest.displayName": "Vroegste", - "Calculations.Options.earliest.label": "Vroegste", - "Calculations.Options.latest.displayName": "Laatste", - "Calculations.Options.latest.label": "Laatste", - "Calculations.Options.max.displayName": "Maximum", - "Calculations.Options.max.label": "Maximum", - "Calculations.Options.median.displayName": "Mediaan", - "Calculations.Options.median.label": "Mediaan", - "Calculations.Options.min.displayName": "Minimum", - "Calculations.Options.min.label": "Minimum", - "Calculations.Options.none.displayName": "Bereken", - "Calculations.Options.none.label": "Geen", - "Calculations.Options.percentChecked.displayName": "Aangevinkt", - "Calculations.Options.percentChecked.label": "Percentage aangevinkt", - "Calculations.Options.percentUnchecked.displayName": "Niet aangevinkt", - "Calculations.Options.percentUnchecked.label": "Percentage niet aangevinkt", - "Calculations.Options.range.displayName": "Bereik", - "Calculations.Options.range.label": "Bereik", - "Calculations.Options.sum.displayName": "Som", - "Calculations.Options.sum.label": "Som", - "CalendarCard.untitled": "Titelloos", - "CardActionsMenu.copiedLink": "Gekopieerd!", - "CardActionsMenu.copyLink": "Kopieer link", - "CardActionsMenu.delete": "Verwijderen", - "CardActionsMenu.duplicate": "Dupliceren", - "CardBadges.title-checkboxes": "Selectievakjes", - "CardBadges.title-comments": "Opmerkingen", - "CardBadges.title-description": "Deze kaart heeft een beschrijving", - "CardDetail.Attach": "Toevoegen", - "CardDetail.Follow": "Volgen", - "CardDetail.Following": "Volgend", - "CardDetail.add-content": "Inhoud toevoegen", - "CardDetail.add-icon": "Pictogram toevoegen", - "CardDetail.add-property": "+ Een eigenschap toevoegen", - "CardDetail.addCardText": "kaarttekst toevoegen", - "CardDetail.limited-body": "Upgrade naar ons Professional- of Enterprise-plan.", - "CardDetail.limited-button": "Upgraden", - "CardDetail.limited-title": "Deze kaart is verborgen", - "CardDetail.moveContent": "Inhoud van de kaart verplaatsen", - "CardDetail.new-comment-placeholder": "Voeg commentaar toe...", - "CardDetailProperty.confirm-delete-heading": "Bevestig verwijderen eigenschap", - "CardDetailProperty.confirm-delete-subtext": "Weet je zeker dat je de eigenschap \"{propertyName}\" wilt verwijderen? Dit verwijderen zal de eigenschap van alle kaarten in dit bord verwijderen.", - "CardDetailProperty.confirm-property-name-change-subtext": "Weet je zeker dat je de eigenschap \"{propertyName}\" {customText} wilt wijzigen? Dit zal invloed hebben op de waarde(n) op de {numOfCards} kaart(en) in dit bord, en kan resulteren in data verlies.", - "CardDetailProperty.confirm-property-type-change": "Bevestig wijziging type eigenschap", - "CardDetailProperty.delete-action-button": "Verwijderen", - "CardDetailProperty.property-change-action-button": "Wijzig eigenschap", - "CardDetailProperty.property-changed": "Eigenschap succesvol gewijzigd!", - "CardDetailProperty.property-deleted": "{propertyName} werd succesvol verwijderd!", - "CardDetailProperty.property-name-change-subtext": "type van \"{oldPropType}\" naar \"{newPropType}\"", - "CardDetial.limited-link": "Meer informatie over onze plannen.", - "CardDialog.delete-confirmation-dialog-attachment": "Bevestig het verwijderen van de bijlage", - "CardDialog.delete-confirmation-dialog-button-text": "Verwijderen", - "CardDialog.delete-confirmation-dialog-heading": "Bevestig verwijderen kaart", - "CardDialog.editing-template": "Je bent een sjabloon aan het bewerken.", - "CardDialog.nocard": "Deze kaart bestaat niet of is ontoegankelijk.", - "Categories.CreateCategoryDialog.CancelText": "Annuleren", - "Categories.CreateCategoryDialog.CreateText": "Aanmaken", - "Categories.CreateCategoryDialog.Placeholder": "Geef je categorie een naam", - "Categories.CreateCategoryDialog.UpdateText": "Bijwerken", - "CenterPanel.Login": "Aanmelden", - "CenterPanel.Share": "Delen", - "ChannelIntro.CreateBoard": "Een bord aanmaken", - "ColorOption.selectColor": "Selecteer {color} Kleur", - "Comment.delete": "Verwijderen", - "CommentsList.send": "Verzenden", - "ConfirmPerson.empty": "Leeg", - "ConfirmPerson.search": "Zoeken...", - "ConfirmationDialog.cancel-action": "Annuleren", - "ConfirmationDialog.confirm-action": "Bevestigen", - "ContentBlock.Delete": "Verwijderen", - "ContentBlock.DeleteAction": "verwijderen", - "ContentBlock.addElement": "voeg {type} toe", - "ContentBlock.checkbox": "selectievakje", - "ContentBlock.divider": "verdeler", - "ContentBlock.editCardCheckbox": "Aangevinkt selectievakje", - "ContentBlock.editCardCheckboxText": "kaarttekst bewerken", - "ContentBlock.editCardText": "kaarttekst bewerken", - "ContentBlock.editText": "Tekst bewerken...", - "ContentBlock.image": "afbeelding", - "ContentBlock.insertAbove": "Hierboven invoegen", - "ContentBlock.moveBlock": "inhoud van de kaart verplaatsen", - "ContentBlock.moveDown": "Naar beneden verplaatsen", - "ContentBlock.moveUp": "Naar boven verplaatsen", - "ContentBlock.text": "tekst", - "DateFilter.empty": "Leeg", - "DateRange.clear": "Wissen", - "DateRange.empty": "Leeg", - "DateRange.endDate": "Einddatum", - "DateRange.today": "Vandaag", - "DeleteBoardDialog.confirm-cancel": "Annuleren", - "DeleteBoardDialog.confirm-delete": "Verwijderen", - "DeleteBoardDialog.confirm-info": "Weet je zeker dat u het bord \"{boardTitle}\" wil verwijderen? Het verwijderen van het bord zal alle kaarten in het bord verwijderen.", - "DeleteBoardDialog.confirm-info-template": "Weet je zeker dat je het boardsjabloon \"{boardTitle}\" wilt verwijderen?", - "DeleteBoardDialog.confirm-tite": "Bevestig verwijderen board", - "DeleteBoardDialog.confirm-tite-template": "Bevestig verwijderen Board-sjabloon", - "Dialog.closeDialog": "Dialoogvenster sluiten", - "EditableDayPicker.today": "Vandaag", - "Error.mobileweb": "Mobiele webondersteuning is momenteel in vroege beta. Het is mogelijk dat niet alle functionaliteit aanwezig is.", - "Error.websocket-closed": "Websocketverbinding gesloten, verbinding onderbroken. Als dit aanhoudt, controleer dan jouw server of web proxy configuratie.", - "Filter.contains": "bevat", - "Filter.ends-with": "eindigt met", - "Filter.includes": "bevat", - "Filter.is": "is", - "Filter.is-after": "is na", - "Filter.is-before": "is voor", - "Filter.is-empty": "is leeg", - "Filter.is-not-empty": "is niet leeg", - "Filter.is-not-set": "is niet ingesteld", - "Filter.is-set": "is ingesteld", - "Filter.isafter": "is na", - "Filter.isbefore": "is voor", - "Filter.not-contains": "bevat niet", - "Filter.not-ends-with": "eindigt niet met", - "Filter.not-includes": "bevat niet", - "Filter.not-starts-with": "begint niet met", - "Filter.starts-with": "begint met", - "FilterByText.placeholder": "filtertekst", - "FilterComponent.add-filter": "+ Filter toevoegen", - "FilterComponent.delete": "Verwijderen", - "FilterValue.empty": "(leeg)", - "FindBoardsDialog.IntroText": "Zoeken naar borden", - "FindBoardsDialog.NoResultsFor": "Geen resultaten voor \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Controleer de spelling of probeer een andere zoekopdracht.", - "FindBoardsDialog.SubTitle": "Typ om een bord te vinden. Gebruik UP/DOWN om te bladeren. ENTER om te selecteren, ESC om te annuleren", - "FindBoardsDialog.Title": "Boards vinden", - "GroupBy.hideEmptyGroups": "Verberg {count} lege groepen", - "GroupBy.showHiddenGroups": "Toon {count} verborgen groepen", - "GroupBy.ungroup": "Groeperen stoppen", - "HideBoard.MenuOption": "Bord verbergen", - "KanbanCard.untitled": "Titelloos", - "MentionSuggestion.is-not-board-member": "Geen deelnemer van het bord", - "Mutator.new-board-from-template": "nieuw board van sjabloon", - "Mutator.new-card-from-template": "nieuwe kaart van sjabloon", - "Mutator.new-template-from-card": "nieuw sjabloon van kaart", - "OnboardingTour.AddComments.Body": "Je kunt commentaar geven op onderwerpen, en zelfs je medeMattermostgebruikers @vermelden om hun aandacht te trekken.", - "OnboardingTour.AddComments.Title": "Opmerkingen toevoegen", - "OnboardingTour.AddDescription.Body": "Voeg een beschrijving toe aan je kaart, zodat je teamgenoten weten waar de kaart over gaat.", - "OnboardingTour.AddDescription.Title": "Beschrijving toevoegen", - "OnboardingTour.AddProperties.Body": "Voeg verschillende eigenschappen toe aan kaarten om ze effectiever te maken.", - "OnboardingTour.AddProperties.Title": "Eigenschappen toevoegen", - "OnboardingTour.AddView.Body": "Kom hier naartoe om een nieuwe weergave te maken om uw bord te organiseren met verschillende lay-outs.", - "OnboardingTour.AddView.Title": "Een nieuwe weergave toevoegen", - "OnboardingTour.CopyLink.Body": "Je kunt je kaarten delen met teamgenoten door de link te kopiëren en in een kanaal, direct bericht of groepsbericht te plakken.", - "OnboardingTour.CopyLink.Title": "Link kopiëren", - "OnboardingTour.OpenACard.Body": "Open een kaart om de krachtige manieren te ontdekken waarop Boards je kunnen helpen je werk te organiseren.", - "OnboardingTour.OpenACard.Title": "Open een kaart", - "OnboardingTour.ShareBoard.Body": "Je kan jouw bord intern delen, binnen jouw team, of het publiek publiceren voor zichtbaarheid buiten jouw organisatie.", - "OnboardingTour.ShareBoard.Title": "Bord delen", - "PersonProperty.board-members": "Deelnemers aan het bord", - "PersonProperty.me": "Ik", - "PersonProperty.non-board-members": "Niet-deelnemers aan het bord", - "PropertyMenu.Delete": "Verwijderen", - "PropertyMenu.changeType": "Type eigenschap wijzigen", - "PropertyMenu.selectType": "Selecteer type eigenschap", - "PropertyMenu.typeTitle": "Type", - "PropertyType.Checkbox": "Selectievakje", - "PropertyType.CreatedBy": "Gemaakt door", - "PropertyType.CreatedTime": "Aangemaakt op", - "PropertyType.Date": "Datum", - "PropertyType.Email": "E-mail", - "PropertyType.MultiPerson": "Meerdere personen", - "PropertyType.MultiSelect": "Multiselect", - "PropertyType.Number": "Nummer", - "PropertyType.Person": "Persoon", - "PropertyType.Phone": "Telefoon", - "PropertyType.Select": "Selecteer", - "PropertyType.Text": "Tekst", - "PropertyType.Unknown": "Onbekend", - "PropertyType.UpdatedBy": "Laatst aangepast door", - "PropertyType.UpdatedTime": "Laatst bijgewerkte tijd", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Leeg", - "RegistrationLink.confirmRegenerateToken": "Dit zal eerder gedeelde links ongeldig maken. Doorgaan?", - "RegistrationLink.copiedLink": "Gekopieerd!", - "RegistrationLink.copyLink": "Kopieer link", - "RegistrationLink.description": "Deel deze link zodat anderen een account kunnen aanmaken:", - "RegistrationLink.regenerateToken": "Token opnieuw genereren", - "RegistrationLink.tokenRegenerated": "Registratielink heraangemaakt", - "ShareBoard.PublishDescription": "Publiceer en deel een \"alleen-lezen\" link met iedereen op het web.", - "ShareBoard.PublishTitle": "Publiceren op het web", - "ShareBoard.ShareInternal": "Intern delen", - "ShareBoard.ShareInternalDescription": "Gebruikers die toegangsrechten hebben, kunnen deze link gebruiken.", - "ShareBoard.Title": "Bord delen", - "ShareBoard.confirmRegenerateToken": "Dit zal eerder gedeelde links ongeldig maken. Doorgaan?", - "ShareBoard.copiedLink": "Gekopieerd!", - "ShareBoard.copyLink": "Link kopiëren", - "ShareBoard.regenerate": "Token opnieuw genereren", - "ShareBoard.searchPlaceholder": "Mensen en kanalen zoeken", - "ShareBoard.teamPermissionsText": "Iedereen van team {teamName}", - "ShareBoard.tokenRegenrated": "Token opnieuw gegenereerd", - "ShareBoard.userPermissionsRemoveMemberText": "Lid verwijderen", - "ShareBoard.userPermissionsYouText": "(jij)", - "ShareTemplate.Title": "Sjabloon delen", - "ShareTemplate.searchPlaceholder": "Mensen zoeken", - "Sidebar.about": "Over Focalboard", - "Sidebar.add-board": "+ Bord toevoegen", - "Sidebar.changePassword": "Wachtwoord wijzigen", - "Sidebar.delete-board": "Verwijder bord", - "Sidebar.duplicate-board": "Board dupliceren", - "Sidebar.export-archive": "Archief exporteren", - "Sidebar.import": "Importeren", - "Sidebar.import-archive": "Archief importeren", - "Sidebar.invite-users": "Gebruikers uitnodigen", - "Sidebar.logout": "Afmelden", - "Sidebar.new-category.badge": "Nieuw", - "Sidebar.new-category.drag-boards-cta": "Sleep borden naar hier...", - "Sidebar.no-boards-in-category": "Geen boards hier", - "Sidebar.product-tour": "Product-rondleiding", - "Sidebar.random-icons": "Willekeurige iconen", - "Sidebar.set-language": "Taal instellen", - "Sidebar.set-theme": "Thema instellen", - "Sidebar.settings": "Instellingen", - "Sidebar.template-from-board": "Nieuw sjabloon van board", - "Sidebar.untitled-board": "(Titelloze bord )", - "Sidebar.untitled-view": "(Naamloze weergave)", - "SidebarCategories.BlocksMenu.Move": "Verplaatsen naar...", - "SidebarCategories.CategoryMenu.CreateNew": "Maak een nieuwe categorie", - "SidebarCategories.CategoryMenu.Delete": "Categorie verwijderen", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Borden in {categoryName} zullen terug verhuizen naar de Boards categorieën. Je zal niet verwijderd worden uit enig board.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Deze categorie verwijderen?", - "SidebarCategories.CategoryMenu.Update": "Categorie hernoemen", - "SidebarTour.ManageCategories.Body": "Maak en beheer aangepaste categorieën. Categorieën zijn gebruikersspecifiek, dus het verplaatsen van een bord naar jouw categorie heeft geen invloed op andere leden die hetzelfde bord gebruiken.", - "SidebarTour.ManageCategories.Title": "Categorieën beheren", - "SidebarTour.SearchForBoards.Body": "Open de bordenswitcher (Cmd/Ctrl + K) om snel borden te zoeken en toe te voegen aan je zijbalk.", - "SidebarTour.SearchForBoards.Title": "Borden zoeken", - "SidebarTour.SidebarCategories.Body": "Al je borden zijn nu georganiseerd in je nieuwe zijbalk. Niet meer schakelen tussen werkruimten. Eenmalige zelfgemaakte categorieën gebaseerd op jouw vorige workspaces kunnen automatisch gemaakt zijn voor jou als onderdeel van jouw v7.2 upgrade. Deze kunnen worden verwijderd of aangepast aan jouw voorkeur.", - "SidebarTour.SidebarCategories.Link": "Meer info", - "SidebarTour.SidebarCategories.Title": "Zijbalk categorieën", - "SiteStats.total_boards": "Totaal aantal borden", - "SiteStats.total_cards": "Totaal aantal kaarten", - "TableComponent.add-icon": "Pictogram toevoegen", - "TableComponent.name": "Naam", - "TableComponent.plus-new": "+ Nieuw", - "TableHeaderMenu.delete": "Verwijderen", - "TableHeaderMenu.duplicate": "Kopiëren", - "TableHeaderMenu.hide": "Verberg", - "TableHeaderMenu.insert-left": "Links invoegen", - "TableHeaderMenu.insert-right": "Rechts invoegen", - "TableHeaderMenu.sort-ascending": "Sorteer oplopend", - "TableHeaderMenu.sort-descending": "Aflopend sorteren", - "TableRow.DuplicateCard": "dupliceren kaart", - "TableRow.MoreOption": "Meer acties", - "TableRow.open": "Openen", - "TopBar.give-feedback": "Geef feedback", - "URLProperty.copiedLink": "Gekopieerd!", - "URLProperty.copy": "Kopiëren", - "URLProperty.edit": "Bewerken", - "UndoRedoHotKeys.canRedo": "Herhaal", - "UndoRedoHotKeys.canRedo-with-description": "Herhaal {description}", - "UndoRedoHotKeys.canUndo": "Ongedaan maken", - "UndoRedoHotKeys.canUndo-with-description": "Ongedaan maken van {description}", - "UndoRedoHotKeys.cannotRedo": "Niets om te herhalen", - "UndoRedoHotKeys.cannotUndo": "Niets om ongedaan te maken", - "ValueSelector.noOptions": "Geen opties. Begin te typen om de eerste toe te voegen!", - "ValueSelector.valueSelector": "Waardekiezer", - "ValueSelectorLabel.openMenu": "Menu openen", - "VersionMessage.help": "Bekijk eens wat nieuw is in deze versie.", - "VersionMessage.learn-more": "Meer info", - "View.AddView": "Weergave toevoegen", - "View.Board": "Bord", - "View.DeleteView": "Weergave verwijderen", - "View.DuplicateView": "Weergave kopiëren", - "View.Gallery": "Galerij", - "View.NewBoardTitle": "Bordweergave", - "View.NewCalendarTitle": "Kalenderweergave", - "View.NewGalleryTitle": "Galerie bekijken", - "View.NewTableTitle": "Tabelweergave", - "View.NewTemplateDefaultTitle": "Naamloos sjabloon", - "View.NewTemplateTitle": "Naamloos", - "View.Table": "Tabel", - "ViewHeader.add-template": "Nieuw sjabloon", - "ViewHeader.delete-template": "Verwijderen", - "ViewHeader.display-by": "Weergegeven op: {property}", - "ViewHeader.edit-template": "Bewerken", - "ViewHeader.empty-card": "Lege kaart", - "ViewHeader.export-board-archive": "Archief bord exporteren", - "ViewHeader.export-complete": "Exporteren gelukt!", - "ViewHeader.export-csv": "Exporteren naar CSV", - "ViewHeader.export-failed": "Export mislukt!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Groepeer op: {property}", - "ViewHeader.new": "Nieuw", - "ViewHeader.properties": "Eigenschappen", - "ViewHeader.properties-menu": "Eigenschappen-menu", - "ViewHeader.search-text": "Kaarten zoeken", - "ViewHeader.select-a-template": "Kies een sjabloon", - "ViewHeader.set-default-template": "Instellen als standaard", - "ViewHeader.sort": "Sorteer", - "ViewHeader.untitled": "Titelloos", - "ViewHeader.view-header-menu": "Menu hoofding weergeven", - "ViewHeader.view-menu": "Menuweergave", - "ViewLimitDialog.Heading": "Limiet aantal views per board bereikt", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Upgraden", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Verwittig Admin", - "ViewLimitDialog.Subtext.Admin": "Upgrade naar ons Professional- of Enterprise-plan.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Meer informatie over onze plannen.", - "ViewLimitDialog.Subtext.RegularUser": "Verwittig jouw Admin om te upgraden naar ons Professioneel of Enterprise plan.", - "ViewLimitDialog.UpgradeImg.AltText": "upgrade afbeelding", - "ViewLimitDialog.notifyAdmin.Success": "Jouw beheerder is op de hoogte gebracht", - "ViewTitle.hide-description": "beschrijving verbergen", - "ViewTitle.pick-icon": "Pictogram kiezen", - "ViewTitle.random-icon": "Willekeurig", - "ViewTitle.remove-icon": "Verwijder pictogram", - "ViewTitle.show-description": "beschrijving tonen", - "ViewTitle.untitled-board": "Titelloos board", - "WelcomePage.Description": "Boards is een projectmanagementtool die helpt bij het definiëren, organiseren, volgen en beheren van werk door teams heen, met behulp van een bekende Kanban-bordweergave.", - "WelcomePage.Explore.Button": "Start een rondleiding", - "WelcomePage.Heading": "Welkom bij Boards", - "WelcomePage.NoThanks.Text": "Nee bedankt, ik zoek het zelf wel uit", - "WelcomePage.StartUsingIt.Text": "Ga het gebruiken", - "Workspace.editing-board-template": "Je bent een bordsjabloon aan het bewerken.", - "badge.guest": "Gast", - "boardPage.confirm-join-button": "Word lid", - "boardPage.confirm-join-text": "Je staat op het punt lid te worden van een privé-bord zonder dat je expliciet bent toegevoegd door de bordbeheerder. Weet je zeker dat je lid wilt worden van dit privé-bord?", - "boardPage.confirm-join-title": "Word lid van het privé-bord", - "boardSelector.confirm-link-board": "Koppel bord aan kanaal", - "boardSelector.confirm-link-board-button": "Ja, koppel het bord", - "boardSelector.confirm-link-board-subtext": "Wanneer je \"{boardName}\" aan het kanaal koppelt, kunnen alle leden van het kanaal (bestaande en nieuwe) het bewerken. Dit sluit leden die gast zijn uit. Je kan de koppeling van een bord naar een kanaal op elk moment ongedaan maken.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Wanneer je \"{boardName}\" aan het kanaal koppelt zullen alle leden van het kanaal (bestaande en nieuwe) het kunnen bewerken. Dit sluit leden die gast zijn uit. {lineBreak} Dit board is momenteel gekoppeld aan een ander kanaal. Het zal worden ontkoppeld als je ervoor kiest om het hier te koppelen.", - "boardSelector.create-a-board": "Maak een bord", - "boardSelector.link": "Link", - "boardSelector.search-for-boards": "Zoeken naar borden", - "boardSelector.title": "Link borden", - "boardSelector.unlink": "Link ongedaan maken", - "calendar.month": "Maand", - "calendar.today": "VANDAAG", - "calendar.week": "Week", - "centerPanel.undefined": "Geen {propertyName}", - "centerPanel.unknown-user": "Onbekende gebruiker", - "cloudMessage.learn-more": "Meer info", - "createImageBlock.failed": "Dit bestand kon niet worden geüpload omdat de bestandslimiet wordt overschreden.", - "default-properties.badges": "Opmerkingen en beschrijving", - "default-properties.title": "Titel", - "error.back-to-home": "Terug naar startpagina", - "error.back-to-team": "Terug naar team", - "error.board-not-found": "Board niet gevonden.", - "error.go-login": "Aanmelden", - "error.invalid-read-only-board": "Je hebt geen toegang tot dit board. Meld je aan om toegang te krijgen tot Boards.", - "error.not-logged-in": "Jouw sessie is misschien verlopen of je bent niet ingelogd. Meldt je opnieuw aan om toegang te krijgen tot Boards.", - "error.page.title": "Sorry, er ging iets mis", - "error.team-undefined": "Geen geldig team.", - "error.unknown": "Er trad een fout op.", - "generic.previous": "Vorige", - "guest-no-board.subtitle": "Je hebt nog geen toegang tot een board in dit team, wacht tot iemand je toevoegt aan een board.", - "guest-no-board.title": "Nog geen borden", - "imagePaste.upload-failed": "Sommige bestanden zijn niet geüpload omdat de limiet voor de bestandsgrootte is bereikt.", - "limitedCard.title": "Verborgen kaarten", - "login.log-in-button": "Aanmelden", - "login.log-in-title": "Aanmelden", - "login.register-button": "of maak een account aan als je er nog geen hebt", - "new_channel_modal.create_board.empty_board_description": "Maak een nieuw leeg bord", - "new_channel_modal.create_board.empty_board_title": "Leeg bord", - "new_channel_modal.create_board.select_template_placeholder": "Kies een sjabloon", - "new_channel_modal.create_board.title": "Maak een bord voor dit kanaal", - "notification-box-card-limit-reached.close-tooltip": "Snooze voor 10 dagen", - "notification-box-card-limit-reached.contact-link": "breng je beheerder op de hoogte", - "notification-box-card-limit-reached.link": "Upgrade naar een betaald plan", - "notification-box-card-limit-reached.title": "{cards} kaarten verborgen van board", - "notification-box-cards-hidden.title": "Deze actie heeft een andere kaart verborgen", - "notification-box.card-limit-reached.not-admin.text": "Om toegang te krijgen tot gearchiveerde kaarten, neem contact op met {contactLink} om te upgraden naar een betaald plan.", - "notification-box.card-limit-reached.text": "Limiet van aantal kaarten bereikt, om oudere kaarten te bekijken, {link}", - "person.add-user-to-board": "{username} toevoegen aan bord", - "person.add-user-to-board-confirm-button": "Toevoegen aan bord", - "person.add-user-to-board-permissions": "Machtigingen", - "person.add-user-to-board-question": "Wil je {username} toevoegen aan het bord?", - "person.add-user-to-board-warning": "{username} is geen lid van het bord en zal er geen meldingen over ontvangen.", - "register.login-button": "of meldt je aan als je al een account hebt", - "register.signup-title": "Maak een nieuw account", - "rhs-board-non-admin-msg": "Je bent geen beheerder van het bord", - "rhs-boards.add": "Toevoegen", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "dit directe bericht", - "rhs-boards.header.gm": "dit groepsbericht", - "rhs-boards.last-update-at": "Laatste wijziging op: {datetime}", - "rhs-boards.link-boards-to-channel": "Koppel borden aan {channelName}", - "rhs-boards.linked-boards": "Gekoppelde borden", - "rhs-boards.no-boards-linked-to-channel": "Er zijn nog geen borden gekoppeld aan {channelName}", - "rhs-boards.no-boards-linked-to-channel-description": "Boards is een projectmanagementtool die helpt bij het definiëren, organiseren, volgen en beheren van werk door teams heen, met behulp van een bekende kanban-bordweergave.", - "rhs-boards.unlink-board": "Bord loskoppelen", - "rhs-boards.unlink-board1": "Bord loskoppelen", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Publiceren", - "share-board.share": "Delen", - "shareBoard.channels-select-group": "Kanalen", - "shareBoard.confirm-change-team-role.body": "Iedereen op dit board met een lagere machtiging dan de \"{role}\" rol zal nu opwaarderen naar {role}. Weet je zeker dat je de minimale rol voor het board wilt veranderen?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Minimale rol van het bord wijzigen", - "shareBoard.confirm-change-team-role.title": "Minimale rol van het bord wijzigen", - "shareBoard.confirm-link-channel": "Bord koppelen aan kanaal", - "shareBoard.confirm-link-channel-button": "Kanaal koppelen", - "shareBoard.confirm-link-channel-button-with-other-channel": "Koppel en ontkoppel hier", - "shareBoard.confirm-link-channel-subtext": "Wanneer je het bord aan het kanaal koppelt zullen alle leden van het kanaal (bestaande en nieuwe) het kunnen bewerken. Dit sluit leden die gast zijn uit.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Wanneer je een kanaal aan een bord koppelt zullen alle leden van het kanaal (bestaande en nieuwe) het kunnen bewerken.Dit sluit leden die gast zijn uit. {lineBreak} Dit board is momenteel gekoppeld aan een ander kanaal. Het zal worden ontkoppeld als je ervoor kiest om het hier te koppelen.", - "shareBoard.confirm-unlink.body": "Wanneer een kanaal afkoppelt van een bord zullen alle leden van het kanaal (bestaande en nieuwe) geen toegang meer hebben tot ze apart toegang gegeven worden.", - "shareBoard.confirm-unlink.confirmBtnText": "Kanaal ontkoppelen", - "shareBoard.confirm-unlink.title": "Kanaal loskoppelen van bord", - "shareBoard.lastAdmin": "Besturen moeten ten minste één beheerder hebben", - "shareBoard.members-select-group": "Leden", - "shareBoard.unknown-channel-display-name": "Onbekend kanaal", - "tutorial_tip.finish_tour": "Klaar", - "tutorial_tip.got_it": "Begrepen", - "tutorial_tip.ok": "Volgende", - "tutorial_tip.out": "Schakel deze tips uit.", - "tutorial_tip.seen": "Heb je dit al gezien?" -} diff --git a/webapp/boards/i18n/oc.json b/webapp/boards/i18n/oc.json deleted file mode 100644 index b044961d6d..0000000000 --- a/webapp/boards/i18n/oc.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Apondre un grop", - "BoardComponent.delete": "Suprimir", - "BoardComponent.hidden-columns": "Colomnas rescondudas", - "BoardComponent.hide": "Rescondre", - "BoardComponent.new": "+ Nòu", - "BoardComponent.no-property": "Cap de {property}", - "BoardComponent.no-property-title": "Los elements sens proprietats {property} seràn plaçats aquí. Se pòt pas suprimir aquesta colomna.", - "BoardComponent.show": "Mostrar", - "BoardPage.syncFailed": "Lo tablèu es benlèu suprimit o l’accès es revocat.", - "BoardsUnfurl.Remainder": "+{remainder} de mai", - "BoardsUnfurl.Updated": "Actualizat {time}", - "CardDetail.add-content": "Apondre contengut", - "CardDetail.add-icon": "Apondre una icòna", - "CardDetail.add-property": "+ Apondre una proprietat", - "CardDetail.addCardText": "apondre una zòna de tèxt", - "CardDetail.moveContent": "desplaçar contengut de la carta", - "CardDetail.new-comment-placeholder": "Apondre un comentari...", - "CardDetailProperty.confirm-delete-subtext": "Volètz vertadièrament suprimir la proprietat « {propertyName} » ? La supression levarà la proprietat de totas las cartas d’aquesta tablèu.", - "CardDetailProperty.property-deleted": "Supression de {propertyName} reüssida !", - "CardDialog.editing-template": "Sètz a modificar un modèl.", - "CardDialog.nocard": "Aquesta zòna existís pas o es pas accessibla.", - "ColorOption.selectColor": "Seleccionar la color {color}", - "Comment.delete": "Suprimir", - "CommentsList.send": "Enviar", - "ConfirmationDialog.cancel-action": "Anullar", - "ContentBlock.Delete": "Suprimir", - "ContentBlock.DeleteAction": "suprimir", - "ContentBlock.addElement": "apondre {type}", - "ContentBlock.checkbox": "cassa de marcar", - "ContentBlock.divider": "separador", - "ContentBlock.editCardCheckbox": "alternar-cassa", - "ContentBlock.editCardCheckboxText": "modificar lo tèxt de la carta", - "ContentBlock.editCardText": "modificar lo tèxt de la carta", - "ContentBlock.editText": "Modificar lo tèxt...", - "ContentBlock.image": "imatge", - "ContentBlock.insertAbove": "Inserir al dessús", - "ContentBlock.moveDown": "Desplaçar al dejós", - "ContentBlock.moveUp": "Desplaçar al dessús", - "ContentBlock.text": "tèxt", - "Dialog.closeDialog": "Tampar la fenèstra de dialòg", - "EditableDayPicker.today": "Uèi", - "Error.websocket-closed": "Connexion al websocket tampada, connexion interrompuda. S’aquò ten de se produire, verificatz la configuracion del servidor o del servidor mandatari.", - "Filter.includes": "inclutz", - "Filter.is-empty": "es void", - "Filter.is-not-empty": "es pas void", - "Filter.not-includes": "inclutz pas", - "FilterComponent.add-filter": "+ Apondre un filtre", - "FilterComponent.delete": "Suprimir", - "GroupBy.ungroup": "Desgropar", - "KanbanCard.untitled": "Sens títol", - "Mutator.new-card-from-template": "zòna novèla a partir d’un modèl", - "Mutator.new-template-from-card": "modèl novèl a partir d’una zòna", - "PropertyMenu.Delete": "Suprimir", - "PropertyMenu.changeType": "Modificar lo tipe de proprietat", - "PropertyMenu.selectType": "Seleccionar tipe de proprietat", - "PropertyMenu.typeTitle": "Tipe", - "PropertyType.Checkbox": "Casa de marcar", - "PropertyType.CreatedBy": "Creat per", - "PropertyType.CreatedTime": "Data de creacion", - "PropertyType.Date": "Data", - "PropertyType.Email": "Adreça e-mail", - "PropertyType.MultiSelect": "Seleccion multipla", - "PropertyType.Number": "Nombre", - "PropertyType.Person": "Persona", - "PropertyType.Phone": "Telefòn", - "PropertyType.Select": "Lista", - "PropertyType.Text": "Tèxt", - "PropertyType.UpdatedBy": "Darrièra actualizacion per", - "PropertyType.UpdatedTime": "Data de darrièra actualizacion", - "PropertyValueElement.empty": "Void", - "RegistrationLink.confirmRegenerateToken": "Aquò desactivarà los ligams de partiment existents. Contunhar ?", - "RegistrationLink.copiedLink": "Copiat !", - "RegistrationLink.copyLink": "Copiar lo ligam", - "RegistrationLink.description": "Partejatz aqueste ligam per que d’autres pòscan crear un compte :", - "RegistrationLink.regenerateToken": "Generar un geton novèl", - "RegistrationLink.tokenRegenerated": "Un ligam novèl d’inscripcion es estat creat", - "ShareBoard.confirmRegenerateToken": "Aquò desactivarà los ligams de partiment existents. Contunhar ?", - "ShareBoard.copiedLink": "Copiat !", - "ShareBoard.copyLink": "Copiar lo ligam", - "ShareBoard.tokenRegenrated": "Geton regenerat", - "Sidebar.about": "A prepaus de Focalboard", - "Sidebar.add-board": "+ Apondre un tablèu", - "Sidebar.changePassword": "Modificar lo senhal", - "Sidebar.delete-board": "Suprimir lo tablèu", - "Sidebar.export-archive": "Exportar un archiu", - "Sidebar.import-archive": "Importar un archiu", - "Sidebar.invite-users": "Convidar utilizaires", - "Sidebar.logout": "Se desconnectar", - "Sidebar.random-icons": "Icònas aleatòrias", - "Sidebar.set-language": "Definir la lenga", - "Sidebar.set-theme": "Causir lo tèma", - "Sidebar.settings": "Paramètres", - "Sidebar.untitled-board": "(Tablèu sens títol)", - "TableComponent.add-icon": "Apondre una icòna", - "TableComponent.name": "Nom", - "TableComponent.plus-new": "+ Novèl", - "TableHeaderMenu.delete": "Suprimir", - "TableHeaderMenu.duplicate": "Duplicar", - "TableHeaderMenu.hide": "Rescondre", - "TableHeaderMenu.insert-left": "Inserir a esquèrra", - "TableHeaderMenu.insert-right": "Inserir a drecha", - "TableHeaderMenu.sort-ascending": "Tria ascendenta", - "TableHeaderMenu.sort-descending": "Tria descendenta", - "TableRow.open": "Dobrir", - "TopBar.give-feedback": "Far un retorn", - "ValueSelector.valueSelector": "Selector de valor", - "ValueSelectorLabel.openMenu": "Dobrir lo menú", - "View.AddView": "Apondre una vista", - "View.Board": "Tablèu", - "View.DeleteView": "Suprimir la vista", - "View.DuplicateView": "Duplicar la vista", - "View.Gallery": "Galariá", - "View.NewBoardTitle": "Vista en tablèu", - "View.NewGalleryTitle": "Vista galariá", - "View.NewTableTitle": "Vista en taula", - "View.Table": "Tablèu", - "ViewHeader.add-template": "Modèl novèl", - "ViewHeader.delete-template": "Suprimir", - "ViewHeader.edit-template": "Modificar", - "ViewHeader.empty-card": "Zòna voida", - "ViewHeader.export-board-archive": "Exportar l’archiu del tablèu", - "ViewHeader.export-complete": "Export acabat !", - "ViewHeader.export-csv": "Exportar al format CSV", - "ViewHeader.export-failed": "Export fracassat !", - "ViewHeader.filter": "Filtre", - "ViewHeader.group-by": "Agropar per : {property}", - "ViewHeader.new": "Novèl", - "ViewHeader.properties": "Proprietats", - "ViewHeader.search-text": "Recercar de tèxt", - "ViewHeader.select-a-template": "Seleccionar un modèl", - "ViewHeader.set-default-template": "Definir per defaut", - "ViewHeader.sort": "Triar", - "ViewHeader.untitled": "Sens títol", - "ViewTitle.hide-description": "rescondre la descripcion", - "ViewTitle.pick-icon": "Causir una icòna", - "ViewTitle.random-icon": "Aleatòria", - "ViewTitle.remove-icon": "Suprimir l'icòna", - "ViewTitle.show-description": "mostrar la descripcion", - "ViewTitle.untitled-board": "Tablèu sens títol", - "WelcomePage.Explore.Button": "Explorar", - "WelcomePage.Heading": "La benvengudas als tablèus", - "Workspace.editing-board-template": "Modificatz un modèl de tablèu.", - "default-properties.title": "Títol", - "login.log-in-button": "Connexion", - "login.log-in-title": "Connexion", - "login.register-button": "o creatz un compte se n’avètz pas un", - "register.login-button": "o connectatz-vos s’avètz un compte", - "register.signup-title": "Vos inscriure per aver un compte" -} diff --git a/webapp/boards/i18n/pl.json b/webapp/boards/i18n/pl.json deleted file mode 100644 index 893ec70168..0000000000 --- a/webapp/boards/i18n/pl.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrator", - "AdminBadge.TeamAdmin": "Administrator Zespołu", - "AppBar.Tooltip": "Przełączanie Podlinkowanych Tablic", - "Attachment.Attachment-title": "Załącznik", - "AttachmentBlock.DeleteAction": "usuń", - "AttachmentBlock.addElement": "dodaj {type}", - "AttachmentBlock.delete": "Załącznik usunięty.", - "AttachmentBlock.failed": "Ten plik nie mógł zostać przesłany, ponieważ został osiągnięty limit rozmiaru pliku.", - "AttachmentBlock.upload": "Przesyłanie załączników.", - "AttachmentBlock.uploadSuccess": "Załącznik przesłany.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Usuń", - "AttachmentElement.download": "Pobierz", - "AttachmentElement.upload-percentage": "Przesyłanie...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Dodaj grupę", - "BoardComponent.delete": "Usuń", - "BoardComponent.hidden-columns": "Ukryte kolumny", - "BoardComponent.hide": "Ukryj", - "BoardComponent.new": "+ Nowy", - "BoardComponent.no-property": "Brak {property}", - "BoardComponent.no-property-title": "Elementy z pustą właściwością {property} trafią tutaj. Tej kolumny nie można usunąć.", - "BoardComponent.show": "Pokaż", - "BoardMember.schemeAdmin": "Administrator", - "BoardMember.schemeCommenter": "Komentujący", - "BoardMember.schemeEditor": "Redaktor", - "BoardMember.schemeNone": "Brak", - "BoardMember.schemeViewer": "Obserwator", - "BoardMember.unlinkChannel": "Odłącz", - "BoardPage.newVersion": "Dostępna jest nowa wersja tablic. Naciśnij tutaj, aby przeładować.", - "BoardPage.syncFailed": "Tablica mogła zostać usunięta lub dostęp do niej cofnięty.", - "BoardTemplateSelector.add-template": "Utwórz nowy szablon", - "BoardTemplateSelector.create-empty-board": "Utwórz pustą tablicę", - "BoardTemplateSelector.delete-template": "Usuń", - "BoardTemplateSelector.description": "Dodaj tablicę do paska bocznego używając dowolnego z poniższych szablonów lub zacznij od nowa.", - "BoardTemplateSelector.edit-template": "Edytuj", - "BoardTemplateSelector.plugin.no-content-description": "Dodaj tablicę do paska bocznego używając jednego z poniższych szablonów lub zacznij od nowa.", - "BoardTemplateSelector.plugin.no-content-title": "Utwórz tablicę", - "BoardTemplateSelector.title": "Utwórz tablicę", - "BoardTemplateSelector.use-this-template": "Użyj tego szablonu", - "BoardsSwitcher.Title": "Wyszukiwanie tablic", - "BoardsUnfurl.Limited": "Dodatkowe szczegóły są ukryte ze względu na archiwizację karty", - "BoardsUnfurl.Remainder": "+{remainder} więcej", - "BoardsUnfurl.Updated": "Zaktualizowano {time}", - "Calculations.Options.average.displayName": "Średnia", - "Calculations.Options.average.label": "Średnia", - "Calculations.Options.count.displayName": "Liczba", - "Calculations.Options.count.label": "Liczba", - "Calculations.Options.countChecked.displayName": "Zaznaczone", - "Calculations.Options.countChecked.label": "Zaznaczono licznik", - "Calculations.Options.countUnchecked.displayName": "Niezaznaczony", - "Calculations.Options.countUnchecked.label": "Licznik niezaznaczony", - "Calculations.Options.countUniqueValue.displayName": "Unikatowe", - "Calculations.Options.countUniqueValue.label": "Policz wartości unikatowe", - "Calculations.Options.countValue.displayName": "Wartości", - "Calculations.Options.countValue.label": "Licznik wartości", - "Calculations.Options.dateRange.displayName": "Zakres", - "Calculations.Options.dateRange.label": "Zakres", - "Calculations.Options.earliest.displayName": "Wcześniejszy", - "Calculations.Options.earliest.label": "Wcześniejszy", - "Calculations.Options.latest.displayName": "Ostatni", - "Calculations.Options.latest.label": "Ostatni", - "Calculations.Options.max.displayName": "Maks.", - "Calculations.Options.max.label": "Maks.", - "Calculations.Options.median.displayName": "Mediana", - "Calculations.Options.median.label": "Mediana", - "Calculations.Options.min.displayName": "Min.", - "Calculations.Options.min.label": "Min.", - "Calculations.Options.none.displayName": "Policz", - "Calculations.Options.none.label": "Brak", - "Calculations.Options.percentChecked.displayName": "Zaznaczony", - "Calculations.Options.percentChecked.label": "Procent sprawdzonych", - "Calculations.Options.percentUnchecked.displayName": "Niezaznaczony", - "Calculations.Options.percentUnchecked.label": "Procent nie zaznaczonych", - "Calculations.Options.range.displayName": "Zakres", - "Calculations.Options.range.label": "Zakres", - "Calculations.Options.sum.displayName": "Suma", - "Calculations.Options.sum.label": "Suma", - "CalendarCard.untitled": "Bez tytułu", - "CardActionsMenu.copiedLink": "Skopiowano!", - "CardActionsMenu.copyLink": "Kopiuj odnośnik", - "CardActionsMenu.delete": "Usuń", - "CardActionsMenu.duplicate": "Duplikuj", - "CardBadges.title-checkboxes": "Pola wyboru", - "CardBadges.title-comments": "Komentarze", - "CardBadges.title-description": "Ta karta ma opis", - "CardDetail.Attach": "Załącz", - "CardDetail.Follow": "Obserwuj", - "CardDetail.Following": "Obserwowane", - "CardDetail.add-content": "Dodaj treść", - "CardDetail.add-icon": "Dodaj ikonę", - "CardDetail.add-property": "+ Dodaj właściwość", - "CardDetail.addCardText": "dodaj tekst karty", - "CardDetail.limited-body": "Uaktualnij do naszego planu Professional lub Enterprise.", - "CardDetail.limited-button": "Zmień plan", - "CardDetail.limited-title": "Ta karta jest ukryta", - "CardDetail.moveContent": "Przenieś zawartość karty", - "CardDetail.new-comment-placeholder": "Dodaj komentarz…", - "CardDetailProperty.confirm-delete-heading": "Potwierdzanie usunięcia właściwości", - "CardDetailProperty.confirm-delete-subtext": "Na pewno chcesz usunąć właściwość „{propertyName}”? Usunięcie tej właściwości spowoduje usunięcie jej z wszystkich kart na tej tablicy.", - "CardDetailProperty.confirm-property-name-change-subtext": "Na pewno chcesz zmienić właściwość „{propertyName}” {customText}? Wpłynie to na wartości na {numOfCards} kartach na tej tablicy i może spowodować utratę danych.", - "CardDetailProperty.confirm-property-type-change": "Potwierdzenie zmiany typu właściwości", - "CardDetailProperty.delete-action-button": "Usuń", - "CardDetailProperty.property-change-action-button": "Zmień właściwość", - "CardDetailProperty.property-changed": "Zmieniono właściwość pomyślnie!", - "CardDetailProperty.property-deleted": "Usunięto pomyślnie {propertyName}!", - "CardDetailProperty.property-name-change-subtext": "typ z \"{oldPropType}\" do \"{newPropType}\"", - "CardDetial.limited-link": "Dowiedz się więcej o naszych planach.", - "CardDialog.delete-confirmation-dialog-attachment": "Potwierdź usunięcie załącznika", - "CardDialog.delete-confirmation-dialog-button-text": "Usuń", - "CardDialog.delete-confirmation-dialog-heading": "Potwierdź usunięcie karty", - "CardDialog.editing-template": "Edytujesz szablon.", - "CardDialog.nocard": "Ta karta nie istnieje lub jest niedostępna.", - "Categories.CreateCategoryDialog.CancelText": "Anuluj", - "Categories.CreateCategoryDialog.CreateText": "Utwórz", - "Categories.CreateCategoryDialog.Placeholder": "Nazwij kategorię", - "Categories.CreateCategoryDialog.UpdateText": "Zmień", - "CenterPanel.Login": "Logowanie", - "CenterPanel.Share": "Udostępnij", - "ChannelIntro.CreateBoard": "Utwórz tablicę", - "ColorOption.selectColor": "Wybierz Kolor {color}", - "Comment.delete": "Usuń", - "CommentsList.send": "Wyślij", - "ConfirmPerson.empty": "Puste", - "ConfirmPerson.search": "Szukaj...", - "ConfirmationDialog.cancel-action": "Anuluj", - "ConfirmationDialog.confirm-action": "Potwierdź", - "ContentBlock.Delete": "Usuń", - "ContentBlock.DeleteAction": "usuń", - "ContentBlock.addElement": "dodaj {type}", - "ContentBlock.checkbox": "pole wyboru", - "ContentBlock.divider": "dzielnik", - "ContentBlock.editCardCheckbox": "pole wyboru", - "ContentBlock.editCardCheckboxText": "edytuj tekst karty", - "ContentBlock.editCardText": "edytuj tekst karty", - "ContentBlock.editText": "Edytuj tekst...", - "ContentBlock.image": "obraz", - "ContentBlock.insertAbove": "Wstaw powyżej", - "ContentBlock.moveBlock": "przenieś zawartość karty", - "ContentBlock.moveDown": "Przenieś w dół", - "ContentBlock.moveUp": "Przenieś w górę", - "ContentBlock.text": "tekst", - "DateFilter.empty": "Puste", - "DateRange.clear": "Wyczyść", - "DateRange.empty": "Puste", - "DateRange.endDate": "Data końcowa", - "DateRange.today": "Dzisiaj", - "DeleteBoardDialog.confirm-cancel": "Anuluj", - "DeleteBoardDialog.confirm-delete": "Usuń", - "DeleteBoardDialog.confirm-info": "Na pewno chcesz usunąć tablicę „{boardTitle}”? Usunięcie tej tablicy spowoduje usunięcie z niej wszystkich kart.", - "DeleteBoardDialog.confirm-info-template": "Na pewno chcesz usunąć szablon tablicy „{boardTitle}”?", - "DeleteBoardDialog.confirm-tite": "Potwierdzenie usunięcia tablicy", - "DeleteBoardDialog.confirm-tite-template": "Potwierdzenie usunięcia szablonu tablicy", - "Dialog.closeDialog": "Zamknij okno dialogowe", - "EditableDayPicker.today": "Dzisiaj", - "Error.mobileweb": "Strona internetowa dla urządzeń mobilnych jest obecnie we wczesnej fazie testów. Nie wszystkie funkcje mogą być dostępne.", - "Error.websocket-closed": "Połączenie WebSocket zostało zamknięte – połączenie przerwane. Jeśli problem się powtarza, sprawdź konfigurację swojego serwera lub serwera pośredniczącego Web.", - "Filter.contains": "zawiera", - "Filter.ends-with": "kończy się na", - "Filter.includes": "zawiera", - "Filter.is": "jest", - "Filter.is-after": "jest po", - "Filter.is-before": "jest przed", - "Filter.is-empty": "jest pusty", - "Filter.is-not-empty": "nie jest pusty", - "Filter.is-not-set": "nie jest ustawiony", - "Filter.is-set": "jest ustawiony", - "Filter.isafter": "jest po", - "Filter.isbefore": "jest przed", - "Filter.not-contains": "nie zawiera", - "Filter.not-ends-with": "nie kończy się na", - "Filter.not-includes": "nie zawiera", - "Filter.not-starts-with": "nie zaczyna się od", - "Filter.starts-with": "zaczyna się od", - "FilterByText.placeholder": "tekst filtra", - "FilterComponent.add-filter": "+ Dodaj filtr", - "FilterComponent.delete": "Usuń", - "FilterValue.empty": "(pusty)", - "FindBoardsDialog.IntroText": "Wyszukiwanie tablic", - "FindBoardsDialog.NoResultsFor": "Brak wyników dla „{searchQuery}”", - "FindBoardsDialog.NoResultsSubtext": "Sprawdź pisownię lub spróbuj innego wyszukiwania.", - "FindBoardsDialog.SubTitle": "Wpisz, aby znaleźć tablicę. Użyj GÓRA/DÓŁ, aby przeglądać. ENTER, aby wybrać, ESC, aby odrzucić", - "FindBoardsDialog.Title": "Znajdź tablice", - "GroupBy.hideEmptyGroups": "Ukryj {count} pustych grup", - "GroupBy.showHiddenGroups": "Pokaż {count} ukrytych grup", - "GroupBy.ungroup": "Rozgrupuj", - "HideBoard.MenuOption": "Ukryj tablicę", - "KanbanCard.untitled": "Bez tytułu", - "MentionSuggestion.is-not-board-member": "(nie jest członkiem tablicy)", - "Mutator.new-board-from-template": "nowa tablica z szablonu", - "Mutator.new-card-from-template": "nowa karta z szablonu", - "Mutator.new-template-from-card": "nowy szablon z karty", - "OnboardingTour.AddComments.Body": "Możesz komentować zagadnienia, a nawet @wspominać innych użytkowników Mattermost, aby uzyskać ich uwagę.", - "OnboardingTour.AddComments.Title": "Dodawanie komentarzy", - "OnboardingTour.AddDescription.Body": "Dodaj opis do karty, aby członkowie zespołu wiedzieli, czego ona dotyczy .", - "OnboardingTour.AddDescription.Title": "Dodaj opis", - "OnboardingTour.AddProperties.Body": "Dodawaj różne właściwości do kart, aby zwiększyć ich moc.", - "OnboardingTour.AddProperties.Title": "Dodawanie właściwości", - "OnboardingTour.AddView.Body": "Tutaj utworzysz nowy widok, którym uporządkujesz tablicę za pomocą różnych układów.", - "OnboardingTour.AddView.Title": "Dodawanie nowego widoku", - "OnboardingTour.CopyLink.Body": "Karty można udostępniać członkom zespołu kopiując łącze i wklejając je w kanale, wiadomości prywatnej lub grupowej.", - "OnboardingTour.CopyLink.Title": "Kopiowanie odnośnika", - "OnboardingTour.OpenACard.Body": "Otwórz kartę i poznaj różne sposoby, dzięki którym zorganizujesz swoją pracę za pomocą tablic.", - "OnboardingTour.OpenACard.Title": "Otwieranie karty", - "OnboardingTour.ShareBoard.Body": "Możesz udostępniać swoją tablicę wewnętrznie, w ramach zespołu albo opublikować ją, aby była widoczna poza organizacją.", - "OnboardingTour.ShareBoard.Title": "Udostępnianie tablicy", - "PersonProperty.board-members": "Członkowie tablicy", - "PersonProperty.me": "Ja", - "PersonProperty.non-board-members": "Nie-członkowie tablicy", - "PropertyMenu.Delete": "Usuń", - "PropertyMenu.changeType": "Zmień typ właściwości", - "PropertyMenu.selectType": "Wybierz typ właściwości", - "PropertyMenu.typeTitle": "Typ", - "PropertyType.Checkbox": "Pole wyboru", - "PropertyType.CreatedBy": "Twórca", - "PropertyType.CreatedTime": "Czas utworzenia", - "PropertyType.Date": "Data", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Wiele osób", - "PropertyType.MultiSelect": "Pole wielokrotnego wyboru", - "PropertyType.Number": "Liczba", - "PropertyType.Person": "Osoba", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Wybór", - "PropertyType.Text": "Tekst", - "PropertyType.Unknown": "Nieznany", - "PropertyType.UpdatedBy": "Ostatni aktualizujący", - "PropertyType.UpdatedTime": "Czas ostatniej aktualizacji", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Puste", - "RegistrationLink.confirmRegenerateToken": "Unieważni to wcześniej udostępnione odnośniki. Kontynuować?", - "RegistrationLink.copiedLink": "Skopiowano!", - "RegistrationLink.copyLink": "Kopiuj odnośnik", - "RegistrationLink.description": "Udostępnij ten odnośnik innym, aby mogli utworzyć konta:", - "RegistrationLink.regenerateToken": "Wygeneruj ponownie poświadczenie", - "RegistrationLink.tokenRegenerated": "Wygenerowano ponownie odnośnik rejestracyjny", - "ShareBoard.PublishDescription": "Publikowanie i udostępnianie linku tylko-do-odczyt\" wszystkim w sieci.", - "ShareBoard.PublishTitle": "Opublikuj w sieci", - "ShareBoard.ShareInternal": "Udostępnij wewnętrznie", - "ShareBoard.ShareInternalDescription": "Użytkownicy z odpowiednimi uprawnieniami będą mogli korzystać z tego łącza.", - "ShareBoard.Title": "Udostępnij Tablicę", - "ShareBoard.confirmRegenerateToken": "Spowoduje to unieważnienie wcześniej udostępnionych linków. Kontynuować?", - "ShareBoard.copiedLink": "Skopiowane!", - "ShareBoard.copyLink": "Kopiuj odnośnik", - "ShareBoard.regenerate": "Wygeneruj ponownie token", - "ShareBoard.searchPlaceholder": "Wyszukiwanie osób", - "ShareBoard.teamPermissionsText": "Wszyscy w zespole {teamName}", - "ShareBoard.tokenRegenrated": "Token wygenerowany", - "ShareBoard.userPermissionsRemoveMemberText": "Usuń użytkownika", - "ShareBoard.userPermissionsYouText": "(Ty)", - "ShareTemplate.Title": "Udostępnij szablon", - "ShareTemplate.searchPlaceholder": "Wyszukiwanie osób", - "Sidebar.about": "O Focalboard", - "Sidebar.add-board": "+ Dodaj tablicę", - "Sidebar.changePassword": "Zmień hasło", - "Sidebar.delete-board": "Usuń tablicę", - "Sidebar.duplicate-board": "Duplikuj tablicę", - "Sidebar.export-archive": "Eksportuj archiwum", - "Sidebar.import": "Importuj", - "Sidebar.import-archive": "Importuj archiwum", - "Sidebar.invite-users": "Zaproś użytkowników", - "Sidebar.logout": "Wyloguj się", - "Sidebar.new-category.badge": "Nowy", - "Sidebar.new-category.drag-boards-cta": "Przenieś tutaj tablice...", - "Sidebar.no-boards-in-category": "Brak tablic wewnątrz", - "Sidebar.product-tour": "Przegląd", - "Sidebar.random-icons": "Losowe ikony", - "Sidebar.set-language": "Ustaw język", - "Sidebar.set-theme": "Ustaw motyw", - "Sidebar.settings": "Ustawienia", - "Sidebar.template-from-board": "Nowy szablon z tablicy", - "Sidebar.untitled-board": "(Tablica bez tytułu)", - "Sidebar.untitled-view": "(Widok bez Tytułu)", - "SidebarCategories.BlocksMenu.Move": "Przenieś Do...", - "SidebarCategories.CategoryMenu.CreateNew": "Utwórz Nową Kategorię", - "SidebarCategories.CategoryMenu.Delete": "Usuń Kategorię", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Tablice w {categoryName} zostaną przeniesione z powrotem do kategorii Tablice. Nie zostaniesz usunięty z żadnej tablicy.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Usunąć tą kategorię?", - "SidebarCategories.CategoryMenu.Update": "Zmień nazwę Kategorii", - "SidebarTour.ManageCategories.Body": "Twórz i zarządzaj własnymi kategoriami. Kategorie są zależne od użytkownika, więc przeniesienie tablicy do twojej kategorii nie będzie miało wpływu na innych członków korzystających z tej samej tablicy.", - "SidebarTour.ManageCategories.Title": "Zarządzaj kategoriami", - "SidebarTour.SearchForBoards.Body": "Otwórz przełącznik tablic (Cmd/Ctrl + K), aby szybko wyszukać i dodać tablice do swojego paska bocznego.", - "SidebarTour.SearchForBoards.Title": "Wyszukiwanie tablic", - "SidebarTour.SidebarCategories.Body": "Wszystkie Twoje tablice są teraz uporządkowane w nowym pasku bocznym. Nie musisz już przełączać się między obszarami roboczymi. Jednorazowe niestandardowe kategorie oparte na Twoich poprzednich obszarach roboczych mogły zostać automatycznie utworzone dla Ciebie w ramach aktualizacji do wersji 7.2. Można je usunąć lub edytować według własnych preferencji.", - "SidebarTour.SidebarCategories.Link": "Dowiedź się więcej", - "SidebarTour.SidebarCategories.Title": "Kategorie paska bocznego", - "SiteStats.total_boards": "Tablice ogółem", - "SiteStats.total_cards": "Karty ogółem", - "TableComponent.add-icon": "Dodaj Ikonę", - "TableComponent.name": "Nazwa", - "TableComponent.plus-new": "+ Nowy", - "TableHeaderMenu.delete": "Usuń", - "TableHeaderMenu.duplicate": "Duplikuj", - "TableHeaderMenu.hide": "Ukryj", - "TableHeaderMenu.insert-left": "Wstaw z lewej", - "TableHeaderMenu.insert-right": "Wstaw z prawej", - "TableHeaderMenu.sort-ascending": "Sortuj rosnąco", - "TableHeaderMenu.sort-descending": "Sortuj malejąco", - "TableRow.DuplicateCard": "duplikuj kartę", - "TableRow.MoreOption": "Więcej działań", - "TableRow.open": "Otwórz", - "TopBar.give-feedback": "Przekaż informację zwrotną", - "URLProperty.copiedLink": "Skopiowane!", - "URLProperty.copy": "Kopia", - "URLProperty.edit": "Edycja", - "UndoRedoHotKeys.canRedo": "Powtórz", - "UndoRedoHotKeys.canRedo-with-description": "Powtórz {description}", - "UndoRedoHotKeys.canUndo": "Cofnij", - "UndoRedoHotKeys.canUndo-with-description": "Cofnij {description}", - "UndoRedoHotKeys.cannotRedo": "Nic do powtórzenia", - "UndoRedoHotKeys.cannotUndo": "Nic do cofnięcia", - "ValueSelector.noOptions": "Brak opcji. Zacznij wpisywać, aby dodać pierwszą z nich!", - "ValueSelector.valueSelector": "Selektor wartości", - "ValueSelectorLabel.openMenu": "Otwórz menu", - "VersionMessage.help": "Sprawdź co nowego w tej wersji.", - "VersionMessage.learn-more": "Dowiedź się więcej", - "View.AddView": "Dodaj widok", - "View.Board": "Tablica", - "View.DeleteView": "Usuń widok", - "View.DuplicateView": "Duplikuj widok", - "View.Gallery": "Galeria", - "View.NewBoardTitle": "Widok Tablicy", - "View.NewCalendarTitle": "Widok Kalendarza", - "View.NewGalleryTitle": "Widok galerii", - "View.NewTableTitle": "Widok tabeli", - "View.NewTemplateDefaultTitle": "Szablon bez tytułu", - "View.NewTemplateTitle": "Bez tytułu", - "View.Table": "Tabela", - "ViewHeader.add-template": "Nowy szablon", - "ViewHeader.delete-template": "Usuń", - "ViewHeader.display-by": "Wyświetl według: {property}", - "ViewHeader.edit-template": "Edytuj", - "ViewHeader.empty-card": "Wyczyść kartę", - "ViewHeader.export-board-archive": "Eksportuj archiwum tablicy", - "ViewHeader.export-complete": "Eksport zakończony!", - "ViewHeader.export-csv": "Eksportuj do CSV", - "ViewHeader.export-failed": "Eksport nie powiódł się!", - "ViewHeader.filter": "Filtr", - "ViewHeader.group-by": "Grupuj wg: {property}", - "ViewHeader.new": "Nowy", - "ViewHeader.properties": "Właściwości", - "ViewHeader.properties-menu": "Menu właściwości", - "ViewHeader.search-text": "Przeszukaj karty", - "ViewHeader.select-a-template": "Wybierz szablon", - "ViewHeader.set-default-template": "Ustaw jako domyślne", - "ViewHeader.sort": "Sortuj", - "ViewHeader.untitled": "Bez tytułu", - "ViewHeader.view-header-menu": "Wyświetl menu nagłówka", - "ViewHeader.view-menu": "Wyświetl menu", - "ViewLimitDialog.Heading": "Osiągnięty limit odsłon na tablicę", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Aktualizuj", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Powiadom Administratora", - "ViewLimitDialog.Subtext.Admin": "Uaktualnij do naszego planu Professional lub Enterprise.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Dowiedz się więcej o naszych planach.", - "ViewLimitDialog.Subtext.RegularUser": "Powiadom swojego Administratora, aby uaktualnić do naszego planu Professional lub Enterprise.", - "ViewLimitDialog.UpgradeImg.AltText": "aktualizuj obraz", - "ViewLimitDialog.notifyAdmin.Success": "Twój administrator został powiadomiony", - "ViewTitle.hide-description": "ukryj opis", - "ViewTitle.pick-icon": "Wybierz ikonę", - "ViewTitle.random-icon": "Losowy", - "ViewTitle.remove-icon": "Usuń ikonę", - "ViewTitle.show-description": "pokaż opis", - "ViewTitle.untitled-board": "Tablica bez tytułu", - "WelcomePage.Description": "Tablice to narzędzie do zarządzania projektami, które pomaga definiować, organizować, śledzić i zarządzać pracą w zespołach wykorzystując widok znanych tablic Kanban.", - "WelcomePage.Explore.Button": "Wybierz się na wycieczkę", - "WelcomePage.Heading": "Witamy w Tablicach", - "WelcomePage.NoThanks.Text": "Nie, dzięki, sam sobie z tym poradzę", - "WelcomePage.StartUsingIt.Text": "Zacznij używać", - "Workspace.editing-board-template": "Edytujesz szablon tablicy.", - "badge.guest": "Gość", - "boardPage.confirm-join-button": "Dołącz", - "boardPage.confirm-join-text": "Zamierzasz dołączyć do prywatnej tablicy bez wyraźnego dodania przez administratora forum. Czy na pewno chcesz dołączyć do tego prywatnego forum?", - "boardPage.confirm-join-title": "Dołącz do prywatnej tablicy", - "boardSelector.confirm-link-board": "Połączenie tablicy z kanałem", - "boardSelector.confirm-link-board-button": "Tak, podlinkuj tablicę", - "boardSelector.confirm-link-board-subtext": "Kiedy połączysz \"{boardName}\" z kanałem, wszyscy członkowie kanału (istniejący i nowi) będą mogli go edytować. Nie dotyczy to członków, którzy są gośćmi. W każdej chwili możesz odłączyć tablicę od kanału.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Kiedy połączysz \"{boardName}\" z kanałem, wszyscy członkowie kanału (istniejący i nowi) będą mogli go edytować. Wyklucza to członków, którzy są gośćmi.{lineBreak} Ta tablica jest obecnie połączona z innym kanałem. Zostanie ona odłączona, jeśli zdecydujesz się połączyć ją tutaj.", - "boardSelector.create-a-board": "Utwórz tablicę", - "boardSelector.link": "Link", - "boardSelector.search-for-boards": "Wyszukiwanie tablic", - "boardSelector.title": "Linki tablic", - "boardSelector.unlink": "Odłącz", - "calendar.month": "Miesiąc", - "calendar.today": "DZIŚ", - "calendar.week": "Tydzień", - "centerPanel.undefined": "Brak {propertyName}", - "centerPanel.unknown-user": "Nieznany użytkownik", - "cloudMessage.learn-more": "Dowiedź się więcej", - "createImageBlock.failed": "Ten plik nie mógł zostać przesłany, ponieważ został osiągnięty limit rozmiaru pliku.", - "default-properties.badges": "Uwagi i opis", - "default-properties.title": "Tytuł", - "error.back-to-home": "Powrót na stronę główną", - "error.back-to-team": "Powrót do zespołu", - "error.board-not-found": "Nie znaleziono tablicy.", - "error.go-login": "Zaloguj się", - "error.invalid-read-only-board": "Nie masz dostępu do tej tablicy. Zaloguj się, aby uzyskać dostęp do Tablic.", - "error.not-logged-in": "Twoja sesja mogła wygasnąć lub nie jesteś zalogowany. Zaloguj się ponownie, aby uzyskać dostęp do Tablic.", - "error.page.title": "Przepraszam, coś poszło nie tak", - "error.team-undefined": "Nieprawidłowy zespół.", - "error.unknown": "Wystąpił błąd.", - "generic.previous": "Wstecz", - "guest-no-board.subtitle": "Nie masz jeszcze dostępu do żadnej tablicy w tym zespole, poczekaj aż ktoś doda Cię do jakiejkolwiek tablicy.", - "guest-no-board.title": "Nie ma jeszcze tablic", - "imagePaste.upload-failed": "Niektóre pliki nie zostały przesłane, ponieważ został osiągnięty limit rozmiaru pliku.", - "limitedCard.title": "Ukryte karty", - "login.log-in-button": "Zaloguj się", - "login.log-in-title": "Zaloguj się", - "login.register-button": "lub załóż konto, jeśli jeszcze go nie masz", - "new_channel_modal.create_board.empty_board_description": "Utwórz nową pustą tablicę", - "new_channel_modal.create_board.empty_board_title": "Wyczyść tablicę", - "new_channel_modal.create_board.select_template_placeholder": "Wybierz szablon", - "new_channel_modal.create_board.title": "Utwórz tablicę dla tego kanału", - "notification-box-card-limit-reached.close-tooltip": "Uśpij na 10 dni", - "notification-box-card-limit-reached.contact-link": "powiadom swojego administratora", - "notification-box-card-limit-reached.link": "Uaktualnienie do planu płatnego", - "notification-box-card-limit-reached.title": "{cards} karty ukryte z tablicy", - "notification-box-cards-hidden.title": "Ta akcja zakryła inną kartę", - "notification-box.card-limit-reached.not-admin.text": "Aby uzyskać dostęp do zarchiwizowanych kart, możesz {contactLink} uaktualnić do płatnego planu.", - "notification-box.card-limit-reached.text": "Osiągnięto limit kart, aby wyświetlić starsze karty, {link}", - "person.add-user-to-board": "Dodaj {username} do tablicy", - "person.add-user-to-board-confirm-button": "Dodaj do tablicy", - "person.add-user-to-board-permissions": "Uprawnienia", - "person.add-user-to-board-question": "Czy chcesz dodać do tablicy {username}?", - "person.add-user-to-board-warning": "{username} nie jest członkiem tablicy i nie będzie otrzymywał żadnych powiadomień na ten temat.", - "register.login-button": "lub zaloguj się, jeśli masz już konto", - "register.signup-title": "Zarejestruj się na swoim koncie", - "rhs-board-non-admin-msg": "Nie jesteś administratorem tablicy", - "rhs-boards.add": "Dodaj", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "ta bezpośrednia wiadomość", - "rhs-boards.header.gm": "ta wiadomość grupowa", - "rhs-boards.last-update-at": "Ostatnia aktualizacja o: {datetime}", - "rhs-boards.link-boards-to-channel": "Połączenie tablic z {channelName}", - "rhs-boards.linked-boards": "Połączone tablice", - "rhs-boards.no-boards-linked-to-channel": "Żadna z tablic nie jest jeszcze połączona z {channelName}", - "rhs-boards.no-boards-linked-to-channel-description": "Tablice to narzędzie do zarządzania projektami, które pomagają definiować, organizować, śledzić i zarządzać pracą w zespołach, wykorzystując widok znanych tablic kanban.", - "rhs-boards.unlink-board": "Odłączenie tablicy", - "rhs-boards.unlink-board1": "Odłączenie tablicy", - "rhs-channel-boards-header.title": "Tablice", - "share-board.publish": "Opublikuj", - "share-board.share": "Udostępnij", - "shareBoard.channels-select-group": "Kanały", - "shareBoard.confirm-change-team-role.body": "Każdy na tej tablicy z niższymi uprawnieniami niż rola \"{role}\" teraz zostanie awansowany do {role}. Czy na pewno chcesz zmienić minimalną rolę dla tablicy?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Zmiana minimalnej roli tablicy", - "shareBoard.confirm-change-team-role.title": "Zmiana minimalnej roli tablicy", - "shareBoard.confirm-link-channel": "Podlinku tablicę do kanału", - "shareBoard.confirm-link-channel-button": "Połączenie kanału", - "shareBoard.confirm-link-channel-button-with-other-channel": "Odłącz i podłącz tutaj", - "shareBoard.confirm-link-channel-subtext": "Kiedy połączysz kanał z tablicą, wszyscy członkowie kanału (istniejący i nowi) będą mogli go edytować. Nie dotyczy to członków, którzy są gośćmi.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Kiedy połączysz kanał z tablicą, wszyscy członkowie kanału (istniejący i nowi) będą mogli go edytować. Nie dotyczy to członków, którzy są gośćmi.{lineBreak}Ta tablica jest obecnie połączona z innym kanałem. Zostanie ona usunięta, jeśli zdecydujesz się połączyć ją tutaj.", - "shareBoard.confirm-unlink.body": "Kiedy odłączysz kanał od tablicy, wszyscy członkowie kanału (istniejący i nowi) stracą do niego dostęp, chyba że otrzymają osobne pozwolenie.", - "shareBoard.confirm-unlink.confirmBtnText": "Tak, odłącz", - "shareBoard.confirm-unlink.title": "Odłączenie kanału od tablicy", - "shareBoard.lastAdmin": "Tablice muszą mieć co najmniej jednego Administratora", - "shareBoard.members-select-group": "Członkowie", - "shareBoard.unknown-channel-display-name": "Nieznany kanał", - "tutorial_tip.finish_tour": "Gotowe", - "tutorial_tip.got_it": "Jasne", - "tutorial_tip.ok": "Dalej", - "tutorial_tip.out": "Zrezygnuj z tych porad.", - "tutorial_tip.seen": "Widziałeś to wcześniej?" -} diff --git a/webapp/boards/i18n/pt.json b/webapp/boards/i18n/pt.json deleted file mode 100644 index 654733b2f1..0000000000 --- a/webapp/boards/i18n/pt.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrador", - "AppBar.Tooltip": "Alternar quadros vinculados", - "Attachment.Attachment-title": "Anexo", - "AttachmentBlock.DeleteAction": "Apagar", - "AttachmentBlock.addElement": "Adicionar {type}", - "AttachmentBlock.delete": "Anexo apagado.", - "AttachmentBlock.failed": "Este arquivo não pôde ser carregado pois ultrapassou o tamanho limite.", - "AttachmentBlock.upload": "Carregando anexo.", - "AttachmentBlock.uploadSuccess": "Anexo carregado.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Apagar", - "AttachmentElement.download": "Baixar", - "BoardComponent.add-a-group": "+ Adicionar um grupo", - "BoardComponent.delete": "Apagar", - "BoardComponent.hidden-columns": "Colunas escondidas", - "BoardComponent.hide": "Esconder", - "BoardComponent.new": "+ Novo", - "BoardComponent.no-property": "Não {property}", - "BoardComponent.show": "Mostrar", - "BoardMember.schemeAdmin": "Admin", - "BoardMember.schemeCommenter": "Comentador", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "Nenhum", - "BoardMember.unlinkChannel": "Desvincular", - "BoardPage.newVersion": "Está disponível uma nova versão do Boards, clique aqui para recarregar.", - "BoardPage.syncFailed": "O Board pode ter sido apagado ou o acesso revogado.", - "BoardTemplateSelector.add-template": "Criar novo modelo", - "BoardTemplateSelector.create-empty-board": "Criar um board vazio", - "BoardTemplateSelector.delete-template": "Apagar", - "BoardTemplateSelector.edit-template": "Editar", - "BoardTemplateSelector.plugin.no-content-title": "Criar um board", - "BoardTemplateSelector.title": "Criar um board", - "BoardTemplateSelector.use-this-template": "Usar este modelo", - "BoardsSwitcher.Title": "Encontrar boards", - "Calculations.Options.average.displayName": "Média", - "Calculations.Options.average.label": "Média", - "Calculations.Options.countUniqueValue.displayName": "Único", - "Calculations.Options.countValue.displayName": "Valores", - "Calculations.Options.dateRange.displayName": "Intervalo", - "Calculations.Options.dateRange.label": "Intervalo", - "Calculations.Options.latest.label": "Mais recente", - "Calculations.Options.max.displayName": "Máx", - "Calculations.Options.max.label": "Máximo", - "Calculations.Options.min.displayName": "Mínimo", - "Calculations.Options.min.label": "Mínimo", - "Calculations.Options.none.label": "Nenhum", - "Calculations.Options.range.displayName": "Intervalo", - "Calculations.Options.range.label": "Intervalo", - "Calculations.Options.sum.displayName": "Soma", - "Calculations.Options.sum.label": "Soma", - "CalendarCard.untitled": "Sem título", - "CardActionsMenu.copiedLink": "Copiado!", - "CardActionsMenu.copyLink": "Copiar link", - "CardActionsMenu.delete": "Apagar", - "CardActionsMenu.duplicate": "Duplicar", - "CardBadges.title-comments": "Comentários", - "CardDetail.Attach": "Anexar", - "CardDetail.Follow": "Seguir", - "CardDetail.Following": "Seguindo", - "CardDetail.add-content": "Adicionar conteúdo", - "CardDetail.add-icon": "Adicionar ícone", - "CardDetail.add-property": "+ Adicionar uma propriedade", - "CardDetail.limited-button": "Atualizar", - "CardDetail.new-comment-placeholder": "Adicionar um comentário...", - "CardDetailProperty.delete-action-button": "Apagar", - "CardDetailProperty.property-change-action-button": "Alterar propriedade", - "CardDetailProperty.property-changed": "Propriedade alterada com sucesso!", - "CardDialog.delete-confirmation-dialog-button-text": "Apagar", - "Categories.CreateCategoryDialog.CancelText": "Cancelar", - "Categories.CreateCategoryDialog.CreateText": "Criar", - "Categories.CreateCategoryDialog.UpdateText": "Atualizar", - "CenterPanel.Login": "Entrar", - "CenterPanel.Share": "Compartilhar", - "ChannelIntro.CreateBoard": "Criar um quadro", - "Comment.delete": "Apagar", - "CommentsList.send": "Enviar", - "ConfirmPerson.empty": "Vazio", - "ConfirmPerson.search": "Procurar...", - "ConfirmationDialog.cancel-action": "Cancelar", - "ConfirmationDialog.confirm-action": "Confirmar", - "ContentBlock.Delete": "Apagar", - "ContentBlock.DeleteAction": "apagar", - "ContentBlock.addElement": "adicionar {type}", - "ContentBlock.editText": "Editar texto...", - "ContentBlock.image": "imagem", - "ContentBlock.moveDown": "Mover pra baixo", - "ContentBlock.moveUp": "Mover pra cima", - "ContentBlock.text": "texto", - "DateFilter.empty": "Vazio", - "DateRange.clear": "Limpar", - "DateRange.empty": "Vazio", - "DateRange.today": "Hoje", - "DeleteBoardDialog.confirm-cancel": "Cancelar", - "DeleteBoardDialog.confirm-delete": "Apagar", - "EditableDayPicker.today": "Hoje", - "shareBoard.members-select-group": "Membros", - "shareBoard.unknown-channel-display-name": "Canal desconhecido", - "tutorial_tip.ok": "Próximo" -} diff --git a/webapp/boards/i18n/pt_BR.json b/webapp/boards/i18n/pt_BR.json deleted file mode 100644 index 8b52e1675f..0000000000 --- a/webapp/boards/i18n/pt_BR.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administrador", - "AdminBadge.TeamAdmin": "Administrador de equipe", - "AppBar.Tooltip": "Ativar Boards Vinculados", - "Attachment.Attachment-title": "Anexo", - "AttachmentBlock.DeleteAction": "excluir", - "AttachmentBlock.addElement": "adicionar {type}", - "AttachmentBlock.delete": "Anexo apagado.", - "AttachmentBlock.failed": "Este arquivo não pôde ser carregado pois ultrapassou o tamanho limite.", - "AttachmentBlock.upload": "Carregando anexo.", - "AttachmentBlock.uploadSuccess": "Anexo enviado.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Excluir", - "AttachmentElement.download": "Baixar", - "AttachmentElement.upload-percentage": "Enviando...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Adicione um grupo", - "BoardComponent.delete": "Excluir", - "BoardComponent.hidden-columns": "Colunas ocultas", - "BoardComponent.hide": "Ocultar", - "BoardComponent.new": "Novo", - "BoardComponent.no-property": "Sem {property}", - "BoardComponent.no-property-title": "Itens com um valor {property} vazio aparecerão aqui. Esta coluna não pode ser excluída.", - "BoardComponent.show": "Exibir", - "BoardMember.schemeAdmin": "Administrador", - "BoardMember.schemeCommenter": "Comentarista", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "Nenhum", - "BoardMember.schemeViewer": "Visualizador", - "BoardMember.unlinkChannel": "Desvincular", - "BoardPage.newVersion": "Uma nova versão do Boards está disponível, clique aqui para recarregar.", - "BoardPage.syncFailed": "O Board pode ter sido excluído ou o acesso revogado.", - "BoardTemplateSelector.add-template": "Criar novo modelo", - "BoardTemplateSelector.create-empty-board": "Criar um board vazio", - "BoardTemplateSelector.delete-template": "Excluir", - "BoardTemplateSelector.description": "Adicione um quadro à barra lateral usando qualquer um dos modelos definidos abaixo ou comece do zero.", - "BoardTemplateSelector.edit-template": "Editar", - "BoardTemplateSelector.plugin.no-content-description": "Adicione um board à barra lateral usando um dos templates disponíveis abaixo ou comece do zero.", - "BoardTemplateSelector.plugin.no-content-title": "Criar um board", - "BoardTemplateSelector.title": "Criar um board", - "BoardTemplateSelector.use-this-template": "Use este template", - "BoardsSwitcher.Title": "Encontrar boards", - "BoardsUnfurl.Limited": "Detalhes adicionais estão ocultos devido ao cartão ter sido arquivado", - "BoardsUnfurl.Remainder": "+{remainder} mais", - "BoardsUnfurl.Updated": "Atualizado {time}", - "Calculations.Options.average.displayName": "Média", - "Calculations.Options.average.label": "Média", - "Calculations.Options.count.displayName": "Total", - "Calculations.Options.count.label": "Total", - "Calculations.Options.countChecked.displayName": "Confirmado", - "Calculations.Options.countChecked.label": "Total de itens confirmados", - "Calculations.Options.countUnchecked.displayName": "Não confirmado", - "Calculations.Options.countUnchecked.label": "Total de itens não confirmados", - "Calculations.Options.countUniqueValue.displayName": "Único", - "Calculations.Options.countUniqueValue.label": "Total de valores únicos", - "Calculations.Options.countValue.displayName": "Valores", - "Calculations.Options.countValue.label": "Valor total", - "Calculations.Options.dateRange.displayName": "Período", - "Calculations.Options.dateRange.label": "Alcance", - "Calculations.Options.earliest.displayName": "Mais antigo", - "Calculations.Options.earliest.label": "Mais antigo", - "Calculations.Options.latest.displayName": "Mais recente", - "Calculations.Options.latest.label": "Mais recente", - "Calculations.Options.max.displayName": "Máx", - "Calculations.Options.max.label": "Máx", - "Calculations.Options.median.displayName": "Mediana", - "Calculations.Options.median.label": "Mediana", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Calcular", - "Calculations.Options.none.label": "Nenhum", - "Calculations.Options.percentChecked.displayName": "Verificado", - "Calculations.Options.percentChecked.label": "Porcentagem verificada", - "Calculations.Options.percentUnchecked.displayName": "Não verificado", - "Calculations.Options.percentUnchecked.label": "Porcentagem não verificada", - "Calculations.Options.range.displayName": "Alcance", - "Calculations.Options.range.label": "Período", - "Calculations.Options.sum.displayName": "Soma", - "Calculations.Options.sum.label": "Soma", - "CalendarCard.untitled": "Sem título", - "CardActionsMenu.copiedLink": "Copiado!", - "CardActionsMenu.copyLink": "Copiar link", - "CardActionsMenu.delete": "Excluir", - "CardActionsMenu.duplicate": "Duplicar", - "CardBadges.title-checkboxes": "Caixa de seleção", - "CardBadges.title-comments": "Comentários", - "CardBadges.title-description": "Este cartão tem uma descrição", - "CardDetail.Attach": "Anexar", - "CardDetail.Follow": "Seguir", - "CardDetail.Following": "Seguindo", - "CardDetail.add-content": "Adicionar conteúdo", - "CardDetail.add-icon": "Adicionar ícone", - "CardDetail.add-property": "+ Adicionar propriedade", - "CardDetail.addCardText": "adicionar texto ao card", - "CardDetail.limited-body": "Atualize para nosso plano Professional ou Enterprise.", - "CardDetail.limited-button": "Upgrade", - "CardDetail.limited-title": "Este cartão está oculto", - "CardDetail.moveContent": "Mover conteúdo do cartão", - "CardDetail.new-comment-placeholder": "Adicionar um comentário...", - "CardDetailProperty.confirm-delete-heading": "Confirmar exclusão da propriedade", - "CardDetailProperty.confirm-delete-subtext": "Tem certeza que quer excluir a propriedade \"{propertyName}\"? Deletando-a excluirá a propriedade de todos os cards nessa board.", - "CardDetailProperty.confirm-property-name-change-subtext": "Tem certeza que deseja alterar propriedade \"{propertyName}\" {customText}? Isto afetará valor(es) em {numOfCards} cartão(ões) neste quadro, podendo resultar em perda de dados.", - "CardDetailProperty.confirm-property-type-change": "Confirmar alteração de tipo de propriedade", - "CardDetailProperty.delete-action-button": "Excluir", - "CardDetailProperty.property-change-action-button": "Alterar propriedade", - "CardDetailProperty.property-changed": "Propriedade alterada com sucesso!", - "CardDetailProperty.property-deleted": "{propertyName} excluído com êxito!", - "CardDetailProperty.property-name-change-subtext": "digite de \"{oldPropType}\" para \"{newPropType}\"", - "CardDetial.limited-link": "Saiba mais sobre nossos planos.", - "CardDialog.delete-confirmation-dialog-attachment": "Confirmar exclusão de anexo", - "CardDialog.delete-confirmation-dialog-button-text": "Excluir", - "CardDialog.delete-confirmation-dialog-heading": "Confirmar exclusão do cartão", - "CardDialog.editing-template": "Você está editando um template.", - "CardDialog.nocard": "Esse cartão não existe ou não está acessível.", - "Categories.CreateCategoryDialog.CancelText": "Cancelar", - "Categories.CreateCategoryDialog.CreateText": "Criar", - "Categories.CreateCategoryDialog.Placeholder": "Nomeie sua categoria", - "Categories.CreateCategoryDialog.UpdateText": "Atualizar", - "CenterPanel.Login": "Login", - "CenterPanel.Share": "Compartilhar", - "ChannelIntro.CreateBoard": "Criar um board", - "ColorOption.selectColor": "Selecione {color} Cor", - "Comment.delete": "Excluir", - "CommentsList.send": "Enviar", - "ConfirmPerson.empty": "Vazio", - "ConfirmPerson.search": "Buscar...", - "ConfirmationDialog.cancel-action": "Cancelar", - "ConfirmationDialog.confirm-action": "Confirmar", - "ContentBlock.Delete": "Excluir", - "ContentBlock.DeleteAction": "Excluir", - "ContentBlock.addElement": "adicionar {type}", - "ContentBlock.checkbox": "caixa de seleção", - "ContentBlock.divider": "Divisor", - "ContentBlock.editCardCheckbox": "Caixa de seleção marcada", - "ContentBlock.editCardCheckboxText": "editar texto do cartão", - "ContentBlock.editCardText": "editar texto do cartão", - "ContentBlock.editText": "Editar texto...", - "ContentBlock.image": "imagem", - "ContentBlock.insertAbove": "Inserir acima", - "ContentBlock.moveBlock": "mover conteúdo do cartão", - "ContentBlock.moveDown": "Mover para baixo", - "ContentBlock.moveUp": "Mover para cima", - "ContentBlock.text": "texto", - "DateFilter.empty": "Vazio", - "DateRange.clear": "Limpar", - "DateRange.empty": "Vazio", - "DateRange.endDate": "data de término", - "DateRange.today": "Hoje", - "DeleteBoardDialog.confirm-cancel": "Cancelar", - "DeleteBoardDialog.confirm-delete": "Excluir", - "DeleteBoardDialog.confirm-info": "Tem certeza que quer excluir o quadro \"{boardTitle}\"? Excluí-lo irá apagar todos os cartões no quadro.", - "DeleteBoardDialog.confirm-info-template": "Tem certeza que deseja excluir o board template “{boardTitle}”?", - "DeleteBoardDialog.confirm-tite": "Confirmar exclusão do board", - "DeleteBoardDialog.confirm-tite-template": "Confirmar exclusão do template de board", - "Dialog.closeDialog": "Fechar diálogo", - "EditableDayPicker.today": "Hoje", - "Error.mobileweb": "Suporte Web móvel está em estágio beta. Algumas funcionalidades podem estar presentes.", - "Error.websocket-closed": "Conexão Websocket fechada, conexão interrompida. Se persistir, verifique a configuração do seu servidor ou proxy de web.", - "Filter.contains": "contém", - "Filter.ends-with": "termina com", - "Filter.includes": "Inclui", - "Filter.is": "é", - "Filter.is-after": "está depois", - "Filter.is-before": "está antes", - "Filter.is-empty": "está vazio", - "Filter.is-not-empty": "Não está vazio", - "Filter.is-not-set": "não está definido", - "Filter.is-set": "está definido", - "Filter.isafter": "está depois", - "Filter.isbefore": "está antes", - "Filter.not-contains": "não contém", - "Filter.not-ends-with": "não termina com", - "Filter.not-includes": "Não inclui", - "Filter.not-starts-with": "não começa com", - "Filter.starts-with": "começa com", - "FilterByText.placeholder": "filtrar texto", - "FilterComponent.add-filter": "+ Adicionar filtro", - "FilterComponent.delete": "Excluir", - "FilterValue.empty": "(vazio)", - "FindBoardsDialog.IntroText": "Procurar por quadros", - "FindBoardsDialog.NoResultsFor": "Sem resultado para \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Verifique a digitação ou tente outra busca.", - "FindBoardsDialog.SubTitle": "Digite para localizar um board. Use CIMA/BAIXO para explorar ENTER para selecionar, ESC para dispensar", - "FindBoardsDialog.Title": "Encontrar quadros", - "GroupBy.hideEmptyGroups": "Ocultar {count} grupos vazios", - "GroupBy.showHiddenGroups": "Mostrar {count} grupos ocultos", - "GroupBy.ungroup": "Desagrupar", - "HideBoard.MenuOption": "Ocultar quadro", - "KanbanCard.untitled": "Sem nome", - "MentionSuggestion.is-not-board-member": "(não membro do board)", - "Mutator.new-board-from-template": "novo board do template", - "Mutator.new-card-from-template": "novo cartão à partir de um template", - "Mutator.new-template-from-card": "novo template à partir de um cartão", - "OnboardingTour.AddComments.Body": "Você pode comentar questões, e até mesmo @mencionar seus usuários companheiros de Mattermost para conseguir suas atenções.", - "OnboardingTour.AddComments.Title": "Adicionar comentários", - "OnboardingTour.AddDescription.Body": "Adicione uma descrição para que seus companheiros de time saibam sobre o que é o cartão.", - "OnboardingTour.AddDescription.Title": "Adicionar descrição", - "OnboardingTour.AddProperties.Body": "Adicione várias propriedades aos cartões para torná-los mais poderosos.", - "OnboardingTour.AddProperties.Title": "Adicionar propriedades", - "OnboardingTour.AddView.Body": "Crie uma nova view aquei para organizar seu board usando diferentes layouts.", - "OnboardingTour.AddView.Title": "Adicionar nova visualização", - "OnboardingTour.CopyLink.Body": "Você pode compartilhar seus cartões com companheiros de times copiando e colando o link em um canal, mensagem direta, ou mensagem de grupo.", - "OnboardingTour.CopyLink.Title": "Copiar link", - "OnboardingTour.OpenACard.Body": "Abra um cartão para explorar formas poderosas que os Boards podem ajudar a organizar seu trabalho.", - "OnboardingTour.OpenACard.Title": "Abrir um cartão", - "OnboardingTour.ShareBoard.Body": "Você pode compartilhar seu board internament, com seu time, ou public para permitir visibilidade fora da sua organização.", - "OnboardingTour.ShareBoard.Title": "Compartilhar quadro", - "PersonProperty.board-members": "Membros do Board", - "PersonProperty.me": "Eu", - "PersonProperty.non-board-members": "Não membros do board", - "PropertyMenu.Delete": "Excluir", - "PropertyMenu.changeType": "Alterar tipo da propriedade", - "PropertyMenu.selectType": "Selecione o tipo de propriedade", - "PropertyMenu.typeTitle": "Tipo", - "PropertyType.Checkbox": "Caixa de seleção", - "PropertyType.CreatedBy": "Criado por", - "PropertyType.CreatedTime": "Horário de criação", - "PropertyType.Date": "Data", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Múltiplas pessoas", - "PropertyType.MultiSelect": "Seleção múltipla", - "PropertyType.Number": "Número", - "PropertyType.Person": "Pessoa", - "PropertyType.Phone": "Telefone", - "PropertyType.Select": "Selcionar", - "PropertyType.Text": "Texto", - "PropertyType.Unknown": "Desconhecido", - "PropertyType.UpdatedBy": "Atualizado pela última vez por", - "PropertyType.UpdatedTime": "Atualizado pela última vez em", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Vazio", - "RegistrationLink.confirmRegenerateToken": "Isso vai invalidar os links compartilhados anteriormente. Continuar?", - "RegistrationLink.copiedLink": "Copiado!", - "RegistrationLink.copyLink": "Copiar link", - "RegistrationLink.description": "Compartilhe esse link para que outras pessoas criarem contas:", - "RegistrationLink.regenerateToken": "Gerar o token novamente", - "RegistrationLink.tokenRegenerated": "Link para registro gerado novamente", - "ShareBoard.PublishDescription": "Publique e compartilhe um link de somente leitura com todos na web.", - "ShareBoard.PublishTitle": "Publicar para a web", - "ShareBoard.ShareInternal": "Compartilhar internamente", - "ShareBoard.ShareInternalDescription": "Usuários que terão permissão para utilizar este link.", - "ShareBoard.Title": "Compartilhar Quadro", - "ShareBoard.confirmRegenerateToken": "Isso vai invalidar links compartilhados anteriormente. Continuar?", - "ShareBoard.copiedLink": "Copiado!", - "ShareBoard.copyLink": "Copiar link", - "ShareBoard.regenerate": "Gerar token novamente", - "ShareBoard.searchPlaceholder": "Procurar por pessoas e canais", - "ShareBoard.teamPermissionsText": "Todos no time {teamName}", - "ShareBoard.tokenRegenrated": "Token gerado novamente", - "ShareBoard.userPermissionsRemoveMemberText": "Remover membro", - "ShareBoard.userPermissionsYouText": "(Você)", - "ShareTemplate.Title": "Compartilhar template", - "ShareTemplate.searchPlaceholder": "Busca por pessoas", - "Sidebar.about": "Sobre o Focalboard", - "Sidebar.add-board": "+ Adicionar quadro", - "Sidebar.changePassword": "Mudar senha", - "Sidebar.delete-board": "Excluir quadro", - "Sidebar.duplicate-board": "Duplicar quadro", - "Sidebar.export-archive": "Exportar arquivo", - "Sidebar.import": "Importar", - "Sidebar.import-archive": "Importar arquivo", - "Sidebar.invite-users": "Convidar usuários", - "Sidebar.logout": "Sair", - "Sidebar.new-category.badge": "Novo", - "Sidebar.new-category.drag-boards-cta": "Solte quadros aqui...", - "Sidebar.no-boards-in-category": "Nenhum board", - "Sidebar.product-tour": "Tour pelo produto", - "Sidebar.random-icons": "Ícones aleatórios", - "Sidebar.set-language": "Definir linguagem", - "Sidebar.set-theme": "Definir tema", - "Sidebar.settings": "Configurações", - "Sidebar.template-from-board": "Novo template vindo do board", - "Sidebar.untitled-board": "(Quadro sem nome)", - "Sidebar.untitled-view": "(View sem título)", - "SidebarCategories.BlocksMenu.Move": "Mover Para...", - "SidebarCategories.CategoryMenu.CreateNew": "Criar Nova Categoria", - "SidebarCategories.CategoryMenu.Delete": "Excluir Categoria", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards em {categoryName} serão movidos de volta para categoria de Boards. Você não será removido de nenhum board.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Excluir esta categoria?", - "SidebarCategories.CategoryMenu.Update": "Renomear Categoria", - "SidebarTour.ManageCategories.Body": "Criar e gerenciar categorias personalizadas. Categorias são específicas para usuário, então mover um board para sua categoria não impactará outros membros usando o mesmo board.", - "SidebarTour.ManageCategories.Title": "Gerenciar categorias", - "SidebarTour.SearchForBoards.Body": "Abrir o alternador de board (Cmd/Ctrl + K) para buscar rapidamente e adicionar boards a sua barra lateral.", - "SidebarTour.SearchForBoards.Title": "Buscar por boards", - "SidebarTour.SidebarCategories.Body": "Todos seus boards agora são organizados sob sua nova barra lateral. Não é mais necessárioa alternar entre espaços de trabalho. Categorias personalizadas em suas estações prévias de trabalho foram automaticamente criadas para você como parte do seu upgrade para v7.2. Estas podem ser removidas ou editadas de acordo com a sua preferência.", - "SidebarTour.SidebarCategories.Link": "Saiba mais", - "SidebarTour.SidebarCategories.Title": "Categorias de barra lateral", - "SiteStats.total_boards": "Total de boards", - "SiteStats.total_cards": "Total de cartões", - "TableComponent.add-icon": "Adicionar Ícone", - "TableComponent.name": "Nome", - "TableComponent.plus-new": "+ Novo", - "TableHeaderMenu.delete": "Excluir", - "TableHeaderMenu.duplicate": "Duplicar", - "TableHeaderMenu.hide": "Esconder", - "TableHeaderMenu.insert-left": "Inserir à esquerda", - "TableHeaderMenu.insert-right": "Inserir à direita", - "TableHeaderMenu.sort-ascending": "Ordem ascendente", - "TableHeaderMenu.sort-descending": "Ordem descendente", - "TableRow.DuplicateCard": "duplicar cartão", - "TableRow.MoreOption": "Mais ações", - "TableRow.open": "Abrir", - "TopBar.give-feedback": "Dar feedback", - "URLProperty.copiedLink": "Copiado!", - "URLProperty.copy": "Copiar", - "URLProperty.edit": "Editar", - "UndoRedoHotKeys.canRedo": "Refazer", - "UndoRedoHotKeys.canRedo-with-description": "Refazer {description}", - "UndoRedoHotKeys.canUndo": "Desfazer", - "UndoRedoHotKeys.canUndo-with-description": "Desfazer {description}", - "UndoRedoHotKeys.cannotRedo": "Nada para Refazer", - "UndoRedoHotKeys.cannotUndo": "Nada para Desfazer", - "ValueSelector.noOptions": "Sem opções. Comece adicionando a primeira!", - "ValueSelector.valueSelector": "Selecionador de valor", - "ValueSelectorLabel.openMenu": "Abrir menu", - "VersionMessage.help": "Verifique o que é novo nesta versão.", - "VersionMessage.learn-more": "Saiba mais", - "View.AddView": "Adicionar visualização", - "View.Board": "Quadro", - "View.DeleteView": "Excluir visualização", - "View.DuplicateView": "Duplicar visualização", - "View.Gallery": "Galeria", - "View.NewBoardTitle": "Visualização de Quadro", - "View.NewCalendarTitle": "Visualização de calendário", - "View.NewGalleryTitle": "Visualização de Galeria", - "View.NewTableTitle": "Visualização de Tabela", - "View.NewTemplateDefaultTitle": "Template sem título", - "View.NewTemplateTitle": "Sem título", - "View.Table": "Tabela", - "ViewHeader.add-template": "+ Novo modelo", - "ViewHeader.delete-template": "Excluir", - "ViewHeader.display-by": "Exibir por: {property}", - "ViewHeader.edit-template": "Editar", - "ViewHeader.empty-card": "Cartão vazio", - "ViewHeader.export-board-archive": "Exportar arquivo do painel", - "ViewHeader.export-complete": "Exportação completa!", - "ViewHeader.export-csv": "Exportar para CSV", - "ViewHeader.export-failed": "Falha ao exportar!", - "ViewHeader.filter": "Filtrar", - "ViewHeader.group-by": "Agrupar por: {property}", - "ViewHeader.new": "Novo", - "ViewHeader.properties": "Propriedades", - "ViewHeader.properties-menu": "Menu de propriedades", - "ViewHeader.search-text": "Pesquisar cartões", - "ViewHeader.select-a-template": "Selecionar um modelo", - "ViewHeader.set-default-template": "Definir como padrão", - "ViewHeader.sort": "Ordenar", - "ViewHeader.untitled": "Sem nome", - "ViewHeader.view-header-menu": "Visualizar menu de cabeçalho", - "ViewHeader.view-menu": "Visualizar menu", - "ViewLimitDialog.Heading": "Limite de views por board alcaçado", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Upgrade", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notificar Admin", - "ViewLimitDialog.Subtext.Admin": "Atualize para nosso plano Profissional ou Enterprise.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Saiba mais sobre nossos planos.", - "ViewLimitDialog.Subtext.RegularUser": "Notifique seu administrador para atualizar para nosso plano Professional ou Enterprise.", - "ViewLimitDialog.UpgradeImg.AltText": "Atualizar imagem", - "ViewLimitDialog.notifyAdmin.Success": "Seu administrador foi notificado", - "ViewTitle.hide-description": "esconder descrição", - "ViewTitle.pick-icon": "Escolher ícone", - "ViewTitle.random-icon": "Aleatório", - "ViewTitle.remove-icon": "Remover ícone", - "ViewTitle.show-description": "mostrar descrição", - "ViewTitle.untitled-board": "Quadro sem título", - "WelcomePage.Description": "Boards é uma ferramenta de gerenciamento de projeto que ajuda a definir, organizar, rastrear, e gerenciar traabalho de vários times utilizando uma estrturtura familiar de quadro de Kanban.", - "WelcomePage.Explore.Button": "Faça um tour", - "WelcomePage.Heading": "Bem vindo ao Boards", - "WelcomePage.NoThanks.Text": "Não obrigado, eu descubrirei sozinho", - "WelcomePage.StartUsingIt.Text": "Começar a usar", - "Workspace.editing-board-template": "Você está editando um modelo de quadro.", - "badge.guest": "Convidado", - "boardPage.confirm-join-button": "Ingressar", - "boardPage.confirm-join-text": "Você está prestes a ingressar em um board privado sem ter sido explicitamente adicionado pelo administrador do quadro. Você tem certeza de que deseja ingressar neste board privado?", - "boardPage.confirm-join-title": "Ingressar board privado", - "boardSelector.confirm-link-board": "Linkar board para canal", - "boardSelector.confirm-link-board-button": "Sim, linkar board", - "boardSelector.confirm-link-board-subtext": "Quando você vincula \"{boardName}\" a um canal, todos os membros daquele canal (existentes e novos) poderão editá-lo. Isto excluirá os membros que são convidados. Você pode desvincular um board de um canal a qualquer hora.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Quando você vincula \"{boardName}\" a um canal, todos membros do canal (existentes e novas) poderão editar. Isto excluirá os membros que forem convidados. {lineBreak} Este board está atualmente vinculado a outro canal. Será desvinculado se você optar por vincular aqui.", - "boardSelector.create-a-board": "Criar um board", - "boardSelector.link": "Link", - "boardSelector.search-for-boards": "Procurar por boards", - "boardSelector.title": "Vincular boards", - "boardSelector.unlink": "Desvincular", - "calendar.month": "Mês", - "calendar.today": "HOJE", - "calendar.week": "Semana", - "centerPanel.undefined": "Sem {propertyName}", - "centerPanel.unknown-user": "Usuário desconhecido", - "cloudMessage.learn-more": "Saiba mais", - "createImageBlock.failed": "Não foi possível enviar o arquivo, pois o tamanho ultrapassou o limite permitido.", - "default-properties.badges": "Comentários e descrição", - "default-properties.title": "Título", - "error.back-to-home": "Volta para o início", - "error.back-to-team": "Volta para o time", - "error.board-not-found": "Quadro não encontrado.", - "error.go-login": "Log in", - "error.invalid-read-only-board": "Você não possui acesso a este board. Faça log in para acessar Boards.", - "error.not-logged-in": "Sua sessão pode ter expirado ou você não está logado. Faça Log in para obter acesso ao Boards.", - "error.page.title": "Desculpe, algo deu errado", - "error.team-undefined": "Não é um time válido.", - "error.unknown": "Um erro ocorreu.", - "generic.previous": "Anterior", - "guest-no-board.subtitle": "Você não tem acesso a nenhum board neste time ainda, por favor aguarde até alguém adicionar você a algum board.", - "guest-no-board.title": "Nenhum board ainda", - "imagePaste.upload-failed": "Alguns arquivos não foram enviados, pois o tamanho ultrapassou o limite permitido.", - "limitedCard.title": "Cartões ocultos", - "login.log-in-button": "Entrar", - "login.log-in-title": "Entrar", - "login.register-button": "ou criar uma conta se você ainda não tiver uma", - "new_channel_modal.create_board.empty_board_description": "Criar um novo quadro vazio", - "new_channel_modal.create_board.empty_board_title": "Quadro vazio", - "new_channel_modal.create_board.select_template_placeholder": "Selecionar um modelo", - "new_channel_modal.create_board.title": "Criar um quadro para este canal", - "notification-box-card-limit-reached.close-tooltip": "Soneca por 10 dias", - "notification-box-card-limit-reached.contact-link": "notificar seu admin", - "notification-box-card-limit-reached.link": "Atualizar para um plano pago", - "notification-box-card-limit-reached.title": "{cards} cartões ocultos do board", - "notification-box-cards-hidden.title": "Esta ação ocultou outro cartão", - "notification-box.card-limit-reached.not-admin.text": "Para acessar cartões arquivados, você pode {contactLink} para atualizar para um plano pago.", - "notification-box.card-limit-reached.text": "Limite de cartão alcançado, para visualizar cartões mais antigos, {link}", - "person.add-user-to-board": "Adicionar {username} ao board", - "person.add-user-to-board-confirm-button": "Adicionar ao board", - "person.add-user-to-board-permissions": "Permissões", - "person.add-user-to-board-question": "Você quer adicionar {username} ao board?", - "person.add-user-to-board-warning": "{username} não é um membro do quadro e não receberá nenhuma notificação sobre ele.", - "register.login-button": "ou entre se você já tem uma conta", - "register.signup-title": "Registrar uma conta", - "rhs-board-non-admin-msg": "Você não é um adminstrador do quadro", - "rhs-boards.add": "Adicionar", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "esta mensagem direta", - "rhs-boards.header.gm": "esta mensagem de grupo", - "rhs-boards.last-update-at": "Última atualização em: {datetime}", - "rhs-boards.link-boards-to-channel": "Vincular boards para {channelName}", - "rhs-boards.linked-boards": "Boards vinculados", - "rhs-boards.no-boards-linked-to-channel": "Nenhum board está vinculado a {channelName} ainda", - "rhs-boards.no-boards-linked-to-channel-description": "Boards é uma ferramenta de gerenciamento de projeto que ajuda a definir, organizar, rastrear e gerenciar o trabalho entre times, usando uma visualização de quadro estilo Kaban familiar.", - "rhs-boards.unlink-board": "Desvincular board", - "rhs-boards.unlink-board1": "Desvincular board", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Publicar", - "share-board.share": "Compartilhar", - "shareBoard.channels-select-group": "Canais", - "shareBoard.confirm-change-team-role.body": "Todos neste quadro com permissão mais baixa que papel \"{role}\" serão serão promovidos para {role}. Tem certeza que deseja alterar o papel mínimo para esse board?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Alterar papel mínimo do board", - "shareBoard.confirm-change-team-role.title": "Alterar papel mínimo do board", - "shareBoard.confirm-link-channel": "Vincular ao canal", - "shareBoard.confirm-link-channel-button": "Vincular canal", - "shareBoard.confirm-link-channel-button-with-other-channel": "Desvincular e vincular aqui", - "shareBoard.confirm-link-channel-subtext": "Quando você vincula um canal a um quadro, todos os membros do canal (existentes e novos) poderão edita-lo. Isto excluirá os membros que forem convidados.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Quando você vincula um canal a um board, todos os membros de um canal (existente e novos) poderão edita-lo. Isto excluirá os membros que forem convidados {lineBreak}. Este board está vinculado a outro canal. Será desvinculado se você optar por vincula-lo aqui.", - "shareBoard.confirm-unlink.body": "QUando você desvincula um canal de um board, todos os membros do canal (existentes e novos) irão perder acesso ao menos que tenham recebido permissão separadamente.", - "shareBoard.confirm-unlink.confirmBtnText": "Desvincular canal", - "shareBoard.confirm-unlink.title": "Desvincular canal do board", - "shareBoard.lastAdmin": "Boards devem ter pelo menos um Administrador", - "shareBoard.members-select-group": "Membros", - "shareBoard.unknown-channel-display-name": "Canal desconhecido", - "tutorial_tip.finish_tour": "Feito", - "tutorial_tip.got_it": "Entendi", - "tutorial_tip.ok": "Próximo", - "tutorial_tip.out": "Desativar estas dicas.", - "tutorial_tip.seen": "Já viu isto antes?" -} diff --git a/webapp/boards/i18n/ru.json b/webapp/boards/i18n/ru.json deleted file mode 100644 index 382943683c..0000000000 --- a/webapp/boards/i18n/ru.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Администратор", - "AdminBadge.TeamAdmin": "Администратор Команды", - "AppBar.Tooltip": "Переключить связанные доски", - "Attachment.Attachment-title": "Вложение", - "AttachmentBlock.DeleteAction": "Удалить", - "AttachmentBlock.addElement": "добавить {type}", - "AttachmentBlock.delete": "Вложение удалено.", - "AttachmentBlock.failed": "Не удалось загрузить файл, так как превышена квота на размер файла.", - "AttachmentBlock.upload": "Загрузка вложения.", - "AttachmentBlock.uploadSuccess": "Вложение загружено.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Удалить", - "AttachmentElement.download": "Скачать", - "AttachmentElement.upload-percentage": "Загрузка...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Добавить группу", - "BoardComponent.delete": "Удалить", - "BoardComponent.hidden-columns": "Скрытые столбцы", - "BoardComponent.hide": "Скрыть", - "BoardComponent.new": "+ Создать", - "BoardComponent.no-property": "{property} пусто", - "BoardComponent.no-property-title": "Здесь будут элементы с пустым свойством {property}. Этот столбец не может быть удален.", - "BoardComponent.show": "Показать", - "BoardMember.schemeAdmin": "Администратор", - "BoardMember.schemeCommenter": "Комментатор", - "BoardMember.schemeEditor": "Редактор", - "BoardMember.schemeNone": "Никто", - "BoardMember.schemeViewer": "Наблюдатель", - "BoardMember.unlinkChannel": "Отключить", - "BoardPage.newVersion": "Доступна новая версия Доски. Нажмите здесь, чтобы перезагрузить.", - "BoardPage.syncFailed": "Доска может быть удалена или доступ аннулирован.", - "BoardTemplateSelector.add-template": "Новый шаблон", - "BoardTemplateSelector.create-empty-board": "Создать пустую доску", - "BoardTemplateSelector.delete-template": "Удалить", - "BoardTemplateSelector.description": "Добавьте доску на боковую панель, используя любой из шаблонов, описанных ниже, или начните с нуля.", - "BoardTemplateSelector.edit-template": "Изменить", - "BoardTemplateSelector.plugin.no-content-description": "Добавьте доску на боковую панель, используя любой из указанных ниже шаблонов, или начните с нуля.", - "BoardTemplateSelector.plugin.no-content-title": "Создать доску", - "BoardTemplateSelector.title": "Создать доску", - "BoardTemplateSelector.use-this-template": "Использовать этот шаблон", - "BoardsSwitcher.Title": "Найти доски", - "BoardsUnfurl.Limited": "Информация скрыта, потому что карточка находится в архиве", - "BoardsUnfurl.Remainder": "+{remainder} ещё", - "BoardsUnfurl.Updated": "Обновлено {time}", - "Calculations.Options.average.displayName": "Среднее", - "Calculations.Options.average.label": "Среднее", - "Calculations.Options.count.displayName": "Итого", - "Calculations.Options.count.label": "Итого", - "Calculations.Options.countChecked.displayName": "Проверено", - "Calculations.Options.countChecked.label": "Итог проверен", - "Calculations.Options.countUnchecked.displayName": "Не проверен", - "Calculations.Options.countUnchecked.label": "Итог не проверен", - "Calculations.Options.countUniqueValue.displayName": "Уникальный", - "Calculations.Options.countUniqueValue.label": "Итого уникальных значений", - "Calculations.Options.countValue.displayName": "Значения", - "Calculations.Options.countValue.label": "Итоговое значение", - "Calculations.Options.dateRange.displayName": "Диапазон", - "Calculations.Options.dateRange.label": "Диапазон", - "Calculations.Options.earliest.displayName": "Ранний", - "Calculations.Options.earliest.label": "Ранний", - "Calculations.Options.latest.displayName": "Последний", - "Calculations.Options.latest.label": "Последний", - "Calculations.Options.max.displayName": "Максимальный", - "Calculations.Options.max.label": "Максимальный", - "Calculations.Options.median.displayName": "Медиана", - "Calculations.Options.median.label": "Медиана", - "Calculations.Options.min.displayName": "Минимальный", - "Calculations.Options.min.label": "Минимальный", - "Calculations.Options.none.displayName": "Вычислить", - "Calculations.Options.none.label": "Ничто", - "Calculations.Options.percentChecked.displayName": "Проверено", - "Calculations.Options.percentChecked.label": "Процент проверенных", - "Calculations.Options.percentUnchecked.displayName": "Непроверенный", - "Calculations.Options.percentUnchecked.label": "Процент непроверенных", - "Calculations.Options.range.displayName": "Диапазон", - "Calculations.Options.range.label": "Диапазон", - "Calculations.Options.sum.displayName": "Сумма", - "Calculations.Options.sum.label": "Сумма", - "CalendarCard.untitled": "Без названия", - "CardActionsMenu.copiedLink": "Скопировано!", - "CardActionsMenu.copyLink": "Копировать ссылку", - "CardActionsMenu.delete": "Удалить", - "CardActionsMenu.duplicate": "Дублировать", - "CardBadges.title-checkboxes": "Флажки", - "CardBadges.title-comments": "Комментарии", - "CardBadges.title-description": "Эта карточка имеет описание", - "CardDetail.Attach": "Прикреплять", - "CardDetail.Follow": "Отслеживать", - "CardDetail.Following": "Отслеживание", - "CardDetail.add-content": "Добавить контент", - "CardDetail.add-icon": "Добавить иконку", - "CardDetail.add-property": "+ Добавить свойство", - "CardDetail.addCardText": "добавить текст карточки", - "CardDetail.limited-body": "Перейдите на наш тарифный план Professional или Enterprise, чтобы просматривать архивные карточки, иметь неограниченное количество просмотров для каждой доски, неограниченное количество карточек и многое другое.", - "CardDetail.limited-button": "Обновление", - "CardDetail.limited-title": "Эта карточка скрыта", - "CardDetail.moveContent": "переместить содержимое карты", - "CardDetail.new-comment-placeholder": "Добавить комментарий...", - "CardDetailProperty.confirm-delete-heading": "Подтвердить удаление свойства", - "CardDetailProperty.confirm-delete-subtext": "Вы действительно хотите удалить свойство \"{propertyName}\"? При его удалении свойство будет удалено со всех карточек на этой доске.", - "CardDetailProperty.confirm-property-name-change-subtext": "Вы действительно хотите изменить свойство \"{propertyName}\" {customText}? Это повлияет на значение(-я) на {numOfCards} карточке(-ах) на этой доске и может привести к потере данных.", - "CardDetailProperty.confirm-property-type-change": "Подтвердите изменение типа свойства", - "CardDetailProperty.delete-action-button": "Удалить", - "CardDetailProperty.property-change-action-button": "Изменить свойство", - "CardDetailProperty.property-changed": "Свойство изменено успешно!", - "CardDetailProperty.property-deleted": "{propertyName} успешно удалено!", - "CardDetailProperty.property-name-change-subtext": "тип из \"{oldPropType}\" в \"{newPropType}\"", - "CardDetial.limited-link": "Узнайте больше о наших планах.", - "CardDialog.delete-confirmation-dialog-attachment": "Подтвердите удаление вложения", - "CardDialog.delete-confirmation-dialog-button-text": "Удалить", - "CardDialog.delete-confirmation-dialog-heading": "Подтвердите удаление карточки", - "CardDialog.editing-template": "Вы редактируете шаблон.", - "CardDialog.nocard": "Эта карточка не существует или недоступна.", - "Categories.CreateCategoryDialog.CancelText": "Отмена", - "Categories.CreateCategoryDialog.CreateText": "Создать", - "Categories.CreateCategoryDialog.Placeholder": "Назовите свою категорию", - "Categories.CreateCategoryDialog.UpdateText": "Обновить", - "CenterPanel.Login": "Логин", - "CenterPanel.Share": "Поделиться", - "ChannelIntro.CreateBoard": "Создать доску", - "ColorOption.selectColor": "Выберите цвет {color}", - "Comment.delete": "Удалить", - "CommentsList.send": "Отправить", - "ConfirmPerson.empty": "Пусто", - "ConfirmPerson.search": "Поиск...", - "ConfirmationDialog.cancel-action": "Отмена", - "ConfirmationDialog.confirm-action": "Подтвердить", - "ContentBlock.Delete": "Удалить", - "ContentBlock.DeleteAction": "удалить", - "ContentBlock.addElement": "добавить {type}", - "ContentBlock.checkbox": "флажок", - "ContentBlock.divider": "разделитель", - "ContentBlock.editCardCheckbox": "помеченный флажок", - "ContentBlock.editCardCheckboxText": "редактировать текст карточки", - "ContentBlock.editCardText": "редактировать текст карточки", - "ContentBlock.editText": "Изменить текст...", - "ContentBlock.image": "изображение", - "ContentBlock.insertAbove": "Вставить выше", - "ContentBlock.moveBlock": "переместить содержимое карточки", - "ContentBlock.moveDown": "Опустить", - "ContentBlock.moveUp": "Поднять", - "ContentBlock.text": "текст", - "DateFilter.empty": "Пусто", - "DateRange.clear": "Очистить", - "DateRange.empty": "Пусто", - "DateRange.endDate": "Дата окончания", - "DateRange.today": "Сегодня", - "DeleteBoardDialog.confirm-cancel": "Отмена", - "DeleteBoardDialog.confirm-delete": "Удалить", - "DeleteBoardDialog.confirm-info": "Вы уверены, что хотите удалить доску \"{boardTitle}\"? Ее удаление приведет к удалению всех карточек на доске.", - "DeleteBoardDialog.confirm-info-template": "Вы уверены, что хотите удалить шаблон доски “{boardTitle}”?", - "DeleteBoardDialog.confirm-tite": "Подтвердить удаление доски", - "DeleteBoardDialog.confirm-tite-template": "Подтвердите удаление шаблона Доски", - "Dialog.closeDialog": "Закрыть диалог", - "EditableDayPicker.today": "Сегодня", - "Error.mobileweb": "Мобильная веб-поддержка в настоящее время находится на ранней стадии бета-тестирования. Могут присутствовать не все функции.", - "Error.websocket-closed": "Соединение через веб-сокет закрыто, соединение прервано. Если это не устраняется, проверьте конфигурацию сервера или веб-прокси.", - "Filter.contains": "содержит", - "Filter.ends-with": "заканчивается", - "Filter.includes": "содержит", - "Filter.is": "является", - "Filter.is-after": "после", - "Filter.is-before": "раньше", - "Filter.is-empty": "пусто", - "Filter.is-not-empty": "не пусто", - "Filter.is-not-set": "не установлен", - "Filter.is-set": "установлен", - "Filter.isafter": "после", - "Filter.isbefore": "раньше", - "Filter.not-contains": "не содержит", - "Filter.not-ends-with": "не заканчивается", - "Filter.not-includes": "не содержит", - "Filter.not-starts-with": "не начинается с", - "Filter.starts-with": "начинается с", - "FilterByText.placeholder": "фильтровать текст", - "FilterComponent.add-filter": "+ Добавить фильтр", - "FilterComponent.delete": "Удалить", - "FilterValue.empty": "(пусто)", - "FindBoardsDialog.IntroText": "Поиск досок", - "FindBoardsDialog.NoResultsFor": "Нет результатов для \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Проверьте правильность написания или попробуйте другой запрос.", - "FindBoardsDialog.SubTitle": "Введите запрос, чтобы найти доску. Используйте ВВЕРХ/ВНИЗ для просмотра. ENTER для выбора, ESC для закрытия", - "FindBoardsDialog.Title": "Найти доски", - "GroupBy.hideEmptyGroups": "Скрыть {count} пустых групп", - "GroupBy.showHiddenGroups": "Показать {count} скрытых групп", - "GroupBy.ungroup": "Разгруппировать", - "HideBoard.MenuOption": "Скрыть доску", - "KanbanCard.untitled": "Без названия", - "MentionSuggestion.is-not-board-member": "(не член правления)", - "Mutator.new-board-from-template": "новая доска из шаблона", - "Mutator.new-card-from-template": "новая карточка из шаблона", - "Mutator.new-template-from-card": "новый шаблон из карточки", - "OnboardingTour.AddComments.Body": "Вы можете комментировать проблемы и даже @упоминать своих коллег-пользователей Mattermost, чтобы привлечь их внимание.", - "OnboardingTour.AddComments.Title": "Добавить комментарии", - "OnboardingTour.AddDescription.Body": "Добавьте описание к своей карточке, чтобы Ваши коллеги по команде знали, о чем эта карточка.", - "OnboardingTour.AddDescription.Title": "Добавить описание", - "OnboardingTour.AddProperties.Body": "Добавляйте различные свойства карточкам, чтобы сделать их более значительными.", - "OnboardingTour.AddProperties.Title": "Добавить свойства", - "OnboardingTour.AddView.Body": "Перейдите сюда, чтобы создать новый вид для организации доски с использованием различных макетов.", - "OnboardingTour.AddView.Title": "Добавить новый вид", - "OnboardingTour.CopyLink.Body": "Вы можете поделиться своими карточками с коллегами по команде, скопировав ссылку и вставив ее в канал, личное сообщение или групповое сообщение.", - "OnboardingTour.CopyLink.Title": "Копировать ссылку", - "OnboardingTour.OpenACard.Body": "Откройте карточку, чтобы изучить мощные способы, с помощью которых Доски могут помочь Вам организовать Вашу работу.", - "OnboardingTour.OpenACard.Title": "Открыть карточку", - "OnboardingTour.ShareBoard.Body": "Вы можете поделиться своей доской внутри своей команды или опубликовать ее для общего доступа за пределами Вашей организации.", - "OnboardingTour.ShareBoard.Title": "Поделиться доской", - "PersonProperty.board-members": "Совет директоров", - "PersonProperty.me": "Я", - "PersonProperty.non-board-members": "Не члены правления", - "PropertyMenu.Delete": "Удалить", - "PropertyMenu.changeType": "Изменить тип свойства", - "PropertyMenu.selectType": "Выберите тип свойства", - "PropertyMenu.typeTitle": "Тип", - "PropertyType.Checkbox": "Флажок", - "PropertyType.CreatedBy": "Создано пользователем", - "PropertyType.CreatedTime": "Время создания", - "PropertyType.Date": "Дата", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Несколько человек", - "PropertyType.MultiSelect": "Многократный выбор", - "PropertyType.Number": "Номер", - "PropertyType.Person": "Персона", - "PropertyType.Phone": "Телефон", - "PropertyType.Select": "Выбрать", - "PropertyType.Text": "Текст", - "PropertyType.Unknown": "Неизвестный", - "PropertyType.UpdatedBy": "Обновлено пользователем", - "PropertyType.UpdatedTime": "Время обновления", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Пустой", - "RegistrationLink.confirmRegenerateToken": "Это сделает недействительными ссылки, которые ранее были общими. Продолжить?", - "RegistrationLink.copiedLink": "Скопировано!", - "RegistrationLink.copyLink": "Скопировать ссылку", - "RegistrationLink.description": "Поделитесь этой ссылкой с другими для создания аккаунтов:", - "RegistrationLink.regenerateToken": "Пересоздать токен", - "RegistrationLink.tokenRegenerated": "Регистрационная ссылка пересоздана", - "ShareBoard.PublishDescription": "Публикуйте и делитесь ссылкой \"только для чтения\" со всеми пользователями сети.", - "ShareBoard.PublishTitle": "Опубликовать в Интернете", - "ShareBoard.ShareInternal": "Поделиться внутри организации", - "ShareBoard.ShareInternalDescription": "Пользователи, у которых есть разрешения, смогут использовать эту ссылку.", - "ShareBoard.Title": "Поделится Доской", - "ShareBoard.confirmRegenerateToken": "Это сделает недействительными ссылки, которые ранее были общими. Продолжить?", - "ShareBoard.copiedLink": "Скопировано!", - "ShareBoard.copyLink": "Скопировать ссылку", - "ShareBoard.regenerate": "Восстановить токен", - "ShareBoard.searchPlaceholder": "Поиск людей", - "ShareBoard.teamPermissionsText": "Все в команде {teamName}", - "ShareBoard.tokenRegenrated": "Токен пересоздан", - "ShareBoard.userPermissionsRemoveMemberText": "Удалить участника", - "ShareBoard.userPermissionsYouText": "(Вы)", - "ShareTemplate.Title": "Поделиться Шаблоном", - "ShareTemplate.searchPlaceholder": "Поиск людей", - "Sidebar.about": "О Focalboard", - "Sidebar.add-board": "+ Добавить доску", - "Sidebar.changePassword": "Изменить пароль", - "Sidebar.delete-board": "Удалить доску", - "Sidebar.duplicate-board": "Дублировать доску", - "Sidebar.export-archive": "Экспорт архива", - "Sidebar.import": "Импорт", - "Sidebar.import-archive": "Импорт архива", - "Sidebar.invite-users": "Пригласить пользователей", - "Sidebar.logout": "Выйти", - "Sidebar.new-category.badge": "Новый", - "Sidebar.new-category.drag-boards-cta": "Перетащите сюда доски...", - "Sidebar.no-boards-in-category": "Без досок внутри", - "Sidebar.product-tour": "Экскурсия по продукту", - "Sidebar.random-icons": "Случайные иконки", - "Sidebar.set-language": "Язык", - "Sidebar.set-theme": "Тема", - "Sidebar.settings": "Настройки", - "Sidebar.template-from-board": "Новый шаблон из доски", - "Sidebar.untitled-board": "(Доска без названия)", - "Sidebar.untitled-view": "(Безымянный вид)", - "SidebarCategories.BlocksMenu.Move": "Перейти к...", - "SidebarCategories.CategoryMenu.CreateNew": "Создать новую категорию", - "SidebarCategories.CategoryMenu.Delete": "Удалить категорию", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Доски в {categoryName} вернутся к категориям \"Доски\". Вы не удалены ни с одной доски.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Удалить эту категорию?", - "SidebarCategories.CategoryMenu.Update": "Переименовать категорию", - "SidebarTour.ManageCategories.Body": "Создавайте и управляйте пользовательскими категориями. Категории зависят от пользователя, поэтому перемещение доски в вашу категорию не повлияет на других участников, использующих ту же доску.", - "SidebarTour.ManageCategories.Title": "Управление категориями", - "SidebarTour.SearchForBoards.Body": "Откройте переключатель досок (Cmd/Ctrl + K), чтобы быстро найти и добавить доски на боковую панель.", - "SidebarTour.SearchForBoards.Title": "Поиск досок", - "SidebarTour.SidebarCategories.Body": "Все ваши доски теперь организованы под новой боковой панелью. Больше не нужно переключаться между рабочими пространствами. Одноразовые настраиваемые категории, основанные на ваших предыдущих рабочих областях, могли быть автоматически созданы для вас в рамках обновления до версии 7.2. Их можно удалить или отредактировать по своему усмотрению.", - "SidebarTour.SidebarCategories.Link": "Учить больше", - "SidebarTour.SidebarCategories.Title": "Категории боковой панели", - "SiteStats.total_boards": "Всего досок", - "SiteStats.total_cards": "Всего карт", - "TableComponent.add-icon": "Добавить иконку", - "TableComponent.name": "Название", - "TableComponent.plus-new": "+ Создать", - "TableHeaderMenu.delete": "Удалить", - "TableHeaderMenu.duplicate": "Создать дубликат", - "TableHeaderMenu.hide": "Скрыть", - "TableHeaderMenu.insert-left": "Вставить слева", - "TableHeaderMenu.insert-right": "Вставить справа", - "TableHeaderMenu.sort-ascending": "Сортировать по возрастанию", - "TableHeaderMenu.sort-descending": "Сортировать по убыванию", - "TableRow.DuplicateCard": "дублировать карточку", - "TableRow.MoreOption": "Больше действий", - "TableRow.open": "Открыть", - "TopBar.give-feedback": "Дать обратную связь", - "URLProperty.copiedLink": "Скопировано!", - "URLProperty.copy": "Копировать", - "URLProperty.edit": "Изменить", - "UndoRedoHotKeys.canRedo": "Повторить", - "UndoRedoHotKeys.canRedo-with-description": "Повторить {description}", - "UndoRedoHotKeys.canUndo": "Отменить", - "UndoRedoHotKeys.canUndo-with-description": "Отменить {description}", - "UndoRedoHotKeys.cannotRedo": "Нечего переделывать", - "UndoRedoHotKeys.cannotUndo": "Нечего отменить", - "ValueSelector.noOptions": "Нет вариантов. Начните печатать, чтобы добавить первый!", - "ValueSelector.valueSelector": "Выбор значения", - "ValueSelectorLabel.openMenu": "Открыть меню", - "VersionMessage.help": "Узнайте, что нового в этой версии.", - "VersionMessage.learn-more": "Узнать больше", - "View.AddView": "Добавить вид", - "View.Board": "Доска", - "View.DeleteView": "Удалить вид", - "View.DuplicateView": "Создать дубликат вида", - "View.Gallery": "Галерея", - "View.NewBoardTitle": "Вид доски", - "View.NewCalendarTitle": "Просмотр календаря", - "View.NewGalleryTitle": "Представление \"галерея\"", - "View.NewTableTitle": "Вид таблицы", - "View.NewTemplateDefaultTitle": "Шаблон без названия", - "View.NewTemplateTitle": "Шаблон", - "View.Table": "Таблица", - "ViewHeader.add-template": "Новый шаблон", - "ViewHeader.delete-template": "Удалить", - "ViewHeader.display-by": "Показать по: {property}", - "ViewHeader.edit-template": "Изменить", - "ViewHeader.empty-card": "Очистить карточку", - "ViewHeader.export-board-archive": "Экспорт архива доски", - "ViewHeader.export-complete": "Экспорт завершен!", - "ViewHeader.export-csv": "Экспорт в CSV", - "ViewHeader.export-failed": "Ошибка экспорта!", - "ViewHeader.filter": "Фильтр", - "ViewHeader.group-by": "Сгруппировать по: {property}", - "ViewHeader.new": "Создать", - "ViewHeader.properties": "Свойства", - "ViewHeader.properties-menu": "Меню свойств", - "ViewHeader.search-text": "Карточки поиска", - "ViewHeader.select-a-template": "Выбрать шаблон", - "ViewHeader.set-default-template": "Установить по умолчанию", - "ViewHeader.sort": "Сортировать", - "ViewHeader.untitled": "Без названия", - "ViewHeader.view-header-menu": "Посмотреть меню заголовка", - "ViewHeader.view-menu": "Посмотреть меню", - "ViewLimitDialog.Heading": "Достигнут лимит просмотров на доске", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Обновление", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Сообщить администратору", - "ViewLimitDialog.Subtext.Admin": "Перейдите на наш план Professional или Enterprise, чтобы иметь неограниченное количество просмотров на досках, неограниченное количество карточек и многое другое.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Узнайте больше о наших тарифах.", - "ViewLimitDialog.Subtext.RegularUser": "Сообщите своему администратору, чтобы перейти на наш план Professional или Enterprise, чтобы иметь неограниченные количество просмотров на доске, карточек и многое другое.", - "ViewLimitDialog.UpgradeImg.AltText": "обновить изображение", - "ViewLimitDialog.notifyAdmin.Success": "Ваш администратор был уведомлен", - "ViewTitle.hide-description": "скрыть описание", - "ViewTitle.pick-icon": "Выбрать иконку", - "ViewTitle.random-icon": "Случайным образом", - "ViewTitle.remove-icon": "Убрать иконку", - "ViewTitle.show-description": "Показать описание", - "ViewTitle.untitled-board": "Доска без названия", - "WelcomePage.Description": "Доски — это инструмент управления проектами, который помогает определять, организовывать, отслеживать и управлять работой между командами, используя знакомое представление доски Kanban.", - "WelcomePage.Explore.Button": "Исследовать", - "WelcomePage.Heading": "Добро пожаловать на Доски", - "WelcomePage.NoThanks.Text": "Нет спасибо, сам разберусь", - "WelcomePage.StartUsingIt.Text": "Начать пользоваться", - "Workspace.editing-board-template": "Вы редактируете шаблон доски.", - "badge.guest": "Гость", - "boardPage.confirm-join-button": "Присоединиться", - "boardPage.confirm-join-text": "Вы собираетесь присоединиться к закрытой доске без явного добавления администратором доски. Вы уверены, что хотите присоединиться к этой закрытой доске?", - "boardPage.confirm-join-title": "Присоединиться к приватной доске", - "boardSelector.confirm-link-board": "Привязать доску к каналу", - "boardSelector.confirm-link-board-button": "Да, ссылка доски", - "boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с каналом даст всем участникам канала доступ на редактирование доски. Вы можете в любое время отвязать доску о канала.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "Привязка \"{boardName}\" с каналом приведет к возможности её редактирования всеми участниками канала (существующими и новыми). Кроме гостей канала.{lineBreak} Эта доска сейчас связана с другим каналом. Он будет отключен, если вы решите изменить привязку.", - "boardSelector.create-a-board": "Создать доску", - "boardSelector.link": "Ссылка", - "boardSelector.search-for-boards": "Поиск досок", - "boardSelector.title": "Ссылки на доски", - "boardSelector.unlink": "Отключить", - "calendar.month": "Месяц", - "calendar.today": "СЕГОДНЯ", - "calendar.week": "Неделя", - "centerPanel.undefined": "Отсутствует {propertyName}", - "centerPanel.unknown-user": "Неизвестный пользователь", - "cloudMessage.learn-more": "Учить больше", - "createImageBlock.failed": "Не удалось загрузить файл. Достигнут предел размера файла.", - "default-properties.badges": "Комментарии и описание", - "default-properties.title": "Заголовок", - "error.back-to-home": "Вернуться на Главную", - "error.back-to-team": "Вернуться в команду", - "error.board-not-found": "Доска не найдена.", - "error.go-login": "Логин", - "error.invalid-read-only-board": "У Вас нет доступа к этой доске. Войдите, чтобы получить доступ к Доскам.", - "error.not-logged-in": "Возможно, срок действия Вашего сеанса истек или Вы не вошли в систему. Войдите еще раз, чтобы получить доступ к Доскам.", - "error.page.title": "Извините, что-то пошло не так", - "error.team-undefined": "Не корректная команда.", - "error.unknown": "Произошла ошибка.", - "generic.previous": "Предыдущий", - "guest-no-board.subtitle": "У вас пока нет доступа ни к одной доске в этой команде, пожалуйста, подождите, пока кто-нибудь не добавит вас к любой из досок.", - "guest-no-board.title": "Пока нет досок", - "imagePaste.upload-failed": "Некоторые файлы не загружены из-за превышения квоты на размер файла.", - "limitedCard.title": "Карточки скрыты", - "login.log-in-button": "Вход в систему", - "login.log-in-title": "Вход в систему", - "login.register-button": "или создать аккаунт, если у Вас его нет", - "new_channel_modal.create_board.empty_board_description": "Создать новую пустую доску", - "new_channel_modal.create_board.empty_board_title": "Пустая доска", - "new_channel_modal.create_board.select_template_placeholder": "Выбрать шаблон", - "new_channel_modal.create_board.title": "Создать доску для этого канала", - "notification-box-card-limit-reached.close-tooltip": "Отложить на 10 дней", - "notification-box-card-limit-reached.contact-link": "уведомить Вашего администратора", - "notification-box-card-limit-reached.link": "Перейти на платный тариф", - "notification-box-card-limit-reached.title": "{cards} карты, скрытые на доске", - "notification-box-cards-hidden.title": "Это действие скрыло другую карточку", - "notification-box.card-limit-reached.not-admin.text": "Чтобы получить доступ к архивным карточкам, Вы можете {contactLink} перейти на платный тариф.", - "notification-box.card-limit-reached.text": "Достигнут лимит карточки, чтобы просмотреть старые карточки, {link}", - "person.add-user-to-board": "Добавить {username} на доску", - "person.add-user-to-board-confirm-button": "Добавить доску", - "person.add-user-to-board-permissions": "Разрешения", - "person.add-user-to-board-question": "Вы хотите добавить {username} на доску?", - "person.add-user-to-board-warning": "{username} не является участником доски и не получает никаких уведомлений о ней.", - "register.login-button": "или войти в систему, если у вас уже есть аккаунт", - "register.signup-title": "Зарегистрируйте свой аккаунт", - "rhs-board-non-admin-msg": "Вы не являетесь администратором этой доски", - "rhs-boards.add": "Добавить", - "rhs-boards.dm": "ЛС", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "это личное сообщение", - "rhs-boards.header.gm": "это групповое сообщение", - "rhs-boards.last-update-at": "Последнее обновление: {datetime}", - "rhs-boards.link-boards-to-channel": "Связать доски с {channelName}", - "rhs-boards.linked-boards": "Связанные доски", - "rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски", - "rhs-boards.no-boards-linked-to-channel-description": "Доски — это инструмент управления проектами, который помогает определять, организовывать, отслеживать и управлять работой между командами, используя знакомое представление доски Канбан.", - "rhs-boards.unlink-board": "Отвязать доску", - "rhs-boards.unlink-board1": "Отвязать доску", - "rhs-channel-boards-header.title": "Доски", - "share-board.publish": "Опубликовать", - "share-board.share": "Поделиться", - "shareBoard.channels-select-group": "Каналы", - "shareBoard.confirm-change-team-role.body": "Все на этой доске с правами ниже, чем роль \"{role}\" теперь будут повышены до {role}. Вы уверены, что хотите изменить минимальную роль для доски?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Изменение минимальной роли доски", - "shareBoard.confirm-change-team-role.title": "Изменение минимальной роли доски", - "shareBoard.confirm-link-channel": "Привязать доску к каналу", - "shareBoard.confirm-link-channel-button": "Привязать канал", - "shareBoard.confirm-link-channel-button-with-other-channel": "Отвязать и привязать это", - "shareBoard.confirm-link-channel-subtext": "Когда вы связываете канал с доской, все участники канала (существующие и новые) смогут редактировать его. За исключением пользователей, которые являются гостями.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Когда вы связываете канал с доской, все участники канала (существующие и новые) смогут редактировать его. За исключением пользователей, которые являются гостями.{lineBreak}Эта доска в настоящее время связана с другим каналом. Она будет удалена, если вы решите связать ее здесь.", - "shareBoard.confirm-unlink.body": "Когда вы отвязываете канал от доски, все участники канала (как существующие, так и новые) теряют к нему доступ, если им не дано отдельное разрешение.", - "shareBoard.confirm-unlink.confirmBtnText": "Отвязать канал", - "shareBoard.confirm-unlink.title": "Отвязать канал от доски", - "shareBoard.lastAdmin": "Доски должны иметь хотя бы одного администратора", - "shareBoard.members-select-group": "Участники", - "shareBoard.unknown-channel-display-name": "Неизвестный канал", - "tutorial_tip.finish_tour": "Готово", - "tutorial_tip.got_it": "Понятно", - "tutorial_tip.ok": "Следующий", - "tutorial_tip.out": "Отказаться от этих советов.", - "tutorial_tip.seen": "Видели это раньше?" -} diff --git a/webapp/boards/i18n/sk.json b/webapp/boards/i18n/sk.json deleted file mode 100644 index 6f3c5f222c..0000000000 --- a/webapp/boards/i18n/sk.json +++ /dev/null @@ -1,286 +0,0 @@ -{ - "Attachment.Attachment-title": "Príloha", - "AttachmentBlock.DeleteAction": "odstrániť", - "AttachmentBlock.delete": "Príloha odstránená.", - "AttachmentBlock.failed": "Tento súbor nebol nahratý, pretože presiahol veľkostný limit.", - "AttachmentBlock.upload": "Príloha sa nahráva.", - "AttachmentBlock.uploadSuccess": "Príloha bola nahratá.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Odstrániť", - "AttachmentElement.download": "Stiahnuť", - "AttachmentElement.upload-percentage": "Nahrávam... ({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Pridaj skupinu", - "BoardComponent.delete": "Odstrániť", - "BoardComponent.hidden-columns": "Skryté stĺpce", - "BoardComponent.hide": "Skryť", - "BoardComponent.new": "+ Nový", - "BoardComponent.no-property": "žiadna {property}", - "BoardComponent.no-property-title": "Položky s prázdnou {property} pôjdu tu. Tento stĺpec nemožno vymazať.", - "BoardComponent.show": "Ukáž", - "BoardMember.schemeAdmin": "Administrátor", - "BoardMember.schemeCommenter": "Komentátor", - "BoardMember.schemeEditor": "Editor", - "BoardMember.schemeNone": "Žiadny", - "BoardMember.schemeViewer": "Sledovateľ", - "BoardMember.unlinkChannel": "Odpojiť", - "BoardPage.newVersion": "Nová verzia je dostupná, kliknite tu pre znovu načítanie.", - "BoardPage.syncFailed": "Nástenka môže byť vymazaná alebo prístup odobraný.", - "BoardTemplateSelector.add-template": "Vytvoriť novú šablónu", - "BoardTemplateSelector.create-empty-board": "Vytvoriť prázdnu nástenku", - "BoardTemplateSelector.delete-template": "Odstrániť", - "BoardTemplateSelector.description": "Pridajte nástenku do bočného panelu pomocou ktorýchkoľvek šablón definovaných dole alebo začnite od začiatku.", - "BoardTemplateSelector.edit-template": "Upraviť", - "BoardTemplateSelector.plugin.no-content-description": "Pridajte nástenku do bočného panelu pomocou ktorýchkoľvek šablón dole alebo začnite od začiatku.", - "BoardTemplateSelector.plugin.no-content-title": "Vytvoriť nástenku", - "BoardTemplateSelector.title": "Vytvoriť nástenku", - "BoardTemplateSelector.use-this-template": "Použiť túto šablónu", - "BoardsSwitcher.Title": "Hľadať nástenky", - "BoardsUnfurl.Limited": "Ďalšie detaily sú skryté, pretože je karta archivovaná", - "BoardsUnfurl.Remainder": "+{remainder} viac", - "BoardsUnfurl.Updated": "Upravené {time}", - "Calculations.Options.average.displayName": "Priemer", - "Calculations.Options.average.label": "Priemer", - "Calculations.Options.count.displayName": "Počet", - "Calculations.Options.count.label": "Počet", - "Calculations.Options.countChecked.displayName": "Označené", - "Calculations.Options.countChecked.label": "Počítať označené", - "Calculations.Options.countUnchecked.displayName": "Neoznačené", - "Calculations.Options.countUnchecked.label": "Počítať neoznačené", - "Calculations.Options.countUniqueValue.displayName": "Unikátne", - "Calculations.Options.countUniqueValue.label": "Počítať unikátne hodnoty", - "Calculations.Options.countValue.displayName": "Hodnoty", - "Calculations.Options.countValue.label": "Počítať hodnotu", - "Calculations.Options.dateRange.displayName": "Rozsah", - "Calculations.Options.dateRange.label": "Rozsah", - "Calculations.Options.earliest.displayName": "Prvý", - "Calculations.Options.earliest.label": "Prvý", - "Calculations.Options.latest.displayName": "Posledný", - "Calculations.Options.latest.label": "Posledný", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Medián", - "Calculations.Options.median.label": "Medián", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Vypočítať", - "Calculations.Options.none.label": "Nič", - "Calculations.Options.percentChecked.displayName": "Označené", - "Calculations.Options.percentChecked.label": "Percent skontrolovaných", - "Calculations.Options.percentUnchecked.displayName": "Neskontrolované", - "Calculations.Options.percentUnchecked.label": "Percent neskontrolovaných", - "Calculations.Options.range.displayName": "Rozsah", - "Calculations.Options.range.label": "Rozsah", - "Calculations.Options.sum.displayName": "Súčet", - "Calculations.Options.sum.label": "Súčet", - "CalendarCard.untitled": "Bez názvu", - "CardActionsMenu.copiedLink": "Skopírované!", - "CardActionsMenu.copyLink": "Skopírovať odkaz", - "CardActionsMenu.delete": "Odstrániť", - "CardActionsMenu.duplicate": "Duplikovať", - "CardBadges.title-checkboxes": "Začiarkávacie políčka", - "CardBadges.title-comments": "Komentáre", - "CardBadges.title-description": "Táto karta má popis", - "CardDetail.Attach": "Priložiť", - "CardDetail.Follow": "Sledovať", - "CardDetail.Following": "Sledujúci", - "CardDetail.add-content": "Pridať obsah", - "CardDetail.add-icon": "Pridať ikonu", - "CardDetail.add-property": "+ Pridať vlastnosť", - "CardDetail.addCardText": "pridať text karty", - "CardDetail.limited-body": "Vylepšiť na náš Professional alebo Enterprise plán.", - "CardDetail.limited-button": "Zmeniť plán", - "CardDetail.limited-title": "Táto karta je skrytá", - "CardDetail.moveContent": "Presunúť obsah karty", - "CardDetail.new-comment-placeholder": "Pridať komentár...", - "CardDetailProperty.confirm-delete-heading": "Potvrdiť vymazanie vlastnosti", - "CardDetailProperty.confirm-delete-subtext": "Skutočne chcete vymazať hodnotu \"{propertyName}\"? Bude odstránená zo všetkých kariet na tejto tabuli.", - "CardDetailProperty.confirm-property-name-change-subtext": "Skutočne chcete zmeniť hodnotu \"{propertyName}\" {customText}? Ovplyvní to {numOfCards} kariet na tabuli a môže viesť k strate dát.", - "CardDetailProperty.confirm-property-type-change": "Potvrdiť zmenu typu vlastnosti", - "CardDetailProperty.delete-action-button": "Odstrániť", - "CardDetailProperty.property-change-action-button": "Zmeniť vlastnosť", - "CardDetailProperty.property-changed": "Zmena vlastnosti úspešná!", - "CardDetailProperty.property-deleted": "Odstránenie {propertyName} úspešné!", - "CardDetailProperty.property-name-change-subtext": "typ z \"{oldPropType}\" na \"{newPropType}\"", - "CardDetial.limited-link": "Dozvedieť sa viac o našich plánoch.", - "CardDialog.delete-confirmation-dialog-attachment": "Potvrdiť odstránenie prílohy", - "CardDialog.delete-confirmation-dialog-button-text": "Odstrániť", - "CardDialog.delete-confirmation-dialog-heading": "Potvrdiť odstránenie karty", - "CardDialog.editing-template": "Upravujete šablónu.", - "CardDialog.nocard": "Táto karta neexistuje alebo nie je prístupná.", - "Categories.CreateCategoryDialog.CancelText": "Zrušiť", - "Categories.CreateCategoryDialog.CreateText": "Vytvoriť", - "Categories.CreateCategoryDialog.Placeholder": "Nazvite Vašu kategóriu", - "Categories.CreateCategoryDialog.UpdateText": "Zmeniť", - "CenterPanel.Login": "Prihlásiť sa", - "CenterPanel.Share": "Zdieľať", - "ChannelIntro.CreateBoard": "Vytvoriť nástenku", - "ColorOption.selectColor": "Vyberte {color} farbu", - "Comment.delete": "Odstrániť", - "CommentsList.send": "Odoslať", - "ConfirmPerson.empty": "Prázdne", - "ConfirmPerson.search": "Vyhľadať...", - "ConfirmationDialog.cancel-action": "Zrušiť", - "ConfirmationDialog.confirm-action": "Potvrdiť", - "ContentBlock.Delete": "Odstrániť", - "ContentBlock.DeleteAction": "odstrániť", - "ContentBlock.addElement": "pridať {type}", - "ContentBlock.checkbox": "začiarkávacie pole", - "ContentBlock.divider": "oddeľovač", - "ContentBlock.editCardCheckbox": "Začiarknuté pole", - "ContentBlock.editCardCheckboxText": "upraviť text karty", - "ContentBlock.editCardText": "upraviť text karty", - "ContentBlock.editText": "Upraviť text...", - "ContentBlock.image": "obrázok", - "ContentBlock.insertAbove": "Vložiť nad", - "ContentBlock.moveBlock": "presunúť obsah karty", - "ContentBlock.moveDown": "Presunúť dole", - "ContentBlock.moveUp": "Presunúť hore", - "ContentBlock.text": "text", - "DateRange.clear": "Vyčistiť", - "DateRange.empty": "Prázdny", - "DateRange.endDate": "Koncový dátum", - "DateRange.today": "Dnes", - "DeleteBoardDialog.confirm-cancel": "Zrušiť", - "DeleteBoardDialog.confirm-delete": "Odstrániť", - "DeleteBoardDialog.confirm-info": "Naozaj chcete odstrániť nástenku “{boardTitle}”? Odstránením vymažete všetky karty na tabuli.", - "DeleteBoardDialog.confirm-info-template": "Naozaj chcete odstrániť nástenkovú šablónu \"{boardTitle}\"?", - "DeleteBoardDialog.confirm-tite": "Potvrďte odstránenie nástenky", - "DeleteBoardDialog.confirm-tite-template": "Potvrdiť odstránenie šablóny nástenky", - "Dialog.closeDialog": "Zatvoriť dialógové okno", - "EditableDayPicker.today": "Dnes", - "Error.mobileweb": "Podpora pre mobilné prehliadače je v skorej bete. Niektoré funkcionality môžu chýbať.", - "Error.websocket-closed": "Websocket pripojenie zlyhalo - bolo prerušené. Pokiaľ problém pretrváva, skontrolujte konfiguráciu servera.", - "Filter.contains": "obsahuje", - "Filter.ends-with": "končí s", - "Filter.includes": "zahŕňa", - "Filter.is": "je", - "Filter.is-empty": "je prázdny", - "Filter.is-not-empty": "nie je prázdny", - "Filter.is-not-set": "nie je nastavený", - "Filter.is-set": "je nastavený", - "Filter.not-contains": "neobsahuje", - "Filter.not-ends-with": "nekončí s", - "Filter.not-includes": "nezahŕňa", - "Filter.not-starts-with": "nezačína s", - "Filter.starts-with": "začína s", - "FilterByText.placeholder": "text filtra", - "FilterComponent.add-filter": "+ Pridaj filter", - "FilterComponent.delete": "Odstrániť", - "FilterValue.empty": "(prázdny)", - "FindBoardsDialog.IntroText": "Vyhľadať nástenky", - "FindBoardsDialog.NoResultsFor": "Žiadne výsledky pre \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Skontrolujte pravopis alebo vyskúšajte iný pojem.", - "FindBoardsDialog.SubTitle": "Nájdite nástenku písaním. Použite HORE/DOLE na prehliadanie, ENTER na vybratie a ESC na zrušenie", - "FindBoardsDialog.Title": "Nájsť nástenky", - "GroupBy.hideEmptyGroups": "Skryť {count} prázdnych skupín", - "GroupBy.showHiddenGroups": "Zobraziť {count} prázdnych skupín", - "GroupBy.ungroup": "Zrušiť zoskupenie", - "HideBoard.MenuOption": "Skryť nástenku", - "KanbanCard.untitled": "Bez názvu", - "MentionSuggestion.is-not-board-member": "(nie je členom nástenky)", - "Mutator.new-board-from-template": "nová nástenka zo šablóny", - "Mutator.new-card-from-template": "nová karta zo šablóny", - "Mutator.new-template-from-card": "nová šablóna z karty", - "PropertyMenu.Delete": "Odstrániť", - "PropertyMenu.changeType": "Zmeniť vlastnosť", - "PropertyMenu.selectType": "Vybrať vlastnosť", - "PropertyMenu.typeTitle": "Typ", - "PropertyType.Checkbox": "Checkbox", - "PropertyType.CreatedBy": "Vytvoril", - "PropertyType.CreatedTime": "Vytvorené", - "PropertyType.Date": "Dátum", - "PropertyType.Email": "Email", - "PropertyType.MultiSelect": "Viacnásobný výber", - "PropertyType.Number": "číslo", - "PropertyType.Person": "Osoba", - "PropertyType.Phone": "Telefón", - "PropertyType.Select": "Vyber", - "PropertyType.Text": "Text", - "PropertyType.UpdatedBy": "Naposledy upravil", - "PropertyType.UpdatedTime": "Posledná úprava", - "PropertyValueElement.empty": "Prázdny", - "RegistrationLink.confirmRegenerateToken": "Toto zruší platnosť predtým zdieľaných odkazov. Pokračovať?", - "RegistrationLink.copiedLink": "Skopírované!", - "RegistrationLink.copyLink": "Skopírovať odkaz", - "RegistrationLink.description": "Zdieľajte tento odkaz pre vytvorenie účtu:", - "RegistrationLink.regenerateToken": "Obnoviť token", - "RegistrationLink.tokenRegenerated": "Registračný odkaz obnovený", - "ShareBoard.confirmRegenerateToken": "Toto zruší platnosť predtým zdieľaných odkazov. Pokračovať?", - "ShareBoard.copiedLink": "Skopírované!", - "ShareBoard.copyLink": "Skopírovať odkaz", - "ShareBoard.tokenRegenrated": "Token obnovený", - "Sidebar.about": "O Focalboard", - "Sidebar.add-board": "+ Pridať nástenku", - "Sidebar.changePassword": "Zmeniť heslo", - "Sidebar.delete-board": "Odstrániť nástenku", - "Sidebar.export-archive": "Export archívu", - "Sidebar.import-archive": "Import archívu", - "Sidebar.invite-users": "Pozvať užívateľa", - "Sidebar.logout": "Odhlásiť sa", - "Sidebar.random-icons": "Náhodné ikony", - "Sidebar.set-language": "Nastaviť jazyk", - "Sidebar.set-theme": "Nastaviť tému", - "Sidebar.settings": "nastavenia", - "Sidebar.untitled-board": "(nástenka bez názvu)", - "TableComponent.add-icon": "Pridať ikonu", - "TableComponent.name": "názov", - "TableComponent.plus-new": "+ Nový", - "TableHeaderMenu.delete": "Odstrániť", - "TableHeaderMenu.duplicate": "Duplikuj", - "TableHeaderMenu.hide": "Skryť", - "TableHeaderMenu.insert-left": "Vložiť vľavo", - "TableHeaderMenu.insert-right": "Vložiť vpravo", - "TableHeaderMenu.sort-ascending": "vzostupne", - "TableHeaderMenu.sort-descending": "zostupne", - "TableRow.open": "Otvoriť", - "TopBar.give-feedback": "Spätná väzba", - "ValueSelector.noOptions": "Žiadne možnosti. Pridajte prvú!", - "ValueSelector.valueSelector": "Výber hodnoty", - "ValueSelectorLabel.openMenu": "Otvor menu", - "View.AddView": "Pridaj pohľad", - "View.Board": "nástenka", - "View.DeleteView": "Odstrániť pohľad", - "View.DuplicateView": "Duplikuj pohľad", - "View.Gallery": "Galéria", - "View.NewBoardTitle": "Náhľad nástenky", - "View.NewCalendarTitle": "Kalendár", - "View.NewGalleryTitle": "Galéria", - "View.NewTableTitle": "Tabuľka", - "View.Table": "Tabuľka", - "ViewHeader.add-template": "Nový template", - "ViewHeader.delete-template": "Odstrániť", - "ViewHeader.display-by": "Zobraziť podľa: {property}", - "ViewHeader.edit-template": "Upraviť", - "ViewHeader.empty-card": "Prázdna karta", - "ViewHeader.export-board-archive": "Export archívu nástenky", - "ViewHeader.export-complete": "Export hotový!", - "ViewHeader.export-csv": "Export do CSV", - "ViewHeader.export-failed": "Export zlyhal!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Zoskupiť podľa: {property}", - "ViewHeader.new": "Nový", - "ViewHeader.properties": "Vlastnosti", - "ViewHeader.search-text": "Hľadať text", - "ViewHeader.select-a-template": "vyber template", - "ViewHeader.set-default-template": "Nastaviť ako predvolenú", - "ViewHeader.sort": "Triediť", - "ViewHeader.untitled": "Bez názvu", - "ViewTitle.hide-description": "Skryť popis", - "ViewTitle.pick-icon": "Vybrať ikonu", - "ViewTitle.random-icon": "Náhodne", - "ViewTitle.remove-icon": "Odstrániť ikonu", - "ViewTitle.show-description": "zobraziť popis", - "ViewTitle.untitled-board": "nástenka bez názvu", - "WelcomePage.Description": "Nástenky sú nástroj na riadenie projektov, ktorý pomáha definovať, organizovať, sledovať a riadiť prácu medzi tímami pomocou zobrazenia kanban", - "WelcomePage.Explore.Button": "Preskúmať", - "WelcomePage.Heading": "Vitajte", - "Workspace.editing-board-template": "Upravujete template nástenky.", - "calendar.month": "Mesiac", - "calendar.today": "Dnes", - "calendar.week": "Týždeň", - "default-properties.title": "Názov", - "login.log-in-button": "Prihlásiť", - "login.log-in-title": "Prihlásiť", - "login.register-button": "alebo vytvoriť účet, ak žiadny nemáte", - "register.login-button": "alebo sa prihláste ak už máte účet", - "register.signup-title": "Zaregistrujte si účet" -} diff --git a/webapp/boards/i18n/sl.json b/webapp/boards/i18n/sl.json deleted file mode 100644 index c9c68b0868..0000000000 --- a/webapp/boards/i18n/sl.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "BoardComponent.add-a-group": "+ Dodaj skupino", - "BoardComponent.delete": "Izbriši", - "BoardComponent.hidden-columns": "Skriti stolpci", - "BoardComponent.hide": "Skrij", - "BoardComponent.new": "+ Novo", - "BoardComponent.no-property": "Ni {property}", - "BoardComponent.no-property-title": "Elementi s prazno lastnostjo {property} bodo šli sem. Tega stolpca ni mogoče odstraniti.", - "BoardComponent.show": "Pokaži", - "BoardPage.newVersion": "Na voljo je nova različica Boards, kliknite tukaj za ponovno nalaganje.", - "BoardPage.syncFailed": "Plošča se lahko izbriše ali dostop prekliče." -} diff --git a/webapp/boards/i18n/sv.json b/webapp/boards/i18n/sv.json deleted file mode 100644 index 8be135400b..0000000000 --- a/webapp/boards/i18n/sv.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Administratör", - "AdminBadge.TeamAdmin": "Teamadministratör", - "AppBar.Tooltip": "Växla länkade tavlor", - "Attachment.Attachment-title": "Bilaga", - "AttachmentBlock.DeleteAction": "radera", - "AttachmentBlock.addElement": "lägg till {type}", - "AttachmentBlock.delete": "Bilagan har tagits bort.", - "AttachmentBlock.failed": "Flen kunde inte laddas upp eftersom gränsen för filstorlek har nåtts.", - "AttachmentBlock.upload": "Bilagor laddas upp.", - "AttachmentBlock.uploadSuccess": "Bilagan är uppladdad.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Radera", - "AttachmentElement.download": "Ladda ner", - "AttachmentElement.upload-percentage": "Laddar upp...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Lägg till grupp", - "BoardComponent.delete": "Radera", - "BoardComponent.hidden-columns": "Dolda kolumner", - "BoardComponent.hide": "Dölj", - "BoardComponent.new": "+ Ny", - "BoardComponent.no-property": "Ingen {property}", - "BoardComponent.no-property-title": "Objekt med en tom {property} egenskap grupperas här. Denna kolumn kan inte tas bort.", - "BoardComponent.show": "Visa", - "BoardMember.schemeAdmin": "Administratör", - "BoardMember.schemeCommenter": "Kommentator", - "BoardMember.schemeEditor": "Redaktör", - "BoardMember.schemeNone": "Inget", - "BoardMember.schemeViewer": "Granskare", - "BoardMember.unlinkChannel": "Koppla ifrån", - "BoardPage.newVersion": "En ny version av Boards finns tillgänglig. Klicka här för att uppdatera.", - "BoardPage.syncFailed": "Denna Board kan ha blivit raderad eller så har din behörighet tagits bort.", - "BoardTemplateSelector.add-template": "Skapa ny mall", - "BoardTemplateSelector.create-empty-board": "Skapa en tom board", - "BoardTemplateSelector.delete-template": "Ta bort", - "BoardTemplateSelector.description": "Lägg till ett Board till sidomenyn genom att välja någon av mallarna nedan eller börja med en tom.", - "BoardTemplateSelector.edit-template": "Ändra", - "BoardTemplateSelector.plugin.no-content-description": "Lägg till en Board till sidofältet genom att använda en av mallarna nedan eller starta med en tom.", - "BoardTemplateSelector.plugin.no-content-title": "Skapa en Board", - "BoardTemplateSelector.title": "Skapa en board", - "BoardTemplateSelector.use-this-template": "Använd den här mallen", - "BoardsSwitcher.Title": "Hitta board", - "BoardsUnfurl.Limited": "Ytterligare uppgifter är dolda eftersom kortet är arkiverat", - "BoardsUnfurl.Remainder": "+{remainder} mer", - "BoardsUnfurl.Updated": "Uppdaterad {time}", - "Calculations.Options.average.displayName": "Genomsnitt", - "Calculations.Options.average.label": "Genomsnitt", - "Calculations.Options.count.displayName": "Räkna", - "Calculations.Options.count.label": "Räkna", - "Calculations.Options.countChecked.displayName": "Vald", - "Calculations.Options.countChecked.label": "Räkna valda", - "Calculations.Options.countUnchecked.displayName": "Ej vald", - "Calculations.Options.countUnchecked.label": "Räkna ej valda", - "Calculations.Options.countUniqueValue.displayName": "Unika", - "Calculations.Options.countUniqueValue.label": "Räkna unika värden", - "Calculations.Options.countValue.displayName": "Värden", - "Calculations.Options.countValue.label": "Räkna värden", - "Calculations.Options.dateRange.displayName": "Intervall", - "Calculations.Options.dateRange.label": "Intervall", - "Calculations.Options.earliest.displayName": "Tidigast", - "Calculations.Options.earliest.label": "Tidigast", - "Calculations.Options.latest.displayName": "Senast", - "Calculations.Options.latest.label": "Senast", - "Calculations.Options.max.displayName": "Max", - "Calculations.Options.max.label": "Max", - "Calculations.Options.median.displayName": "Median", - "Calculations.Options.median.label": "Median", - "Calculations.Options.min.displayName": "Min", - "Calculations.Options.min.label": "Min", - "Calculations.Options.none.displayName": "Beräkna", - "Calculations.Options.none.label": "Ingen", - "Calculations.Options.percentChecked.displayName": "Avbockad", - "Calculations.Options.percentChecked.label": "Procent avbockade", - "Calculations.Options.percentUnchecked.displayName": "Ej avbockad", - "Calculations.Options.percentUnchecked.label": "Procent ej avbockade", - "Calculations.Options.range.displayName": "Intervall", - "Calculations.Options.range.label": "Intervall", - "Calculations.Options.sum.displayName": "Summa", - "Calculations.Options.sum.label": "Summa", - "CalendarCard.untitled": "Saknar titel", - "CardActionsMenu.copiedLink": "Kopierad!", - "CardActionsMenu.copyLink": "Kopiera länk", - "CardActionsMenu.delete": "Radera", - "CardActionsMenu.duplicate": "Duplicera", - "CardBadges.title-checkboxes": "Kryssrutor", - "CardBadges.title-comments": "Kommentarer", - "CardBadges.title-description": "Detta kort har en beskrivning", - "CardDetail.Attach": "Bifoga", - "CardDetail.Follow": "Följ", - "CardDetail.Following": "Följer", - "CardDetail.add-content": "Lägg till innehåll", - "CardDetail.add-icon": "Lägg till ikon", - "CardDetail.add-property": "+ Lägg till egenskap", - "CardDetail.addCardText": "lägg till korttext", - "CardDetail.limited-body": "Uppgradera till Professional- eller Enterprise-abonnemang.", - "CardDetail.limited-button": "Uppgradera", - "CardDetail.limited-title": "Detta kort är dolt", - "CardDetail.moveContent": "Flytta kortinnehåll", - "CardDetail.new-comment-placeholder": "Lägg till kommentar...", - "CardDetailProperty.confirm-delete-heading": "Bekräfta ta bort egenskap", - "CardDetailProperty.confirm-delete-subtext": "Är du säker på att du vill ta bort egenskapen \"{propertyName}\"? Om du raderar den kommer egenskapen tas bort från alla kort på tavlan.", - "CardDetailProperty.confirm-property-name-change-subtext": "Är du säker du vill ändra egenskapen \"{propertyName}\" {customText}? Detta kommer påverka alla värden på {numOfCards} kort på den här boarden och kan innebära att du förlorar information.", - "CardDetailProperty.confirm-property-type-change": "Bekräfta ändring av egenskapstyp", - "CardDetailProperty.delete-action-button": "Ta bort", - "CardDetailProperty.property-change-action-button": "Ändra egenskap", - "CardDetailProperty.property-changed": "Ändrade egenskap!", - "CardDetailProperty.property-deleted": "{propertyName} har raderats!", - "CardDetailProperty.property-name-change-subtext": "typ från \"{oldPropType}\" till \"{newPropType}\"", - "CardDetial.limited-link": "Läs mer om våra abonnemang.", - "CardDialog.delete-confirmation-dialog-attachment": "Bekräfta att bilagor ska raderas", - "CardDialog.delete-confirmation-dialog-button-text": "Radera", - "CardDialog.delete-confirmation-dialog-heading": "Bekräfta att kortet ska raderas", - "CardDialog.editing-template": "Du redigerar en mall.", - "CardDialog.nocard": "Detta kort existerar inte eller är oåtkomligt.", - "Categories.CreateCategoryDialog.CancelText": "Avbryt", - "Categories.CreateCategoryDialog.CreateText": "Skapa", - "Categories.CreateCategoryDialog.Placeholder": "Namnge din kategori", - "Categories.CreateCategoryDialog.UpdateText": "Uppdatera", - "CenterPanel.Login": "Logga in", - "CenterPanel.Share": "Dela", - "ChannelIntro.CreateBoard": "Skapa en board", - "ColorOption.selectColor": "Välj {color} färg", - "Comment.delete": "Radera", - "CommentsList.send": "Skicka", - "ConfirmPerson.empty": "Tom", - "ConfirmPerson.search": "Sök...", - "ConfirmationDialog.cancel-action": "Avbryt", - "ConfirmationDialog.confirm-action": "Godkänn", - "ContentBlock.Delete": "Radera", - "ContentBlock.DeleteAction": "radera", - "ContentBlock.addElement": "lägg till {type}", - "ContentBlock.checkbox": "kryssrutan", - "ContentBlock.divider": "avdelare", - "ContentBlock.editCardCheckbox": "markerad krysskruta", - "ContentBlock.editCardCheckboxText": "redigera korttext", - "ContentBlock.editCardText": "redigera korttext", - "ContentBlock.editText": "Redigera text...", - "ContentBlock.image": "bild", - "ContentBlock.insertAbove": "Lägg till ovanför", - "ContentBlock.moveBlock": "flytta kortets innehåll", - "ContentBlock.moveDown": "Flytta ned", - "ContentBlock.moveUp": "Flytta upp", - "ContentBlock.text": "text", - "DateFilter.empty": "Tom", - "DateRange.clear": "Rensa", - "DateRange.empty": "Tom", - "DateRange.endDate": "Slutdatum", - "DateRange.today": "Idag", - "DeleteBoardDialog.confirm-cancel": "Avbryt", - "DeleteBoardDialog.confirm-delete": "Radera", - "DeleteBoardDialog.confirm-info": "Är du säker på att du vill ta bort board “{boardTitle}”? När du tar bort den kommer du radera alla kort på board.", - "DeleteBoardDialog.confirm-info-template": "Är du säker på att du vill ta bort board-mallen \"{boardTitle}\"?", - "DeleteBoardDialog.confirm-tite": "Bekräfta att ta bort board", - "DeleteBoardDialog.confirm-tite-template": "Bekräfta att board-mallen ska raderas", - "Dialog.closeDialog": "Stäng dialog", - "EditableDayPicker.today": "Idag", - "Error.mobileweb": "Webbåtkomst via mobilen är i tidig betaversion. All funktionalitet kanske inte är tillgänglig.", - "Error.websocket-closed": "Websocketanslutningen stängdes då anslutningen avbröts. Om detta fortgår, kontrollera din server eller webproxykonfigurationen.", - "Filter.contains": "innehåller", - "Filter.ends-with": "slutar med", - "Filter.includes": "inkluderar", - "Filter.is": "är", - "Filter.is-after": "är efter", - "Filter.is-before": "är före", - "Filter.is-empty": "är tomt", - "Filter.is-not-empty": "är inte tomt", - "Filter.is-not-set": "är inte inställd", - "Filter.is-set": "är inställd", - "Filter.isafter": "är efter", - "Filter.isbefore": "är före", - "Filter.not-contains": "innehåller inte", - "Filter.not-ends-with": "slutar inte med", - "Filter.not-includes": "inkluderar inte", - "Filter.not-starts-with": "börjar inte med", - "Filter.starts-with": "börjar med", - "FilterByText.placeholder": "filtrera text", - "FilterComponent.add-filter": "+ Lägg till filter", - "FilterComponent.delete": "Radera", - "FilterValue.empty": "(tom)", - "FindBoardsDialog.IntroText": "Sök efter boards", - "FindBoardsDialog.NoResultsFor": "Inga sökresultat för \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Kontrollera stavningen eller sök igen.", - "FindBoardsDialog.SubTitle": "Skriv för att hitta en board. Använd UPP/NER för att bläddra. RETUR för att välja, ESC för att avbryta", - "FindBoardsDialog.Title": "Hitta board", - "GroupBy.hideEmptyGroups": "Dölj {count} tomma grupper", - "GroupBy.showHiddenGroups": "Visa {count} dolda grupper", - "GroupBy.ungroup": "Dela upp grupp", - "HideBoard.MenuOption": "Dölj board", - "KanbanCard.untitled": "Saknar titel", - "MentionSuggestion.is-not-board-member": "(inte board-medlem)", - "Mutator.new-board-from-template": "ny board från en mall", - "Mutator.new-card-from-template": "nytt kort från mall", - "Mutator.new-template-from-card": "ny mall från kort", - "OnboardingTour.AddComments.Body": "Du kan kommentera ämnen och till och med @omnämna andra Mattermost-användare för att få deras uppmärksamhet.", - "OnboardingTour.AddComments.Title": "Lägg till kommentarer", - "OnboardingTour.AddDescription.Body": "Lägg till en beskrivning till kortet så ditt team vet vad kortet handlar om.", - "OnboardingTour.AddDescription.Title": "Lägg till beskrivning", - "OnboardingTour.AddProperties.Body": "Lägg till egenskaper till korten för att göra dem mer informativa.", - "OnboardingTour.AddProperties.Title": "Lägg till egenskaper", - "OnboardingTour.AddView.Body": "Gå hit för att skapa en ny vy för att organisera ditt Board med hjälp av olika layouter.", - "OnboardingTour.AddView.Title": "Lägg till en ny vy", - "OnboardingTour.CopyLink.Body": "Du kan dela dina kort med ditt team genom att kopiera länken och klistra in den i en kanal, ett direktmeddelande eller ett gruppmeddelande.", - "OnboardingTour.CopyLink.Title": "Kopiera länken", - "OnboardingTour.OpenACard.Body": "Öppna ett kort för att utforska produktiva sätt som Boards kan hjälpa dig att organisera ditt arbete.", - "OnboardingTour.OpenACard.Title": "Öppna ett kort", - "OnboardingTour.ShareBoard.Body": "Du kan dela ditt Board internt, inom ditt team, eller publicera den publikt för att visa det utanför din organisation.", - "OnboardingTour.ShareBoard.Title": "Dela Board", - "PersonProperty.board-members": "Board-medlemmar", - "PersonProperty.me": "Jag", - "PersonProperty.non-board-members": "Inte board-medlemmar", - "PropertyMenu.Delete": "Radera", - "PropertyMenu.changeType": "Ändra egenskapstyp", - "PropertyMenu.selectType": "Välj typ av egenskap", - "PropertyMenu.typeTitle": "Typ", - "PropertyType.Checkbox": "Checkruta", - "PropertyType.CreatedBy": "Skapad av", - "PropertyType.CreatedTime": "Skapad tid", - "PropertyType.Date": "Datum", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Flera personer", - "PropertyType.MultiSelect": "Flervalsalternativ", - "PropertyType.Number": "Tal", - "PropertyType.Person": "Person", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Alternativ", - "PropertyType.Text": "Text", - "PropertyType.Unknown": "Okänd", - "PropertyType.UpdatedBy": "Senast ändrad av", - "PropertyType.UpdatedTime": "Senast uppdaterad", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Tom", - "RegistrationLink.confirmRegenerateToken": "Det här kommer att göra tidigare delade länkar ogiltiga. Vill du fortsätta?", - "RegistrationLink.copiedLink": "Kopierad!", - "RegistrationLink.copyLink": "Kopiera länk", - "RegistrationLink.description": "Dela denna länk med andra för att skapa konton:", - "RegistrationLink.regenerateToken": "Återskapa åtkomstnyckel", - "RegistrationLink.tokenRegenerated": "Registreringslänk återskapad", - "ShareBoard.PublishDescription": "Publicera och dela en skrivskyddad länk med alla på webben.", - "ShareBoard.PublishTitle": "Publicera på webben", - "ShareBoard.ShareInternal": "Dela internt", - "ShareBoard.ShareInternalDescription": "Användare som har behörighet kan använda denna länk.", - "ShareBoard.Title": "Dela Board", - "ShareBoard.confirmRegenerateToken": "Det här kommer att göra tidigare delade länkar ogiltiga. Vill du fortsätta?", - "ShareBoard.copiedLink": "Kopierad!", - "ShareBoard.copyLink": "Kopiera länk", - "ShareBoard.regenerate": "Generera nytt Token", - "ShareBoard.searchPlaceholder": "Sök efter personer och kanaler", - "ShareBoard.teamPermissionsText": "Alla i teamet {teamName}", - "ShareBoard.tokenRegenrated": "Åtkomstnyckel återskapad", - "ShareBoard.userPermissionsRemoveMemberText": "Ta bort användare", - "ShareBoard.userPermissionsYouText": "(du)", - "ShareTemplate.Title": "Dela mallen", - "ShareTemplate.searchPlaceholder": "Sök efter personer", - "Sidebar.about": "Om Focalboard", - "Sidebar.add-board": "+ Lägg till tavla", - "Sidebar.changePassword": "Ändra lösenord", - "Sidebar.delete-board": "Radera tavla", - "Sidebar.duplicate-board": "Duplicera Board", - "Sidebar.export-archive": "Exportera arkiv", - "Sidebar.import": "Importera", - "Sidebar.import-archive": "Importera arkiv", - "Sidebar.invite-users": "Bjud in användare", - "Sidebar.logout": "Logga ut", - "Sidebar.new-category.badge": "Ny", - "Sidebar.new-category.drag-boards-cta": "Släpp Boards här...", - "Sidebar.no-boards-in-category": "Inga Boards", - "Sidebar.product-tour": "Produktvisning", - "Sidebar.random-icons": "Slumpmässiga ikoner", - "Sidebar.set-language": "Välj språk", - "Sidebar.set-theme": "Välj tema", - "Sidebar.settings": "Inställningar", - "Sidebar.template-from-board": "Ny mall från Board", - "Sidebar.untitled-board": "(Tavla saknar titel)", - "Sidebar.untitled-view": "(vy utan titel)", - "SidebarCategories.BlocksMenu.Move": "Flytta till...", - "SidebarCategories.CategoryMenu.CreateNew": "Skapa ny kategori", - "SidebarCategories.CategoryMenu.Delete": "Ta bort kategori", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards i {categoryName} flyttas tillbaka till kategorierna Boards. Du har inte tagits bort från några Boards.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Radera kategorin?", - "SidebarCategories.CategoryMenu.Update": "Byt namn på kategori", - "SidebarTour.ManageCategories.Body": "Skapa och hantera egna kategorier. Kategorier är användarspecifika, så om du flyttar en Board till din kategori påverkas inte andra medlemmar som använder samma Board.", - "SidebarTour.ManageCategories.Title": "Hantera kategorier", - "SidebarTour.SearchForBoards.Body": "Öppna Board-växlaren (Cmd/Ctrl + K) för att snabbt söka och lägga till boards i sidofältet.", - "SidebarTour.SearchForBoards.Title": "Sök efter boards", - "SidebarTour.SidebarCategories.Body": "Alla dina boards är nu organiserade i ditt nya sidofält. Du behöver inte längre växla mellan olika arbetsområden. Anpassade kategorier baserade på dina tidigare arbetsytor kan ha skapats automatiskt för dig som en del av din uppgradering av v7.2. Dessa kan tas bort eller redigeras enligt dina önskemål.", - "SidebarTour.SidebarCategories.Link": "Mer information", - "SidebarTour.SidebarCategories.Title": "Kategorier i sidoomenyn", - "SiteStats.total_boards": "Totalt antal tavlor", - "SiteStats.total_cards": "Totalt antal kort", - "TableComponent.add-icon": "Lägg till ikon", - "TableComponent.name": "Namn", - "TableComponent.plus-new": "+ Ny", - "TableHeaderMenu.delete": "Radera", - "TableHeaderMenu.duplicate": "Duplicera", - "TableHeaderMenu.hide": "Dölj", - "TableHeaderMenu.insert-left": "Infoga till vänster", - "TableHeaderMenu.insert-right": "Infoga till höger", - "TableHeaderMenu.sort-ascending": "Sortera stigande", - "TableHeaderMenu.sort-descending": "Sortera fallande", - "TableRow.DuplicateCard": "duplicera kort", - "TableRow.MoreOption": "Fler åtgärder", - "TableRow.open": "Öppna", - "TopBar.give-feedback": "Ge återkoppling", - "URLProperty.copiedLink": "Kopierad!", - "URLProperty.copy": "Kopiera", - "URLProperty.edit": "Ändra", - "UndoRedoHotKeys.canRedo": "Gör om", - "UndoRedoHotKeys.canRedo-with-description": "Gör om {description}", - "UndoRedoHotKeys.canUndo": "Ångra", - "UndoRedoHotKeys.canUndo-with-description": "Ångra {description}", - "UndoRedoHotKeys.cannotRedo": "Inget att göra om igen", - "UndoRedoHotKeys.cannotUndo": "Inget att ångra", - "ValueSelector.noOptions": "Inga alternativ. Börja skriva för att lägga till den första!", - "ValueSelector.valueSelector": "Värdeväljare", - "ValueSelectorLabel.openMenu": "Öppna meny", - "VersionMessage.help": "Kolla in vad som är nytt i den här versionen.", - "VersionMessage.learn-more": "Mer information", - "View.AddView": "Lägg till vy", - "View.Board": "Tavla", - "View.DeleteView": "Radera vy", - "View.DuplicateView": "Duplicera vy", - "View.Gallery": "Galleri", - "View.NewBoardTitle": "Tavelvy", - "View.NewCalendarTitle": "Kalendervy", - "View.NewGalleryTitle": "Galleri vy", - "View.NewTableTitle": "Tabellvy", - "View.NewTemplateDefaultTitle": "Namnlös mall", - "View.NewTemplateTitle": "Namnlös", - "View.Table": "Tabell", - "ViewHeader.add-template": "Ny mall", - "ViewHeader.delete-template": "Radera", - "ViewHeader.display-by": "Visa efter: {property}", - "ViewHeader.edit-template": "Redigera", - "ViewHeader.empty-card": "Blankt kort", - "ViewHeader.export-board-archive": "Exportera tavelarkivet", - "ViewHeader.export-complete": "Export slutförd!", - "ViewHeader.export-csv": "Exportera till CSV", - "ViewHeader.export-failed": "Export misslyckades!", - "ViewHeader.filter": "Filter", - "ViewHeader.group-by": "Gruppera på: {property}", - "ViewHeader.new": "Ny", - "ViewHeader.properties": "Egenskaper", - "ViewHeader.properties-menu": "Menyn Egenskaper", - "ViewHeader.search-text": "Sök efter kort", - "ViewHeader.select-a-template": "Välj en mall", - "ViewHeader.set-default-template": "Sätt som förvald", - "ViewHeader.sort": "Sortera", - "ViewHeader.untitled": "Saknar titel", - "ViewHeader.view-header-menu": "Visa huvudmenyn", - "ViewHeader.view-menu": "Visa menyn", - "ViewLimitDialog.Heading": "Gränsen för antalet visningar per board har uppnåtts", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Uppgradera", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Meddela administratör", - "ViewLimitDialog.Subtext.Admin": "Uppgradera till Professional- eller Enterprise-abonnemang.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Läs mer om våra abonnemang.", - "ViewLimitDialog.Subtext.RegularUser": "Meddela din administratör att uppgradera till Professional- eller Enterprise-abonnemang.", - "ViewLimitDialog.UpgradeImg.AltText": "bild som föreställer en uppgradering", - "ViewLimitDialog.notifyAdmin.Success": "Din systemadministratör har blivit notifierad", - "ViewTitle.hide-description": "dölj beskrivning", - "ViewTitle.pick-icon": "Välj ikon", - "ViewTitle.random-icon": "Slumpmässig", - "ViewTitle.remove-icon": "Ta bort ikon", - "ViewTitle.show-description": "visa beskrivning", - "ViewTitle.untitled-board": "Board utan titel", - "WelcomePage.Description": "Boards är ett projekthanteringsverktyg som hjälper till att definiera, organisera, spåra och hantera arbete mellan team med hjälp av en välbekant Kanban-vy.", - "WelcomePage.Explore.Button": "Starta en rundtur", - "WelcomePage.Heading": "Välkommen till Boards", - "WelcomePage.NoThanks.Text": "Nej tack, jag löser det själv", - "WelcomePage.StartUsingIt.Text": "Börja använda den", - "Workspace.editing-board-template": "Du redigerar en tavelmall.", - "badge.guest": "Gäst", - "boardPage.confirm-join-button": "Gå med", - "boardPage.confirm-join-text": "Du är på väg att gå med i en privat tavla utan att läggas till av taveladministratören. Vill du gå med i den här privata tavlan?", - "boardPage.confirm-join-title": "Gå med i en privat tavla", - "boardSelector.confirm-link-board": "Koppla board till kanal", - "boardSelector.confirm-link-board-button": "Ja, koppla board", - "boardSelector.confirm-link-board-subtext": "När du kopplar \"{boardName}\" till kanalen kommer alla medlemmar i kanalen (befintliga och nya) att kunna redigera den, gäster exkluderade. Du kan när som helst koppla bort ett board från kanalen.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "När du kopplar \"{boardName}\" till kanalen kommer alla medlemmar i kanalen (befintliga och nya) att kunna redigera den, exkluderat gäster.{lineBreak}Denna board är kopplad till en annan kanal. Den kommer kopplas bort om du väljer att koppla den hit.", - "boardSelector.create-a-board": "Skapa en board", - "boardSelector.link": "Länk", - "boardSelector.search-for-boards": "Sök efter boards", - "boardSelector.title": "Länkade Boards", - "boardSelector.unlink": "Koppla ifrån", - "calendar.month": "Månad", - "calendar.today": "IDAG", - "calendar.week": "Vecka", - "centerPanel.undefined": "Ingen {propertyName}", - "centerPanel.unknown-user": "Okänd användare", - "cloudMessage.learn-more": "Läs mer", - "createImageBlock.failed": "Filen kunde inte laddas upp eftersom gränsen för filstorlek har uppnåtts.", - "default-properties.badges": "Kommentarer och beskrivning", - "default-properties.title": "Titel", - "error.back-to-home": "Tillbaka till förstasidan", - "error.back-to-team": "Tillbaka till team", - "error.board-not-found": "Board finns inte.", - "error.go-login": "Logga in", - "error.invalid-read-only-board": "Du har inte tillgång till denna board. Logga in för att få tillgång till Boards.", - "error.not-logged-in": "Din session kan ha löpt ut eller så är du inte inloggad. Logga in igen för att få tillgång till Boards.", - "error.page.title": "Oops, något gick fel", - "error.team-undefined": "Inte ett giltigt team.", - "error.unknown": "Ett fel inträffade.", - "generic.previous": "Föregående", - "guest-no-board.subtitle": "Du har inte tillgång till något board i teamet ännu, vänta tills någon lägger till dig i ett board.", - "guest-no-board.title": "Inga board ännu", - "imagePaste.upload-failed": "Några filer kunde inte laddas upp eftersom gränsen för filstorlek har nåtts.", - "limitedCard.title": "Dolda kort", - "login.log-in-button": "Logga in", - "login.log-in-title": "Logga in", - "login.register-button": "eller skapa ett konto om du inte redan har ett", - "new_channel_modal.create_board.empty_board_description": "Skapa en ny tom tavla", - "new_channel_modal.create_board.empty_board_title": "Tom tavla", - "new_channel_modal.create_board.select_template_placeholder": "Välj en mall", - "new_channel_modal.create_board.title": "Skapa en tavla för den här kanalen", - "notification-box-card-limit-reached.close-tooltip": "Sov i 10 dagar", - "notification-box-card-limit-reached.contact-link": "notifiera din administratör", - "notification-box-card-limit-reached.link": "Uppgradera till ett betal-abonnemang", - "notification-box-card-limit-reached.title": "{cards} kort dolda från board", - "notification-box-cards-hidden.title": "Åtgärden dolde ett annat kort", - "notification-box.card-limit-reached.not-admin.text": "Om du vill komma åt arkiverade kort kan du {contactLink} för att uppgradera till ett betal-abonnemang.", - "notification-box.card-limit-reached.text": "Gränsen för kort har nåtts, för att visa äldre kort, {link}", - "person.add-user-to-board": "Lägg till {username} till board", - "person.add-user-to-board-confirm-button": "Lägg till i board", - "person.add-user-to-board-permissions": "Behörigheter", - "person.add-user-to-board-question": "Vill du lägga till {username} till board?", - "person.add-user-to-board-warning": "{username} är inte medlem i board och kommer inte att få några meddelanden om den.", - "register.login-button": "eller logga in om du redan har ett konto", - "register.signup-title": "Registrera dig för ett konto", - "rhs-board-non-admin-msg": "Du är inte administratör för board", - "rhs-boards.add": "Lägg till", - "rhs-boards.dm": "DM", - "rhs-boards.gm": "GM", - "rhs-boards.header.dm": "detta direktmeddelande", - "rhs-boards.header.gm": "detta gruppmeddelande", - "rhs-boards.last-update-at": "Senast uppdaterad: {datetime}", - "rhs-boards.link-boards-to-channel": "Länka boards till {channelName}", - "rhs-boards.linked-boards": "Länkade boards", - "rhs-boards.no-boards-linked-to-channel": "Inga boards är länkade till {channelName} ännu", - "rhs-boards.no-boards-linked-to-channel-description": "Boards är ett projekthanteringsverktyg som hjälper till att definiera, organisera, spåra och hantera arbete mellan olika team med hjälp av en välbekant Kanban-vy.", - "rhs-boards.unlink-board": "Ta bort länk till Board", - "rhs-boards.unlink-board1": "Koppla bort board", - "rhs-channel-boards-header.title": "Boards", - "share-board.publish": "Publicera", - "share-board.share": "Dela", - "shareBoard.channels-select-group": "Channels", - "shareBoard.confirm-change-team-role.body": "Alla i denna board som har lägre behörighet än rollen \"{role}\" kommer nu att befordras till {role}. Är du säker på att du vill ändra den minsta rollen för board?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Ändra lägsta board-rollen", - "shareBoard.confirm-change-team-role.title": "Ändra lägsta board-rollen", - "shareBoard.confirm-link-channel": "Koppla board till kanal", - "shareBoard.confirm-link-channel-button": "Koppla kanal", - "shareBoard.confirm-link-channel-button-with-other-channel": "Koppla och koppla bort här", - "shareBoard.confirm-link-channel-subtext": "När du kopplar en kanal till en board kommer alla medlemmar (befintliga och nya) kunna redigera den, exklusive gäster.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "När du kopplar en kanal till en board kommer alla medlemmar (befintliga och nya) kunna redigera den, exklusive gäster.{lineBreak}Denna Board är just nu kopplad till en annan kanal. Den kommer kopplas bort om du väljer att koppla den till denna kanal.", - "shareBoard.confirm-unlink.body": "När du kopplar bort en kanal från en board kommer alla medlemmar (befintliga och nya) tappa behörigheten om de inte blir tilldelade en egen behörighet.", - "shareBoard.confirm-unlink.confirmBtnText": "Koppla bort kanal", - "shareBoard.confirm-unlink.title": "Koppla bort kanal från board", - "shareBoard.lastAdmin": "En Board måste ha minst en administratör", - "shareBoard.members-select-group": "Medlemmar", - "shareBoard.unknown-channel-display-name": "Okänd kanal", - "tutorial_tip.finish_tour": "Klar", - "tutorial_tip.got_it": "Då förstår jag", - "tutorial_tip.ok": "Nästa", - "tutorial_tip.out": "Välj bort att få tips.", - "tutorial_tip.seen": "Sett detta tidigare?" -} diff --git a/webapp/boards/i18n/tr.json b/webapp/boards/i18n/tr.json deleted file mode 100644 index 0c9614a803..0000000000 --- a/webapp/boards/i18n/tr.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "AdminBadge.SystemAdmin": "Yönetici", - "AdminBadge.TeamAdmin": "Takım yöneticisi", - "AppBar.Tooltip": "Bağlantılı panoları aç/kapat", - "Attachment.Attachment-title": "Ek dosya", - "AttachmentBlock.DeleteAction": "sil", - "AttachmentBlock.addElement": "{type} ekle", - "AttachmentBlock.delete": "Ek dosya silindi.", - "AttachmentBlock.failed": "Dosya boyutu sınırı aşıldığından bu dosya yüklenemedi.", - "AttachmentBlock.upload": "Ek dosya yükleniyor.", - "AttachmentBlock.uploadSuccess": "Ek dosya yüklendi.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Sil", - "AttachmentElement.download": "İndir", - "AttachmentElement.upload-percentage": "Yükleniyor...(%{uploadPercent})", - "BoardComponent.add-a-group": "+ Grup ekle", - "BoardComponent.delete": "Sil", - "BoardComponent.hidden-columns": "Gizli sütunlar", - "BoardComponent.hide": "Gizle", - "BoardComponent.new": "+ Yeni", - "BoardComponent.no-property": "{property} yok", - "BoardComponent.no-property-title": "{property} alanı boş olan ögeler buraya atanır. Bu sütun silinemez.", - "BoardComponent.show": "Görüntüle", - "BoardMember.schemeAdmin": "Yönetici", - "BoardMember.schemeCommenter": "Yorumcu", - "BoardMember.schemeEditor": "Düzenleyici", - "BoardMember.schemeNone": "Yok", - "BoardMember.schemeViewer": "Görüntüleyici", - "BoardMember.unlinkChannel": "Bağlantıyı kaldır", - "BoardPage.newVersion": "Yeni bir pano sürümü yayınlanmış. Yeniden yüklemek için buraya tıklayın.", - "BoardPage.syncFailed": "Pano silinmiş ya da erişim izni geri alınmış olabilir.", - "BoardTemplateSelector.add-template": "Yeni kalıp ekle", - "BoardTemplateSelector.create-empty-board": "Boş bir pano ekle", - "BoardTemplateSelector.delete-template": "Sil", - "BoardTemplateSelector.description": "Kalıplardan birini kullanarak ya da sıfırdan başlayarak yan çubuğa bir pano ekleyin.", - "BoardTemplateSelector.edit-template": "Düzenle", - "BoardTemplateSelector.plugin.no-content-description": "Aşağıdaki kalıplardan birini kullanarak ya da sıfırdan başlayarak yan çubuğa bir pano ekleyin.", - "BoardTemplateSelector.plugin.no-content-title": "Bir pano ekleyin", - "BoardTemplateSelector.title": "Bir pano ekle", - "BoardTemplateSelector.use-this-template": "Bu kalıp kullanılsın", - "BoardsSwitcher.Title": "Pano arama", - "BoardsUnfurl.Limited": "Kart arşivlendiğinden ek bilgiler gizleniyor", - "BoardsUnfurl.Remainder": "+{remainder} diğer", - "BoardsUnfurl.Updated": "Güncellenme: {time}", - "Calculations.Options.average.displayName": "Ortalama", - "Calculations.Options.average.label": "Ortalama", - "Calculations.Options.count.displayName": "Sayı", - "Calculations.Options.count.label": "Sayı", - "Calculations.Options.countChecked.displayName": "İşaretlenmiş", - "Calculations.Options.countChecked.label": "İşaretlenmiş sayısı", - "Calculations.Options.countUnchecked.displayName": "İşaretlenmemiş", - "Calculations.Options.countUnchecked.label": "İşaretlenmemiş sayısı", - "Calculations.Options.countUniqueValue.displayName": "Eşsiz", - "Calculations.Options.countUniqueValue.label": "Eşsiz değer sayısı", - "Calculations.Options.countValue.displayName": "Değer", - "Calculations.Options.countValue.label": "Değer sayısı", - "Calculations.Options.dateRange.displayName": "Aralık", - "Calculations.Options.dateRange.label": "Aralık", - "Calculations.Options.earliest.displayName": "En erken", - "Calculations.Options.earliest.label": "En erken", - "Calculations.Options.latest.displayName": "En geç", - "Calculations.Options.latest.label": "En geç", - "Calculations.Options.max.displayName": "En fazla", - "Calculations.Options.max.label": "En fazla", - "Calculations.Options.median.displayName": "Orta değer", - "Calculations.Options.median.label": "Orta değer", - "Calculations.Options.min.displayName": "En az", - "Calculations.Options.min.label": "En az", - "Calculations.Options.none.displayName": "Hesapla", - "Calculations.Options.none.label": "Yok", - "Calculations.Options.percentChecked.displayName": "İşaretlenmiş", - "Calculations.Options.percentChecked.label": "İşaretlenmiş yüzdesi", - "Calculations.Options.percentUnchecked.displayName": "İşaretlenmemiş", - "Calculations.Options.percentUnchecked.label": "İşaretlenmemiş yüzdesi", - "Calculations.Options.range.displayName": "Aralık", - "Calculations.Options.range.label": "Aralık", - "Calculations.Options.sum.displayName": "Toplam", - "Calculations.Options.sum.label": "Toplam", - "CalendarCard.untitled": "Adlandırılmamış", - "CardActionsMenu.copiedLink": "Kopyalandı!", - "CardActionsMenu.copyLink": "Bağlantıyı kopyala", - "CardActionsMenu.delete": "Sil", - "CardActionsMenu.duplicate": "Kopyala", - "CardBadges.title-checkboxes": "İşaret kutuları", - "CardBadges.title-comments": "Yorumlar", - "CardBadges.title-description": "Bu kartın bir açıklaması var", - "CardDetail.Attach": "Dosya ekle", - "CardDetail.Follow": "İzle", - "CardDetail.Following": "İzleniyor", - "CardDetail.add-content": "İçerik ekle", - "CardDetail.add-icon": "Simge ekle", - "CardDetail.add-property": "+ Bir özellik ekle", - "CardDetail.addCardText": "kart metni ekle", - "CardDetail.limited-body": "Professional ya da Enterprise tarifesine geçin.", - "CardDetail.limited-button": "Üst tarifeye geç", - "CardDetail.limited-title": "Bu kart gizli", - "CardDetail.moveContent": "Kart içeriğini taşı", - "CardDetail.new-comment-placeholder": "Bir yorum ekle...", - "CardDetailProperty.confirm-delete-heading": "Özelliği silmeyi onaylayın", - "CardDetailProperty.confirm-delete-subtext": "\"{propertyName}\" özelliğini silmek istediğinize emin misiniz? Bu işlem özelliği panodaki tüm kartlardan siler.", - "CardDetailProperty.confirm-property-name-change-subtext": "\"{propertyName}\" {customText} özelliğini değiştirmek istediğinize emin misiniz? Bu işlem bu panodaki {numOfCards} kartı etkiler ve veri kaybına yol açabilir.", - "CardDetailProperty.confirm-property-type-change": "Özellik türü değişimini onaylayın", - "CardDetailProperty.delete-action-button": "Sil", - "CardDetailProperty.property-change-action-button": "Özelliği değiştir", - "CardDetailProperty.property-changed": "Özellik değiştirildi!", - "CardDetailProperty.property-deleted": "{propertyName} silindi!", - "CardDetailProperty.property-name-change-subtext": "\"{oldPropType}\" türünden \"{newPropType}\" türüne", - "CardDetial.limited-link": "Tarifelerimiz hakkında ayrıntılı bilgi alın.", - "CardDialog.delete-confirmation-dialog-attachment": "Ek dosyanın silinmesini onaylayın", - "CardDialog.delete-confirmation-dialog-button-text": "Sil", - "CardDialog.delete-confirmation-dialog-heading": "Kartı silmeyi onaylayın", - "CardDialog.editing-template": "Bir kalıbı düzenliyorsunuz.", - "CardDialog.nocard": "Bu kart bulunamadı ya da erişilebilir değil.", - "Categories.CreateCategoryDialog.CancelText": "İptal", - "Categories.CreateCategoryDialog.CreateText": "Ekle", - "Categories.CreateCategoryDialog.Placeholder": "Kategorinize bir ad verin", - "Categories.CreateCategoryDialog.UpdateText": "Güncelle", - "CenterPanel.Login": "Oturum aç", - "CenterPanel.Share": "Paylaş", - "ChannelIntro.CreateBoard": "Bir pano ekle", - "ColorOption.selectColor": "{color} rengi seçin", - "Comment.delete": "Sil", - "CommentsList.send": "Gönder", - "ConfirmPerson.empty": "Boş", - "ConfirmPerson.search": "Arama...", - "ConfirmationDialog.cancel-action": "İptal", - "ConfirmationDialog.confirm-action": "Onayla", - "ContentBlock.Delete": "Sil", - "ContentBlock.DeleteAction": "sil", - "ContentBlock.addElement": "{type} ekle", - "ContentBlock.checkbox": "işaret kutusu", - "ContentBlock.divider": "ayıraç", - "ContentBlock.editCardCheckbox": "değiştirilmiş işaret kutusu", - "ContentBlock.editCardCheckboxText": "kart metnini düzenle", - "ContentBlock.editCardText": "kart metnini düzenle", - "ContentBlock.editText": "Metni düzenle...", - "ContentBlock.image": "görsel", - "ContentBlock.insertAbove": "Üste ekle", - "ContentBlock.moveBlock": "kart içeriğini taşı", - "ContentBlock.moveDown": "Alta taşı", - "ContentBlock.moveUp": "Üste taşı", - "ContentBlock.text": "metin", - "DateFilter.empty": "Boş", - "DateRange.clear": "Temizle", - "DateRange.empty": "Boş", - "DateRange.endDate": "Bitiş tarihi", - "DateRange.today": "Bugün", - "DeleteBoardDialog.confirm-cancel": "İptal", - "DeleteBoardDialog.confirm-delete": "Sil", - "DeleteBoardDialog.confirm-info": "“{boardTitle}” panosunu silmek istediğinize emin misiniz? Silme işlemi bu panodaki tüm kartları siler.", - "DeleteBoardDialog.confirm-info-template": "“{boardTitle}” pano kalıbını silmek istediğinize emin misiniz?", - "DeleteBoardDialog.confirm-tite": "Panoyu silmeyi onayla", - "DeleteBoardDialog.confirm-tite-template": "Pano kalıbını silmeyi onayla", - "Dialog.closeDialog": "Pencereyi kapat", - "EditableDayPicker.today": "Bugün", - "Error.mobileweb": "Mobil web desteği şu anda erken beta aşamasındadır. Tüm işlevler kullanılamıyor olabilir.", - "Error.websocket-closed": "Websoket bağlantısı kesildi. Bu sorun sürerse, sunucu ya da web vekil sunucu yapılandırmanızı denetleyin.", - "Filter.contains": "şunu içeren", - "Filter.ends-with": "şununla biten", - "Filter.includes": "şunu içeren", - "Filter.is": "şu olan", - "Filter.is-after": "şundan sonra", - "Filter.is-before": "şundan önce", - "Filter.is-empty": "boş olan", - "Filter.is-not-empty": "boş olmayan", - "Filter.is-not-set": "şuna ayarlanmamış olan", - "Filter.is-set": "şuna ayarlanmış olan", - "Filter.isafter": "şundan sonra", - "Filter.isbefore": "şundan önce", - "Filter.not-contains": "şunu içermeyen", - "Filter.not-ends-with": "şununla bitmeyen", - "Filter.not-includes": "şunu içermeyen", - "Filter.not-starts-with": "şununla başlamayan", - "Filter.starts-with": "şununla başlayan", - "FilterByText.placeholder": "metni süz", - "FilterComponent.add-filter": "+ Süzgeç ekle", - "FilterComponent.delete": "Sil", - "FilterValue.empty": "(boş)", - "FindBoardsDialog.IntroText": "Pano arama", - "FindBoardsDialog.NoResultsFor": "\"{searchQuery}\" için bir sonuç bulunamadı", - "FindBoardsDialog.NoResultsSubtext": "Yazımı denetleyin ya da başka bir arama yapmayı deneyin.", - "FindBoardsDialog.SubTitle": "Bulmak istediğiniz pano adını yazmaya başlayın. Gezinmek için YUKAR/AŞAĞI, seçmek için ENTER, vazgeçmek için ESC tuşlarını kullanın", - "FindBoardsDialog.Title": "Pano arama", - "GroupBy.hideEmptyGroups": "{count} boş grubu gizle", - "GroupBy.showHiddenGroups": "{count} gizli grubu görüntüle", - "GroupBy.ungroup": "Gruplamayı kaldır", - "HideBoard.MenuOption": "Panoyu gizle", - "KanbanCard.untitled": "Adlandırılmamış", - "MentionSuggestion.is-not-board-member": "(pano üyesi değil)", - "Mutator.new-board-from-template": "kalıptan yeni pano", - "Mutator.new-card-from-template": "kalıptan yeni kart oluştur", - "Mutator.new-template-from-card": "karttan yeni kalıp oluştur", - "OnboardingTour.AddComments.Body": "Sorunlar hakkında yorum yapabilir ve Mattermost kullanıcılarının dikkatini çekmek için @anabilirsiniz.", - "OnboardingTour.AddComments.Title": "Yorum yap", - "OnboardingTour.AddDescription.Body": "Takım arkadaşlarınızın kartın ne ile ilgili olduğunu anlaması için kartınıza bir açıklama ekleyin.", - "OnboardingTour.AddDescription.Title": "Açıklama ekle", - "OnboardingTour.AddProperties.Body": "Daha güçlü kılmak için kartlara çeşitli özellikler ekleyin.", - "OnboardingTour.AddProperties.Title": "Özellikler ekle", - "OnboardingTour.AddView.Body": "Farklı görünümler kullanarak panonuzu düzenleyecek yeni bir görünüm oluşturmak için buraya gidin.", - "OnboardingTour.AddView.Title": "Yeni bir görünüm ekle", - "OnboardingTour.CopyLink.Body": "Kartlarınızı takım arkadaşlarınızla paylaşmak için bağlantıyı kopyalayıp bir kanala, doğrudan iletiye veya grup iletisine yapıştırın.", - "OnboardingTour.CopyLink.Title": "Bağlantıyı kopyala", - "OnboardingTour.OpenACard.Body": "Panoların işinizi düzenlemenize yardımcı olabileceği güçlü yolları keşfetmek için bir kart açın.", - "OnboardingTour.OpenACard.Title": "Bir kart açın", - "OnboardingTour.ShareBoard.Body": "Panonuzu içeride, ekibiniz ile paylaşabilir ya da kuruluşunuzun dışında herkese açık olarak yayınlayabilirsiniz.", - "OnboardingTour.ShareBoard.Title": "Panoyu paylaş", - "PersonProperty.board-members": "Pano üyeleri", - "PersonProperty.me": "Benim", - "PersonProperty.non-board-members": "Pano üyesi olmayanlar", - "PropertyMenu.Delete": "Sil", - "PropertyMenu.changeType": "Özellik türünü değiştir", - "PropertyMenu.selectType": "Özellik türünü seçin", - "PropertyMenu.typeTitle": "Tür", - "PropertyType.Checkbox": "İşaret kutusu", - "PropertyType.CreatedBy": "Oluşturan", - "PropertyType.CreatedTime": "Oluşturulma zamanı", - "PropertyType.Date": "Tarih", - "PropertyType.Email": "E-posta", - "PropertyType.MultiPerson": "Çok kişi", - "PropertyType.MultiSelect": "Çoklu seçim", - "PropertyType.Number": "Sayı", - "PropertyType.Person": "Kişi", - "PropertyType.Phone": "Telefon", - "PropertyType.Select": "Seçin", - "PropertyType.Text": "Metin", - "PropertyType.Unknown": "Bilinmiyor", - "PropertyType.UpdatedBy": "Son güncelleyen", - "PropertyType.UpdatedTime": "Son güncelleme zamanı", - "PropertyType.Url": "Adres", - "PropertyValueElement.empty": "Boş", - "RegistrationLink.confirmRegenerateToken": "Bu işlem daha önce paylaşılmış bağlantıları geçersiz kılacak. İlerlemek istiyor musunuz?", - "RegistrationLink.copiedLink": "Kopyalandı!", - "RegistrationLink.copyLink": "Bağlantıyı kopyala", - "RegistrationLink.description": "Başkalarının hesap ekleyebilmesi için bu bağlantıyı paylaş:", - "RegistrationLink.regenerateToken": "Kodu yeniden oluştur", - "RegistrationLink.tokenRegenerated": "Kayıt bağlantısı yeniden oluşturuldu", - "ShareBoard.PublishDescription": "Web üzerinde herkese açık olarak \"salt okunur\" bir bağlantı yayınlayın ve paylaşın.", - "ShareBoard.PublishTitle": "Web üzerinde yayınla", - "ShareBoard.ShareInternal": "İçeride paylaş", - "ShareBoard.ShareInternalDescription": "İzni olan kullanıcılar bu bağlantıyı kullanabilecek.", - "ShareBoard.Title": "Panoyu paylaş", - "ShareBoard.confirmRegenerateToken": "Bu işlem daha önce paylaşılmış bağlantıları geçersiz kılacak. İlerlemek istiyor musunuz?", - "ShareBoard.copiedLink": "Kopyalandı!", - "ShareBoard.copyLink": "Bağlantıyı kopyala", - "ShareBoard.regenerate": "Kodu yeniden oluştur", - "ShareBoard.searchPlaceholder": "Kişi ve kanal arama", - "ShareBoard.teamPermissionsText": "{teamName} takımındaki herkes", - "ShareBoard.tokenRegenrated": "Kod yeniden oluşturuldu", - "ShareBoard.userPermissionsRemoveMemberText": "Üyelikten çıkar", - "ShareBoard.userPermissionsYouText": "(Siz)", - "ShareTemplate.Title": "Kalıbı paylaş", - "ShareTemplate.searchPlaceholder": "Kişi arama", - "Sidebar.about": "Focalboard hakkında", - "Sidebar.add-board": "+ Pano ekle", - "Sidebar.changePassword": "Parola değiştir", - "Sidebar.delete-board": "Panoyu sil", - "Sidebar.duplicate-board": "Panoyu kopyala", - "Sidebar.export-archive": "Arşivi dışa aktar", - "Sidebar.import": "İçe aktar", - "Sidebar.import-archive": "Arşivi içe aktar", - "Sidebar.invite-users": "Kullanıcıları çağır", - "Sidebar.logout": "Oturumu kapat", - "Sidebar.new-category.badge": "Yeni", - "Sidebar.new-category.drag-boards-cta": "Panoları sürükleyip buraya bırakın...", - "Sidebar.no-boards-in-category": "İçeride bir pano yok", - "Sidebar.product-tour": "Tanıtım turu", - "Sidebar.random-icons": "Rastgele simgeler", - "Sidebar.set-language": "Dili ayarla", - "Sidebar.set-theme": "Temayı ayarla", - "Sidebar.settings": "Ayarlar", - "Sidebar.template-from-board": "Panodan yeni kalıp", - "Sidebar.untitled-board": "(Adlandırılmamış pano)", - "Sidebar.untitled-view": "(Adlandırılmamış görünüm)", - "SidebarCategories.BlocksMenu.Move": "Şuraya taşı...", - "SidebarCategories.CategoryMenu.CreateNew": "Yeni kategori ekle", - "SidebarCategories.CategoryMenu.Delete": "Kategoriyi sił", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} içindeki panolar Panolar kategorisine taşınacak. Herhangi bir panodan çıkarılmayacaksınız.", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Bu kategori silinsin mi?", - "SidebarCategories.CategoryMenu.Update": "Kategoriyi yeniden adlandır", - "SidebarTour.ManageCategories.Body": "Özel kategoriler oluşturun ve yönetin. Kategoriler kullanıcıya özeldir, bu nedenle bir panoyu kendi kategorinize taşımanız aynı panoyu kullanan diğer üyeleri etkilemez.", - "SidebarTour.ManageCategories.Title": "Kategori yönetimi", - "SidebarTour.SearchForBoards.Body": "Panoları hızlıca aramak ve yan çubuğunuza eklemek için pano değiştiriciyi (Cmd/Ctrl + K) açın.", - "SidebarTour.SearchForBoards.Title": "Pano arama", - "SidebarTour.SidebarCategories.Body": "Tüm panolarınızı artık yeni yan çubuğunuz altında bulabilirsiniz. Artık çalışma alanları arasında geçiş yapmanıza gerek yok. Önceki çalışma alanlarınıza göre eklenmiş tek seferlik özel kategoriler, 7.2 sürümüne güncellemenizin bir parçası olarak otomatik şekilde eklenmiş olabilir. Bunları isteğinize göre kaldırabilir ya da düzenleyebilirsiniz.", - "SidebarTour.SidebarCategories.Link": "Ayrıntılı bilgi alın", - "SidebarTour.SidebarCategories.Title": "Yan çubuk kategorileri", - "SiteStats.total_boards": "Toplam pano", - "SiteStats.total_cards": "Toplam kart", - "TableComponent.add-icon": "Simge ekle", - "TableComponent.name": "Ad", - "TableComponent.plus-new": "+ Yeni", - "TableHeaderMenu.delete": "Sil", - "TableHeaderMenu.duplicate": "Kopya oluştur", - "TableHeaderMenu.hide": "Gizle", - "TableHeaderMenu.insert-left": "Sola ekle", - "TableHeaderMenu.insert-right": "Sağa ekle", - "TableHeaderMenu.sort-ascending": "Artan sıralama", - "TableHeaderMenu.sort-descending": "Azalan sıralama", - "TableRow.DuplicateCard": "kartı kopyala", - "TableRow.MoreOption": "Diğer işlemler", - "TableRow.open": "Aç", - "TopBar.give-feedback": "Geri bildirimde bulunun", - "URLProperty.copiedLink": "Kopyalandı!", - "URLProperty.copy": "Kopyala", - "URLProperty.edit": "Düzenle", - "UndoRedoHotKeys.canRedo": "Yinele", - "UndoRedoHotKeys.canRedo-with-description": "{description} yinele", - "UndoRedoHotKeys.canUndo": "Geri al", - "UndoRedoHotKeys.canUndo-with-description": "{description} geri al", - "UndoRedoHotKeys.cannotRedo": "Yinelenecek bir işlem yok", - "UndoRedoHotKeys.cannotUndo": "Geri alınacak bir işlem yok", - "ValueSelector.noOptions": "Herhangi bir seçenek yok. İlk seçeneği eklemek için yazmaya başlayın!", - "ValueSelector.valueSelector": "Değer seçici", - "ValueSelectorLabel.openMenu": "Menüyü aç", - "VersionMessage.help": "Bu sürümdeki yeniliklere bakın.", - "VersionMessage.learn-more": "Ayrıntılı bilgi alın", - "View.AddView": "Görünüm ekle", - "View.Board": "Pano", - "View.DeleteView": "Görünümü sil", - "View.DuplicateView": "Görünümü kopyala", - "View.Gallery": "Galeri", - "View.NewBoardTitle": "Pano görünümü", - "View.NewCalendarTitle": "Takvim görünümü", - "View.NewGalleryTitle": "Galeri görünümü", - "View.NewTableTitle": "Tablo görünümü", - "View.NewTemplateDefaultTitle": "Adlandırılmamış kalıp", - "View.NewTemplateTitle": "Adlandırılmamış", - "View.Table": "Tablo", - "ViewHeader.add-template": "Yeni kalıp", - "ViewHeader.delete-template": "Sil", - "ViewHeader.display-by": "Görünüm: {property}", - "ViewHeader.edit-template": "Düzenle", - "ViewHeader.empty-card": "Boş kart", - "ViewHeader.export-board-archive": "Pano arşivini dışa aktar", - "ViewHeader.export-complete": "Dışa aktarıldı!", - "ViewHeader.export-csv": "CSV olarak dışa aktar", - "ViewHeader.export-failed": "Dışa aktarılamadı!", - "ViewHeader.filter": "Süz", - "ViewHeader.group-by": "Grupla: {property}", - "ViewHeader.new": "Yeni", - "ViewHeader.properties": "Özellikler", - "ViewHeader.properties-menu": "Özellikler menüsü", - "ViewHeader.search-text": "Kart arama", - "ViewHeader.select-a-template": "Bir kalıp seçin", - "ViewHeader.set-default-template": "Varsayılan olarak ata", - "ViewHeader.sort": "Sırala", - "ViewHeader.untitled": "Adlandırılmamış", - "ViewHeader.view-header-menu": "Başlık menüsünü görüntüle", - "ViewHeader.view-menu": "Menüyü görüntüle", - "ViewLimitDialog.Heading": "Bir panoyu görüntüleme sınırına ulaşıldı", - "ViewLimitDialog.PrimaryButton.Title.Admin": "Üst tarifeye geç", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "Yöneticiyi bilgilendir", - "ViewLimitDialog.Subtext.Admin": "Professional ya da Enterprise tarifemize geçin.", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "Tarifelerimiz hakkında ayrıntılı bilgi alın.", - "ViewLimitDialog.Subtext.RegularUser": "Yöneticinizi Professional ya da Enterprise tarifesine geçmesi hakkında bilgilendirin.", - "ViewLimitDialog.UpgradeImg.AltText": "üst tarifeye geçiş görseli", - "ViewLimitDialog.notifyAdmin.Success": "Yöneticiniz bilgilendirildi", - "ViewTitle.hide-description": "açıklamayı gizle", - "ViewTitle.pick-icon": "Simge seçin", - "ViewTitle.random-icon": "Rastgele", - "ViewTitle.remove-icon": "Simgeyi kaldır", - "ViewTitle.show-description": "açıklamayı görüntüle", - "ViewTitle.untitled-board": "Adlandırılmamış pano", - "WelcomePage.Description": "Pano, alışılmış Kanban panosu görünümünde takımların işleri tanımlamasını, düzenlemesini, izlemesi ve yönetmesini sağlayan bir proje yönetimi aracıdır.", - "WelcomePage.Explore.Button": "Tura çıkın", - "WelcomePage.Heading": "Panolara hoş geldiniz", - "WelcomePage.NoThanks.Text": "Hayır teşekkürler, kendim anlayacağım", - "WelcomePage.StartUsingIt.Text": "Kullanmaya başlayın", - "Workspace.editing-board-template": "Bir pano kalıbını düzenliyorsunuz.", - "badge.guest": "Konuk", - "boardPage.confirm-join-button": "Katıl", - "boardPage.confirm-join-text": "Bir özel kanala, pano yöneticisi tarafından açıkça eklenmeden katılmak üzeresiniz. Bu özel kanala katılmak istediğinize emin misiniz?", - "boardPage.confirm-join-title": "Özel kanala katıl", - "boardSelector.confirm-link-board": "Panoyu kanala bağla", - "boardSelector.confirm-link-board-button": "Evet, panoyu bağla", - "boardSelector.confirm-link-board-subtext": "\"{boardName}\" panosunu kanala bağladığınızda, kanalın tüm üyeleri (var olan ve yeni) panoyu düzenleyebilir. Bu işlem konuk üyeleri kaldırır. Bir pano ile bir kanalın bağlantısını istediğiniz zaman kaldırabilirsiniz.", - "boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" panosunu bir kanala bağladığınızda, kanalın tüm üyeleri (var olan ve yeni) panoyu düzenleyebilir. Bu işlem konukl üyeleri kaldırır.{lineBreak}Bu pano şu anda başka bir kanal ile bağlantılı. Bu kanala bağlamayı seçerseniz diğer kanal ile bağlantısı kesilecek.", - "boardSelector.create-a-board": "Bir pano ekle", - "boardSelector.link": "Bağlantı", - "boardSelector.search-for-boards": "Pano arama", - "boardSelector.title": "Panoları bağla", - "boardSelector.unlink": "Bağlantıyı kaldır", - "calendar.month": "Ay", - "calendar.today": "Bugün", - "calendar.week": "Hafta", - "centerPanel.undefined": "{propertyName} yok", - "centerPanel.unknown-user": "Kullanıcı bilinmiyor", - "cloudMessage.learn-more": "Ayrıntılı bilgi alın", - "createImageBlock.failed": "Dosya boyutu sınırı aşıldığından bu dosya yüklenemedi.", - "default-properties.badges": "Yorumlar ve açıklama", - "default-properties.title": "Başlık", - "error.back-to-home": "Girişe dön", - "error.back-to-team": "Takıma dön", - "error.board-not-found": "Pano bulunamadı.", - "error.go-login": "Oturum aç", - "error.invalid-read-only-board": "Bu panoya erişme izniniz yok. Panolara erişmek için oturum açın.", - "error.not-logged-in": "Oturumunuzun süresi dolmuş ya da oturum açmamışsınız. Panolara erişmek için yeniden oturum açın.", - "error.page.title": "Bir şeyler ters gitti", - "error.team-undefined": "Geçerli bir takım değil.", - "error.unknown": "Bir sorun çıktı.", - "generic.previous": "Önceki", - "guest-no-board.subtitle": "Henüz bu takımdaki herhangi bir panoya erişme izniniz yok. Lütfen biri sizi bir panoya ekleyene kadar bekleyin.", - "guest-no-board.title": "Henüz bir pano yok", - "imagePaste.upload-failed": "Dosya boyutu sınırı aşıldığından bazı dosyalar yüklenemedi.", - "limitedCard.title": "Kartlar gizli", - "login.log-in-button": "Oturum aç", - "login.log-in-title": "Oturum açın", - "login.register-button": "ya da hesabınız yoksa bir hesap açın", - "new_channel_modal.create_board.empty_board_description": "Yeni boş bir pano oluştur", - "new_channel_modal.create_board.empty_board_title": "Boş pano", - "new_channel_modal.create_board.select_template_placeholder": "Bir kalıp seçin", - "new_channel_modal.create_board.title": "Bu kanal için bir pano oluştur", - "notification-box-card-limit-reached.close-tooltip": "10 gün için sustur", - "notification-box-card-limit-reached.contact-link": "yöneticinizi bilgilendirin", - "notification-box-card-limit-reached.link": "Ücretli bir tarifeye geçin", - "notification-box-card-limit-reached.title": "panoda {cards} kart gizli", - "notification-box-cards-hidden.title": "Bu işlem başka bir kartı gizledi", - "notification-box.card-limit-reached.not-admin.text": "Arşivlenmiş kartlara erişmek için {contactLink} ile görüşerek ücretli bir tarifeye geçmesini isteyin.", - "notification-box.card-limit-reached.text": "Kart sınırına ulaşıldı. Eski kartları görüntülemek için {link}", - "person.add-user-to-board": "{username} kullanıcısını panoya ekle", - "person.add-user-to-board-confirm-button": "Panoya ekle", - "person.add-user-to-board-permissions": "İzinler", - "person.add-user-to-board-question": "{username} kullanıcısını panoya eklemek ister misiniz?", - "person.add-user-to-board-warning": "{username} panonun bir üyesi değil ve pano ile ilgili herhangi bir bildirim almayacak.", - "register.login-button": "ya da bir hesabınız varsa oturum açın", - "register.signup-title": "Hesap açın", - "rhs-board-non-admin-msg": "Panonun yöneticilerinden değilsiniz", - "rhs-boards.add": "Ekle", - "rhs-boards.dm": "Dİ", - "rhs-boards.gm": "Gİ", - "rhs-boards.header.dm": "bu doğrudan ileti", - "rhs-boards.header.gm": "bu grup iletisi", - "rhs-boards.last-update-at": "Son güncelleme: {datetime}", - "rhs-boards.link-boards-to-channel": "Panoları {channelName} kanalına bağla", - "rhs-boards.linked-boards": "Bağlı panolar", - "rhs-boards.no-boards-linked-to-channel": "Henüz {channelName} kanalına bağlanmış bir pano yok", - "rhs-boards.no-boards-linked-to-channel-description": "Panolar, takımlar arasındaki çalışmaları tanımlamak, organize etmek, izlemek ve yönetmek için kullanılabilen kandan panosuna benzer bir proje yönetimi aracıdır.", - "rhs-boards.unlink-board": "Panonun bağlantısını kaldır", - "rhs-boards.unlink-board1": "Pano bağlantısını kaldır", - "rhs-channel-boards-header.title": "Panolar", - "share-board.publish": "Yayınla", - "share-board.share": "Paylaş", - "shareBoard.channels-select-group": "Kanallar", - "shareBoard.confirm-change-team-role.body": "Bu panoda izinleri \"{role}\" rolünden daha aşağıda olan herkes {role} rolüne yükseltilecek. Panonunen düşük rolünü değiştirmek istediğinize emin misiniz?", - "shareBoard.confirm-change-team-role.confirmBtnText": "Panonun en düşük rolünü değiştir", - "shareBoard.confirm-change-team-role.title": "Panonun en düşük rolünü değiştir", - "shareBoard.confirm-link-channel": "Panoyu kanala bağla", - "shareBoard.confirm-link-channel-button": "Kanalı bağla", - "shareBoard.confirm-link-channel-button-with-other-channel": "Eski bağlantıyı kes ve bu kanala bağla", - "shareBoard.confirm-link-channel-subtext": "Bir kanalı bir panoya bağladığınızda, kanalın tüm üyeleri (var olan ve yeni) panoyu düzenleyebilir. Bu işlem konuk üyeleri kaldırır.", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "Bir kanalı bir panoya bağladığınızda, kanalın tüm üyeleri (var olan ve yeni) panoyu düzenleyebilir. Bu işlem konuk üyeleri kaldırır.{lineBreak}Bu pano şu anda başka bir kanal ile bağlantılı. Bu kanala bağlamayı seçerseniz diğer kanal ile bağlantısı kesilecek.", - "shareBoard.confirm-unlink.body": "Bir kanalın bir pano ile bağlantısını kaldırdığınızda, kanalın tüm üyeleri (var olan ve yeni), kendilerine özel olarak izin verilmedikçe, panoya erişimi kaybeder.", - "shareBoard.confirm-unlink.confirmBtnText": "Kanalın bağlantısını kaldır", - "shareBoard.confirm-unlink.title": "Kanalın pano ile bağlantısı kaldır", - "shareBoard.lastAdmin": "Panoların en az bir yöneticisi olmalıdır", - "shareBoard.members-select-group": "Üyeler", - "shareBoard.unknown-channel-display-name": "Kanal bilinmiyor", - "tutorial_tip.finish_tour": "Tamam", - "tutorial_tip.got_it": "Anladım", - "tutorial_tip.ok": "Sonraki", - "tutorial_tip.out": "Bu ipuçları görüntülenmesin.", - "tutorial_tip.seen": "Daha önce gördünüz mü?" -} diff --git a/webapp/boards/i18n/uk.json b/webapp/boards/i18n/uk.json deleted file mode 100644 index 3e4f7d7cd1..0000000000 --- a/webapp/boards/i18n/uk.json +++ /dev/null @@ -1,247 +0,0 @@ -{ - "AppBar.Tooltip": "Перемкнути пов’язані дошки", - "Attachment.Attachment-title": "Прикріплення", - "AttachmentBlock.DeleteAction": "видалити", - "AttachmentBlock.addElement": "додати {type}", - "AttachmentBlock.delete": "Прикріплення успішно видалено.", - "AttachmentBlock.failed": "Не вдалося завантажити цей файл, оскільки досягнуто обмеження розміру файлу.", - "AttachmentBlock.upload": "Прикріплення завантажуються.", - "AttachmentBlock.uploadSuccess": "Вкладення завантажено.", - "AttachmentElement.delete-confirmation-dialog-button-text": "Видалити", - "AttachmentElement.download": "Завантажити", - "AttachmentElement.upload-percentage": "Завантаження...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ Додати групу", - "BoardComponent.delete": "Видалити", - "BoardComponent.hidden-columns": "Приховані стовпці", - "BoardComponent.hide": "Приховати", - "BoardComponent.new": "+ Створити", - "BoardComponent.no-property": "Немає {property}", - "BoardComponent.no-property-title": "Елементи з порожнім полем {property} потраплять сюди. Цей стовпець неможливо видалити.", - "BoardComponent.show": "Показати", - "BoardMember.schemeAdmin": "Адміністратор", - "BoardMember.schemeCommenter": "Коментатор", - "BoardMember.schemeEditor": "Редактор", - "BoardMember.schemeNone": "Жоден", - "BoardMember.schemeViewer": "Спостерігач", - "BoardMember.unlinkChannel": "Від’єднати", - "BoardPage.newVersion": "Доступна оновлена версія Панелі, тицьни тут щоб оновити.", - "BoardPage.syncFailed": "Можливо Панель видалено або права анульовано.", - "BoardTemplateSelector.add-template": "Створити новий шаблон", - "BoardTemplateSelector.create-empty-board": "Створити порожню доску", - "BoardTemplateSelector.delete-template": "Видалити", - "BoardTemplateSelector.description": "Додайте дошку на бічній панелі, використовуючи будь-який із наведених нижче шаблонів, або почніть з нуля.", - "BoardTemplateSelector.edit-template": "Редагувати", - "BoardTemplateSelector.plugin.no-content-description": "Додайте дошку на бічній панелі, використовуючи будь-який із наведених нижче шаблонів, або почніть з нуля.", - "BoardTemplateSelector.plugin.no-content-title": "Створити дошку", - "BoardTemplateSelector.title": "Створити доску", - "BoardTemplateSelector.use-this-template": "Використати цей шаблон", - "BoardsSwitcher.Title": "Знайти дошки", - "BoardsUnfurl.Limited": "Додаткові деталі приховані бо картку архівовано", - "BoardsUnfurl.Remainder": "+{remainder} більше", - "BoardsUnfurl.Updated": "Оновлено {time}", - "Calculations.Options.average.displayName": "Середній", - "Calculations.Options.average.label": "Середній", - "Calculations.Options.count.displayName": "Кількість", - "Calculations.Options.count.label": "Кількість", - "Calculations.Options.countChecked.displayName": "Перевірено", - "Calculations.Options.countChecked.label": "Кількість перевірено", - "Calculations.Options.countUnchecked.displayName": "Не перевірено", - "Calculations.Options.countUnchecked.label": "Підрахунок не перевірено", - "Calculations.Options.countUniqueValue.displayName": "Унікальний", - "Calculations.Options.countUniqueValue.label": "Підрахувати унікальні значення", - "Calculations.Options.countValue.displayName": "Значення", - "Calculations.Options.countValue.label": "Розрахунок значення", - "Calculations.Options.dateRange.displayName": "Діапазон", - "Calculations.Options.dateRange.label": "Діапазон", - "Calculations.Options.earliest.displayName": "Найраніший", - "Calculations.Options.earliest.label": "Найраніший", - "Calculations.Options.latest.displayName": "Останній", - "Calculations.Options.latest.label": "Останній", - "Calculations.Options.max.displayName": "Макс", - "Calculations.Options.max.label": "Макс", - "Calculations.Options.median.displayName": "Медіана", - "Calculations.Options.median.label": "Медіана", - "Calculations.Options.min.displayName": "Мін", - "Calculations.Options.min.label": "Мін", - "Calculations.Options.none.displayName": "Обчислити", - "Calculations.Options.none.label": "Жодного", - "Calculations.Options.percentChecked.displayName": "Перевірено", - "Calculations.Options.percentChecked.label": "Відсоток перевірено", - "Calculations.Options.percentUnchecked.displayName": "Не перевірено", - "Calculations.Options.percentUnchecked.label": "Відсоток не перевірено", - "Calculations.Options.range.displayName": "Діапазон", - "Calculations.Options.range.label": "Діапазон", - "Calculations.Options.sum.displayName": "Сума", - "Calculations.Options.sum.label": "Сума", - "CalendarCard.untitled": "Без назви", - "CardActionsMenu.copiedLink": "Скопійовано!", - "CardActionsMenu.copyLink": "Копіювати посилання", - "CardActionsMenu.delete": "Видалити", - "CardActionsMenu.duplicate": "Дублювати", - "CardBadges.title-checkboxes": "Прапорці", - "CardBadges.title-comments": "Коментарі", - "CardBadges.title-description": "Ця картка має опис", - "CardDetail.Attach": "Прикріпити", - "CardDetail.Follow": "Слідкувати", - "CardDetail.Following": "Відслідковувати", - "CardDetail.add-content": "Додайте вміст", - "CardDetail.add-icon": "Додати значок", - "CardDetail.add-property": "+ Додати властивість", - "CardDetail.addCardText": "додати текст картки", - "CardDetail.limited-body": "Перейдіть на наш план Professional або Enterprise.", - "CardDetail.limited-button": "Оновлення", - "CardDetail.limited-title": "Ця прихована картка", - "CardDetail.moveContent": "Перемістити вміст картки", - "CardDetail.new-comment-placeholder": "Додати коментар...", - "CardDetailProperty.confirm-delete-heading": "Підтвердьте видалення властивості", - "CardDetailProperty.confirm-delete-subtext": "Ви впевнені, що хочете видалити властивість \"{propertyName}\"? При видаленні властивість буде видалено з усіх карток на цій дошці.", - "CardDetailProperty.confirm-property-name-change-subtext": "Ви дійсно хочете змінити властивість \"{propertyName}\" {customText}? Це вплине на значення(-я) на {numOfCards} картці(-ах) на цій дошці і може призвести до втрати даних.", - "CardDetailProperty.confirm-property-type-change": "Підтвердити зміну типу власності", - "CardDetailProperty.delete-action-button": "Видалити", - "CardDetailProperty.property-change-action-button": "Змінити властивість", - "CardDetailProperty.property-changed": "Властивість змінена успішно!", - "CardDetailProperty.property-deleted": "{propertyName} успішно видалено!", - "CardDetailProperty.property-name-change-subtext": "тип з \"{oldPropType}\" в \"{newPropType}\"", - "CardDetial.limited-link": "Дізнайтеся більше про наші плани.", - "CardDialog.delete-confirmation-dialog-attachment": "Підтвердити видалення вкладення", - "CardDialog.delete-confirmation-dialog-button-text": "Видалити", - "CardDialog.delete-confirmation-dialog-heading": "Підтвердити видалення картки", - "CardDialog.editing-template": "Ви редагуєте шаблон.", - "CardDialog.nocard": "Ця картка не існує або недоступна.", - "Categories.CreateCategoryDialog.CancelText": "Скасувати", - "Categories.CreateCategoryDialog.CreateText": "Створити", - "Categories.CreateCategoryDialog.Placeholder": "Назвіть свою категорію", - "Categories.CreateCategoryDialog.UpdateText": "Оновити", - "CenterPanel.Login": "Логін", - "CenterPanel.Share": "Поділитися", - "ChannelIntro.CreateBoard": "Створити дошку", - "ColorOption.selectColor": "Виберіть колір {color}", - "Comment.delete": "Видалити", - "CommentsList.send": "Надіслати", - "ConfirmPerson.empty": "Порожній", - "ConfirmPerson.search": "Пошук...", - "ConfirmationDialog.cancel-action": "Скасувати", - "ConfirmationDialog.confirm-action": "Підтвердити", - "ContentBlock.Delete": "Видалити", - "ContentBlock.DeleteAction": "видалити", - "ContentBlock.addElement": "додати {type}", - "ContentBlock.checkbox": "прапорець", - "ContentBlock.divider": "роздільник", - "ContentBlock.editCardCheckbox": "позначений прапорець", - "ContentBlock.editCardCheckboxText": "редагувати текст картки", - "ContentBlock.editCardText": "редагувати текст картки", - "ContentBlock.editText": "Редагувати текст...", - "ContentBlock.image": "зображення", - "ContentBlock.insertAbove": "Вставте вище", - "ContentBlock.moveBlock": "перемістити вміст картки", - "ContentBlock.moveDown": "Опустити", - "ContentBlock.moveUp": "Підняти", - "ContentBlock.text": "текст", - "DateRange.clear": "Очистити", - "DateRange.empty": "Пусто", - "DateRange.endDate": "Дата закінчення", - "DateRange.today": "Сьогодні", - "DeleteBoardDialog.confirm-cancel": "Скасувати", - "DeleteBoardDialog.confirm-delete": "Видалити", - "DeleteBoardDialog.confirm-info": "Ви впевнені, що хочете видалити дошку “{boardTitle}”? Видалення призведе до видалення всіх карток на дошці.", - "DeleteBoardDialog.confirm-info-template": "Ви впевнені, що хочете видалити шаблон дошки «{boardTitle}»?", - "DeleteBoardDialog.confirm-tite": "Підтвердьте видалення дошки", - "DeleteBoardDialog.confirm-tite-template": "Підтвердьте видалення шаблону дошки", - "Dialog.closeDialog": "Закрити діалогове вікно", - "EditableDayPicker.today": "Сьогодні", - "Error.mobileweb": "Мобільна веб-підтримка зараз знаходиться на ранній стадії бета-тестування. Не всі функції можуть бути присутніми.", - "Error.websocket-closed": "З'єднання через веб-сокет закрито, з'єднання перервано. Якщо це продовжується й далі, перевірте конфігурацію сервера або веб-проксі.", - "Filter.contains": "містить", - "Filter.ends-with": "закінчується на", - "Filter.includes": "включає в себе", - "Filter.is": "є", - "Filter.is-empty": "пусто", - "Filter.is-not-empty": "не порожній", - "Filter.is-not-set": "не встановлено", - "Filter.is-set": "встановлено", - "Filter.not-contains": "не містить", - "Filter.not-ends-with": "не закінчується", - "Filter.not-includes": "не включає", - "Filter.not-starts-with": "не починається з", - "Filter.starts-with": "починається з", - "FilterByText.placeholder": "фільтрувати текст", - "FilterComponent.add-filter": "+ Додати фільтр", - "FilterComponent.delete": "Видалити", - "FilterValue.empty": "(порожній)", - "FindBoardsDialog.IntroText": "Пошук дощок", - "FindBoardsDialog.NoResultsFor": "Немає результатів для \"{searchQuery}\"", - "FindBoardsDialog.NoResultsSubtext": "Перевірте правильність написання або спробуйте інший запит.", - "FindBoardsDialog.SubTitle": "Введіть, щоб знайти дошку. Використовуйте ВГОРУ/ВНИЗ для перегляду. ENTER, щоб вибрати, ESC, щоб закрити", - "FindBoardsDialog.Title": "Знайти дошки", - "GroupBy.hideEmptyGroups": "Сховати {count} порожні групи", - "GroupBy.showHiddenGroups": "Показати {count} прихованих груп", - "GroupBy.ungroup": "Розгрупувати", - "HideBoard.MenuOption": "Сховати дошку", - "KanbanCard.untitled": "Без назви", - "MentionSuggestion.is-not-board-member": "(не член правління)", - "Mutator.new-board-from-template": "нова дошка з шаблону", - "Mutator.new-card-from-template": "нова картка із шаблону", - "Mutator.new-template-from-card": "новий шаблон із картки", - "OnboardingTour.AddComments.Body": "Ви можете коментувати проблеми та навіть @згадувати інших користувачів Mattermost, щоб привернути їх увагу.", - "OnboardingTour.AddComments.Title": "Додати коментарі", - "OnboardingTour.AddDescription.Body": "Додайте опис до картки, щоб ваші товариші по команді знали, про що йде мова.", - "OnboardingTour.AddDescription.Title": "Додайте опис", - "OnboardingTour.AddProperties.Body": "Додайте карткам різні властивості, щоб зробити їх потужнішими.", - "OnboardingTour.AddProperties.Title": "Додайте властивості", - "OnboardingTour.AddView.Body": "Перейдіть сюди, щоб створити новий вид для організації дошки за допомогою різних макетів.", - "OnboardingTour.AddView.Title": "Додайте новий вид", - "OnboardingTour.CopyLink.Body": "Ви можете поділитися своїми картками з товаришами по команді, скопіювавши посилання та вставивши його в канал, пряме або групове повідомлення.", - "OnboardingTour.CopyLink.Title": "Копіювати посилання", - "OnboardingTour.OpenACard.Body": "Відкрийте картку, щоб дослідити потужні способи, за допомогою яких дошки можуть допомогти вам організувати вашу роботу.", - "OnboardingTour.OpenACard.Title": "Відкрити картку", - "OnboardingTour.ShareBoard.Body": "Ви можете поділитися своєю дошкою всередині, у своїй команді або опублікувати її публічно для видимості за межами вашої організації.", - "OnboardingTour.ShareBoard.Title": "Поділитися дошкою", - "PersonProperty.board-members": "Члени команди", - "PersonProperty.me": "Я", - "PersonProperty.non-board-members": "Не учасник команди", - "PropertyMenu.Delete": "Видалити", - "PropertyMenu.changeType": "Змінити тип власності", - "PropertyMenu.selectType": "Виберіть тип властивості", - "PropertyMenu.typeTitle": "Тип", - "PropertyType.Checkbox": "Прапорець", - "PropertyType.CreatedBy": "Створений", - "PropertyType.CreatedTime": "Час створення", - "PropertyType.Date": "Дата", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "Кілька осіб", - "PropertyType.MultiSelect": "Множинний вибір", - "PropertyType.Number": "Номер", - "PropertyType.Person": "Особа", - "PropertyType.Phone": "Телефон", - "PropertyType.Select": "Обрати", - "PropertyType.Text": "Текст", - "PropertyType.Unknown": "Невідомий", - "PropertyType.UpdatedBy": "Оновлено користувачем", - "PropertyType.UpdatedTime": "Час останнього оновлення", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "Пусто", - "RegistrationLink.confirmRegenerateToken": "Це призведе до скасування попередніх спільних посилань. Продовжити?", - "RegistrationLink.copiedLink": "Скопійовано!", - "RegistrationLink.copyLink": "Копіювати посилання", - "RegistrationLink.description": "Поділіться цим посиланням, щоб інші могли створити облікові записи:", - "RegistrationLink.regenerateToken": "Згенерувати новий токен", - "RegistrationLink.tokenRegenerated": "Реєстраційне посилання відновлено", - "ShareBoard.PublishDescription": "Опублікуйте та поділіться посиланням лише для читання з усіма в Інтернеті.", - "ShareBoard.PublishTitle": "Опублікувати в Інтернеті", - "ShareBoard.ShareInternal": "Поділитися всередині організації", - "ShareBoard.ShareInternalDescription": "Користувачі, які мають дозволи, зможуть використовувати це посилання.", - "ShareBoard.Title": "Поділиться Дошкою", - "Sidebar.delete-board": "Видалити дошку", - "SidebarCategories.CategoryMenu.Delete": "Видалити категорію", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "Видалити дану категорію?", - "TableHeaderMenu.delete": "Видалити", - "View.DeleteView": "Видалити вид", - "ViewHeader.delete-template": "Видалити", - "generic.previous": "Попередній", - "shareBoard.unknown-channel-display-name": "Невідомий канал", - "tutorial_tip.finish_tour": "Готово", - "tutorial_tip.got_it": "Зрозуміло", - "tutorial_tip.ok": "Далі", - "tutorial_tip.out": "Відмовтеся від цих порад.", - "tutorial_tip.seen": "Ви бачили це раніше?" -} diff --git a/webapp/boards/i18n/vi.json b/webapp/boards/i18n/vi.json deleted file mode 100644 index aec863456a..0000000000 --- a/webapp/boards/i18n/vi.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "AppBar.Tooltip": "Chuyển sang các bảng đã liên kết", - "Attachment.Attachment-title": "Đính kèm", - "AttachmentBlock.DeleteAction": "xóa", - "BoardComponent.add-a-group": "+ Thêm nhóm", - "BoardComponent.delete": "Xóa", - "BoardComponent.hidden-columns": "Cột ẩn", - "BoardComponent.hide": "Ẩn", - "BoardComponent.new": "+ Thêm", - "BoardComponent.no-property": "Không {property}", - "BoardComponent.show": "Hiện", - "BoardMember.schemeAdmin": "Quản trị", - "BoardMember.schemeCommenter": "Người bình luận", - "BoardMember.schemeEditor": "Người soạn thảo", - "BoardMember.schemeNone": "Không", - "BoardMember.schemeViewer": "Người xem", - "BoardMember.unlinkChannel": "Gỡ liên kết", - "BoardPage.newVersion": "Có một phiên bản mới của bảng, click vào đây để nạp lại.", - "Calculations.Options.average.displayName": "Trung bình", - "Calculations.Options.average.label": "Trung bình", - "TableComponent.add-icon": "Thêm icon", - "TableComponent.name": "Tên", - "TableComponent.plus-new": "+ Mới", - "TableHeaderMenu.delete": "Xóa", - "share-board.publish": "Công khai", - "share-board.share": "Chia sẻ", - "shareBoard.channels-select-group": "Kênh", - "shareBoard.members-select-group": "Thành viên", - "tutorial_tip.finish_tour": "Xong", - "tutorial_tip.got_it": "Đã hiểu", - "tutorial_tip.ok": "Tiếp theo" -} diff --git a/webapp/boards/i18n/zh_Hans.json b/webapp/boards/i18n/zh_Hans.json deleted file mode 100644 index f3c7fb7bfc..0000000000 --- a/webapp/boards/i18n/zh_Hans.json +++ /dev/null @@ -1,454 +0,0 @@ -{ - "AppBar.Tooltip": "切换已链接的板块", - "Attachment.Attachment-title": "附件", - "AttachmentBlock.DeleteAction": "删除", - "AttachmentBlock.addElement": "添加 {type}", - "AttachmentBlock.delete": "附件已删除。", - "AttachmentBlock.failed": "该文件无法上传,因为已经达到了文件大小的限制。", - "AttachmentBlock.upload": "附件正在上传。", - "AttachmentBlock.uploadSuccess": "附件已上传。", - "AttachmentElement.delete-confirmation-dialog-button-text": "删除", - "AttachmentElement.download": "下载", - "AttachmentElement.upload-percentage": "上传中…({uploadPercent}%)", - "BoardComponent.add-a-group": "+ 新增群组", - "BoardComponent.delete": "删除", - "BoardComponent.hidden-columns": "隐藏列", - "BoardComponent.hide": "隐藏", - "BoardComponent.new": "+ 新增", - "BoardComponent.no-property": "无 {property}", - "BoardComponent.no-property-title": "{property} 属性为空的项目将转到此处,该列无法删除。", - "BoardComponent.show": "显示", - "BoardMember.schemeAdmin": "管理", - "BoardMember.schemeCommenter": "评论者", - "BoardMember.schemeEditor": "编辑器", - "BoardMember.schemeNone": "无", - "BoardMember.schemeViewer": "视图", - "BoardMember.unlinkChannel": "断开", - "BoardPage.newVersion": "Boards 的新版本已可用,点击这里重新加载。", - "BoardPage.syncFailed": "板块或许已被删除或访问授权已被撤销。", - "BoardTemplateSelector.add-template": "创建新模板", - "BoardTemplateSelector.create-empty-board": "创建空白板块", - "BoardTemplateSelector.delete-template": "删除", - "BoardTemplateSelector.description": "选择一个模板助你开始。或者创建一个空白板块,从零开始。", - "BoardTemplateSelector.edit-template": "编辑", - "BoardTemplateSelector.plugin.no-content-description": "使用下面定义好的任意模板给侧栏添加一个看板,或者从头开始。", - "BoardTemplateSelector.plugin.no-content-title": "创建一个看板", - "BoardTemplateSelector.title": "创建一个看板", - "BoardTemplateSelector.use-this-template": "使用该模板", - "BoardsSwitcher.Title": "查找板块", - "BoardsUnfurl.Limited": "由于卡片被存档,其他细节被隐藏", - "BoardsUnfurl.Remainder": "+{remainder} 更多", - "BoardsUnfurl.Updated": "于 {time} 更新", - "Calculations.Options.average.displayName": "平均", - "Calculations.Options.average.label": "平均", - "Calculations.Options.count.displayName": "计数", - "Calculations.Options.count.label": "计数", - "Calculations.Options.countChecked.displayName": "选中", - "Calculations.Options.countChecked.label": "选中计数", - "Calculations.Options.countUnchecked.displayName": "未选中", - "Calculations.Options.countUnchecked.label": "未选中计数", - "Calculations.Options.countUniqueValue.displayName": "唯一", - "Calculations.Options.countUniqueValue.label": "唯一值计数", - "Calculations.Options.countValue.displayName": "值", - "Calculations.Options.countValue.label": "值计数", - "Calculations.Options.dateRange.displayName": "范围", - "Calculations.Options.dateRange.label": "范围", - "Calculations.Options.earliest.displayName": "最早的", - "Calculations.Options.earliest.label": "最早的", - "Calculations.Options.latest.displayName": "最新的", - "Calculations.Options.latest.label": "最新的", - "Calculations.Options.max.displayName": "最大的", - "Calculations.Options.max.label": "最大的", - "Calculations.Options.median.displayName": "中位数", - "Calculations.Options.median.label": "中位数", - "Calculations.Options.min.displayName": "最小的", - "Calculations.Options.min.label": "最小值", - "Calculations.Options.none.displayName": "计算", - "Calculations.Options.none.label": "无", - "Calculations.Options.percentChecked.displayName": "已选中", - "Calculations.Options.percentChecked.label": "已选中的百分比", - "Calculations.Options.percentUnchecked.displayName": "未选中", - "Calculations.Options.percentUnchecked.label": "未选中的百分比", - "Calculations.Options.range.displayName": "范围", - "Calculations.Options.range.label": "范围", - "Calculations.Options.sum.displayName": "总计", - "Calculations.Options.sum.label": "总计", - "CalendarCard.untitled": "无内容", - "CardActionsMenu.copiedLink": "复制成功!", - "CardActionsMenu.copyLink": "复制链接", - "CardActionsMenu.delete": "删除", - "CardActionsMenu.duplicate": "重复", - "CardBadges.title-checkboxes": "Checkbox", - "CardBadges.title-comments": "评论", - "CardBadges.title-description": "此卡片有描述内容", - "CardDetail.Attach": "附加", - "CardDetail.Follow": "关注", - "CardDetail.Following": "关注中", - "CardDetail.add-content": "新增内容", - "CardDetail.add-icon": "新增图标", - "CardDetail.add-property": "+ 新增属性", - "CardDetail.addCardText": "新增卡片文本", - "CardDetail.limited-body": "升级到我们的专业或企业计划。", - "CardDetail.limited-button": "升级", - "CardDetail.limited-title": "这张卡片是隐藏的", - "CardDetail.moveContent": "移动卡片内容", - "CardDetail.new-comment-placeholder": "新增评论...", - "CardDetailProperty.confirm-delete-heading": "确认删除此属性", - "CardDetailProperty.confirm-delete-subtext": "确定要删除属性:{propertyName}?也会同时删除这个版面中所有其他卡片的属性。", - "CardDetailProperty.confirm-property-name-change-subtext": "你确定要改变属性名称\"{propertyName}\" {customText}?这将影响此板块中的{numOfCards}个卡片,并可能导致数据丢失。", - "CardDetailProperty.confirm-property-type-change": "确认修改此属性的类型", - "CardDetailProperty.delete-action-button": "删除", - "CardDetailProperty.property-change-action-button": "修改属性", - "CardDetailProperty.property-changed": "已成功修改属性!", - "CardDetailProperty.property-deleted": "成功删除 {propertyName}!", - "CardDetailProperty.property-name-change-subtext": "属性的类型从\"{oldPropType}\" 更改为\"{newPropType}\"", - "CardDetial.limited-link": "了解更多关于我们的计划。", - "CardDialog.delete-confirmation-dialog-attachment": "确认删除附件", - "CardDialog.delete-confirmation-dialog-button-text": "删除", - "CardDialog.delete-confirmation-dialog-heading": "确认删除卡片", - "CardDialog.editing-template": "您正在编辑模板。", - "CardDialog.nocard": "卡片不存在或者无法被存取。", - "Categories.CreateCategoryDialog.CancelText": "取消", - "Categories.CreateCategoryDialog.CreateText": "新增", - "Categories.CreateCategoryDialog.Placeholder": "命名你的类别", - "Categories.CreateCategoryDialog.UpdateText": "更新", - "CenterPanel.Login": "登录", - "CenterPanel.Share": "分享", - "ChannelIntro.CreateBoard": "创建一个板块", - "ColorOption.selectColor": "选择{color}", - "Comment.delete": "删除", - "CommentsList.send": "发送", - "ConfirmPerson.empty": "空", - "ConfirmPerson.search": "搜索...", - "ConfirmationDialog.cancel-action": "取消", - "ConfirmationDialog.confirm-action": "确认", - "ContentBlock.Delete": "删除", - "ContentBlock.DeleteAction": "删除", - "ContentBlock.addElement": "新增 {type}", - "ContentBlock.checkbox": "复选框", - "ContentBlock.divider": "分割线", - "ContentBlock.editCardCheckbox": "切换复选框", - "ContentBlock.editCardCheckboxText": "编辑卡片文字", - "ContentBlock.editCardText": "编辑卡片文字", - "ContentBlock.editText": "编辑文字...", - "ContentBlock.image": "图片", - "ContentBlock.insertAbove": "在上方插入", - "ContentBlock.moveBlock": "移动卡片内容", - "ContentBlock.moveDown": "下移", - "ContentBlock.moveUp": "上移", - "ContentBlock.text": "文字", - "DateFilter.empty": "空", - "DateRange.clear": "清除", - "DateRange.empty": "空的", - "DateRange.endDate": "结束日期", - "DateRange.today": "今天", - "DeleteBoardDialog.confirm-cancel": "取消", - "DeleteBoardDialog.confirm-delete": "删除", - "DeleteBoardDialog.confirm-info": "确定要删除版块\"{boardTitle}\"?删除后,也将删除此版块中的所有卡片。", - "DeleteBoardDialog.confirm-info-template": "你确定要删除板块模板\"{boardTitle}\"吗?", - "DeleteBoardDialog.confirm-tite": "确认删除板块", - "DeleteBoardDialog.confirm-tite-template": "确认删除板块模板", - "Dialog.closeDialog": "关闭对话框", - "EditableDayPicker.today": "今天", - "Error.mobileweb": "移动端页面的支持目前处于早期测试阶段。不是所有的功能都已实现。", - "Error.websocket-closed": "Websocket 连接关闭,连接中断。如果这种情况仍然存在,请检查您的服务器或网页代理配置。", - "Filter.contains": "包含", - "Filter.ends-with": "结束于", - "Filter.includes": "含有", - "Filter.is": "是", - "Filter.is-empty": "为空", - "Filter.is-not-empty": "不为空", - "Filter.is-not-set": "未设置", - "Filter.is-set": "被设定为", - "Filter.not-contains": "不包含", - "Filter.not-ends-with": "不结束于", - "Filter.not-includes": "不含有", - "Filter.not-starts-with": "不开始于", - "Filter.starts-with": "开始于", - "FilterByText.placeholder": "过滤文本", - "FilterComponent.add-filter": "+ 增加过滤条件", - "FilterComponent.delete": "删除", - "FilterValue.empty": "(空)", - "FindBoardsDialog.IntroText": "搜索板块", - "FindBoardsDialog.NoResultsFor": "没有\"{searchQuery}\"相关的结果", - "FindBoardsDialog.NoResultsSubtext": "请检查拼写或者查找其他内容。", - "FindBoardsDialog.SubTitle": "输入内容来查找板块。使用上/下浏览。ENTER选择,ESC取消", - "FindBoardsDialog.Title": "查找板块", - "GroupBy.hideEmptyGroups": "隐藏{count}个空组", - "GroupBy.showHiddenGroups": "显示已隐藏的{count}个组", - "GroupBy.ungroup": "未分组", - "HideBoard.MenuOption": "隐藏板块", - "KanbanCard.untitled": "无标题", - "MentionSuggestion.is-not-board-member": "(非板块成员)", - "Mutator.new-board-from-template": "从模板创建板块", - "Mutator.new-card-from-template": "使用模板新增卡片", - "Mutator.new-template-from-card": "从卡片新增模板", - "OnboardingTour.AddComments.Body": "你可以对问题进行评论,甚至可以@提及你的Mattermost同伴,以引起他们的注意。", - "OnboardingTour.AddComments.Title": "添加评论", - "OnboardingTour.AddDescription.Body": "在你的卡片上添加描述,以便其他人了解卡片的内容。", - "OnboardingTour.AddDescription.Title": "添加描述", - "OnboardingTour.AddProperties.Body": "为卡片添加各种属性,使其更加强大。", - "OnboardingTour.AddProperties.Title": "添加属性", - "OnboardingTour.AddView.Body": "在这里创建一个新的视图,用不同的布局来组织你的板块。", - "OnboardingTour.AddView.Title": "添加一个新的视图", - "OnboardingTour.CopyLink.Body": "你可以通过频道,私信和群聊分享链接来和成员们一起共享卡片。", - "OnboardingTour.CopyLink.Title": "复制链接", - "OnboardingTour.OpenACard.Body": "打开卡片来探索板块的高效使用方法,从而助力你的整理项目。", - "OnboardingTour.OpenACard.Title": "打开一个卡片", - "OnboardingTour.ShareBoard.Body": "你可以分享板块,不管是与内部成员,还是公开发布到外部的机构。", - "OnboardingTour.ShareBoard.Title": "分享板块", - "PersonProperty.board-members": "板块成员", - "PersonProperty.me": "我", - "PersonProperty.non-board-members": "非板块成员", - "PropertyMenu.Delete": "删除", - "PropertyMenu.changeType": "修改属性类型", - "PropertyMenu.selectType": "选择属性类型", - "PropertyMenu.typeTitle": "类型", - "PropertyType.Checkbox": "复选框", - "PropertyType.CreatedBy": "创建者", - "PropertyType.CreatedTime": "创建时间", - "PropertyType.Date": "日期", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "多人", - "PropertyType.MultiSelect": "多选", - "PropertyType.Number": "数字", - "PropertyType.Person": "个人", - "PropertyType.Phone": "电话号码", - "PropertyType.Select": "选取", - "PropertyType.Text": "文字框", - "PropertyType.Unknown": "未知", - "PropertyType.UpdatedBy": "最后更新者", - "PropertyType.UpdatedTime": "上次更新时间", - "PropertyType.Url": "URL", - "PropertyValueElement.empty": "空的", - "RegistrationLink.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?", - "RegistrationLink.copiedLink": "已复制!", - "RegistrationLink.copyLink": "复制链接", - "RegistrationLink.description": "将此链接分享给他人以建立帐号:", - "RegistrationLink.regenerateToken": "重新生成令牌", - "RegistrationLink.tokenRegenerated": "已重新生成注册链接", - "ShareBoard.PublishDescription": "发布并与所有人分享 \"只读 \"链接。", - "ShareBoard.PublishTitle": "发布到网上", - "ShareBoard.ShareInternal": "内部分享", - "ShareBoard.ShareInternalDescription": "有权限的用户将能够使用这个链接。", - "ShareBoard.Title": "分享板块", - "ShareBoard.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?", - "ShareBoard.copiedLink": "已复制!", - "ShareBoard.copyLink": "复制链接", - "ShareBoard.regenerate": "重新生成令牌", - "ShareBoard.searchPlaceholder": "搜索成员和频道", - "ShareBoard.teamPermissionsText": "在{teamName}团队的每个人", - "ShareBoard.tokenRegenrated": "已重新产生令牌", - "ShareBoard.userPermissionsRemoveMemberText": "移除成员", - "ShareBoard.userPermissionsYouText": "(你)", - "ShareTemplate.Title": "分享模板", - "ShareTemplate.searchPlaceholder": "搜索成员", - "Sidebar.about": "关于 Focalboard", - "Sidebar.add-board": "+ 新增版面", - "Sidebar.changePassword": "变更密码", - "Sidebar.delete-board": "删除版面", - "Sidebar.duplicate-board": "复制板块", - "Sidebar.export-archive": "导出档案", - "Sidebar.import": "导入", - "Sidebar.import-archive": "导入档案", - "Sidebar.invite-users": "邀请使用者", - "Sidebar.logout": "登出", - "Sidebar.new-category.badge": "新建", - "Sidebar.new-category.drag-boards-cta": "拖动板块到这里...", - "Sidebar.no-boards-in-category": "里面没有板块", - "Sidebar.product-tour": "产品导览", - "Sidebar.random-icons": "随机图标", - "Sidebar.set-language": "设定语言", - "Sidebar.set-theme": "设置主题", - "Sidebar.settings": "设定", - "Sidebar.template-from-board": "从板块新增一个模板", - "Sidebar.untitled-board": "(无标题版面)", - "Sidebar.untitled-view": "(未命名视图)", - "SidebarCategories.BlocksMenu.Move": "移动到...", - "SidebarCategories.CategoryMenu.CreateNew": "创建新类别", - "SidebarCategories.CategoryMenu.Delete": "删除类别", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "在于{categoryName}的板块会被移回板块类别。这并不会移除任何板块。", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "删除此类别?", - "SidebarCategories.CategoryMenu.Update": "重命名类别", - "SidebarTour.ManageCategories.Body": "新建并管理自定义的类别。类别是用户专属的,所以移动板块到你的类别不会影响到使用同个板块的其他成员。", - "SidebarTour.ManageCategories.Title": "管理类别", - "SidebarTour.SearchForBoards.Body": "打开类别切换器(Cmd/Ctrl+K)来快速查找并添加板块到你的侧边栏。", - "SidebarTour.SearchForBoards.Title": "搜索板块", - "SidebarTour.SidebarCategories.Body": "你所有的板块现会在侧边栏下被管理。无需在不同工作区中进行切换。基于你之前工作区的一次性自定义板块,将会作为v7.2版本更新自动创建。这个特性可以在设置里更改会或移除。", - "SidebarTour.SidebarCategories.Link": "了解更多", - "SidebarTour.SidebarCategories.Title": "侧边栏类别", - "SiteStats.total_boards": "所有板块", - "SiteStats.total_cards": "所有卡片", - "TableComponent.add-icon": "加入图标", - "TableComponent.name": "姓名", - "TableComponent.plus-new": "+ 新增", - "TableHeaderMenu.delete": "删除", - "TableHeaderMenu.duplicate": "制作副本", - "TableHeaderMenu.hide": "隐藏", - "TableHeaderMenu.insert-left": "在左侧插入", - "TableHeaderMenu.insert-right": "在右侧插入", - "TableHeaderMenu.sort-ascending": "升序排列", - "TableHeaderMenu.sort-descending": "降序排列", - "TableRow.DuplicateCard": "复制卡片", - "TableRow.MoreOption": "更多操作", - "TableRow.open": "开启", - "TopBar.give-feedback": "反馈问题", - "URLProperty.copiedLink": "已复制!", - "URLProperty.copy": "复制", - "URLProperty.edit": "编辑", - "UndoRedoHotKeys.canRedo": "撤回", - "UndoRedoHotKeys.canRedo-with-description": "撤回 {description}", - "UndoRedoHotKeys.canUndo": "撤销", - "UndoRedoHotKeys.canUndo-with-description": "撤销 {description}", - "UndoRedoHotKeys.cannotRedo": "已没有操作可撤回", - "UndoRedoHotKeys.cannotUndo": "已没有操作可撤销", - "ValueSelector.noOptions": "没有选项。现在添加一个!", - "ValueSelector.valueSelector": "值选择器", - "ValueSelectorLabel.openMenu": "打开菜单", - "VersionMessage.help": "了解查看新版本有什么新特性。", - "VersionMessage.learn-more": "了解更多", - "View.AddView": "添加视图", - "View.Board": "板块", - "View.DeleteView": "删除视图", - "View.DuplicateView": "复制视图", - "View.Gallery": "画廊", - "View.NewBoardTitle": "版面视图", - "View.NewCalendarTitle": "日历视图", - "View.NewGalleryTitle": "画廊视图", - "View.NewTableTitle": "图表视图", - "View.NewTemplateDefaultTitle": "未命名模板", - "View.NewTemplateTitle": "未命名", - "View.Table": "图表", - "ViewHeader.add-template": "+ 新模板", - "ViewHeader.delete-template": "删除", - "ViewHeader.display-by": "以{property}显示", - "ViewHeader.edit-template": "编辑", - "ViewHeader.empty-card": "空白卡片", - "ViewHeader.export-board-archive": "导出版面归档", - "ViewHeader.export-complete": "导出完成!", - "ViewHeader.export-csv": "导出为 CSV", - "ViewHeader.export-failed": "导出失败!", - "ViewHeader.filter": "筛选", - "ViewHeader.group-by": "以{property}分组", - "ViewHeader.new": "新", - "ViewHeader.properties": "属性", - "ViewHeader.properties-menu": "属性菜单", - "ViewHeader.search-text": "搜索卡片", - "ViewHeader.select-a-template": "选择范本", - "ViewHeader.set-default-template": "设为默认范本", - "ViewHeader.sort": "排序", - "ViewHeader.untitled": "无标题", - "ViewHeader.view-header-menu": "查看标题菜单", - "ViewHeader.view-menu": "查看菜单", - "ViewLimitDialog.Heading": "已达到板块观看的限制", - "ViewLimitDialog.PrimaryButton.Title.Admin": "升级", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理员", - "ViewLimitDialog.Subtext.Admin": "升级到专业版或企业版。", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多关于我们的付费套装。", - "ViewLimitDialog.Subtext.RegularUser": "通知你的管理员来升级到专业版和企业版。", - "ViewLimitDialog.UpgradeImg.AltText": "升级图片", - "ViewLimitDialog.notifyAdmin.Success": "已通知管理员", - "ViewTitle.hide-description": "隐藏描述", - "ViewTitle.pick-icon": "挑选图标", - "ViewTitle.random-icon": "随机", - "ViewTitle.remove-icon": "移除图标", - "ViewTitle.show-description": "显示描述", - "ViewTitle.untitled-board": "无标题板块", - "WelcomePage.Description": "板块是一个项目管理工具,使用熟悉的看板视图,帮助你的团队策划、组织、跟踪和管理跨团队的工作。", - "WelcomePage.Explore.Button": "探索", - "WelcomePage.Heading": "欢迎来到板块", - "WelcomePage.NoThanks.Text": "不了,请让我自己设置", - "WelcomePage.StartUsingIt.Text": "开始使用", - "Workspace.editing-board-template": "您正在编辑版面模板。", - "badge.guest": "访客", - "boardPage.confirm-join-button": "加入", - "boardSelector.confirm-link-board": "连接板块到频道", - "boardSelector.confirm-link-board-button": "是的,连接板块", - "boardSelector.confirm-link-board-subtext": "当你连接“{boardName}”到频道时,所有频道的成员(现有的或新的)都可以编辑。这并不包括访客。你随时都可以取消板块与频道的连接。", - "boardSelector.confirm-link-board-subtext-with-other-channel": "当你连接\"{boardName}\"到频道时,此频道的所有成员(现有的和新的)将可以进行编辑,这并不包括访客。{lineBreak} 此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。", - "boardSelector.create-a-board": "创建板块", - "boardSelector.link": "连接", - "boardSelector.search-for-boards": "搜索板块", - "boardSelector.title": "连接板块", - "boardSelector.unlink": "取消连接", - "calendar.month": "月", - "calendar.today": "今天", - "calendar.week": "周", - "centerPanel.undefined": "不{propertyName}", - "centerPanel.unknown-user": "陌生用户", - "cloudMessage.learn-more": "了解更多", - "createImageBlock.failed": "图片上传失败,超过大小限制。", - "default-properties.badges": "评论和描述", - "default-properties.title": "标题", - "error.back-to-home": "回到主页", - "error.back-to-team": "回到团队", - "error.board-not-found": "未找到板块。", - "error.go-login": "登陆", - "error.invalid-read-only-board": "你没有权限访问此板块,请登陆后再进行访问板块。", - "error.not-logged-in": "尚未登陆或会话超时,请登陆后再进行访问板块。", - "error.page.title": "抱歉,出现了一些错误", - "error.team-undefined": "不是有效的团队。", - "error.unknown": "发生了一些错误。", - "generic.previous": "上一个", - "guest-no-board.subtitle": "你尚未有权限访问此团队的任何一个板块,请等待某人把你添加到某个板块。", - "guest-no-board.title": "尚未有板块", - "imagePaste.upload-failed": "图片上传失败,超过大小限制。", - "limitedCard.title": "卡片已隐藏", - "login.log-in-button": "登录", - "login.log-in-title": "登录", - "login.register-button": "或创建一个帐户(如果您没有帐户)", - "new_channel_modal.create_board.empty_board_description": "创建一个空白的板块", - "new_channel_modal.create_board.empty_board_title": "空白板块", - "new_channel_modal.create_board.select_template_placeholder": "选择模板", - "new_channel_modal.create_board.title": "为此频道创建一个新板块", - "notification-box-card-limit-reached.close-tooltip": "小睡十天", - "notification-box-card-limit-reached.contact-link": "通知你的管理员", - "notification-box-card-limit-reached.link": "升级到付费版", - "notification-box-card-limit-reached.title": "板块上的{cards}卡片已隐藏", - "notification-box-cards-hidden.title": "此行动已隐藏其他卡片", - "notification-box.card-limit-reached.not-admin.text": "要访问存档的卡片,你需要通过 {contactLink} 来升级到付费版。", - "notification-box.card-limit-reached.text": "已达到卡片上限,如需查看旧卡片请点{link}", - "person.add-user-to-board": "将 {username} 加入板块", - "person.add-user-to-board-confirm-button": "添加到板块", - "person.add-user-to-board-permissions": "权限", - "person.add-user-to-board-question": "你想将 {username} 加入板块吗?", - "person.add-user-to-board-warning": "{username} 不是此板块的成员,因此不会受到任何关于此板块的通知。", - "register.login-button": "或登录(如果您已拥有帐户)", - "register.signup-title": "注册您的帐户", - "rhs-board-non-admin-msg": "你不是板块的管理员", - "rhs-boards.add": "添加", - "rhs-boards.dm": "私信", - "rhs-boards.gm": "群聊", - "rhs-boards.header.dm": "此私信", - "rhs-boards.header.gm": "此群聊信息", - "rhs-boards.last-update-at": "最后更新日为:{datetime}", - "rhs-boards.link-boards-to-channel": "把板块连接到 {channelName}", - "rhs-boards.linked-boards": "连接板块", - "rhs-boards.no-boards-linked-to-channel": "尚未有板块与{channelName} 连接", - "rhs-boards.no-boards-linked-to-channel-description": "板块是一个能帮助我们定义,组织,追踪和管理团队工作的一个专业管理工具,可通过使用熟悉的看板视图。", - "rhs-boards.unlink-board": "取消连接板块", - "rhs-boards.unlink-board1": "取消连接板块", - "rhs-channel-boards-header.title": "板块", - "share-board.publish": "发布", - "share-board.share": "分享", - "shareBoard.channels-select-group": "频道", - "shareBoard.confirm-change-team-role.body": "此板块低于“{role}”的所有人都将于现在被提升到{role}。你确认要更改此板块的最低职责?", - "shareBoard.confirm-change-team-role.confirmBtnText": "更改板块的最低职责", - "shareBoard.confirm-change-team-role.title": "更改板块的最低职责", - "shareBoard.confirm-link-channel": "连接板块到频道", - "shareBoard.confirm-link-channel-button": "连接频道", - "shareBoard.confirm-link-channel-button-with-other-channel": "再此取消或进行连接", - "shareBoard.confirm-link-channel-subtext": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。{lineBreak}此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。", - "shareBoard.confirm-unlink.body": "当你取消频道与板块的连接,频道的所有成员(现有的或新的)将会失去板块的访问权限,除非单独给予许可。", - "shareBoard.confirm-unlink.confirmBtnText": "取消连接频道", - "shareBoard.confirm-unlink.title": "取消连接此板块的频道", - "shareBoard.lastAdmin": "板块至少得有一位管理员", - "shareBoard.members-select-group": "成员", - "shareBoard.unknown-channel-display-name": "未知频道", - "tutorial_tip.finish_tour": "完成", - "tutorial_tip.got_it": "了解", - "tutorial_tip.ok": "下一个", - "tutorial_tip.out": "选择不使用这些提示。", - "tutorial_tip.seen": "之前有见到过吗?" -} diff --git a/webapp/boards/i18n/zh_Hant.json b/webapp/boards/i18n/zh_Hant.json deleted file mode 100644 index f5438884f6..0000000000 --- a/webapp/boards/i18n/zh_Hant.json +++ /dev/null @@ -1,451 +0,0 @@ -{ - "AppBar.Tooltip": "切換看板", - "Attachment.Attachment-title": "附件", - "AttachmentBlock.DeleteAction": "刪除", - "AttachmentBlock.addElement": "添加 {type}", - "AttachmentBlock.delete": "已刪除附件", - "AttachmentBlock.failed": "無法上傳文件。 附件大小已達到限制。", - "AttachmentBlock.upload": "附件正在上傳。", - "AttachmentBlock.uploadSuccess": "附件已上傳", - "AttachmentElement.delete-confirmation-dialog-button-text": "刪除", - "AttachmentElement.download": "下載", - "AttachmentElement.upload-percentage": "正在上傳...({uploadPercent}%)", - "BoardComponent.add-a-group": "+ 新增群組", - "BoardComponent.delete": "刪除", - "BoardComponent.hidden-columns": "隱藏列", - "BoardComponent.hide": "隱藏", - "BoardComponent.new": "+ 新增", - "BoardComponent.no-property": "無 {property}", - "BoardComponent.no-property-title": "{property} 屬性為空的項目將轉到此處。該列無法刪除。", - "BoardComponent.show": "顯示", - "BoardMember.schemeAdmin": "管理員", - "BoardMember.schemeCommenter": "評論者", - "BoardMember.schemeEditor": "編輯者", - "BoardMember.schemeNone": "無", - "BoardMember.schemeViewer": "閱覽者", - "BoardMember.unlinkChannel": "取消連結", - "BoardPage.newVersion": "新版本的版面已可用,點此處重新載入。", - "BoardPage.syncFailed": "版面可能已被刪除或已被撤銷存取。", - "BoardTemplateSelector.add-template": "創建新模板", - "BoardTemplateSelector.create-empty-board": "建立空看板", - "BoardTemplateSelector.delete-template": "刪除", - "BoardTemplateSelector.description": "使用下方定義的模板或從頭開始,從側邊欄新增一個區塊。", - "BoardTemplateSelector.edit-template": "編輯", - "BoardTemplateSelector.plugin.no-content-description": "在側邊欄新增一個板塊,可以使用下方定義的任意範本或從新開始。", - "BoardTemplateSelector.plugin.no-content-title": "建立看板", - "BoardTemplateSelector.title": "建立看板", - "BoardTemplateSelector.use-this-template": "使用此範本", - "BoardsSwitcher.Title": "尋找看板", - "BoardsUnfurl.Limited": "由於該卡片被封存,其他細節都被影藏", - "BoardsUnfurl.Remainder": "+{remainder} 更多", - "BoardsUnfurl.Updated": "更新時間 {time}", - "Calculations.Options.average.displayName": "平均", - "Calculations.Options.average.label": "平均", - "Calculations.Options.count.displayName": "數量", - "Calculations.Options.count.label": "數量", - "Calculations.Options.countChecked.displayName": "已選取", - "Calculations.Options.countChecked.label": "選取數量", - "Calculations.Options.countUnchecked.displayName": "未選取", - "Calculations.Options.countUnchecked.label": "未選取數量", - "Calculations.Options.countUniqueValue.displayName": "唯一值", - "Calculations.Options.countUniqueValue.label": "唯一值數量", - "Calculations.Options.countValue.displayName": "值", - "Calculations.Options.countValue.label": "總計", - "Calculations.Options.dateRange.displayName": "區間", - "Calculations.Options.dateRange.label": "區間", - "Calculations.Options.earliest.displayName": "最前的", - "Calculations.Options.earliest.label": "最前的", - "Calculations.Options.latest.displayName": "最後的", - "Calculations.Options.latest.label": "最後的", - "Calculations.Options.max.displayName": "最大的", - "Calculations.Options.max.label": "最大的", - "Calculations.Options.median.displayName": "中位數", - "Calculations.Options.median.label": "中位數", - "Calculations.Options.min.displayName": "最小的", - "Calculations.Options.min.label": "最小的", - "Calculations.Options.none.displayName": "計算", - "Calculations.Options.none.label": "無", - "Calculations.Options.percentChecked.displayName": "已選取", - "Calculations.Options.percentChecked.label": "已選取百分比", - "Calculations.Options.percentUnchecked.displayName": "未選取", - "Calculations.Options.percentUnchecked.label": "未選取百分比", - "Calculations.Options.range.displayName": "範圍", - "Calculations.Options.range.label": "範圍", - "Calculations.Options.sum.displayName": "總和", - "Calculations.Options.sum.label": "總和", - "CalendarCard.untitled": "無標題", - "CardActionsMenu.copiedLink": "複製!", - "CardActionsMenu.copyLink": "複製連結", - "CardActionsMenu.delete": "刪除", - "CardActionsMenu.duplicate": "重複", - "CardBadges.title-checkboxes": "選取框", - "CardBadges.title-comments": "評論", - "CardBadges.title-description": "此卡片有說明", - "CardDetail.Attach": "附加", - "CardDetail.Follow": "追蹤", - "CardDetail.Following": "追蹤中", - "CardDetail.add-content": "新增內容", - "CardDetail.add-icon": "新增圖示", - "CardDetail.add-property": "+ 新增屬性", - "CardDetail.addCardText": "新增卡片文本", - "CardDetail.limited-body": "升級到專業版或是企業版", - "CardDetail.limited-button": "升級", - "CardDetail.limited-title": "此卡片被影藏", - "CardDetail.moveContent": "移動卡片內容", - "CardDetail.new-comment-placeholder": "新增評論…", - "CardDetailProperty.confirm-delete-heading": "確認刪除屬性", - "CardDetailProperty.confirm-delete-subtext": "您確定要刪除屬性“{propertyName}”嗎? 刪除它會從該板的所有卡中刪除該屬性。", - "CardDetailProperty.confirm-property-name-change-subtext": "您確定要更改屬性“{propertyName}”{customText} 嗎? 這將影響此板中 {numOfCards} 卡的值,並可能導致數據丟失。", - "CardDetailProperty.confirm-property-type-change": "確認屬性變更", - "CardDetailProperty.delete-action-button": "刪除", - "CardDetailProperty.property-change-action-button": "變更屬性", - "CardDetailProperty.property-changed": "已成功變更屬性!", - "CardDetailProperty.property-deleted": "成功刪除 {propertyName}!", - "CardDetailProperty.property-name-change-subtext": "類型從 \"{oldPropType}\" 變更為 \"{newPropType}\"", - "CardDetial.limited-link": "了解更多我們的計畫.", - "CardDialog.delete-confirmation-dialog-attachment": "確認刪除附件", - "CardDialog.delete-confirmation-dialog-button-text": "刪除", - "CardDialog.delete-confirmation-dialog-heading": "確認刪除卡片", - "CardDialog.editing-template": "您正在編輯範本。", - "CardDialog.nocard": "卡片不存在或者無法被存取。", - "Categories.CreateCategoryDialog.CancelText": "取消", - "Categories.CreateCategoryDialog.CreateText": "新增", - "Categories.CreateCategoryDialog.Placeholder": "命名你的類別", - "Categories.CreateCategoryDialog.UpdateText": "更新", - "CenterPanel.Login": "登入", - "CenterPanel.Share": "分享", - "ChannelIntro.CreateBoard": "建立看板", - "ColorOption.selectColor": "{color} 選擇顏色", - "Comment.delete": "刪除", - "CommentsList.send": "發送", - "ConfirmPerson.empty": "空白", - "ConfirmPerson.search": "查詢...", - "ConfirmationDialog.cancel-action": "取消", - "ConfirmationDialog.confirm-action": "確認", - "ContentBlock.Delete": "刪除", - "ContentBlock.DeleteAction": "刪除", - "ContentBlock.addElement": "新增 {type}", - "ContentBlock.checkbox": "復選框", - "ContentBlock.divider": "分隔線", - "ContentBlock.editCardCheckbox": "切換復選框", - "ContentBlock.editCardCheckboxText": "編輯卡片文字", - "ContentBlock.editCardText": "編輯卡片文字", - "ContentBlock.editText": "編輯文字...", - "ContentBlock.image": "圖片", - "ContentBlock.insertAbove": "在上方插入", - "ContentBlock.moveBlock": "移動卡片內容", - "ContentBlock.moveDown": "下移", - "ContentBlock.moveUp": "上移", - "ContentBlock.text": "文字", - "DateRange.clear": "清除", - "DateRange.empty": "空白", - "DateRange.endDate": "結束日期", - "DateRange.today": "今日", - "DeleteBoardDialog.confirm-cancel": "取消", - "DeleteBoardDialog.confirm-delete": "刪除", - "DeleteBoardDialog.confirm-info": "您確定要刪除圖板“{boardTitle}”嗎? 刪除它會刪除棋盤中的所有卡片。", - "DeleteBoardDialog.confirm-info-template": "你確定要刪除此板塊名稱{boardTitle}範例?", - "DeleteBoardDialog.confirm-tite": "確認刪除看板", - "DeleteBoardDialog.confirm-tite-template": "確認刪除看板範本", - "Dialog.closeDialog": "關閉對話框", - "EditableDayPicker.today": "今天", - "Error.mobileweb": "手機板目前處於測試階段,不會呈現所有功能.", - "Error.websocket-closed": "Websocket 連線中斷,如果此問題持續發生,請檢查網路。", - "Filter.contains": "包含", - "Filter.ends-with": "結尾是", - "Filter.includes": "含有", - "Filter.is": "是", - "Filter.is-empty": "為空", - "Filter.is-not-empty": "不為空", - "Filter.is-not-set": "尚未設定", - "Filter.is-set": "已設定", - "Filter.not-contains": "不包含", - "Filter.not-ends-with": "不以結束", - "Filter.not-includes": "不包含", - "Filter.not-starts-with": "不以開始", - "Filter.starts-with": "起始於", - "FilterByText.placeholder": "過濾文字", - "FilterComponent.add-filter": "+ 增加過濾條件", - "FilterComponent.delete": "刪除", - "FilterValue.empty": "(空白)", - "FindBoardsDialog.IntroText": "查詢看板", - "FindBoardsDialog.NoResultsFor": "「{searchQuery}」搜尋未果", - "FindBoardsDialog.NoResultsSubtext": "檢查錯字或嘗試其他搜尋.", - "FindBoardsDialog.SubTitle": "輸入已找到面板.使用 UP/DOWN來瀏覽.ENTER來搜尋, ESC 來取消", - "FindBoardsDialog.Title": "尋找看板", - "GroupBy.hideEmptyGroups": "隱藏 {count}個空群組", - "GroupBy.showHiddenGroups": "顯示{count}個被隱藏的空群組", - "GroupBy.ungroup": "未分组", - "HideBoard.MenuOption": "隱藏面板", - "KanbanCard.untitled": "無標題", - "MentionSuggestion.is-not-board-member": "(非面板管理者)", - "Mutator.new-board-from-template": "新的面板模組", - "Mutator.new-card-from-template": "使用範本新增卡片", - "Mutator.new-template-from-card": "從卡片新增範本", - "OnboardingTour.AddComments.Body": "你可以對問題進行評論,甚至標記提到你的Mattermost夥伴,引起他們的注意。", - "OnboardingTour.AddComments.Title": "新增評論", - "OnboardingTour.AddDescription.Body": "在卡片上新增描述讓其他成員知道此卡片內容.", - "OnboardingTour.AddDescription.Title": "新增敘述", - "OnboardingTour.AddProperties.Body": "為卡片新增各式屬性使其更加強大", - "OnboardingTour.AddProperties.Title": "新增屬性", - "OnboardingTour.AddView.Body": "轉到此處創建一個新視圖以使用不同的佈局組織您的看板。", - "OnboardingTour.AddView.Title": "新增視圖", - "OnboardingTour.CopyLink.Body": "您可以通過複製鏈接並將其粘貼到頻道、直接消息或群組消息中來與隊友分享您的名片。", - "OnboardingTour.CopyLink.Title": "複製連結", - "OnboardingTour.OpenACard.Body": "打開卡片查看看板可以幫助你組織工作的優秀方法。", - "OnboardingTour.OpenACard.Title": "瀏覽卡片", - "OnboardingTour.ShareBoard.Body": "您可以在內部、團隊內部分享看板,或公開發布讓組織外部查看。", - "OnboardingTour.ShareBoard.Title": "分享看板", - "PersonProperty.board-members": "看板成員", - "PersonProperty.me": "我", - "PersonProperty.non-board-members": "不是看板成員", - "PropertyMenu.Delete": "刪除", - "PropertyMenu.changeType": "修改屬性類型", - "PropertyMenu.selectType": "選擇屬性類型", - "PropertyMenu.typeTitle": "類型", - "PropertyType.Checkbox": "勾選方塊", - "PropertyType.CreatedBy": "建立者", - "PropertyType.CreatedTime": "建立時間", - "PropertyType.Date": "日期", - "PropertyType.Email": "Email", - "PropertyType.MultiPerson": "多人", - "PropertyType.MultiSelect": "多選", - "PropertyType.Number": "數字", - "PropertyType.Person": "個人", - "PropertyType.Phone": "電話號碼", - "PropertyType.Select": "選取", - "PropertyType.Text": "文字框", - "PropertyType.Unknown": "未知", - "PropertyType.UpdatedBy": "最後更新者", - "PropertyType.UpdatedTime": "最後更新時間", - "PropertyType.Url": "網址", - "PropertyValueElement.empty": "空白", - "RegistrationLink.confirmRegenerateToken": "此動作將使先前分享的連結無效。確定要進行嗎?", - "RegistrationLink.copiedLink": "已複製!", - "RegistrationLink.copyLink": "複製連結", - "RegistrationLink.description": "將此連結分享給他人以建立帳號:", - "RegistrationLink.regenerateToken": "重新產生 token", - "RegistrationLink.tokenRegenerated": "已重新產生註冊鏈結", - "ShareBoard.PublishDescription": "發布只能讀取的連結。", - "ShareBoard.PublishTitle": "發布至網路", - "ShareBoard.ShareInternal": "內部分享", - "ShareBoard.ShareInternalDescription": "擁有權限的使用者才能使用此連結。", - "ShareBoard.Title": "分享看板", - "ShareBoard.confirmRegenerateToken": "此動作將使先前分享的鏈結無效。確定要進行嗎?", - "ShareBoard.copiedLink": "已複製!", - "ShareBoard.copyLink": "複製連結", - "ShareBoard.regenerate": "重新產生權杖", - "ShareBoard.searchPlaceholder": "查詢人和頻道", - "ShareBoard.teamPermissionsText": "在{teamName}的所有人", - "ShareBoard.tokenRegenrated": "已重新產生權杖", - "ShareBoard.userPermissionsRemoveMemberText": "移除成員", - "ShareBoard.userPermissionsYouText": "(你)", - "ShareTemplate.Title": "分享範本", - "ShareTemplate.searchPlaceholder": "查詢人", - "Sidebar.about": "關於 Focalboard", - "Sidebar.add-board": "+ 新增看板", - "Sidebar.changePassword": "變更密碼", - "Sidebar.delete-board": "刪除版面", - "Sidebar.duplicate-board": "複製看板", - "Sidebar.export-archive": "匯出打包檔", - "Sidebar.import": "匯入", - "Sidebar.import-archive": "匯入打包檔", - "Sidebar.invite-users": "邀請使用者", - "Sidebar.logout": "登出", - "Sidebar.new-category.badge": "新的", - "Sidebar.new-category.drag-boards-cta": "拖板到這裡...", - "Sidebar.no-boards-in-category": "沒有看板在裡面", - "Sidebar.product-tour": "產品導覽", - "Sidebar.random-icons": "隨機圖示", - "Sidebar.set-language": "設定語言", - "Sidebar.set-theme": "設定佈景主題", - "Sidebar.settings": "設定", - "Sidebar.template-from-board": "新的看板模板", - "Sidebar.untitled-board": "(無標題版面)", - "Sidebar.untitled-view": "(無題視圖)", - "SidebarCategories.BlocksMenu.Move": "移動至…", - "SidebarCategories.CategoryMenu.CreateNew": "新增分類", - "SidebarCategories.CategoryMenu.Delete": "刪除分類", - "SidebarCategories.CategoryMenu.DeleteModal.Body": "{categoryName} 中的看板將移回看板類別。 您不會從任何板上刪除。", - "SidebarCategories.CategoryMenu.DeleteModal.Title": "刪除這個分類?", - "SidebarCategories.CategoryMenu.Update": "重新命名分類", - "SidebarTour.ManageCategories.Body": "創建和管理自定義類別。 類別是特定於用戶的,因此將圖板移至您的類別不會影響使用同一圖板的其他成員。", - "SidebarTour.ManageCategories.Title": "管理分類", - "SidebarTour.SearchForBoards.Body": "打開板切換器 (Cmd/Ctrl + K) 以快速搜索板並將其添加到側邊欄。", - "SidebarTour.SearchForBoards.Title": "查詢看板", - "SidebarTour.SidebarCategories.Body": "您所有的看板現在都在您的新側邊欄下進行了組織。 不再在工作區之間切換。 作為 v7.2 升級的一部分,可能會自動為您創建基於您之前工作區的一次性自定義類別。 這些可以根據您的喜好刪除或編輯。", - "SidebarTour.SidebarCategories.Link": "更多", - "SidebarTour.SidebarCategories.Title": "邊欄類別", - "SiteStats.total_boards": "所有看板", - "SiteStats.total_cards": "總卡片數", - "TableComponent.add-icon": "加入圖示", - "TableComponent.name": "姓名", - "TableComponent.plus-new": "+ 新增", - "TableHeaderMenu.delete": "刪除", - "TableHeaderMenu.duplicate": "制作副本", - "TableHeaderMenu.hide": "隱藏", - "TableHeaderMenu.insert-left": "在左側插入", - "TableHeaderMenu.insert-right": "在右側插入", - "TableHeaderMenu.sort-ascending": "升序排列", - "TableHeaderMenu.sort-descending": "降序排列", - "TableRow.DuplicateCard": "複製卡片", - "TableRow.MoreOption": "更多操作", - "TableRow.open": "開啟", - "TopBar.give-feedback": "提供回饋", - "URLProperty.copiedLink": "已複製!", - "URLProperty.copy": "複製", - "URLProperty.edit": "編輯", - "UndoRedoHotKeys.canRedo": "重新執行", - "UndoRedoHotKeys.canRedo-with-description": "撤銷{description}", - "UndoRedoHotKeys.canUndo": "撤銷", - "UndoRedoHotKeys.canUndo-with-description": "重新執行 {description}", - "UndoRedoHotKeys.cannotRedo": "沒有可以重寫的", - "UndoRedoHotKeys.cannotUndo": "沒有可以取消的", - "ValueSelector.noOptions": "沒有選項.開始輸入第一個字!", - "ValueSelector.valueSelector": "值選擇器", - "ValueSelectorLabel.openMenu": "開啟選單", - "VersionMessage.help": "查看這個版本有什麼新功能.", - "View.AddView": "新增視圖", - "View.Board": "版面", - "View.DeleteView": "刪除視圖", - "View.DuplicateView": "建立視圖副本", - "View.Gallery": "畫廊", - "View.NewBoardTitle": "版面視圖", - "View.NewCalendarTitle": "行事曆檢視", - "View.NewGalleryTitle": "畫廊視圖", - "View.NewTableTitle": "圖表視圖", - "View.NewTemplateDefaultTitle": "沒有標題的模板", - "View.NewTemplateTitle": "沒有標題", - "View.Table": "圖表", - "ViewHeader.add-template": "新範本", - "ViewHeader.delete-template": "刪除", - "ViewHeader.display-by": "依據{property}顯示", - "ViewHeader.edit-template": "編輯", - "ViewHeader.empty-card": "清空卡片", - "ViewHeader.export-board-archive": "匯出版面打包檔", - "ViewHeader.export-complete": "匯出完成!", - "ViewHeader.export-csv": "匯出為 CSV", - "ViewHeader.export-failed": "匯出失敗!", - "ViewHeader.filter": "篩選", - "ViewHeader.group-by": "以 {property} 分組", - "ViewHeader.new": "新", - "ViewHeader.properties": "屬性", - "ViewHeader.properties-menu": "屬性菜單", - "ViewHeader.search-text": "搜尋文字", - "ViewHeader.select-a-template": "選擇範本", - "ViewHeader.set-default-template": "設為預設", - "ViewHeader.sort": "排序", - "ViewHeader.untitled": "無標題", - "ViewHeader.view-header-menu": "查看標題菜單", - "ViewHeader.view-menu": "查看菜單", - "ViewLimitDialog.Heading": "已達到每個看板觀看限制", - "ViewLimitDialog.PrimaryButton.Title.Admin": "升級", - "ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理者", - "ViewLimitDialog.Subtext.Admin": "升級到專業版或企業版", - "ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多我們的計畫。", - "ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版", - "ViewLimitDialog.UpgradeImg.AltText": "升級圖片", - "ViewLimitDialog.notifyAdmin.Success": "已經通知管理者", - "ViewTitle.hide-description": "隱藏敘述", - "ViewTitle.pick-icon": "挑選圖示", - "ViewTitle.random-icon": "隨機", - "ViewTitle.remove-icon": "移除圖示", - "ViewTitle.show-description": "顯示敘述", - "ViewTitle.untitled-board": "未命名版面", - "WelcomePage.Description": "看板是一個專案管理工具,可以使用熟悉的圖表幫助我們定義、組織、追蹤和管理跨團隊工作。", - "WelcomePage.Explore.Button": "探索", - "WelcomePage.Heading": "歡迎來到看板", - "WelcomePage.NoThanks.Text": "不需要,自己想辦法", - "WelcomePage.StartUsingIt.Text": "開始使用", - "Workspace.editing-board-template": "您正在編輯版面範本。", - "badge.guest": "訪客", - "boardSelector.confirm-link-board": "連結看板與頻道", - "boardSelector.confirm-link-board-button": "是,連結看板", - "boardSelector.confirm-link-board-subtext": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。你可以在任何時候從一個頻道上取消看板的連接。", - "boardSelector.confirm-link-board-subtext-with-other-channel": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。{lineBreak} 看板目前正連接到另一個頻道。如果选择在這裡連接它,將取消另一個連接。", - "boardSelector.create-a-board": "建立看板", - "boardSelector.link": "連結", - "boardSelector.search-for-boards": "搜尋看板", - "boardSelector.title": "連結看板", - "boardSelector.unlink": "未連結", - "calendar.month": "月份", - "calendar.today": "今日", - "calendar.week": "週別", - "centerPanel.undefined": "沒有 {propertyName}", - "centerPanel.unknown-user": "未知使用者", - "cloudMessage.learn-more": "學習更多", - "createImageBlock.failed": "無法上傳檔案,檔案大小超過限制。", - "default-properties.badges": "評論和描述", - "default-properties.title": "標題", - "error.back-to-home": "返回首頁", - "error.back-to-team": "回到團隊", - "error.board-not-found": "沒有找到看板.", - "error.go-login": "登入", - "error.invalid-read-only-board": "沒有權限進入此看板.登入後才能訪問.", - "error.not-logged-in": "已被登出,請再次登入使用看板。", - "error.page.title": "很抱歉,發生了些錯誤", - "error.team-undefined": "不是有效的團隊。", - "error.unknown": "發生一個錯誤。", - "generic.previous": "上一篇", - "guest-no-board.subtitle": "你尚未有權限進入此看板,請等人把你加入任何看板。", - "guest-no-board.title": "尚未有看板", - "imagePaste.upload-failed": "有些檔案無法上傳.檔案大小達上限", - "limitedCard.title": "影藏卡片", - "login.log-in-button": "登錄", - "login.log-in-title": "登錄", - "login.register-button": "或創建一個帳戶(如果您沒有帳戶)", - "new_channel_modal.create_board.empty_board_description": "建立新的空白看板", - "new_channel_modal.create_board.empty_board_title": "空白看板", - "new_channel_modal.create_board.select_template_placeholder": "選擇一個範本", - "new_channel_modal.create_board.title": "在這個頻道新建一個看板", - "notification-box-card-limit-reached.close-tooltip": "小睡十天", - "notification-box-card-limit-reached.contact-link": "通知管理員", - "notification-box-card-limit-reached.link": "升級到付費版", - "notification-box-card-limit-reached.title": "將看板上{cards}卡片隱藏", - "notification-box-cards-hidden.title": "此行為隱藏了其他卡片", - "notification-box.card-limit-reached.not-admin.text": "要存取已封存的卡片,你可以點擊{contactLink}升級到付費版。", - "notification-box.card-limit-reached.text": "已達卡片上限,觀看舊卡片請點{link}", - "person.add-user-to-board": "將{username} 加入看板", - "person.add-user-to-board-confirm-button": "新增看板", - "person.add-user-to-board-permissions": "權限", - "person.add-user-to-board-question": "你想將{username} 加入到看板嗎?", - "person.add-user-to-board-warning": "{username}不是看板的成員,也不會收到任何有關的通知.", - "register.login-button": "或登錄(如果您已擁有帳戶)", - "register.signup-title": "註冊您的帳戶", - "rhs-board-non-admin-msg": "你不是看板的管理者", - "rhs-boards.add": "新增", - "rhs-boards.dm": "私人訊息", - "rhs-boards.gm": "群組訊息", - "rhs-boards.header.dm": "此私人訊息", - "rhs-boards.header.gm": "此群組訊息", - "rhs-boards.last-update-at": "最後更新日: {datetime}", - "rhs-boards.link-boards-to-channel": "將看板連接到{channelName}", - "rhs-boards.linked-boards": "連結看板", - "rhs-boards.no-boards-linked-to-channel": "還沒有看板與{channelName}連接", - "rhs-boards.no-boards-linked-to-channel-description": "看板是一個專案管理工具,可以使用熟悉的圖表幫助我們定義、組織、追蹤和管理跨團隊工作。", - "rhs-boards.unlink-board": "未連結看板", - "rhs-boards.unlink-board1": "未連結看板", - "rhs-channel-boards-header.title": "板塊", - "share-board.publish": "發布", - "share-board.share": "分享", - "shareBoard.channels-select-group": "頻道", - "shareBoard.confirm-change-team-role.body": "此看板上所有低於\"{role}\"的人都將被提升到{role}。你確定要改變這個看板最低角色?", - "shareBoard.confirm-change-team-role.confirmBtnText": "改變最小的看板規則", - "shareBoard.confirm-change-team-role.title": "改變最小的看板規則", - "shareBoard.confirm-link-channel": "連接看板到頻道", - "shareBoard.confirm-link-channel-button": "連接頻道", - "shareBoard.confirm-link-channel-button-with-other-channel": "解除連接或連接這", - "shareBoard.confirm-link-channel-subtext": "當你連接頻道到看板,該頻道所有成員(包含新的與現有的)都可以編輯,不包括訪客。", - "shareBoard.confirm-link-channel-subtext-with-other-channel": "當你將一個頻道連接到看板時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分{lineBreak}看板目前正連接到另一個頻道。如果选择在這裡連接它,將取消另一個連接。", - "shareBoard.confirm-unlink.body": "當你取消頻道與看板連接,所有頻道成員(現在和新的)都將無法失去查看權限,除非單獨獲得許可。", - "shareBoard.confirm-unlink.confirmBtnText": "解除連結頻道", - "shareBoard.confirm-unlink.title": "從看板上取消頻道連接", - "shareBoard.lastAdmin": "看板必須有一位管理者", - "shareBoard.members-select-group": "會員", - "shareBoard.unknown-channel-display-name": "未知管道", - "tutorial_tip.finish_tour": "完成", - "tutorial_tip.got_it": "了解", - "tutorial_tip.ok": "下一步", - "tutorial_tip.out": "不接受這個提示.", - "tutorial_tip.seen": "以前有見過嗎?" -} diff --git a/webapp/boards/jest.config.js b/webapp/boards/jest.config.js deleted file mode 100644 index f02b27668f..0000000000 --- a/webapp/boards/jest.config.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/** @type {import('jest').Config} */ - -const config = { - transform: { - '^.+\\.(t|j)sx?$': ['@swc/jest'], - }, - moduleFileExtensions: [ - 'ts', - 'tsx', - 'js', - 'jsx', - 'json', - 'node', - ], - extensionsToTreatAsEsm: ['.ts', '.tsx'], - transformIgnorePatterns: [ - '/nanoevents/', - 'node_modules/(?!react-native|react-router|react-day-picker)', - ], - testEnvironment: 'jsdom', - collectCoverage: true, - collectCoverageFrom: [ - 'src/**/*.{ts,tsx,js,jsx}', - '!src/test/**', - ], - testPathIgnorePatterns: [ - '/node_modules/', - ], - clearMocks: true, - coverageReporters: [ - 'lcov', - 'text-summary', - ], - moduleNameMapper: { - '^.+\\.(scss|css)$': '/src/test/style_mock.json', - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js', - '\\.(scss|css)$': '/__mocks__/styleMock.js', - '^src(.*)$': '/src$1', - '^i18n(.*)$': '/i18n$1', - '^static(.*)$': '/static$1', - '^moment(.*)$': '/../node_modules/moment$1', - }, - moduleDirectories: [ - 'src', - 'node_modules', - ], - reporters: [ - 'default', - 'jest-junit', - ], - setupFiles: [ - 'jest-canvas-mock', - ], - setupFilesAfterEnv: [ - '/src/test/setup.tsx', - ], - testTimeout: 60000, - testEnvironmentOptions: { - url: 'http://localhost:8065', - }, -}; - -module.exports = config; diff --git a/webapp/boards/loaders/globalScssClassLoader.js b/webapp/boards/loaders/globalScssClassLoader.js deleted file mode 100644 index 4e0f25229e..0000000000 --- a/webapp/boards/loaders/globalScssClassLoader.js +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -function blockList(line) { - return line.startsWith('.focalboard-body') || - line.startsWith('.GlobalHeaderComponent') || - line.startsWith('.boards-rhs-icon') || - line.startsWith('.focalboard-plugin-root') || - line.startsWith('.FocalboardUnfurl') || - line.startsWith('.CreateBoardFromTemplate'); -} - -module.exports = function loader(source) { - var newSource = []; - source.split('\n').forEach((line) => { - if ((line.startsWith('.') || line.startsWith('#')) && !blockList(line)) { - newSource.push('.focalboard-body ' + line); - } else { - newSource.push(line); - } - }); - - return newSource.join('\n'); -}; diff --git a/webapp/boards/package.json b/webapp/boards/package.json deleted file mode 100644 index f09c0348c0..0000000000 --- a/webapp/boards/package.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "name": "boards", - "version": "8.0.0", - "private": true, - "description": "", - "scripts": { - "build": "webpack --mode=production", - "build:watch": "webpack --mode=production --watch", - "start:product": "webpack --mode=development --watch", - "check-lint": "eslint --ignore-pattern ../.git-ignore --ignore-pattern dist --ext js,jsx,tsx,ts . --quiet --cache", - "check-lint:fix": "npm run check-lint -- --fix", - "check-style": "stylelint **/*.scss", - "check-style:fix": "npm run check-style -- --fix", - "check-style:fix:prettier": "prettier --write './src/**/*.scss'", - "check-types": "tsc -b", - "check-types:fix": "npm run check-types -- --noEmit --fix", - "check": "npm run check-lint && npm run check-style", - "i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format simple --out-file i18n/en.json", - "fix": "npm run check-lint:fix && npm run check-style:fix", - "test": "cross-env TZ=Etc/UTC jest", - "test:watch": "cross-env TZ=Etc/UTC jest --watch", - "test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot", - "test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose ", - "test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%", - "clean": "rm -rf node_modules .eslintcache" - }, - "dependencies": { - "@draft-js-plugins/editor": "^4.1.2", - "@draft-js-plugins/emoji": "^4.6.0", - "@draft-js-plugins/mention": "^5.1.2", - "@fullcalendar/core": "^5.10.1", - "@fullcalendar/daygrid": "^5.10.1", - "@fullcalendar/interaction": "^5.10.1", - "@fullcalendar/react": "^5.10.1", - "@mattermost/compass-icons": "^0.1.28", - "@reduxjs/toolkit": "^1.8.0", - "@tippyjs/react": "4.2.6", - "color": "^4.2.1", - "core-js": "3.21.1", - "draft-js": "^0.11.7", - "emoji-mart": "^3.0.1", - "fstream": "^1.0.12", - "fullcalendar": "^5.10.2", - "glob-parent": "6.0.2", - "lodash": "^4.17.21", - "marked": "4.0.17", - "moment": "^2.29.1", - "nanoevents": "^5.1.13", - "react": "17.0.2", - "react-beautiful-dnd": "^13.1.1", - "react-day-picker": "7.4.10", - "react-dnd": "^14.0.2", - "react-dnd-html5-backend": "^14.0.0", - "react-dnd-scrolling": "^1.2.1", - "react-dnd-touch-backend": "^14.0.0", - "react-dom": "17.0.2", - "react-hot-keys": "^2.7.1", - "react-hotkeys-hook": "^3.4.4", - "react-intl": "*", - "react-redux": "^7.2.1", - "react-router-dom": "^5.2.0", - "react-select": "5.5.9", - "trim-newlines": "^4.0.2" - }, - "devDependencies": { - "@formatjs/cli": "^4.8.2", - "@formatjs/ts-transformer": "^3.9.2", - "@swc/core": "1.3.40", - "@swc/jest": "0.2.24", - "@testing-library/dom": "8.20.0", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "12.1.5", - "@testing-library/user-event": "14.4.3", - "@types/color": "^3.0.3", - "@types/draft-js": "^0.11.9", - "@types/emoji-mart": "^3.0.9", - "@types/enzyme": "3.10.11", - "@types/jest": "29.4.2", - "@types/lodash": "4.14.182", - "@types/marked": "^4.0.3", - "@types/nanoevents": "^1.0.0", - "@types/node": "16.11.7", - "@types/react": "17.0.53", - "@types/react-beautiful-dnd": "^13.1.2", - "@types/react-dom": "17.0.19", - "@types/react-redux": "^7.1.23", - "@types/react-router-dom": "^5.3.3", - "@types/react-transition-group": "4.4.4", - "@types/redux-mock-store": "1.0.3", - "@typescript-eslint/eslint-plugin": "5.57.1", - "@typescript-eslint/parser": "5.57.1", - "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-formatjs": "4.9.0", - "eslint-plugin-header": "3.1.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-import-newlines": "1.3.1", - "eslint-plugin-no-only-tests": "2.6.0", - "eslint-plugin-no-relative-import-paths": "1.5.2", - "eslint-plugin-react": "7.29.4", - "eslint-plugin-react-hooks": "4.3.0", - "eslint-plugin-unused-imports": "2.0.0", - "fetch-mock-jest": "1.5.1", - "identity-obj-proxy": "3.0.0", - "imagemin-gifsicle": "^7.0.0", - "imagemin-mozjpeg": "^10.0.0", - "imagemin-optipng": "^8.0.0", - "imagemin-pngquant": "^9.0.2", - "imagemin-svgo": "^10.0.1", - "imagemin-webp": "7.0.0", - "isomorphic-fetch": "3.0.0", - "jest": "29.5.0", - "jest-canvas-mock": "2.4.0", - "jest-environment-jsdom": "29.5.0", - "jest-fail-on-console": "3.0.2", - "jest-junit": "15.0.0", - "jest-mock": "29.4.3", - "prettier": "^2.6.1", - "redux-mock-store": "^1.5.4", - "start-server-and-test": "^1.14.0", - "stylelint": "^14.6.1", - "stylelint-config-sass-guidelines": "^9.0.1", - "ts-loader": "9.4.2", - "typescript": "4.6.2" - } -} diff --git a/webapp/boards/src/app.tsx b/webapp/boards/src/app.tsx deleted file mode 100644 index 28d40e4fa4..0000000000 --- a/webapp/boards/src/app.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import React, {useEffect} from 'react' -import {DndProvider} from 'react-dnd' -import {HTML5Backend} from 'react-dnd-html5-backend' -import {TouchBackend} from 'react-dnd-touch-backend' -import {History} from 'history' - -import TelemetryClient from './telemetry/telemetryClient' - -import FlashMessages from './components/flashMessages' -import NewVersionBanner from './components/newVersionBanner' -import {Utils} from './utils' -import {fetchMe, getMe} from './store/users' -import {useAppDispatch, useAppSelector} from './store/hooks' -import {fetchClientConfig} from './store/clientConfig' -import FocalboardRouter from './router' - -import {IUser} from './user' - -type Props = { - history?: History -} - -const App = (props: Props): JSX.Element => { - const me = useAppSelector(getMe) - const dispatch = useAppDispatch() - - useEffect(() => { - dispatch(fetchMe()) - dispatch(fetchClientConfig()) - }, []) - - useEffect(() => { - if (me) { - TelemetryClient.setUser(me) - } - }, [me]) - - return ( - - -
-
- - -
-
-
- ) -} - -export default React.memo(App) diff --git a/webapp/boards/src/archiver.ts b/webapp/boards/src/archiver.ts deleted file mode 100644 index 01dfb30f28..0000000000 --- a/webapp/boards/src/archiver.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {IAppWindow} from './types' -import {Block} from './blocks/block' -import {Board} from './blocks/board' -import mutator from './mutator' -import {Utils} from './utils' - -declare let window: IAppWindow - -class Archiver { - static async exportBoardArchive(board: Board): Promise { - this.exportArchive(mutator.exportBoardArchive(board.id)) - } - - static async exportFullArchive(teamID: string): Promise { - this.exportArchive(mutator.exportFullArchive(teamID)) - } - - private static exportArchive(prom: Promise): void { - // TODO: don't download whole archive before presenting SaveAs dialog. - prom.then((response) => { - response.blob() - .then((blob) => { - const link = document.createElement('a') - link.style.display = 'none' - - const date = new Date() - const filename = `archive-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.boardarchive` - - const file = new Blob([blob], {type: 'application/octet-stream'}) - link.href = URL.createObjectURL(file) - link.download = filename - document.body.appendChild(link) // FireFox support - - link.click() - - // TODO: Review if this is needed in the future, this is to fix the problem with linux webview links - if (window.openInNewBrowser) { - window.openInNewBrowser(link.href) - } - - // TODO: Remove or reuse link and revolkObjectURL to avoid memory leak - }) - }) - } - - private static async importArchiveFromFile(file: File): Promise { - const response = await mutator.importFullArchive(file) - if (response.status !== 200) { - Utils.log('ERROR importing archive: ' + response.text()) - } - } - - static isValidBlock(block: Block): boolean { - if (!block.id || !block.boardId) { - return false - } - - return true - } - - static importFullArchive(onComplete?: () => void): void { - const input = document.createElement('input') - input.type = 'file' - input.accept = '.boardarchive' - input.onchange = async () => { - const file = input.files && input.files[0] - if (file) { - await Archiver.importArchiveFromFile(file) - } - - onComplete?.() - } - - input.style.display = 'none' - document.body.appendChild(input) - input.click() - - // TODO: Remove or reuse input - } -} - -export {Archiver} diff --git a/webapp/boards/src/blockIcons.ts b/webapp/boards/src/blockIcons.ts deleted file mode 100644 index 2390f0acf3..0000000000 --- a/webapp/boards/src/blockIcons.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {randomEmojiList} from './emojiList' - -class BlockIcons { - static readonly shared = new BlockIcons() - - randomIcon(): string { - const index = Math.floor(Math.random() * randomEmojiList.length) - const icon = randomEmojiList[index] - - return icon - } -} - -export {BlockIcons} diff --git a/webapp/boards/src/blocks/__snapshots__/block.test.ts.snap b/webapp/boards/src/blocks/__snapshots__/block.test.ts.snap deleted file mode 100644 index ed6cd80e54..0000000000 --- a/webapp/boards/src/blocks/__snapshots__/block.test.ts.snap +++ /dev/null @@ -1,63 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`block tests correctly generate patches from two blocks should add fields on the new fields added and remove it in the undo 1`] = ` -[ - { - "deletedFields": [], - "updatedFields": { - "newField": "new field", - }, - }, - { - "deletedFields": [ - "newField", - ], - "updatedFields": {}, - }, -] -`; - -exports[`block tests correctly generate patches from two blocks should generate two empty patches for the same block 1`] = ` -[ - { - "deletedFields": [], - "updatedFields": {}, - }, - { - "deletedFields": [], - "updatedFields": {}, - }, -] -`; - -exports[`block tests correctly generate patches from two blocks should remove field on the new block added and add it again in the undo 1`] = ` -[ - { - "deletedFields": [ - "test", - ], - "updatedFields": {}, - }, - { - "deletedFields": [], - "updatedFields": { - "test": "test", - }, - }, -] -`; - -exports[`block tests correctly generate patches from two blocks should update propertie on the main object and revert it back on the undo 1`] = ` -[ - { - "deletedFields": [], - "parentId": "new-parent-id", - "updatedFields": {}, - }, - { - "deletedFields": [], - "parentId": "old-parent-id", - "updatedFields": {}, - }, -] -`; diff --git a/webapp/boards/src/blocks/__snapshots__/board.test.ts.snap b/webapp/boards/src/blocks/__snapshots__/board.test.ts.snap deleted file mode 100644 index c5042946da..0000000000 --- a/webapp/boards/src/blocks/__snapshots__/board.test.ts.snap +++ /dev/null @@ -1,259 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`board tests correctly generate patches for boards and blocks should add fields on update and remove it in the undo 1`] = ` -[ - { - "blockIDs": [ - "test-old-block-id", - ], - "blockPatches": [ - { - "deletedFields": [], - "updatedFields": { - "newField": "new field", - }, - }, - ], - "boardIDs": [ - "test-board-id", - ], - "boardPatches": [ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, - ], - }, - { - "blockIDs": [ - "test-old-block-id", - ], - "blockPatches": [ - { - "deletedFields": [ - "newField", - ], - "updatedFields": {}, - }, - ], - "boardIDs": [ - "test-board-id", - ], - "boardPatches": [ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, - ], - }, -] -`; - -exports[`board tests correctly generate patches for boards and blocks should generate two empty patches for the same board and block 1`] = ` -[ - { - "blockIDs": [ - "test-card-id", - ], - "blockPatches": [ - { - "deletedFields": [], - "updatedFields": {}, - }, - ], - "boardIDs": [ - "test-board-id", - ], - "boardPatches": [ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, - ], - }, - { - "blockIDs": [ - "test-card-id", - ], - "blockPatches": [ - { - "deletedFields": [], - "updatedFields": {}, - }, - ], - "boardIDs": [ - "test-board-id", - ], - "boardPatches": [ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, - ], - }, -] -`; - -exports[`board tests correctly generate patches from two boards should add card properties on the redo and remove them on the undo 1`] = ` -[ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [ - { - "id": "new-property-id", - "name": "property-name", - "options": [ - { - "color": "propColorYellow", - "id": "opt", - "value": "val", - }, - ], - "type": "select", - }, - ], - "updatedProperties": {}, - }, - { - "deletedCardProperties": [ - "new-property-id", - ], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, -] -`; - -exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but differ 1`] = ` -[ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [ - { - "id": "new-property-id", - "name": "property-name", - "options": [ - { - "color": "propColorYellow", - "id": "opt", - "value": "val", - }, - ], - "type": "select", - }, - ], - "updatedProperties": {}, - }, - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [ - { - "id": "new-property-id", - "name": "a-different-name", - "options": [ - { - "color": "propColorYellow", - "id": "opt", - "value": "val", - }, - ], - "type": "select", - }, - ], - "updatedProperties": {}, - }, -] -`; - -exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but their options are different 1`] = ` -[ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [ - { - "id": "new-property-id", - "name": "property-name", - "options": [ - { - "color": "propColorYellow", - "id": "opt", - "value": "val", - }, - ], - "type": "select", - }, - ], - "updatedProperties": {}, - }, - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [ - { - "id": "new-property-id", - "name": "property-name", - "options": [ - { - "color": "propColorBrown", - "id": "another-opt", - "value": "val", - }, - ], - "type": "select", - }, - ], - "updatedProperties": {}, - }, -] -`; - -exports[`board tests correctly generate patches from two boards should add properties on the update patch and remove them on the undo 1`] = ` -[ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": { - "prop1": "val1", - }, - }, - { - "deletedCardProperties": [], - "deletedProperties": [ - "prop1", - ], - "updatedCardProperties": [], - "updatedProperties": {}, - }, -] -`; - -exports[`board tests correctly generate patches from two boards should generate two empty patches for the same board 1`] = ` -[ - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, - { - "deletedCardProperties": [], - "deletedProperties": [], - "updatedCardProperties": [], - "updatedProperties": {}, - }, -] -`; diff --git a/webapp/boards/src/blocks/attachmentBlock.tsx b/webapp/boards/src/blocks/attachmentBlock.tsx deleted file mode 100644 index f84e6d051a..0000000000 --- a/webapp/boards/src/blocks/attachmentBlock.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Block, createBlock} from './block' - -type AttachmentBlockFields = { - fileId: string -} - -type AttachmentBlock = Block & { - type: 'attachment' - fields: AttachmentBlockFields - isUploading: boolean - uploadingPercent: number -} - -function createAttachmentBlock(block?: Block): AttachmentBlock { - return { - ...createBlock(block), - type: 'attachment', - fields: { - fileId: block?.fields.attachmentId || block?.fields.fileId || '', - }, - isUploading: false, - uploadingPercent: 0, - } -} - -export {AttachmentBlock, createAttachmentBlock} diff --git a/webapp/boards/src/blocks/block.test.ts b/webapp/boards/src/blocks/block.test.ts deleted file mode 100644 index 3da8d852a5..0000000000 --- a/webapp/boards/src/blocks/block.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {TestBlockFactory} from 'src/test/testBlockFactory' - -import {createBlock, createPatchesFromBlocks} from './block' - -describe('block tests', () => { - const board = TestBlockFactory.createBoard() - const card = TestBlockFactory.createCard(board) - - describe('correctly generate patches from two blocks', () => { - it('should generate two empty patches for the same block', () => { - const textBlock = TestBlockFactory.createText(card) - const result = createPatchesFromBlocks(textBlock, textBlock) - expect(result).toMatchSnapshot() - }) - - it('should add fields on the new fields added and remove it in the undo', () => { - const oldBlock = TestBlockFactory.createText(card) - const newBlock = createBlock(oldBlock) - newBlock.fields.newField = 'new field' - const result = createPatchesFromBlocks(newBlock, oldBlock) - expect(result).toMatchSnapshot() - }) - - it('should remove field on the new block added and add it again in the undo', () => { - const oldBlock = TestBlockFactory.createText(card) - const newBlock = createBlock(oldBlock) - oldBlock.fields.test = 'test' - const result = createPatchesFromBlocks(newBlock, oldBlock) - expect(result).toMatchSnapshot() - }) - - it('should update propertie on the main object and revert it back on the undo', () => { - const oldBlock = TestBlockFactory.createText(card) - const newBlock = createBlock(oldBlock) - oldBlock.parentId = 'old-parent-id' - newBlock.parentId = 'new-parent-id' - const result = createPatchesFromBlocks(newBlock, oldBlock) - expect(result).toMatchSnapshot() - }) - }) -}) diff --git a/webapp/boards/src/blocks/block.ts b/webapp/boards/src/blocks/block.ts deleted file mode 100644 index 0e9e4e3a9b..0000000000 --- a/webapp/boards/src/blocks/block.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import difference from 'lodash/difference' - -import {Utils} from 'src/utils' - -const contentBlockTypes = ['text', 'image', 'divider', 'checkbox', 'h1', 'h2', 'h3', 'list-item', 'attachment', 'quote', 'video'] as const - -// ToDo: remove type board -const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment', 'attachment', 'unknown'] as const -type ContentBlockTypes = typeof contentBlockTypes[number] -type BlockTypes = typeof blockTypes[number] - -interface BlockPatch { - parentId?: string - schema?: number - type?: BlockTypes - title?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - updatedFields?: Record - deletedFields?: string[] - deleteAt?: number -} - -interface Block { - id: string - boardId: string - parentId: string - createdBy: string - modifiedBy: string - - schema: number - type: BlockTypes - title: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: Record - - createAt: number - updateAt: number - deleteAt: number - - limited?: boolean -} - -interface FileInfo { - url?: string - archived?: boolean - extension?: string - name?: string - size?: number -} - -function createBlock(block?: Block): Block { - const now = Date.now() - - return { - id: block?.id || Utils.createGuid(Utils.blockTypeToIDType(block?.type)), - schema: 1, - boardId: block?.boardId || '', - parentId: block?.parentId || '', - createdBy: block?.createdBy || '', - modifiedBy: block?.modifiedBy || '', - type: block?.type || 'unknown', - fields: block?.fields ? {...block?.fields} : {}, - title: block?.title || '', - createAt: block?.createAt || now, - updateAt: block?.updateAt || now, - deleteAt: block?.deleteAt || 0, - limited: Boolean(block?.limited), - } -} - -// createPatchesFromBlocks creates two BlockPatch instances, one that -// contains the delta to update the block and another one for the undo -// action, in case it happens -function createPatchesFromBlocks(newBlock: Block, oldBlock: Block): BlockPatch[] { - const newDeletedFields = difference(Object.keys(newBlock.fields), Object.keys(oldBlock.fields)) - const newUpdatedFields: Record = {} - const newUpdatedData: Record = {} - Object.keys(newBlock.fields).forEach((val) => { - if (oldBlock.fields[val] !== newBlock.fields[val]) { - newUpdatedFields[val] = newBlock.fields[val] - } - }) - Object.keys(newBlock).forEach((val) => { - if (val !== 'fields' && (oldBlock as any)[val] !== (newBlock as any)[val]) { - newUpdatedData[val] = (newBlock as any)[val] - } - }) - - const oldDeletedFields = difference(Object.keys(oldBlock.fields), Object.keys(newBlock.fields)) - const oldUpdatedFields: Record = {} - const oldUpdatedData: Record = {} - Object.keys(oldBlock.fields).forEach((val) => { - if (oldBlock.fields[val] !== newBlock.fields[val]) { - oldUpdatedFields[val] = oldBlock.fields[val] - } - }) - Object.keys(oldBlock).forEach((val) => { - if (val !== 'fields' && (oldBlock as any)[val] !== (newBlock as any)[val]) { - oldUpdatedData[val] = (oldBlock as any)[val] - } - }) - - return [ - { - ...newUpdatedData, - updatedFields: newUpdatedFields, - deletedFields: oldDeletedFields, - }, - { - ...oldUpdatedData, - updatedFields: oldUpdatedFields, - deletedFields: newDeletedFields, - }, - ] -} - -export type {ContentBlockTypes, BlockTypes, FileInfo} -export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock, createPatchesFromBlocks} diff --git a/webapp/boards/src/blocks/board.test.ts b/webapp/boards/src/blocks/board.test.ts deleted file mode 100644 index 2b3ff84e2c..0000000000 --- a/webapp/boards/src/blocks/board.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {TestBlockFactory} from 'src/test/testBlockFactory' - -import { - IPropertyTemplate, - createBoard, - createPatchesFromBoards, - createPatchesFromBoardsAndBlocks, -} from './board' -import {createBlock} from './block' - -describe('board tests', () => { - describe('correctly generate patches from two boards', () => { - it('should generate two empty patches for the same board', () => { - const board = TestBlockFactory.createBoard() - const result = createPatchesFromBoards(board, board) - expect(result).toMatchSnapshot() - }) - - it('should add properties on the update patch and remove them on the undo', () => { - const board = TestBlockFactory.createBoard() - board.properties = { - prop1: 'val1', - prop2: 'val2', - } - const oldBoard = createBoard(board) - oldBoard.properties = { - prop2: 'val2', - } - - const result = createPatchesFromBoards(board, oldBoard) - expect(result).toMatchSnapshot() - }) - - it('should add card properties on the redo and remove them on the undo', () => { - const board = TestBlockFactory.createBoard() - const oldBoard = createBoard(board) - board.cardProperties.push({ - id: 'new-property-id', - name: 'property-name', - type: 'select', - options: [{ - id: 'opt', - value: 'val', - color: 'propColorYellow', - }], - }) - - const result = createPatchesFromBoards(board, oldBoard) - expect(result).toMatchSnapshot() - }) - - it('should add card properties on the redo and undo if they exists in both, but differ', () => { - const cardProperty = { - id: 'new-property-id', - name: 'property-name', - type: 'select', - options: [{ - id: 'opt', - value: 'val', - color: 'propColorYellow', - }], - } as IPropertyTemplate - - const board = TestBlockFactory.createBoard() - const oldBoard = createBoard(board) - board.cardProperties = [cardProperty] - oldBoard.cardProperties = [{...cardProperty, name: 'a-different-name'}] - - const result = createPatchesFromBoards(board, oldBoard) - expect(result).toMatchSnapshot() - }) - - it('should add card properties on the redo and undo if they exists in both, but their options are different', () => { - const cardProperty = { - id: 'new-property-id', - name: 'property-name', - type: 'select', - options: [{ - id: 'opt', - value: 'val', - color: 'propColorYellow', - }], - } as IPropertyTemplate - - const board = TestBlockFactory.createBoard() - const oldBoard = createBoard(board) - board.cardProperties = [cardProperty] - oldBoard.cardProperties = [{ - ...cardProperty, - options: [{ - id: 'another-opt', - value: 'val', - color: 'propColorBrown', - }], - }] - - const result = createPatchesFromBoards(board, oldBoard) - expect(result).toMatchSnapshot() - }) - }) - - describe('correctly generate patches for boards and blocks', () => { - const board = TestBlockFactory.createBoard() - board.id = 'test-board-id' - const card = TestBlockFactory.createCard() - card.id = 'test-card-id' - - it('should generate two empty patches for the same board and block', () => { - const result = createPatchesFromBoardsAndBlocks(board, board, [card.id], [card], [card]) - expect(result).toMatchSnapshot() - }) - - it('should add fields on update and remove it in the undo', () => { - const oldBlock = TestBlockFactory.createText(card) - oldBlock.id = 'test-old-block-id' - const newBlock = createBlock(oldBlock) - newBlock.fields.newField = 'new field' - - const result = createPatchesFromBoardsAndBlocks(board, board, [newBlock.id], [newBlock], [oldBlock]) - expect(result).toMatchSnapshot() - }) - }) -}) diff --git a/webapp/boards/src/blocks/board.ts b/webapp/boards/src/blocks/board.ts deleted file mode 100644 index b00a5c68ed..0000000000 --- a/webapp/boards/src/blocks/board.ts +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import difference from 'lodash/difference' - -import {IDType, Utils} from 'src/utils' - -import {Block, BlockPatch, createPatchesFromBlocks} from './block' -import {Card} from './card' - -const BoardTypeOpen = 'O' -const BoardTypePrivate = 'P' -const boardTypes = [BoardTypeOpen, BoardTypePrivate] -type BoardTypes = typeof boardTypes[number] - -enum MemberRole { - Viewer = 'viewer', - Commenter = 'commenter', - Editor = 'editor', - Admin = 'admin', - None = '', -} - -type Board = { - id: string - teamId: string - channelId?: string - createdBy: string - modifiedBy: string - type: BoardTypes - minimumRole: MemberRole - - title: string - description: string - icon?: string - showDescription: boolean - isTemplate: boolean - templateVersion: number - properties: Record - cardProperties: IPropertyTemplate[] - - createAt: number - updateAt: number - deleteAt: number -} - -type BoardPatch = { - type?: BoardTypes - minimumRole?: MemberRole - title?: string - description?: string - icon?: string - showDescription?: boolean - // eslint-disable-next-line @typescript-eslint/no-explicit-any - updatedProperties?: Record - deletedProperties?: string[] - - updatedCardProperties?: IPropertyTemplate[] - deletedCardProperties?: string[] -} - -type BoardMember = { - boardId: string - userId: string - roles?: string - minimumRole: MemberRole - schemeAdmin: boolean - schemeEditor: boolean - schemeCommenter: boolean - schemeViewer: boolean - synthetic: boolean -} - -type BoardsAndBlocks = { - boards: Board[] - blocks: Block[] -} - -type BoardsAndBlocksPatch = { - boardIDs: string[] - boardPatches: BoardPatch[] - blockIDs: string[] - blockPatches: BlockPatch[] -} - -type PropertyTypeEnum = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'multiPerson' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy' | 'unknown' - -interface IPropertyOption { - id: string - value: string - color: string -} - -// A template for card properties attached to a board -interface IPropertyTemplate { - id: string - name: string - type: PropertyTypeEnum - options: IPropertyOption[] -} - -function createBoard(board?: Board): Board { - const now = Date.now() - let cardProperties: IPropertyTemplate[] = [] - const selectProperties = cardProperties.find((o) => o.type === 'select') - if (!selectProperties) { - const property: IPropertyTemplate = { - id: Utils.createGuid(IDType.BlockID), - name: 'Status', - type: 'select', - options: [], - } - cardProperties.push(property) - } - - if (board?.cardProperties) { - // Deep clone of card properties and their options - cardProperties = board?.cardProperties.map((o: IPropertyTemplate) => { - return { - id: o.id, - name: o.name, - type: o.type, - options: o.options ? o.options.map((option) => ({...option})) : [], - } - }) - } - - return { - id: board?.id || Utils.createGuid(IDType.Board), - teamId: board?.teamId || '', - channelId: board?.channelId || '', - createdBy: board?.createdBy || '', - modifiedBy: board?.modifiedBy || '', - type: board?.type || BoardTypePrivate, - minimumRole: board?.minimumRole || MemberRole.None, - title: board?.title || '', - description: board?.description || '', - icon: board?.icon || '', - showDescription: board?.showDescription || false, - isTemplate: board?.isTemplate || false, - templateVersion: board?.templateVersion || 0, - properties: board?.properties || {}, - cardProperties, - createAt: board?.createAt || now, - updateAt: board?.updateAt || now, - deleteAt: board?.deleteAt || 0, - } -} - -type BoardGroup = { - option: IPropertyOption - cards: Card[] -} - -// getPropertiesDifference returns a list of the property IDs that are -// contained in propsA but are not contained in propsB -function getPropertiesDifference(propsA: IPropertyTemplate[], propsB: IPropertyTemplate[]): string[] { - const diff: string[] = [] - propsA.forEach((val) => { - if (!propsB.find((p) => p.id === val.id)) { - diff.push(val.id) - } - }) - - return diff -} - -// isPropertyEqual checks that both the contents of the property and -// its options are equal -function isPropertyEqual(propA: IPropertyTemplate, propB: IPropertyTemplate): boolean { - for (const val of Object.keys(propA)) { - if (val !== 'options' && (propA as any)[val] !== (propB as any)[val]) { - return false - } - } - - if (propA.options.length !== propB.options.length) { - return false - } - - for (const opt of propA.options) { - const optionB = propB.options.find((o) => o.id === opt.id) - if (!optionB) { - return false - } - - for (const val of Object.keys(opt)) { - if ((opt as any)[val] !== (optionB as any)[val]) { - return false - } - } - } - - return true -} - -// createCardPropertiesPatches creates two BoardPatch instances, one that -// contains the delta to update the board cardProperties and another one for -// the undo action, in case it happens -function createCardPropertiesPatches(newCardProperties: IPropertyTemplate[], oldCardProperties: IPropertyTemplate[]): BoardPatch[] { - const newDeletedCardProperties = getPropertiesDifference(newCardProperties, oldCardProperties) - const oldDeletedCardProperties = getPropertiesDifference(oldCardProperties, newCardProperties) - const newUpdatedCardProperties: IPropertyTemplate[] = [] - newCardProperties.forEach((val) => { - const oldCardProperty = oldCardProperties.find((o) => o.id === val.id) - if (!oldCardProperty || !isPropertyEqual(val, oldCardProperty)) { - newUpdatedCardProperties.push(val) - } - }) - const oldUpdatedCardProperties: IPropertyTemplate[] = [] - oldCardProperties.forEach((val) => { - const newCardProperty = newCardProperties.find((o) => o.id === val.id) - if (!newCardProperty || !isPropertyEqual(val, newCardProperty)) { - oldUpdatedCardProperties.push(val) - } - }) - - return [ - { - updatedCardProperties: newUpdatedCardProperties, - deletedCardProperties: oldDeletedCardProperties, - }, - { - updatedCardProperties: oldUpdatedCardProperties, - deletedCardProperties: newDeletedCardProperties, - }, - ] -} - -// createPatchesFromBoards creates two BoardPatch instances, one that -// contains the delta to update the board and another one for the undo -// action, in case it happens -function createPatchesFromBoards(newBoard: Board, oldBoard: Board): BoardPatch[] { - const newDeletedProperties = difference(Object.keys(newBoard.properties || {}), Object.keys(oldBoard.properties || {})) - - const newUpdatedProperties: Record = {} - Object.keys(newBoard.properties || {}).forEach((val) => { - if (oldBoard.properties[val] !== newBoard.properties[val]) { - newUpdatedProperties[val] = newBoard.properties[val] - } - }) - - const newData: Record = {} - Object.keys(newBoard).forEach((val) => { - if (val !== 'properties' && - val !== 'cardProperties' && - (oldBoard as any)[val] !== (newBoard as any)[val]) { - newData[val] = (newBoard as any)[val] - } - }) - - const oldDeletedProperties = difference(Object.keys(oldBoard.properties || {}), Object.keys(newBoard.properties || {})) - - const oldUpdatedProperties: Record = {} - Object.keys(oldBoard.properties || {}).forEach((val) => { - if (newBoard.properties[val] !== oldBoard.properties[val]) { - oldUpdatedProperties[val] = oldBoard.properties[val] - } - }) - - const oldData: Record = {} - Object.keys(oldBoard).forEach((val) => { - if (val !== 'properties' && - val !== 'cardProperties' && - (newBoard as any)[val] !== (oldBoard as any)[val]) { - oldData[val] = (oldBoard as any)[val] - } - }) - - const [cardPropertiesPatch, cardPropertiesUndoPatch] = createCardPropertiesPatches(newBoard.cardProperties, oldBoard.cardProperties) - - return [ - { - ...newData, - ...cardPropertiesPatch, - updatedProperties: newUpdatedProperties, - deletedProperties: oldDeletedProperties, - }, - { - ...oldData, - ...cardPropertiesUndoPatch, - updatedProperties: oldUpdatedProperties, - deletedProperties: newDeletedProperties, - }, - ] -} - -function createPatchesFromBoardsAndBlocks(updatedBoard: Board, oldBoard: Board, updatedBlockIDs: string[], updatedBlocks: Block[], oldBlocks: Block[]): BoardsAndBlocksPatch[] { - const blockUpdatePatches = [] as BlockPatch[] - const blockUndoPatches = [] as BlockPatch[] - updatedBlocks.forEach((newBlock, i) => { - const [updatePatch, undoPatch] = createPatchesFromBlocks(newBlock, oldBlocks[i]) - blockUpdatePatches.push(updatePatch) - blockUndoPatches.push(undoPatch) - }) - - const [boardUpdatePatch, boardUndoPatch] = createPatchesFromBoards(updatedBoard, oldBoard) - - const updatePatch: BoardsAndBlocksPatch = { - blockIDs: updatedBlockIDs, - blockPatches: blockUpdatePatches, - boardIDs: [updatedBoard.id], - boardPatches: [boardUpdatePatch], - } - - const undoPatch: BoardsAndBlocksPatch = { - blockIDs: updatedBlockIDs, - blockPatches: blockUndoPatches, - boardIDs: [updatedBoard.id], - boardPatches: [boardUndoPatch], - } - - return [updatePatch, undoPatch] -} - -export { - Board, - BoardPatch, - BoardMember, - BoardsAndBlocks, - BoardsAndBlocksPatch, - PropertyTypeEnum, - IPropertyOption, - IPropertyTemplate, - BoardGroup, - createBoard, - BoardTypes, - BoardTypeOpen, - BoardTypePrivate, - MemberRole, - createPatchesFromBoards, - createPatchesFromBoardsAndBlocks, - createCardPropertiesPatches, -} diff --git a/webapp/boards/src/blocks/boardView.test.ts b/webapp/boards/src/blocks/boardView.test.ts deleted file mode 100644 index 09c090bb05..0000000000 --- a/webapp/boards/src/blocks/boardView.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {TestBlockFactory} from 'src/test/testBlockFactory' - -import {sortBoardViewsAlphabetically} from './boardView' - -test('boardView: sort with ASCII', async () => { - const view1 = TestBlockFactory.createBoardView() - view1.title = 'Maybe' - const view2 = TestBlockFactory.createBoardView() - view2.title = 'Active' - - const views = [view1, view2] - const sorted = sortBoardViewsAlphabetically(views) - expect(sorted).toEqual([view2, view1]) -}) - -test('boardView: sort with leading emoji', async () => { - const view1 = TestBlockFactory.createBoardView() - view1.title = '🤔 Maybe' - const view2 = TestBlockFactory.createBoardView() - view2.title = '🚀 Active' - - const views = [view1, view2] - const sorted = sortBoardViewsAlphabetically(views) - expect(sorted).toEqual([view2, view1]) -}) - -test('boardView: sort with non-latin characters', async () => { - const view1 = TestBlockFactory.createBoardView() - view1.title = 'zebra' - const view2 = TestBlockFactory.createBoardView() - view2.title = 'ñu' - - const views = [view1, view2] - const sorted = sortBoardViewsAlphabetically(views) - expect(sorted).toEqual([view2, view1]) -}) diff --git a/webapp/boards/src/blocks/boardView.ts b/webapp/boards/src/blocks/boardView.ts deleted file mode 100644 index acbf6f5080..0000000000 --- a/webapp/boards/src/blocks/boardView.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Block, createBlock} from './block' -import {FilterGroup, createFilterGroup} from './filterGroup' - -type IViewType = 'board' | 'table' | 'gallery' | 'calendar' -type ISortOption = { propertyId: '__title' | string, reversed: boolean } - -type KanbanCalculationFields = { - calculation: string - propertyId: string -} - -type BoardViewFields = { - viewType: IViewType - groupById?: string - dateDisplayPropertyId?: string - sortOptions: ISortOption[] - visiblePropertyIds: string[] - visibleOptionIds: string[] - hiddenOptionIds: string[] - collapsedOptionIds: string[] - filter: FilterGroup - cardOrder: string[] - columnWidths: Record - columnCalculations: Record - kanbanCalculations: Record - defaultTemplateId: string -} - -type BoardView = Block & { - fields: BoardViewFields -} - -function createBoardView(block?: Block): BoardView { - return { - ...createBlock(block), - type: 'view', - fields: { - viewType: block?.fields.viewType || 'board', - groupById: block?.fields.groupById, - dateDisplayPropertyId: block?.fields.dateDisplayPropertyId, - sortOptions: block?.fields.sortOptions?.map((o: ISortOption) => ({...o})) || [], - visiblePropertyIds: block?.fields.visiblePropertyIds?.slice() || [], - visibleOptionIds: block?.fields.visibleOptionIds?.slice() || [], - hiddenOptionIds: block?.fields.hiddenOptionIds?.slice() || [], - collapsedOptionIds: block?.fields.collapsedOptionIds?.slice() || [], - filter: createFilterGroup(block?.fields.filter), - cardOrder: block?.fields.cardOrder?.slice() || [], - columnWidths: {...(block?.fields.columnWidths || {})}, - columnCalculations: {...(block?.fields.columnCalculations) || {}}, - kanbanCalculations: {...(block?.fields.kanbanCalculations) || {}}, - defaultTemplateId: block?.fields.defaultTemplateId || '', - }, - } -} - -function sortBoardViewsAlphabetically(views: BoardView[]): BoardView[] { - // Strip leading emoji to prevent unintuitive results - return views.map((v) => { - return {view: v, title: v.title.replace(/^\p{Emoji}*\s*/u, '')} - }).sort((v1, v2) => v1.title.localeCompare(v2.title)).map((v) => v.view) -} - -export {BoardView, IViewType, ISortOption, sortBoardViewsAlphabetically, createBoardView, KanbanCalculationFields} diff --git a/webapp/boards/src/blocks/card.ts b/webapp/boards/src/blocks/card.ts deleted file mode 100644 index ba047e0a72..0000000000 --- a/webapp/boards/src/blocks/card.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Block, createBlock} from './block' - -type CardFields = { - icon?: string - isTemplate?: boolean - properties: Record - contentOrder: Array -} - -type Card = Block & { - fields: CardFields -} - -function createCard(block?: Block): Card { - const contentOrder: Array = [] - const contentIds = block?.fields?.contentOrder?.filter((id: any) => id !== null) - - if (contentIds?.length > 0) { - for (const contentId of contentIds) { - if (typeof contentId === 'string') { - contentOrder.push(contentId) - } else { - contentOrder.push(contentId.slice()) - } - } - } - - return { - ...createBlock(block), - type: 'card', - fields: { - icon: block?.fields.icon || '', - properties: {...(block?.fields.properties || {})}, - contentOrder, - isTemplate: block?.fields.isTemplate || false, - }, - } -} - -export {Card, createCard} diff --git a/webapp/boards/src/blocks/checkboxBlock.ts b/webapp/boards/src/blocks/checkboxBlock.ts deleted file mode 100644 index 0c5721fb8e..0000000000 --- a/webapp/boards/src/blocks/checkboxBlock.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {ContentBlock} from './contentBlock' -import {Block, createBlock} from './block' - -type CheckboxBlock = ContentBlock & { - type: 'checkbox' -} - -function createCheckboxBlock(block?: Block): CheckboxBlock { - return { - ...createBlock(block), - type: 'checkbox', - } -} - -export {CheckboxBlock, createCheckboxBlock} diff --git a/webapp/boards/src/blocks/commentBlock.ts b/webapp/boards/src/blocks/commentBlock.ts deleted file mode 100644 index b8994e07d7..0000000000 --- a/webapp/boards/src/blocks/commentBlock.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Block, createBlock} from './block' - -type CommentBlock = Block & { - type: 'comment' -} - -function createCommentBlock(block?: Block): CommentBlock { - return { - ...createBlock(block), - type: 'comment', - } -} - -export {CommentBlock, createCommentBlock} diff --git a/webapp/boards/src/blocks/contentBlock.ts b/webapp/boards/src/blocks/contentBlock.ts deleted file mode 100644 index d328095c03..0000000000 --- a/webapp/boards/src/blocks/contentBlock.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Block, createBlock} from './block' - -type IContentBlockWithCords = { - block: Block - cords: {x: number, y?: number, z?: number} -} - -type ContentBlock = Block - -const createContentBlock = createBlock - -export {ContentBlock, IContentBlockWithCords, createContentBlock} diff --git a/webapp/boards/src/blocks/dividerBlock.ts b/webapp/boards/src/blocks/dividerBlock.ts deleted file mode 100644 index d353b98a3c..0000000000 --- a/webapp/boards/src/blocks/dividerBlock.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Block, createBlock} from './block' -import {ContentBlock} from './contentBlock' - -type DividerBlock = ContentBlock & { - type: 'divider' -} - -function createDividerBlock(block?: Block): DividerBlock { - return { - ...createBlock(block), - type: 'divider', - } -} - -export {DividerBlock, createDividerBlock} diff --git a/webapp/boards/src/blocks/filterClause.test.ts b/webapp/boards/src/blocks/filterClause.test.ts deleted file mode 100644 index e55660564e..0000000000 --- a/webapp/boards/src/blocks/filterClause.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {areEqual, createFilterClause} from './filterClause' - -describe('filterClause tests', () => { - it('create filter clause', () => { - const clause = createFilterClause({ - propertyId: 'myPropertyId', - condition: 'contains', - values: [], - }) - - expect(clause).toEqual({ - propertyId: 'myPropertyId', - condition: 'contains', - values: [], - }) - }) - - it('test filter clauses are equal', () => { - const clause = createFilterClause({ - propertyId: 'myPropertyId', - condition: 'contains', - values: ['abc', 'def'], - }) - const newClause = createFilterClause(clause) - const testEqual = areEqual(clause, newClause) - expect(testEqual).toBeTruthy() - }) - - it('test filter clauses are Not equal property ID', () => { - const clause = createFilterClause({ - propertyId: 'myPropertyId', - condition: 'contains', - values: ['abc', 'def'], - }) - const newClause = createFilterClause(clause) - newClause.propertyId = 'DifferentID' - const testEqual = areEqual(clause, newClause) - expect(testEqual).toBeFalsy() - }) - it('test filter clauses are Not equal condition', () => { - const clause = createFilterClause({ - propertyId: 'myPropertyId', - condition: 'contains', - values: ['abc', 'def'], - }) - const newClause = createFilterClause(clause) - newClause.condition = 'notContains' - const testEqual = areEqual(clause, newClause) - expect(testEqual).toBeFalsy() - }) - it('test filter clauses are Not equal values', () => { - const clause = createFilterClause({ - propertyId: 'myPropertyId', - condition: 'contains', - values: ['abc', 'def'], - }) - const newClause = createFilterClause(clause) - newClause.values = ['abc, def'] - const testEqual = areEqual(clause, newClause) - expect(testEqual).toBeFalsy() - }) -}) diff --git a/webapp/boards/src/blocks/filterClause.ts b/webapp/boards/src/blocks/filterClause.ts deleted file mode 100644 index 9953b8dd6e..0000000000 --- a/webapp/boards/src/blocks/filterClause.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Utils} from 'src/utils' - -type FilterCondition = - 'includes' | 'notIncludes' | - 'isEmpty' | 'isNotEmpty' | - 'isSet' | 'isNotSet' | - 'is' | - 'contains' | 'notContains' | - 'startsWith' | 'notStartsWith' | - 'endsWith' | 'notEndsWith' | - 'isBefore' | 'isAfter' - -type FilterClause = { - propertyId: string - condition: FilterCondition - values: string[] -} - -function createFilterClause(o?: FilterClause): FilterClause { - return { - propertyId: o?.propertyId || '', - condition: o?.condition || 'includes', - values: o?.values?.slice() || [], - } -} - -function areEqual(a: FilterClause, b: FilterClause): boolean { - return ( - a.propertyId === b.propertyId && - a.condition === b.condition && - Utils.arraysEqual(a.values, b.values) - ) -} - -export {FilterClause, FilterCondition, createFilterClause, areEqual} diff --git a/webapp/boards/src/blocks/filterGroup.ts b/webapp/boards/src/blocks/filterGroup.ts deleted file mode 100644 index e304e38edc..0000000000 --- a/webapp/boards/src/blocks/filterGroup.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {FilterClause, createFilterClause} from './filterClause' - -type FilterGroupOperation = 'and' | 'or' - -// A FilterGroup has 2 forms: (A or B or C) OR (A and B and C) -type FilterGroup = { - operation: FilterGroupOperation - filters: Array -} - -function isAFilterGroupInstance(object: (FilterClause | FilterGroup)): object is FilterGroup { - return 'operation' in object && 'filters' in object -} - -function createFilterGroup(o?: FilterGroup): FilterGroup { - let filters: Array = [] - if (o?.filters) { - filters = o.filters.map((p: (FilterClause | FilterGroup)) => { - if (isAFilterGroupInstance(p)) { - return createFilterGroup(p) - } - - return createFilterClause(p) - }) - } - - return { - operation: o?.operation || 'and', - filters, - } -} - -export {FilterGroup, FilterGroupOperation, createFilterGroup, isAFilterGroupInstance} diff --git a/webapp/boards/src/blocks/h1Block.tsx b/webapp/boards/src/blocks/h1Block.tsx deleted file mode 100644 index 246c8d493c..0000000000 --- a/webapp/boards/src/blocks/h1Block.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {ContentBlock} from './contentBlock' -import {Block, createBlock} from './block' - -type H1Block = ContentBlock & { - type: 'h1' -} - -function createH1Block(block?: Block): H1Block { - return { - ...createBlock(block), - type: 'h1', - } -} - -export {H1Block, createH1Block} - diff --git a/webapp/boards/src/blocks/h2Block.tsx b/webapp/boards/src/blocks/h2Block.tsx deleted file mode 100644 index 77e78c5c7c..0000000000 --- a/webapp/boards/src/blocks/h2Block.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {ContentBlock} from './contentBlock' -import {Block, createBlock} from './block' - -type H2Block = ContentBlock & { - type: 'h2' -} - -function createH2Block(block?: Block): H2Block { - return { - ...createBlock(block), - type: 'h2', - } -} - -export {H2Block, createH2Block} - diff --git a/webapp/boards/src/blocks/h3Block.tsx b/webapp/boards/src/blocks/h3Block.tsx deleted file mode 100644 index 6cb19061af..0000000000 --- a/webapp/boards/src/blocks/h3Block.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {ContentBlock} from './contentBlock' -import {Block, createBlock} from './block' - -type H3Block = ContentBlock & { - type: 'h3' -} - -function createH3Block(block?: Block): H3Block { - return { - ...createBlock(block), - type: 'h3', - } -} - -export {H3Block, createH3Block} - diff --git a/webapp/boards/src/blocks/imageBlock.ts b/webapp/boards/src/blocks/imageBlock.ts deleted file mode 100644 index 5e2b7be38a..0000000000 --- a/webapp/boards/src/blocks/imageBlock.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Block, createBlock} from './block' -import {ContentBlock} from './contentBlock' - -type ImageBlockFields = { - fileId: string -} - -type ImageBlock = ContentBlock & { - type: 'image' - fields: ImageBlockFields -} - -function createImageBlock(block?: Block): ImageBlock { - return { - ...createBlock(block), - type: 'image', - fields: { - fileId: block?.fields.fileId || '', - }, - } -} - -export {ImageBlock, createImageBlock} diff --git a/webapp/boards/src/blocks/sharing.ts b/webapp/boards/src/blocks/sharing.ts deleted file mode 100644 index 923f9c2753..0000000000 --- a/webapp/boards/src/blocks/sharing.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -interface ISharing { - id: string - enabled: boolean - token: string - modifiedBy?: string - updateAt?: number -} - -export {ISharing} diff --git a/webapp/boards/src/blocks/team.ts b/webapp/boards/src/blocks/team.ts deleted file mode 100644 index 6ff8061834..0000000000 --- a/webapp/boards/src/blocks/team.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -interface ITeam { - readonly id: string - readonly title: string - readonly signupToken: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly settings: Readonly> - readonly modifiedBy?: string - readonly updateAt?: number -} - -export {ITeam} diff --git a/webapp/boards/src/blocks/textBlock.ts b/webapp/boards/src/blocks/textBlock.ts deleted file mode 100644 index 1729d1e7be..0000000000 --- a/webapp/boards/src/blocks/textBlock.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {ContentBlock} from './contentBlock' -import {Block, createBlock} from './block' - -type TextBlock = ContentBlock & { - type: 'text' -} - -function createTextBlock(block?: Block): TextBlock { - return { - ...createBlock(block), - type: 'text', - } -} - -export {TextBlock, createTextBlock} diff --git a/webapp/boards/src/blocks/workspace.ts b/webapp/boards/src/blocks/workspace.ts deleted file mode 100644 index 6980501498..0000000000 --- a/webapp/boards/src/blocks/workspace.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -interface IWorkspace { - readonly id: string - readonly title: string - readonly signupToken: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly settings: Readonly> - readonly modifiedBy?: string - readonly updateAt?: number -} - -export {IWorkspace} diff --git a/webapp/boards/src/boardCloudLimits/index.ts b/webapp/boards/src/boardCloudLimits/index.ts deleted file mode 100644 index 7f86ed84ca..0000000000 --- a/webapp/boards/src/boardCloudLimits/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -export const LimitUnlimited = 0 - -export interface BoardsCloudLimits { - cards: number - used_cards: number - card_limit_timestamp: number - views: number -} diff --git a/webapp/boards/src/boardUtils.ts b/webapp/boards/src/boardUtils.ts deleted file mode 100644 index f8d9484ca6..0000000000 --- a/webapp/boards/src/boardUtils.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {Card} from './blocks/card' -import {BoardGroup, IPropertyOption, IPropertyTemplate} from './blocks/board' - -function groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] { - const groups = [] - for (const optionId of optionIds) { - if (optionId) { - const option = groupByProperty?.options.find((o) => o.id === optionId) - if (option) { - const c = cards.filter((o) => optionId === o.fields?.properties[groupByProperty!.id]) - const group: BoardGroup = { - option, - cards: c, - } - groups.push(group) - } else { - // if optionId not found, its an old (deleted) option that can be ignored - } - } else { - // Empty group - const emptyGroupCards = cards.filter((card) => { - const groupByOptionId = card.fields.properties[groupByProperty?.id || ''] - - return !groupByOptionId || !groupByProperty?.options.find((option) => option.id === groupByOptionId) - }) - const group: BoardGroup = { - option: {id: '', value: `No ${groupByProperty?.name}`, color: ''}, - cards: emptyGroupCards, - } - groups.push(group) - } - } - - return groups -} - -function getOptionGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} { - let unassignedOptionIds: string[] = [] - if (groupByProperty) { - unassignedOptionIds = groupByProperty.options - .filter((o: IPropertyOption) => !visibleOptionIds.includes(o.id) && !hiddenOptionIds.includes(o.id)) - .map((o: IPropertyOption) => o.id) - } - const allVisibleOptionIds = [...visibleOptionIds, ...unassignedOptionIds] - - // If the empty group positon is not explicitly specified, make it the first visible column - if (!allVisibleOptionIds.includes('') && !hiddenOptionIds.includes('')) { - allVisibleOptionIds.unshift('') - } - - const visibleGroups = groupCardsByOptions(cards, allVisibleOptionIds, groupByProperty) - const hiddenGroups = groupCardsByOptions(cards, hiddenOptionIds, groupByProperty) - - return {visible: visibleGroups, hidden: hiddenGroups} -} -export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} { - if (groupByProperty?.type === 'createdBy' || groupByProperty?.type === 'updatedBy' || groupByProperty?.type === 'person') { - return getPersonGroups(cards, groupByProperty, hiddenOptionIds) - } - - return getOptionGroups(cards, visibleOptionIds, hiddenOptionIds, groupByProperty) -} - -function getPersonGroups(cards: Card[], groupByProperty: IPropertyTemplate, hiddenOptionIds: string[]): {visible: BoardGroup[], hidden: BoardGroup[]} { - const groups = cards.reduce((unique: {[key: string]: Card[]}, item: Card): {[key: string]: Card[]} => { - let key = item.fields.properties[groupByProperty.id] as string - if (groupByProperty?.type === 'createdBy') { - key = item.createdBy - } else if (groupByProperty?.type === 'updatedBy') { - key = item.modifiedBy - } - - const curGroup = unique[key] ?? [] - - return {...unique, [key]: [...curGroup, item]} - }, {}) - - const hiddenGroups: BoardGroup[] = [] - const visibleGroups: BoardGroup[] = [] - Object.entries(groups).forEach(([key, value]) => { - const propertyOption = {id: key, value: key, color: ''} as IPropertyOption - if (hiddenOptionIds.find((e) => e === key)) { - hiddenGroups.push({option: propertyOption, cards: value}) - } else { - visibleGroups.push({option: propertyOption, cards: value}) - } - }) - - return {visible: visibleGroups, hidden: hiddenGroups} -} diff --git a/webapp/boards/src/boardsCloudLimits/index.ts b/webapp/boards/src/boardsCloudLimits/index.ts deleted file mode 100644 index 7f86ed84ca..0000000000 --- a/webapp/boards/src/boardsCloudLimits/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -export const LimitUnlimited = 0 - -export interface BoardsCloudLimits { - cards: number - used_cards: number - card_limit_timestamp: number - views: number -} diff --git a/webapp/boards/src/cardFilter.test.ts b/webapp/boards/src/cardFilter.test.ts deleted file mode 100644 index 4a2f89e695..0000000000 --- a/webapp/boards/src/cardFilter.test.ts +++ /dev/null @@ -1,728 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {mocked} from 'jest-mock' - -import {createFilterClause} from './blocks/filterClause' - -import {createFilterGroup} from './blocks/filterGroup' -import {CardFilter} from './cardFilter' -import {TestBlockFactory} from './test/testBlockFactory' -import {Utils} from './utils' - -import {IPropertyTemplate} from './blocks/board' - -jest.mock('./utils') -const mockedUtils = mocked(Utils) - -const dayMillis = 24 * 60 * 60 * 1000 - -describe('src/cardFilter', () => { - const board = TestBlockFactory.createBoard() - board.id = '1' - - const card1 = TestBlockFactory.createCard(board) - card1.id = '1' - card1.title = 'card1' - card1.fields.properties.propertyId = 'Status' - const filterClause = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - - describe('verify isClauseMet method', () => { - test('should be true with isNotEmpty clause', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - const result = CardFilter.isClauseMet(filterClauseIsNotEmpty, [], card1) - expect(result).toBeTruthy() - }) - test('should be false with isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isEmpty', values: ['Status']}) - const result = CardFilter.isClauseMet(filterClauseIsEmpty, [], card1) - expect(result).toBeFalsy() - }) - test('should be true with includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [], card1) - expect(result).toBeTruthy() - }) - test('should be true with includes and no values clauses', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: []}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [], card1) - expect(result).toBeTruthy() - }) - test('should be false with notIncludes clause', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const result = CardFilter.isClauseMet(filterClauseNotIncludes, [], card1) - expect(result).toBeFalsy() - }) - test('should be true with notIncludes and no values clauses', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: []}) - const result = CardFilter.isClauseMet(filterClauseNotIncludes, [], card1) - expect(result).toBeTruthy() - }) - }) - - describe('verify isClauseMet method - person property', () => { - const personCard = TestBlockFactory.createCard(board) - personCard.id = '1' - personCard.title = 'card1' - personCard.fields.properties.personPropertyID = 'personid1' - - const template: IPropertyTemplate = { - id: 'personPropertyID', - name: 'myPerson', - type: 'person', - options: [], - } - - test('should be true with isNotEmpty clause', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isNotEmpty', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsNotEmpty, [template], personCard) - expect(result).toBeTruthy() - }) - test('should be false with isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isEmpty', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsEmpty, [template], personCard) - expect(result).toBeFalsy() - }) - test('verify empty includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause multiple values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid2', 'personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify not includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid2']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - }) - - describe('verify isClauseMet method - multi-person property', () => { - const personCard = TestBlockFactory.createCard(board) - personCard.id = '1' - personCard.title = 'card1' - personCard.fields.properties.personPropertyID = ['personid1', 'personid2'] - - const template: IPropertyTemplate = { - id: 'personPropertyID', - name: 'myPerson', - type: 'multiPerson', - options: [], - } - - test('should be true with isNotEmpty clause', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isNotEmpty', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsNotEmpty, [template], personCard) - expect(result).toBeTruthy() - }) - test('should be false with isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isEmpty', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsEmpty, [template], personCard) - expect(result).toBeFalsy() - }) - test('verify empty includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause 2', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid2']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause multiple values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause multiple values 2', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid2']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify not includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid3']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify not includes clause, multiple values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid3', 'personid4']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - }) - - describe('verify isClauseMet method - (createdBy) person property', () => { - const personCard = TestBlockFactory.createCard(board) - personCard.id = '1' - personCard.title = 'card1' - personCard.createdBy = 'personid1' - - const template: IPropertyTemplate = { - id: 'personPropertyID', - name: 'myPerson', - type: 'createdBy', - options: [], - } - - test('verify empty includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify includes clause multiple values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid1']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - test('verify not includes clause', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid2']}) - const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard) - expect(result).toBeTruthy() - }) - }) - - describe('verify isClauseMet method - single date property', () => { - // Date Properties are stored as 12PM UTC. - const now = new Date(Date.now()) - const propertyDate = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 12) - - const dateCard = TestBlockFactory.createCard(board) - dateCard.id = '1' - dateCard.title = 'card1' - dateCard.fields.properties.datePropertyID = '{ "from": ' + propertyDate.toString() + ' }' - - const checkDayBefore = propertyDate - dayMillis - const checkDayAfter = propertyDate + dayMillis - - const template: IPropertyTemplate = { - id: 'datePropertyID', - name: 'myDate', - type: 'date', - options: [], - } - - test('should be true with isSet clause', () => { - const filterClauseIsSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isSet', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsSet, [template], dateCard) - expect(result).toBeTruthy() - }) - test('should be false with notSet clause', () => { - const filterClauseIsNotSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isNotSet', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsNotSet, [template], dateCard) - expect(result).toBeFalsy() - }) - test('verify isBefore clause', () => { - const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayAfter.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard) - expect(result).toBeTruthy() - - const filterClauseIsNotBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayBefore.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsNotBefore, [template], dateCard) - expect(result2).toBeFalsy() - }) - test('verify isAfter clauses', () => { - const filterClauseisAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayBefore.toString()]}) - const result = CardFilter.isClauseMet(filterClauseisAfter, [template], dateCard) - expect(result).toBeTruthy() - - const filterClauseisNotAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayAfter.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseisNotAfter, [template], dateCard) - expect(result2).toBeFalsy() - }) - test('verify is clause', () => { - const filterClauseIs = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [propertyDate.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIs, [template], dateCard) - expect(result).toBeTruthy() - - const filterClauseIsNot = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayBefore.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsNot, [template], dateCard) - expect(result2).toBeFalsy() - }) - }) - - describe('verify isClauseMet method - date range property', () => { - // Date Properties are stored as 12PM UTC. - const now = new Date(Date.now()) - const fromDate = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 12) - const toDate = fromDate + (2 * dayMillis) - const dateCard = TestBlockFactory.createCard(board) - dateCard.id = '1' - dateCard.title = 'card1' - dateCard.fields.properties.datePropertyID = '{ "from": ' + fromDate.toString() + ', "to": ' + toDate.toString() + ' }' - - const beforeRange = fromDate - dayMillis - const afterRange = toDate + dayMillis - const inRange = fromDate + dayMillis - - const template: IPropertyTemplate = { - id: 'datePropertyID', - name: 'myDate', - type: 'date', - options: [], - } - - test('verify isBefore clause', () => { - const filterClauseIsBeforeEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []}) - const resulta = CardFilter.isClauseMet(filterClauseIsBeforeEmpty, [template], dateCard) - expect(resulta).toBeTruthy() - - const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [beforeRange.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard) - expect(result).toBeFalsy() - - const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [inRange.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard) - expect(result2).toBeTruthy() - - const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [afterRange.toString()]}) - const result3 = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard) - expect(result3).toBeTruthy() - }) - - test('verify isAfter clauses', () => { - const filterClauseIsAfterEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []}) - const resulta = CardFilter.isClauseMet(filterClauseIsAfterEmpty, [template], dateCard) - expect(resulta).toBeTruthy() - - const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [afterRange.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard) - expect(result).toBeFalsy() - - const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [inRange.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard) - expect(result2).toBeTruthy() - - const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [beforeRange.toString()]}) - const result3 = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard) - expect(result3).toBeTruthy() - }) - - test('verify is clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: []}) - const resulta = CardFilter.isClauseMet(filterClauseIsEmpty, [template], dateCard) - expect(resulta).toBeTruthy() - - const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [beforeRange.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], dateCard) - expect(result).toBeFalsy() - - const filterClauseIsInRange = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [inRange.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsInRange, [template], dateCard) - expect(result2).toBeTruthy() - - const filterClauseIsAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [afterRange.toString()]}) - const result3 = CardFilter.isClauseMet(filterClauseIsAfter, [template], dateCard) - expect(result3).toBeFalsy() - }) - }) - - describe('verify isClauseMet method - (createdTime) date property', () => { - const createDate = new Date(card1.createAt) - const checkDate = Date.UTC(createDate.getFullYear(), createDate.getMonth(), createDate.getDate(), 12) - const checkDayBefore = checkDate - dayMillis - const checkDayAfter = checkDate + dayMillis - - const template: IPropertyTemplate = { - id: 'datePropertyID', - name: 'myDate', - type: 'createdTime', - options: [], - } - - test('should be true with isSet clause', () => { - const filterClauseIsSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isSet', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsSet, [template], card1) - expect(result).toBeTruthy() - }) - test('should be false with notSet clause', () => { - const filterClauseIsNotSet = createFilterClause({propertyId: 'datePropertyID', condition: 'isNotSet', values: []}) - const result = CardFilter.isClauseMet(filterClauseIsNotSet, [template], card1) - expect(result).toBeFalsy() - }) - test('verify isBefore clause', () => { - const filterClauseIsBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDayAfter.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIsBefore, [template], card1) - expect(result).toBeTruthy() - - const filterClauseIsNotBefore = createFilterClause({propertyId: 'datePropertyID', condition: 'isBefore', values: [checkDate.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsNotBefore, [template], card1) - expect(result2).toBeFalsy() - }) - test('verify isAfter clauses', () => { - const filterClauseisAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDayBefore.toString()]}) - const result = CardFilter.isClauseMet(filterClauseisAfter, [template], card1) - expect(result).toBeTruthy() - - const filterClauseisNotAfter = createFilterClause({propertyId: 'datePropertyID', condition: 'isAfter', values: [checkDate.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseisNotAfter, [template], card1) - expect(result2).toBeFalsy() - }) - test('verify is clause', () => { - // Is should find on that date regardless of time. - const filterClauseIs = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDate.toString()]}) - const result = CardFilter.isClauseMet(filterClauseIs, [template], card1) - expect(result).toBeTruthy() - - const filterClauseIsNot = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayBefore.toString()]}) - const result2 = CardFilter.isClauseMet(filterClauseIsNot, [template], card1) - expect(result2).toBeFalsy() - - const filterClauseIsNot2 = createFilterClause({propertyId: 'datePropertyID', condition: 'is', values: [checkDayAfter.toString()]}) - const result3 = CardFilter.isClauseMet(filterClauseIsNot2, [template], card1) - expect(result3).toBeFalsy() - }) - }) - - describe('verify isFilterGroupMet method', () => { - test('should return true with no filter', () => { - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [], - }) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeTruthy() - }) - test('should return true with or operation and 2 filterCause, one is false ', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseNotIncludes, - filterClause, - ], - }) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeTruthy() - }) - test('should return true with or operation and 2 filterCause, 1 filtergroup in filtergroup, one filterClause is false ', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterGroupInFilterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseNotIncludes, - filterClause, - ], - }) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [], - }) - filterGroup.filters.push(filterGroupInFilterGroup) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeTruthy() - }) - test('should return false with or operation and two filterCause, two are false ', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterClauseEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isEmpty', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseNotIncludes, - filterClauseEmpty, - ], - }) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeFalsy() - }) - test('should return false with and operation and 2 filterCause, one is false ', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseNotIncludes, - filterClause, - ], - }) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeFalsy() - }) - test('should return true with and operation and 2 filterCause, two are true ', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseIncludes, - filterClause, - ], - }) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeTruthy() - }) - test('should return true with or operation and 2 filterCause, 1 filtergroup in filtergroup, one filterClause is false ', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterGroupInFilterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseNotIncludes, - filterClause, - ], - }) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [], - }) - filterGroup.filters.push(filterGroupInFilterGroup) - const result = CardFilter.isFilterGroupMet(filterGroup, [], card1) - expect(result).toBeFalsy() - }) - }) - describe('verify propertyThatMeetsFilterClause method', () => { - test('should return Utils.assertFailure and filterClause propertyId ', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsNotEmpty, []) - expect(mockedUtils.assertFailure).toBeCalledTimes(1) - expect(result.id).toEqual(filterClauseIsNotEmpty.propertyId) - }) - test('should return filterClause propertyId with non-select template and isNotEmpty clause ', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsNotEmpty.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsNotEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsNotEmpty.propertyId) - expect(result.value).toBeFalsy() - }) - test('should return filterClause propertyId with select template , an option and isNotEmpty clause ', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsNotEmpty.propertyId, - name: 'template', - type: 'select', - options: [{ - id: 'idOption', - value: '', - color: '', - }], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsNotEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsNotEmpty.propertyId) - expect(result.value).toEqual('idOption') - }) - test('should return filterClause propertyId with select template , no option and isNotEmpty clause ', () => { - const filterClauseIsNotEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isNotEmpty', values: ['Status']}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsNotEmpty.propertyId, - name: 'template', - type: 'select', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsNotEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsNotEmpty.propertyId) - expect(result.value).toBeFalsy() - }) - - test('should return filterClause propertyId with template, and includes clause with values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIncludes.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIncludes, [templateFilter]) - expect(result.id).toEqual(filterClauseIncludes.propertyId) - expect(result.value).toEqual(filterClauseIncludes.values[0]) - }) - test('should return filterClause propertyId with template, and includes clause with no values', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: []}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIncludes.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIncludes, [templateFilter]) - expect(result.id).toEqual(filterClauseIncludes.propertyId) - expect(result.value).toBeFalsy() - }) - test('should return filterClause propertyId with template, and notIncludes clause', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: []}) - const templateFilter: IPropertyTemplate = { - id: filterClauseNotIncludes.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseNotIncludes, [templateFilter]) - expect(result.id).toEqual(filterClauseNotIncludes.propertyId) - expect(result.value).toBeFalsy() - }) - test('should return filterClause propertyId with template, and isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'isEmpty', values: []}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsEmpty.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsEmpty.propertyId) - expect(result.value).toBeFalsy() - }) - }) - describe('verify propertyThatMeetsFilterClause method - Person properties', () => { - test('should return filterClause propertyId with template, and isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'is', values: []}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsEmpty.propertyId, - name: 'template', - type: 'createdBy', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsEmpty.propertyId) - expect(result.value).toBeFalsy() - }) - test('should return filterClause propertyId with template, and isEmpty clause', () => { - const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'is', values: []}) - const templateFilter: IPropertyTemplate = { - id: filterClauseIsEmpty.propertyId, - name: 'template', - type: 'createdBy', - options: [], - } - const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsEmpty, [templateFilter]) - expect(result.id).toEqual(filterClauseIsEmpty.propertyId) - expect(result.value).toBeFalsy() - }) - }) - describe('verify propertiesThatMeetFilterGroup method', () => { - test('should return {} with undefined filterGroup', () => { - const result = CardFilter.propertiesThatMeetFilterGroup(undefined, []) - expect(result).toEqual({}) - }) - test('should return {} with filterGroup without filter', () => { - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [], - }) - const result = CardFilter.propertiesThatMeetFilterGroup(filterGroup, []) - expect(result).toEqual({}) - }) - test('should return {} with filterGroup, or operation and no template', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseIncludes, - filterClause, - ], - }) - const result = CardFilter.propertiesThatMeetFilterGroup(filterGroup, []) - expect(result).toEqual({}) - }) - test('should return a result with filterGroup, or operation and template', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseIncludes, - filterClause, - ], - }) - const templateFilter: IPropertyTemplate = { - id: filterClauseIncludes.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertiesThatMeetFilterGroup(filterGroup, [templateFilter]) - expect(result).toBeDefined() - expect(result.propertyId).toEqual(filterClauseIncludes.values[0]) - }) - test('should return {} with filterGroup, and operation and no template', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseIncludes, - filterClause, - ], - }) - const result = CardFilter.propertiesThatMeetFilterGroup(filterGroup, []) - expect(result).toEqual({}) - }) - - test('should return a result with filterGroup, and operation and template', () => { - const filterClauseIncludes = createFilterClause({propertyId: 'propertyId', condition: 'includes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseIncludes, - filterClause, - ], - }) - const templateFilter: IPropertyTemplate = { - id: filterClauseIncludes.propertyId, - name: 'template', - type: 'text', - options: [], - } - const result = CardFilter.propertiesThatMeetFilterGroup(filterGroup, [templateFilter]) - expect(result).toBeDefined() - expect(result.propertyId).toEqual(filterClauseIncludes.values[0]) - }) - }) - describe('verify applyFilterGroup method', () => { - test('should return array with card1', () => { - const filterClauseNotIncludes = createFilterClause({propertyId: 'propertyId', condition: 'notIncludes', values: ['Status']}) - const filterGroup = createFilterGroup({ - operation: 'or', - filters: [ - filterClauseNotIncludes, - filterClause, - ], - }) - const result = CardFilter.applyFilterGroup(filterGroup, [], [card1]) - expect(result).toBeDefined() - expect(result[0]).toEqual(card1) - }) - }) - describe('verfiy applyFilterGroup method for case-sensitive search', () => { - test('should return array with card1 when search by test as Card1', () => { - const filterClauseNotContains = createFilterClause({propertyId: 'title', condition: 'contains', values: ['Card1']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseNotContains, - ], - }) - const result = CardFilter.applyFilterGroup(filterGroup, [], [card1]) - expect(result.length).toEqual(1) - }) - }) - describe('verify applyFilter for title', () => { - test('should not return array with card1', () => { - const filterClauseNotContains = createFilterClause({propertyId: 'title', condition: 'notContains', values: ['card1']}) - const filterGroup = createFilterGroup({ - operation: 'and', - filters: [ - filterClauseNotContains, - ], - }) - const result = CardFilter.applyFilterGroup(filterGroup, [], [card1]) - expect(result.length).toEqual(0) - }) - }) -}) diff --git a/webapp/boards/src/cardFilter.ts b/webapp/boards/src/cardFilter.ts deleted file mode 100644 index ae4da8b7f7..0000000000 --- a/webapp/boards/src/cardFilter.ts +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {DateUtils} from 'react-day-picker' - -import {DateProperty} from './properties/date/date' - -import {IPropertyTemplate} from './blocks/board' -import {Card} from './blocks/card' -import {FilterClause} from './blocks/filterClause' -import {FilterGroup, isAFilterGroupInstance} from './blocks/filterGroup' -import {Utils} from './utils' - -const halfDay = 12 * 60 * 60 * 1000 - -class CardFilter { - static createDatePropertyFromString(initialValue: string): DateProperty { - let dateProperty: DateProperty = {} - if (initialValue) { - const singleDate = new Date(Number(initialValue)) - if (singleDate && DateUtils.isDate(singleDate)) { - dateProperty.from = singleDate.getTime() - } else { - try { - dateProperty = JSON.parse(initialValue) - } catch { - //Don't do anything, return empty dateProperty - } - } - } - - return dateProperty - } - - static applyFilterGroup(filterGroup: FilterGroup, templates: readonly IPropertyTemplate[], cards: Card[]): Card[] { - return cards.filter((card) => this.isFilterGroupMet(filterGroup, templates, card)) - } - - static isFilterGroupMet(filterGroup: FilterGroup, templates: readonly IPropertyTemplate[], card: Card): boolean { - const {filters} = filterGroup - - if (filterGroup.filters.length < 1) { - return true // No filters = always met - } - - if (filterGroup.operation === 'or') { - for (const filter of filters) { - if (isAFilterGroupInstance(filter)) { - if (this.isFilterGroupMet(filter, templates, card)) { - return true - } - } else if (this.isClauseMet(filter, templates, card)) { - return true - } - } - - return false - } - Utils.assert(filterGroup.operation === 'and') - for (const filter of filters) { - if (isAFilterGroupInstance(filter)) { - if (!this.isFilterGroupMet(filter, templates, card)) { - return false - } - } else if (!this.isClauseMet(filter, templates, card)) { - return false - } - } - - return true - } - - static isClauseMet(filter: FilterClause, templates: readonly IPropertyTemplate[], card: Card): boolean { - let value = card.fields.properties[filter.propertyId] - if (filter.propertyId === 'title') { - value = card.title.toLowerCase() - } - const template = templates.find((o) => o.id === filter.propertyId) - let dateValue: DateProperty | undefined - if (template?.type === 'date') { - dateValue = this.createDatePropertyFromString(value as string) - } - if (!value && template) { - if (template.type === 'createdBy') { - value = card.createdBy - } else if (template.type === 'updatedBy') { - value = card.modifiedBy - } else if (template && template.type === 'createdTime') { - value = card.createAt.toString() - dateValue = this.createDatePropertyFromString(value as string) - } else if (template && template.type === 'updatedTime') { - value = card.updateAt.toString() - dateValue = this.createDatePropertyFromString(value as string) - } - } - - switch (filter.condition) { - case 'includes': { - if (filter.values?.length < 1) { - break - } // No values = ignore clause (always met) - - return (filter.values.find((cValue) => (Array.isArray(value) ? value.includes(cValue) : cValue === value)) !== undefined) - } - case 'notIncludes': { - if (filter.values?.length < 1) { - break - } // No values = ignore clause (always met) - - return (filter.values.find((cValue) => (Array.isArray(value) ? value.includes(cValue) : cValue === value)) === undefined) - } - case 'isEmpty': { - return (value || '').length <= 0 - } - case 'isNotEmpty': { - return (value || '').length > 0 - } - case 'isSet': { - return Boolean(value) - } - case 'isNotSet': { - return !value - } - case 'is': { - if (filter.values.length === 0) { - return true - } - if (dateValue !== undefined) { - const numericFilter = parseInt(filter.values[0], 10) - if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) { - // createdTime and updatedTime include the time - // So to check if create and/or updated "is" date. - // Need to add and subtract 12 hours and check range - if (dateValue.from) { - return dateValue.from > (numericFilter - halfDay) && dateValue.from < (numericFilter + halfDay) - } - - return false - } - - if (dateValue.from && dateValue.to) { - return dateValue.from <= numericFilter && dateValue.to >= numericFilter - } - - return dateValue.from === numericFilter - } - - return filter.values[0]?.toLowerCase() === value - } - case 'contains': { - if (filter.values.length === 0) { - return true - } - - return (value as string || '').includes(filter.values[0]?.toLowerCase()) - } - case 'notContains': { - if (filter.values.length === 0) { - return true - } - - return !(value as string || '').includes(filter.values[0]?.toLowerCase()) - } - case 'startsWith': { - if (filter.values.length === 0) { - return true - } - - return (value as string || '').startsWith(filter.values[0]?.toLowerCase()) - } - case 'notStartsWith': { - if (filter.values.length === 0) { - return true - } - - return !(value as string || '').startsWith(filter.values[0]?.toLowerCase()) - } - case 'endsWith': { - if (filter.values.length === 0) { - return true - } - - return (value as string || '').endsWith(filter.values[0]?.toLowerCase()) - } - case 'notEndsWith': { - if (filter.values.length === 0) { - return true - } - - return !(value as string || '').endsWith(filter.values[0]?.toLowerCase()) - } - case 'isBefore': { - if (filter.values.length === 0) { - return true - } - if (dateValue !== undefined) { - const numericFilter = parseInt(filter.values[0], 10) - if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) { - // createdTime and updatedTime include the time - // So to check if create and/or updated "isBefore" date. - // Need to subtract 12 hours to filter - if (dateValue.from) { - return dateValue.from < (numericFilter - halfDay) - } - - return false - } - - return dateValue.from ? dateValue.from < numericFilter : false - } - - return false - } - case 'isAfter': { - if (filter.values.length === 0) { - return true - } - if (dateValue !== undefined) { - const numericFilter = parseInt(filter.values[0], 10) - if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) { - // createdTime and updatedTime include the time - // So to check if create and/or updated "isAfter" date. - // Need to add 12 hours to filter - if (dateValue.from) { - return dateValue.from > (numericFilter + halfDay) - } - - return false - } - - if (dateValue.to) { - return dateValue.to > numericFilter - } - - return dateValue.from ? dateValue.from > numericFilter : false - } - - return false - } - - default: { - Utils.assertFailure(`Invalid filter condition ${filter.condition}`) - } - } - - return true - } - - static propertiesThatMeetFilterGroup(filterGroup: FilterGroup | undefined, templates: readonly IPropertyTemplate[]): Record { - // TODO: Handle filter groups - if (!filterGroup) { - return {} - } - - const filters = filterGroup.filters.filter((o) => !isAFilterGroupInstance(o)) - if (filters.length < 1) { - return {} - } - - if (filterGroup.operation === 'or') { - // Just need to meet the first clause - const property = this.propertyThatMeetsFilterClause(filters[0] as FilterClause, templates) - const result: Record = {} - if (property.value) { - result[property.id] = property.value - } - - return result - } - - // And: Need to meet all clauses - const result: Record = {} - filters.forEach((filterClause) => { - const property = this.propertyThatMeetsFilterClause(filterClause as FilterClause, templates) - if (property.value) { - result[property.id] = property.value - } - }) - - return result - } - - static propertyThatMeetsFilterClause(filterClause: FilterClause, templates: readonly IPropertyTemplate[]): { id: string, value?: string } { - const template = templates.find((o) => o.id === filterClause.propertyId) - if (!template) { - Utils.assertFailure(`propertyThatMeetsFilterClause. Cannot find template: ${filterClause.propertyId}`) - - return {id: filterClause.propertyId} - } - - if (template.type === 'createdBy' || template.type === 'updatedBy') { - return {id: filterClause.propertyId} - } - - switch (filterClause.condition) { - case 'includes': { - if (filterClause.values.length < 1) { - return {id: filterClause.propertyId} - } - - return {id: filterClause.propertyId, value: filterClause.values[0]} - } - case 'notIncludes': { - return {id: filterClause.propertyId} - } - case 'isEmpty': { - return {id: filterClause.propertyId} - } - case 'isNotEmpty': { - if (template.type === 'select') { - if (template.options.length > 0) { - const option = template.options[0] - - return {id: filterClause.propertyId, value: option.id} - } - - return {id: filterClause.propertyId} - } - - // TODO: Handle non-select types - return {id: filterClause.propertyId} - } - default: { - // Handle filter clause that cannot be set - return {id: filterClause.propertyId} - } - } - } -} - -export {CardFilter} diff --git a/webapp/boards/src/components/__snapshots__/addContentMenuItem.test.tsx.snap b/webapp/boards/src/components/__snapshots__/addContentMenuItem.test.tsx.snap deleted file mode 100644 index 60bac19732..0000000000 --- a/webapp/boards/src/components/__snapshots__/addContentMenuItem.test.tsx.snap +++ /dev/null @@ -1,163 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/addContentMenuItem return a checkbox menu item 1`] = ` -
- -`; - -exports[`components/addContentMenuItem return a divider menu item 1`] = ` -
- -`; - -exports[`components/addContentMenuItem return a text menu item 1`] = ` -
- -`; - -exports[`components/addContentMenuItem return an error and empty element from unknown type 1`] = `
`; - -exports[`components/addContentMenuItem return an image menu item 1`] = ` -
- -`; diff --git a/webapp/boards/src/components/__snapshots__/blockIconSelector.test.tsx.snap b/webapp/boards/src/components/__snapshots__/blockIconSelector.test.tsx.snap deleted file mode 100644 index eab1550688..0000000000 --- a/webapp/boards/src/components/__snapshots__/blockIconSelector.test.tsx.snap +++ /dev/null @@ -1,199 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/blockIconSelector return an icon correctly 1`] = ` -
-
- -
-
-`; - -exports[`components/blockIconSelector return menu on click 1`] = ` -
-
-