1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-26 20:53:55 +02:00

Merge branch 'main' into GH2520

This commit is contained in:
Mattermod 2022-08-30 22:23:27 +03:00 committed by GitHub
commit ba479b9a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
202 changed files with 5242 additions and 1061 deletions

View File

@ -100,12 +100,12 @@ server-linux-package-docker:
rm -rf package
generate: ## Install and run code generators.
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
cd server; go get github.com/golang/mock/mockgen
cd server; go generate ./...
server-lint: templates-archive ## Run linters on server code.
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
exit 1; \
fi;
cd server; golangci-lint run ./...

View File

@ -1,6 +1,8 @@
run:
timeout: 5m
modules-download-mode: readonly
skip-files:
- product/boards_product.go
linters-settings:
gofmt:

View File

@ -75,7 +75,7 @@ endif
ifneq ($(HAS_SERVER),)
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
exit 1; \
fi; \

View File

@ -214,5 +214,23 @@ 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{}

View File

@ -44,6 +44,7 @@ func init() {
app.KVStoreKey: {},
app.StoreKey: {},
app.SystemKey: {},
app.PreferencesKey: {},
},
})
}
@ -66,11 +67,11 @@ type boardsProduct struct {
kvStoreService product.KVStoreService
storeService product.StoreService
systemService product.SystemService
preferencesService product.PreferencesService
boardsApp *boards.BoardsApp
}
//nolint:gocyclo,exhaustive
func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (app.Product, error) {
boards := &boardsProduct{}
@ -178,6 +179,12 @@ func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (a
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boards.systemService = systemService
case app.PreferencesKey:
preferencesService, ok := service.(product.PreferencesService)
if !ok {
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
}
boards.preferencesService = preferencesService
case app.HooksKey: // not needed
}
}

View File

@ -216,5 +216,37 @@ func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
// NOOP for plugin
}
//
// Preferences service.
//
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
preferences, appErr := a.api.GetPreferencesForUser(userID)
if appErr != nil {
return nil, normalizeAppErr(appErr)
}
boardsPreferences := mm_model.Preferences{}
// Mattermost API gives us all preferences.
// We want just the Focalboard ones.
for _, preference := range preferences {
if preference.Category == model.PreferencesCategoryFocalboard {
boardsPreferences = append(boardsPreferences, preference)
}
}
return boardsPreferences, nil
}
func (a *pluginAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.UpdatePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
func (a *pluginAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
appErr := a.api.DeletePreferencesForUser(userID, preferences)
return normalizeAppErr(appErr)
}
// Ensure the adapter implements ServicesAPI.
var _ model.ServicesAPI = &pluginAPIAdapter{}

View File

@ -23,7 +23,7 @@ exports[`components/boardSelector renders with no results 1`] = `
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>
@ -126,7 +126,7 @@ exports[`components/boardSelector renders with some results 1`] = `
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>
@ -327,7 +327,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>

View File

@ -23,15 +23,8 @@
position: relative;
width: 600px;
height: 450px;
.toolbar {
flex-direction: row-reverse;
padding: 0;
position: absolute;
right: 18px;
top: 18px;
}
}
.confirmation-dialog-box {
.dialog {
position: fixed;

View File

@ -8,7 +8,7 @@ const PostTypeCloudUpgradeNudge = (props: {post: Post}): JSX.Element => {
const ctaHandler = (e: React.MouseEvent) => {
e.preventDefault()
const windowAny = (window as any)
windowAny?.openPricingModal()()
windowAny?.openPricingModal()({trackingLocation: 'boards > click_view_upgrade_options_nudge'})
}
// custom post type doesn't support styling via CSS stylesheet.

View File

@ -165,6 +165,13 @@ func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
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, api string, code int, message string, sourceError error) {

View File

@ -134,6 +134,16 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
return
}
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
fmt.Fprintf(w, "%v", err)
@ -206,7 +216,13 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("TeamID", teamID)
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return

View File

@ -381,17 +381,6 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
UpdateAt: now,
}
user, err := a.app.GetUser(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", err)
return
}
if user.IsGuest {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "guests not supported", nil)
return
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
return

View File

@ -104,6 +104,19 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
return
}
}
if board.IsTemplate {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guest are not allowed to get board templates"})
return
}
}
}
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
@ -221,13 +234,6 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
// in phase 1 we use "manage_board_cards", but we would have to
// check on specific actions for phase 2
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
return
}
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
@ -242,6 +248,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
return
}
hasComments := false
hasContents := false
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
@ -250,6 +258,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
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.URL.Path, http.StatusBadRequest, message, nil)
@ -269,6 +283,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
}
}
if hasContents {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
return
}
}
if hasComments {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"})
return
}
}
blocks = model.GenerateBlockIDs(blocks, a.logger)
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
@ -748,9 +775,16 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
return
if block.Type == model.TypeComment {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"})
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"})
return
}
}
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)

View File

@ -65,8 +65,14 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
// retrieve boards list
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
@ -143,6 +149,16 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
return
}
if err = newBoard.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
return
@ -233,6 +249,19 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
return
}
} else {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
return
}
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
return
@ -502,6 +531,16 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
return
}
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)

View File

@ -98,6 +98,16 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
return
}
for _, block := range newBab.Blocks {
// Error checking
if len(block.Type) < 1 {

View File

@ -246,6 +246,16 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guests not allowed to join boards"})
return
}
newBoardMember := &model.BoardMember{
UserID: userID,
BoardID: boardID,
@ -421,6 +431,16 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
SchemeViewer: reqBoardMember.SchemeViewer,
}
isGuest, err := a.userIsGuest(paramsUserID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
newBoardMember.SchemeAdmin = false
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
return

View File

@ -53,6 +53,16 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
return
}
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)

View File

@ -146,8 +146,14 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
@ -299,8 +305,14 @@ func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
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.URL.Path, http.StatusInternalServerError, "", err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, userID)
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return

View File

@ -226,7 +226,17 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
users, err := a.app.SearchTeamUsers(teamID, searchQuery)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
asGuestUser := ""
if isGuest {
asGuestUser = userID
}
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err)
return

View File

@ -51,6 +51,16 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to templates"})
return
}
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)

View File

@ -18,6 +18,7 @@ func (a *API) registerUsersRoutes(r *mux.Router) {
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) {
@ -219,6 +220,19 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if !canSeeUser {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
@ -302,3 +316,44 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
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.URL.Path, http.StatusInternalServerError, "", err)
return
}
data, err := json.Marshal(preferences)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}

View File

@ -256,8 +256,8 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
return bab, members, err
}
func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
return a.store.GetBoardsForUserAndTeam(userID, teamID)
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) {
@ -552,8 +552,8 @@ func (a *App) DeleteBoardMember(boardID, userID string) error {
return nil
}
func (a *App) SearchBoardsForUser(term, userID string) ([]*model.Board, error) {
return a.store.SearchBoardsForUser(term, userID)
func (a *App) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
return a.store.SearchBoardsForUser(term, userID, includePublicBoards)
}
func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {

View File

@ -65,7 +65,7 @@ func (a *App) GetUserTimezone(userID string) (string, error) {
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)
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID, true)
if err != nil {
return nil, errors.New("error getting boards for user")
}

View File

@ -49,7 +49,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
IsGuest: false,
}
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
Return(mockTeamInsightsList, nil)
@ -72,7 +72,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
IsGuest: false,
}
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
th.Store.EXPECT().
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
Return(nil, insightError{"board-insight-error"})

View File

@ -7,10 +7,9 @@ import (
)
const (
KeyPrefix = "focalboard_" // use key prefix to namespace focalboard props
KeyOnboardingTourStarted = KeyPrefix + "onboardingTourStarted"
KeyOnboardingTourCategory = KeyPrefix + "tourCategory"
KeyOnboardingTourStep = KeyPrefix + "onboardingTourStep"
KeyOnboardingTourStarted = "onboardingTourStarted"
KeyOnboardingTourCategory = "tourCategory"
KeyOnboardingTourStep = "onboardingTourStep"
ValueOnboardingFirstStep = "0"
ValueTourCategoryOnboarding = "onboarding"

View File

@ -5,25 +5,52 @@ import (
mmModel "github.com/mattermost/mattermost-server/v6/model"
)
func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) {
return a.store.GetUsersByTeam(teamID)
func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, error) {
return a.store.GetUsersByTeam(teamID, asGuestID)
}
func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) {
return a.store.SearchUsersByTeam(teamID, searchQuery)
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID)
}
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) ([]mmModel.Preference, error) {
if err := a.store.PatchUserProps(userID, patch); err != nil {
return nil, err
}
user, err := a.store.GetUserByID(userID)
updatedPreferences, err := a.store.GetUserPreferences(userID)
if err != nil {
return nil, err
}
return user.Props, nil
return updatedPreferences, nil
}
func (a *App) GetUserPreferences(userID string) ([]mmModel.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) ([]*mmModel.Channel, error) {

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
//go:generate mockgen -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
package auth
import (

View File

@ -263,7 +263,7 @@ func TestCreateBoard(t *testing.T) {
th.CheckBadRequest(resp)
require.Nil(t, board)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
require.NoError(t, err)
require.Empty(t, boards)
})
@ -277,7 +277,7 @@ func TestCreateBoard(t *testing.T) {
th.CheckBadRequest(resp)
require.Nil(t, board)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
require.NoError(t, err)
require.Empty(t, boards)
})
@ -292,7 +292,7 @@ func TestCreateBoard(t *testing.T) {
th.CheckForbidden(resp)
require.Nil(t, board)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
require.NoError(t, err)
require.Empty(t, boards)
})
@ -530,7 +530,7 @@ func TestSearchBoards(t *testing.T) {
Type: model.BoardTypePrivate,
TeamID: "other-team-id",
}
_, err = th.Server.App().CreateBoard(board5, user1.ID, true)
rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true)
require.NoError(t, err)
testCases := []struct {
@ -543,13 +543,13 @@ func TestSearchBoards(t *testing.T) {
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},
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},
ExpectedIDs: []string{rBoard1.ID, rBoard3.ID, rBoard5.ID},
},
{
Name: "matching part of the word",
@ -1595,6 +1595,52 @@ func TestUpdateMember(t *testing.T) {
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) {

View File

@ -37,6 +37,7 @@ const (
userCommenter string = "commenter"
userEditor string = "editor"
userAdmin string = "admin"
userGuest string = "guest"
)
var (
@ -47,6 +48,7 @@ var (
userCommenterID = userCommenter
userEditorID = userEditor
userAdminID = userAdmin
userGuestID = userGuest
)
type LicenseType int
@ -62,6 +64,8 @@ type TestHelper struct {
Server *server.Server
Client *client.Client
Client2 *client.Client
origEnvUnitTesting string
}
type FakePermissionPluginAPI struct{}
@ -248,8 +252,16 @@ func newTestServerLocalMode() *server.Server {
}
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
sessionToken := "TESTTOKEN"
th := &TestHelper{T: t}
th := &TestHelper{
T: t,
origEnvUnitTesting: origUnitTesting,
}
th.Server = newTestServer(sessionToken)
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
@ -261,21 +273,42 @@ func SetupTestHelper(t *testing.T) *TestHelper {
}
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
th := &TestHelper{T: t}
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
th := &TestHelper{
T: t,
origEnvUnitTesting: origUnitTesting,
}
th.Server = NewTestServerPluginMode()
th.Start()
return th
}
func SetupTestHelperLocalMode(t *testing.T) *TestHelper {
th := &TestHelper{T: t}
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
th := &TestHelper{
T: t,
origEnvUnitTesting: origUnitTesting,
}
th.Server = newTestServerLocalMode()
th.Start()
return th
}
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
th := &TestHelper{T: t}
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
th := &TestHelper{
T: t,
origEnvUnitTesting: origUnitTesting,
}
th.Server = newTestServerWithLicense("", licenseType)
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
@ -343,6 +376,8 @@ func (th *TestHelper) InitBasic() *TestHelper {
var ErrRegisterFail = errors.New("register failed")
func (th *TestHelper) TearDown() {
os.Setenv("FOCALBOARD_UNIT_TESTING", th.origEnvUnitTesting)
logger := th.Server.Logger()
if l, ok := logger.(*mlog.Logger); ok {

View File

@ -54,7 +54,7 @@ func TestExportBoard(t *testing.T) {
require.NoError(t, resp.Error)
// check for test card
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID)
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID, true)
require.NoError(t, err)
require.Len(t, boardsImported, 1)
boardImported := boardsImported[0]

File diff suppressed because it is too large Load Diff

View File

@ -73,6 +73,15 @@ func NewPluginTestStore(innerStore store.Store) *PluginTestStore {
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
},
"guest": {
ID: "guest",
Props: map[string]interface{}{},
Username: "guest",
Email: "guest@sample.com",
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
IsGuest: true,
},
},
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
@ -109,6 +118,8 @@ func (s *PluginTestStore) GetTeamsForUser(userID string) ([]*model.Team, error)
return []*model.Team{s.testTeam, s.otherTeam}, nil
case "admin":
return []*model.Team{s.testTeam, s.otherTeam}, nil
case "guest":
return []*model.Team{s.testTeam}, nil
}
return nil, errTestStore
}
@ -160,7 +171,30 @@ func (s *PluginTestStore) PatchUserProps(userID string, patch model.UserPropPatc
return nil
}
func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
func (s *PluginTestStore) GetUserPreferences(userID string) (mmModel.Preferences, error) {
if userID == userTeamMember {
return mmModel.Preferences{{
UserId: userTeamMember,
Category: "focalboard",
Name: "test",
Value: "test",
}}, nil
}
return nil, errTestStore
}
func (s *PluginTestStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
if asGuestID == "guest" {
return []*model.User{
s.users["viewer"],
s.users["commenter"],
s.users["editor"],
s.users["admin"],
s.users["guest"],
}, nil
}
switch {
case teamID == s.testTeam.ID:
return []*model.User{
@ -169,6 +203,7 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
s.users["commenter"],
s.users["editor"],
s.users["admin"],
s.users["guest"],
}, nil
case teamID == s.otherTeam.ID:
return []*model.User{
@ -184,9 +219,9 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
return nil, errTestStore
}
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
users := []*model.User{}
teamUsers, err := s.GetUsersByTeam(teamID)
teamUsers, err := s.GetUsersByTeam(teamID, asGuestID)
if err != nil {
return nil, err
}
@ -199,6 +234,32 @@ func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) (
return users, nil
}
func (s *PluginTestStore) CanSeeUser(seerID string, seenID string) (bool, error) {
user, err := s.GetUserByID(seerID)
if err != nil {
return false, err
}
if !user.IsGuest {
return true, nil
}
seerMembers, err := s.GetMembersForUser(seerID)
if err != nil {
return false, err
}
seenMembers, err := s.GetMembersForUser(seenID)
if err != nil {
return false, err
}
for _, seerMember := range seerMembers {
for _, seenMember := range seenMembers {
if seerMember.BoardID == seenMember.BoardID {
return true, nil
}
}
}
return false, nil
}
func (s *PluginTestStore) SearchUserChannels(teamID, userID, query string) ([]*mmModel.Channel, error) {
return []*mmModel.Channel{
{
@ -235,8 +296,8 @@ func (s *PluginTestStore) GetChannel(teamID, channel string) (*mmModel.Channel,
return nil, errTestStore
}
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
boards, err := s.Store.SearchBoardsForUser(term, userID)
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
boards, err := s.Store.SearchBoardsForUser(term, userID, includePublicBoards)
if err != nil {
return nil, err
}

View File

@ -168,6 +168,14 @@ func (p *BlockPatch) Patch(block *Block) *Block {
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=-1, 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

View File

@ -67,6 +67,20 @@ func (mr *MockServicesAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0)
}
// 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()
@ -243,6 +257,21 @@ func (mr *MockServicesAPIMockRecorder) GetMasterDB() *gomock.Call {
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()
@ -399,6 +428,20 @@ func (mr *MockServicesAPIMockRecorder) RegisterRouter(arg0 interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0)
}
// 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()

View File

@ -17,4 +17,6 @@ var (
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
PermissionCommentBoardCards = &mmModel.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
PermissionDeleteOthersComments = &mmModel.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
)

View File

@ -85,4 +85,9 @@ type ServicesAPI interface {
// 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
}

View File

@ -6,9 +6,10 @@ import (
)
const (
SingleUser = "single-user"
GlobalTeamID = "0"
SystemUserID = "system"
SingleUser = "single-user"
GlobalTeamID = "0"
SystemUserID = "system"
PreferencesCategoryFocalboard = "focalboard"
)
// User is a user

View File

@ -67,10 +67,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
return member.SchemeAdmin
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:

View File

@ -99,10 +99,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
return member.SchemeAdmin
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:

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
package mmpermissions
import (

View File

@ -457,6 +457,21 @@ func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call {
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()
@ -681,6 +696,21 @@ func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interf
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()

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

View File

@ -36,6 +36,9 @@ type servicesAPI interface {
GetCloudLimits() (*mmModel.ProductLimits, error)
EnsureBot(bot *mmModel.Bot) (string, error)
CreatePost(post *mmModel.Post) (*mmModel.Post, error)
GetPreferencesForUser(userID string) (mmModel.Preferences, error)
DeletePreferencesForUser(userID string, preferences mmModel.Preferences) error
UpdatePreferencesForUser(userID string, preferences mmModel.Preferences) error
}
// Store represents the abstraction of the data storage.
@ -71,8 +74,7 @@ func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
query := s.getQueryBuilder().
Select("count(*)").
From("Users").
Where(sq.Eq{"deleteAt": 0}).
Where(sq.NotEq{"roles": "system_guest"})
Where(sq.Eq{"deleteAt": 0})
row := query.QueryRow()
var count int
@ -128,32 +130,50 @@ func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) er
}
func (s *MattermostAuthLayer) PatchUserProps(userID string, patch model.UserPropPatch) error {
user, err := s.servicesAPI.GetUserByID(userID)
if err != nil {
s.logger.Error("failed to fetch user", mlog.String("userID", userID), mlog.Err(err))
return err
if len(patch.UpdatedFields) > 0 {
updatedPreferences := mmModel.Preferences{}
for key, value := range patch.UpdatedFields {
preference := mmModel.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 err
}
}
props := user.Props
if len(patch.DeletedFields) > 0 {
deletedPreferences := mmModel.Preferences{}
for _, key := range patch.DeletedFields {
preference := mmModel.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
}
for _, key := range patch.DeletedFields {
delete(props, key)
}
deletedPreferences = append(deletedPreferences, preference)
}
for key, value := range patch.UpdatedFields {
props[key] = value
}
user.Props = props
if _, err := s.servicesAPI.UpdateUser(user); err != nil {
s.logger.Error("failed to update user", mlog.String("userID", userID), mlog.Err(err))
return err
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 err
}
}
return nil
}
func (s *MattermostAuthLayer) GetUserPreferences(userID string) (mmModel.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().
@ -267,16 +287,32 @@ func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
return builder.RunWith(s.mmDB)
}
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) {
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
query := s.getQueryBuilder().
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
From("Users as u").
Join("TeamMembers as tm ON tm.UserID = u.id").
LeftJoin("Bots b ON ( b.UserID = u.id )").
Where(sq.Eq{"u.deleteAt": 0}).
Where(sq.NotEq{"u.roles": "system_guest"}).
Where(sq.Eq{"tm.TeamId": teamID})
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 {
@ -295,7 +331,7 @@ func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, erro
func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, error) {
query := s.getQueryBuilder().
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
From("Users as u").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Eq{"u.id": userIDs})
@ -314,12 +350,11 @@ func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, err
return users, nil
}
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
query := s.getQueryBuilder().
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
From("Users as u").
Join("TeamMembers as tm ON tm.UserID = u.id").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Eq{"u.deleteAt": 0}).
Where(sq.Or{
@ -328,11 +363,27 @@ func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery strin
sq.Like{"u.firstname": "%" + searchQuery + "%"},
sq.Like{"u.lastname": "%" + searchQuery + "%"},
}).
Where(sq.Eq{"tm.TeamId": teamID}).
Where(sq.NotEq{"u.roles": "system_guest"}).
OrderBy("u.username").
Limit(10)
if asGuestID == "" {
query = query.
Join("TeamMembers as tm ON tm.UserID = u.id").
Where(sq.Eq{"tm.TeamId": teamID})
} else {
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
if err != nil {
return nil, err
}
boardsIDs := []string{}
for _, board := range boards {
boardsIDs = append(boardsIDs, board.ID)
}
query = query.
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
Where(sq.Eq{"bm.BoardId": boardsIDs})
}
rows, err := query.Query()
if err != nil {
return nil, err
@ -366,6 +417,7 @@ func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, erro
&user.UpdateAt,
&user.DeleteAt,
&user.IsBot,
&user.IsGuest,
)
if err != nil {
return nil, err
@ -558,7 +610,7 @@ func boardFields(prefix string) []string {
// 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, userID string) ([]*model.Board, error) {
func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder().
Select(boardFields("b.")...).
Distinct().
@ -568,17 +620,20 @@ func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string) ([]*model
LeftJoin("ChannelMembers as cm on cm.channelId=b.channel_id").
Where(sq.Eq{"b.is_template": false}).
Where(sq.Eq{"tm.userID": userID}).
Where(sq.Eq{"tm.deleteAt": 0}).
Where(sq.Or{
Where(sq.Eq{"tm.deleteAt": 0})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.And{
sq.Eq{"b.type": model.BoardTypePrivate},
sq.Or{
sq.Eq{"bm.user_id": userID},
sq.Eq{"cm.userId": userID},
},
},
sq.Eq{"bm.user_id": userID},
sq.Eq{"cm.userId": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
sq.Eq{"cm.userId": userID},
})
}
if term != "" {
// break search query into space separated words
@ -799,12 +854,14 @@ func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.Board
return members, nil
}
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
members, err := s.GetMembersForUser(userID)
if err != nil {
return nil, err
}
// TODO: Handle the includePublicBoards
boardIDs := []string{}
for _, m := range members {
boardIDs = append(boardIDs, m.BoardID)
@ -919,3 +976,67 @@ func (s *MattermostAuthLayer) GetUserTimezone(userID string) (string, error) {
timezone := user.Timezone
return mmModel.GetPreferredTimezone(timezone), nil
}
func (s *MattermostAuthLayer) CanSeeUser(seerID string, seenID string) (bool, error) {
mmuser, appErr := s.servicesAPI.GetUserByID(seerID)
if appErr != nil {
return false, appErr
}
if !mmuser.IsGuest() {
return true, nil
}
query := s.getQueryBuilder().
Select("1").
From(s.tablePrefix + "board_members AS BM1").
Join(s.tablePrefix + "board_members AS BM2 ON BM1.BoardID=BM2.BoardID").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Or{
sq.And{
sq.Eq{"BM1.UserID": seerID},
sq.Eq{"BM2.UserID": seenID},
},
sq.And{
sq.Eq{"BM1.UserID": seenID},
sq.Eq{"BM2.UserID": seerID},
},
}).Limit(1)
rows, err := query.Query()
if err != nil {
return false, err
}
defer s.CloseRows(rows)
for rows.Next() {
return true, err
}
query = s.getQueryBuilder().
Select("1").
From("ChannelMembers AS CM1").
Join("ChannelMembers AS CM2 ON CM1.BoardID=CM2.BoardID").
LeftJoin("Bots b ON ( b.UserId = u.id )").
Where(sq.Or{
sq.And{
sq.Eq{"CM1.UserID": seerID},
sq.Eq{"CM2.UserID": seenID},
},
sq.And{
sq.Eq{"CM1.UserID": seenID},
sq.Eq{"CM2.UserID": seerID},
},
}).Limit(1)
rows, err = query.Query()
if err != nil {
return false, err
}
defer s.CloseRows(rows)
for rows.Next() {
return true, err
}
return false, nil
}

View File

@ -50,6 +50,21 @@ func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1, arg2 interfa
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2)
}
// 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()
@ -399,6 +414,21 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
}
// GetBlocks mocks base method.
func (m *MockStore) GetBlocks(arg0 model.QueryBlocksOptions) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocks", arg0)
ret0, _ := ret[0].([]model.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) ([]model.Block, error) {
m.ctrl.T.Helper()
@ -429,21 +459,6 @@ func (mr *MockStoreMockRecorder) GetBlocksForBoard(arg0 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0)
}
// GetBlocksWithBoardID mocks base method.
func (m *MockStore) GetBlocksWithBoardID(arg0 string) ([]model.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithBoardID", arg0)
ret0, _ := ret[0].([]model.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithBoardID indicates an expected call of GetBlocksWithBoardID.
func (mr *MockStoreMockRecorder) GetBlocksWithBoardID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithBoardID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithBoardID), arg0)
}
// GetBlocksWithParent mocks base method.
func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]model.Block, error) {
m.ctrl.T.Helper()
@ -567,18 +582,18 @@ func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interfac
}
// GetBoardsForUserAndTeam mocks base method.
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) {
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1)
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1, arg2)
ret0, _ := ret[0].([]*model.Board)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam.
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1, arg2)
}
// GetBoardsInTeamByIds mocks base method.
@ -1075,6 +1090,21 @@ func (mr *MockStoreMockRecorder) GetUserCategoryBoards(arg0, arg1 interface{}) *
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) (model0.Preferences, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserPreferences", arg0)
ret0, _ := ret[0].(model0.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()
@ -1091,18 +1121,18 @@ func (mr *MockStoreMockRecorder) GetUserTimezone(arg0 interface{}) *gomock.Call
}
// GetUsersByTeam mocks base method.
func (m *MockStore) GetUsersByTeam(arg0 string) ([]*model.User, error) {
func (m *MockStore) GetUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0)
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0, arg1)
ret0, _ := ret[0].([]*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUsersByTeam indicates an expected call of GetUsersByTeam.
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0, arg1)
}
// GetUsersList mocks base method.
@ -1338,18 +1368,18 @@ func (mr *MockStoreMockRecorder) SaveMember(arg0 interface{}) *gomock.Call {
}
// SearchBoardsForUser mocks base method.
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string) ([]*model.Board, error) {
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1)
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1, arg2)
ret0, _ := ret[0].([]*model.Board)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SearchBoardsForUser indicates an expected call of SearchBoardsForUser.
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1, arg2)
}
// SearchBoardsForUserInTeam mocks base method.
@ -1383,18 +1413,18 @@ func (mr *MockStoreMockRecorder) SearchUserChannels(arg0, arg1, arg2 interface{}
}
// SearchUsersByTeam mocks base method.
func (m *MockStore) SearchUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
func (m *MockStore) SearchUsersByTeam(arg0, arg1, arg2 string) ([]*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1)
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1, arg2)
ret0, _ := ret[0].([]*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SearchUsersByTeam indicates an expected call of SearchUsersByTeam.
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1, arg2)
}
// SendMessage mocks base method.

View File

@ -63,17 +63,34 @@ func (s *SQLStore) blockFields() []string {
}
}
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]model.Block, error) {
func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"parent_id": parentID}).
Where(sq.Eq{"type": blockType})
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))
}
if opts.PerPage > 0 {
query = query.Limit(uint64(opts.PerPage))
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err))
s.logger.Error(`getBlocks ERROR`, mlog.Err(err))
return nil, err
}
@ -82,22 +99,21 @@ func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentI
return s.blocksFromRows(rows)
}
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"parent_id": parentID}).
Where(sq.Eq{"board_id": boardID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBlocksWithParent ERROR`, mlog.Err(err))
return nil, err
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
BlockType: model.BlockType(blockType),
}
defer s.CloseRows(rows)
return s.getBlocks(db, opts)
}
return s.blocksFromRows(rows)
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block, error) {
@ -127,21 +143,11 @@ func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block
}
func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string) ([]model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"type": blockType}).
Where(sq.Eq{"board_id": boardID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err))
return nil, err
opts := model.QueryBlocksOptions{
BoardID: boardID,
BlockType: model.BlockType(blockType),
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
return s.getBlocks(db, opts)
}
// getSubTree2 returns blocks within 2 levels of the given blockID.
@ -177,19 +183,10 @@ func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string,
}
func (s *SQLStore) getBlocksForBoard(db sq.BaseRunner, boardID string) ([]model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"board_id": boardID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getAllBlocksForBoard ERROR`, mlog.Err(err))
return nil, err
opts := model.QueryBlocksOptions{
BoardID: boardID,
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
return s.getBlocks(db, opts)
}
func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) {

View File

@ -252,21 +252,25 @@ func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, err
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
}
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
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}).
Where(sq.Or{
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.And{
sq.Eq{"b.type": model.BoardTypePrivate},
sq.Eq{"bm.user_id": userID},
},
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 {
@ -643,20 +647,24 @@ func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*mode
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string) ([]*model.Board, error) {
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
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.Or{
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.And{
sq.Eq{"b.type": model.BoardTypePrivate},
sq.Eq{"bm.user_id": userID},
},
sq.Eq{"bm.user_id": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
})
}
if term != "" {
// break search query into space separated words

View File

@ -0,0 +1,39 @@
DROP INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions;
DROP INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks;
{{if .mysql}}
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
{{end}}
{{if .postgres}}
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
{{end}}
{{if .sqlite}}
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
CREATE TABLE {{.prefix}}blocks (
id VARCHAR(36),
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
parent_id VARCHAR(36),
schema BIGINT,
type TEXT,
title TEXT,
fields TEXT,
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
root_id VARCHAR(36),
modified_by VARCHAR(36),
channel_id VARCHAR(36),
created_by VARCHAR(36),
board_id VARCHAR(36),
PRIMARY KEY (channel_id, id)
);
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
DROP TABLE {{.prefix}}blocks_tmp;
{{end}}

View File

@ -0,0 +1,44 @@
{{- /* delete old blocks PK and add id as the new one */ -}}
{{if .mysql}}
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
{{end}}
{{if .postgres}}
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
{{end}}
{{- /* there is no way for SQLite to update the PK or add a unique constraint */ -}}
{{if .sqlite}}
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
CREATE TABLE {{.prefix}}blocks (
id VARCHAR(36),
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
parent_id VARCHAR(36),
schema BIGINT,
type TEXT,
title TEXT,
fields TEXT,
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
root_id VARCHAR(36),
modified_by VARCHAR(36),
channel_id VARCHAR(36),
created_by VARCHAR(36),
board_id VARCHAR(36),
PRIMARY KEY (id)
);
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
DROP TABLE {{.prefix}}blocks_tmp;
{{end}}
{{- /* most block searches use board_id or a combination of board and parent ids */ -}}
CREATE INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks (board_id, parent_id);
{{- /* get subscriptions is used once per board page load */ -}}
CREATE INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions (subscriber_id);

View File

@ -0,0 +1 @@
DROP TABLE {{.prefix}}preferences;

View File

@ -0,0 +1,14 @@
create table {{.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)
);
create index idx_{{.prefix}}preferences_category
on {{.prefix}}preferences (category);
create index idx_{{.prefix}}preferences_name
on {{.prefix}}preferences (name);

View File

@ -0,0 +1,53 @@
{{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;
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;
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;
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;
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;
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;
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;
UPDATE users SET props = (props - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion');
{{else}}
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
UPDATE {{.prefix}}users SET props = (props::jsonb - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion')::json;
{{else}}
{{- /* Surprisingly SQLite and MySQL have same JSON functions and syntax! */ -}}
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;
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;
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;
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;
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;
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;
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;
UPDATE {{.prefix}}users SET props = JSON_REMOVE(props, '$.focalboard_welcomePageViewed', '$.hiddenBoardIDs', '$.focalboard_tourCategory', '$.focalboard_onboardingTourStep', '$.focalboard_onboardingTourStarted', '$.focalboard_version72MessageCanceled', '$.focalboard_lastWelcomeVersion');
{{end}}
{{end}}

View File

@ -46,6 +46,11 @@ func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, bloc
}
func (s *SQLStore) CanSeeUser(seerID string, seenID string) (bool, error) {
return s.canSeeUser(s.db, seerID, seenID)
}
func (s *SQLStore) CleanUpSessions(expireTime int64) error {
return s.cleanUpSessions(s.db, expireTime)
@ -294,6 +299,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
}
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)
@ -344,8 +354,8 @@ func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit ui
}
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) {
return s.getBoardsForUserAndTeam(s.db, userID, teamID)
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) {
return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards)
}
@ -514,13 +524,18 @@ func (s *SQLStore) GetUserCategoryBoards(userID string, teamID string) ([]model.
}
func (s *SQLStore) GetUserPreferences(userID string) (mmModel.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) ([]*model.User, error) {
return s.getUsersByTeam(s.db, teamID)
func (s *SQLStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
return s.getUsersByTeam(s.db, teamID, asGuestID)
}
@ -756,8 +771,8 @@ func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
}
func (s *SQLStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
return s.searchBoardsForUser(s.db, term, userID)
func (s *SQLStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
return s.searchBoardsForUser(s.db, term, userID, includePublicBoards)
}
@ -771,8 +786,8 @@ func (s *SQLStore) SearchUserChannels(teamID string, userID string, query string
}
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
return s.searchUsersByTeam(s.db, teamID, searchQuery)
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
return s.searchUsersByTeam(s.db, teamID, searchQuery, asGuestID)
}

View File

@ -2,8 +2,10 @@ package sqlstore
import (
"database/sql"
"errors"
"fmt"
"net/url"
"strings"
sq "github.com/Masterminds/squirrel"
@ -15,6 +17,9 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
//nolint:lll
var ErrInvalidMariaDB = errors.New("MariaDB database is not supported, you can find more information at https://docs.mattermost.com/install/software-hardware-requirements.html#database-software")
// SQLStore is a SQL database.
type SQLStore struct {
db *sql.DB
@ -53,6 +58,10 @@ func New(params Params) (*SQLStore, error) {
servicesAPI: params.ServicesAPI,
}
if store.IsMariaDB() {
return nil, ErrInvalidMariaDB
}
var err error
store.isBinaryParam, err = store.computeBinaryParam()
if err != nil {
@ -70,6 +79,22 @@ func New(params Params) (*SQLStore, error) {
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) {

View File

@ -6,6 +6,9 @@ import (
"errors"
"fmt"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/focalboard/server/model"
@ -211,11 +214,11 @@ func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password str
return nil
}
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string) ([]*model.User, error) {
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string) ([]*model.User, error) {
return s.getUsersByCondition(db, nil, 0)
}
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string) ([]*model.User, error) {
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string) ([]*model.User, error) {
return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
}
@ -255,24 +258,78 @@ func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
}
func (s *SQLStore) patchUserProps(db sq.BaseRunner, userID string, patch model.UserPropPatch) error {
user, err := s.getUserByID(db, userID)
if err != nil {
return err
if len(patch.UpdatedFields) > 0 {
for key, value := range patch.UpdatedFields {
preference := mmModel.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
Value: value,
}
if err := s.updateUserProps(db, preference); err != nil {
return err
}
}
}
if user.Props == nil {
user.Props = map[string]interface{}{}
if len(patch.DeletedFields) > 0 {
for _, key := range patch.DeletedFields {
preference := mmModel.Preference{
UserId: userID,
Category: model.PreferencesCategoryFocalboard,
Name: key,
}
if err := s.deleteUserProps(db, preference); err != nil {
return err
}
}
}
for _, key := range patch.DeletedFields {
delete(user.Props, key)
return nil
}
func (s *SQLStore) updateUserProps(db sq.BaseRunner, preference mmModel.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))
case model.SqliteDBType:
query = query.SuffixExpr(sq.Expr(" on conflict(userid, category, name) do update set value = excluded.value"))
default:
return store.NewErrNotImplemented("failed to update preference because of missing driver")
}
for key, value := range patch.UpdatedFields {
user.Props[key] = value
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 s.updateUser(db, user)
return nil
}
func (s *SQLStore) deleteUserProps(db sq.BaseRunner, preference mmModel.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 {
@ -286,3 +343,52 @@ func (s *SQLStore) postMessage(db sq.BaseRunner, message, postType string, chann
func (s *SQLStore) getUserTimezone(_ sq.BaseRunner, _ string) (string, error) {
return "", errUnsupportedOperation
}
func (s *SQLStore) getUserPreferences(db sq.BaseRunner, userID string) (mmModel.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) ([]mmModel.Preference, error) {
preferences := []mmModel.Preference{}
for rows.Next() {
var preference mmModel.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
}

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mockstore/mockstore.go -package mockstore . Store
//go:generate mockgen -destination=mockstore/mockstore.go -package mockstore . Store
//go:generate go run ./generators/main.go
package store
@ -14,6 +14,7 @@ const CardLimitTimestampSystemKey = "card_limit_timestamp"
// Store represents the abstraction of the data storage.
type Store interface {
GetBlocks(opts model.QueryBlocksOptions) ([]model.Block, error)
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error)
GetBlocksWithParent(boardID, parentID string) ([]model.Block, error)
GetBlocksByIDs(ids []string) ([]model.Block, error)
@ -61,9 +62,10 @@ type Store interface {
UpdateUser(user *model.User) error
UpdateUserPassword(username, password string) error
UpdateUserPasswordByID(userID, password string) error
GetUsersByTeam(teamID string) ([]*model.User, error)
SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error)
GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error)
SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error)
PatchUserProps(userID string, patch model.UserPropPatch) error
GetUserPreferences(userID string) (mmModel.Preferences, error)
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
GetSession(token string, expireTime int64) (*model.Session, error)
@ -89,7 +91,7 @@ type Store interface {
// @withTransaction
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
GetBoard(id string) (*model.Board, error)
GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error)
GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error)
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
// @withTransaction
DeleteBoard(boardID, userID string) error
@ -100,7 +102,8 @@ type Store interface {
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
GetMembersForUser(userID string) ([]*model.BoardMember, error)
SearchBoardsForUser(term, userID string) ([]*model.Board, error)
CanSeeUser(seerID string, seenID string) (bool, error)
SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error)
SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error)
// @withTransaction

View File

@ -952,12 +952,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
})
t.Run("get block history after updateAt", func(t *testing.T) {
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
rBlock, err2 := store.GetBlock("block3")
require.NoError(t, err2)
require.NotZero(t, rBlocks[2].UpdateAt)
require.NotZero(t, rBlock.UpdateAt)
opts := model.QueryBlockHistoryOptions{
AfterUpdateAt: rBlocks[2].UpdateAt,
AfterUpdateAt: rBlock.UpdateAt,
Descending: false,
}
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
@ -970,12 +970,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
})
t.Run("get block history before updateAt", func(t *testing.T) {
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
rBlock, err2 := store.GetBlock("block3")
require.NoError(t, err2)
require.NotZero(t, rBlocks[2].UpdateAt)
require.NotZero(t, rBlock.UpdateAt)
opts := model.QueryBlockHistoryOptions{
BeforeUpdateAt: rBlocks[2].UpdateAt,
BeforeUpdateAt: rBlock.UpdateAt,
Descending: true,
}
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)

View File

@ -68,8 +68,8 @@ func getBoardsInsightsTest(t *testing.T, store store.Store) {
_, _ = store.SaveMember(bm)
boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID)
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID)
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, testUserID,

View File

@ -168,7 +168,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
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)
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, true)
require.NoError(t, err)
require.ElementsMatch(t, []*model.Board{
rBoard1,
@ -177,8 +177,17 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
}, 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)
boards, err := store.GetBoardsForUserAndTeam(userID, teamID2, true)
require.NoError(t, err)
require.Len(t, boards, 1)
require.Equal(t, board5.ID, boards[0].ID)
@ -688,7 +697,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
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("", userID)
boards, err := store.SearchBoardsForUser("", userID, true)
require.NoError(t, err)
require.Empty(t, boards)
})
@ -743,6 +752,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
TeamID string
UserID string
Term string
IncludePublic bool
ExpectedBoardIDs []string
}{
{
@ -750,6 +760,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
TeamID: teamID1,
UserID: userID,
Term: "",
IncludePublic: true,
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
},
{
@ -757,13 +768,23 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
TeamID: teamID1,
UserID: userID,
Term: "board",
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",
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",
IncludePublic: true,
ExpectedBoardIDs: []string{board1.ID, board2.ID, board5.ID},
},
{
@ -771,6 +792,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
TeamID: teamID1,
UserID: userID,
Term: "priv",
IncludePublic: true,
ExpectedBoardIDs: []string{board3.ID},
},
{
@ -778,13 +800,14 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
TeamID: teamID2,
UserID: userID,
Term: "non-matching-term",
IncludePublic: true,
ExpectedBoardIDs: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID)
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID, tc.IncludePublic)
require.NoError(t, err)
boardIDs := []string{}

View File

@ -41,7 +41,7 @@ func testCreateBoardsAndBlocks(t *testing.T, store store.Store) {
teamID := testTeamID
userID := testUserID
boards, err := store.GetBoardsForUserAndTeam(userID, teamID)
boards, err := store.GetBoardsForUserAndTeam(userID, teamID, true)
require.Nil(t, err)
require.Empty(t, boards)

View File

@ -27,7 +27,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
testCreateAndGetUser(t, store)
})
t.Run("CreateAndUpateUser", func(t *testing.T) {
t.Run("CreateAndUpdateUser", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testCreateAndUpdateUser(t, store)
@ -47,7 +47,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
func testGetTeamUsers(t *testing.T, store store.Store) {
t.Run("GetTeamUSers", func(t *testing.T) {
users, err := store.GetUsersByTeam("team_1")
users, err := store.GetUsersByTeam("team_1", "")
require.Equal(t, 0, len(users))
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
@ -66,7 +66,7 @@ func testGetTeamUsers(t *testing.T, store store.Store) {
})
}()
users, err = store.GetUsersByTeam("team_1")
users, err = store.GetUsersByTeam("team_1", "")
require.Equal(t, 1, len(users))
require.Equal(t, "darth.vader", users[0].Username)
require.NoError(t, err)
@ -176,53 +176,81 @@ func testPatchUserProps(t *testing.T, store store.Store) {
err := store.CreateUser(user)
require.NoError(t, err)
key1 := "new_key_1"
key2 := "new_key_2"
key3 := "new_key_3"
// Only update props
patch := model.UserPropPatch{
UpdatedFields: map[string]string{
"new_key_1": "new_value_1",
"new_key_2": "new_value_2",
"new_key_3": "new_value_3",
key1: "new_value_1",
key2: "new_value_2",
key3: "new_value_3",
},
}
err = store.PatchUserProps(user.ID, patch)
require.NoError(t, err)
fetchedUser, err := store.GetUserByID(user.ID)
userPreferences, err := store.GetUserPreferences(user.ID)
require.NoError(t, err)
require.Equal(t, fetchedUser.Props["new_key_1"], "new_value_1")
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
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.UserPropPatch{
DeletedFields: []string{
"new_key_1",
key1,
},
}
err = store.PatchUserProps(user.ID, patch)
require.NoError(t, err)
fetchedUser, err = store.GetUserByID(user.ID)
userPreferences, err = store.GetUserPreferences(user.ID)
require.NoError(t, err)
_, ok := fetchedUser.Props["new_key_1"]
require.False(t, ok)
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
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.UserPropPatch{
UpdatedFields: map[string]string{
"new_key_3": "new_value_3_new_again",
key3: "new_value_3_new_again",
},
DeletedFields: []string{
"new_key_2",
key2,
},
}
err = store.PatchUserProps(user.ID, patch)
require.NoError(t, err)
fetchedUser, err = store.GetUserByID(user.ID)
userPreferences, err = store.GetUserPreferences(user.ID)
require.NoError(t, err)
_, ok = fetchedUser.Props["new_key_2"]
require.False(t, ok)
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3_new_again")
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)
}
}
}

View File

@ -557,6 +557,27 @@ definitions:
x-go-name: ErrorCode
type: object
x-go-package: github.com/mattermost/focalboard/server/model
Preference:
description: Preference represents a single user preference. A user can have multiple preferences.
properties:
user_id:
description: The associated user's ID
type: string
x-go-name: UserId
Category:
description: The category of preference. Its always "Focalboard" for this project
type: string
x-go-name: Category
Name:
description: Preference's name
type: string
x-go-name: Name
value:
description: Preference's value
type: string
x-go-name: Value
type: object
x-go-package: github.com/mattermost/focalboard/server/model
FileUploadResponse:
description: FileUploadResponse is the response to a file upload
properties:

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
package ws
import (

View File

@ -457,6 +457,21 @@ func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call {
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()
@ -681,6 +696,21 @@ func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interf
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()

View File

@ -1,4 +1,4 @@
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
package ws
import (

View File

@ -87,7 +87,7 @@ Cypress.Commands.add('apiResetBoards', () => {
Cypress.Commands.add('apiSkipTour', (userID: string) => {
const body: UserConfigPatch = {
updatedFields: {
focalboard_welcomePageViewed: '1',
welcomePageViewed: '1',
[versionProperty]: 'true',
},
}

View File

@ -50,11 +50,9 @@
"PropertyType.CreatedBy": "تم الإنشاء بواسطة",
"PropertyType.Date": "التاريخ",
"PropertyType.Email": "البريد الإلكتروني",
"PropertyType.File": "ملف أو وسائط",
"PropertyType.Number": "رقم",
"PropertyType.Person": "شخص",
"PropertyType.Text": "نص",
"PropertyType.URL": "رابط",
"RegistrationLink.copiedLink": "منسوخ!",
"RegistrationLink.copyLink": "انسخ الرابط",
"ShareBoard.copiedLink": "منسوخ!",

View File

@ -51,14 +51,12 @@
"PropertyType.CreatedTime": "Moment de creació",
"PropertyType.Date": "Data",
"PropertyType.Email": "Correu electrònic",
"PropertyType.File": "Fitxer o elements multimèdia",
"PropertyType.MultiSelect": "Multi selecció",
"PropertyType.Number": "Número",
"PropertyType.Person": "Persona",
"PropertyType.Phone": "Telèfon",
"PropertyType.Select": "Selecciona",
"PropertyType.Text": "Text",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Actualitzat per",
"PropertyType.UpdatedTime": "Moment de actualització",
"RegistrationLink.confirmRegenerateToken": "Això invalidarà enllaços compartits anteriorment. Continuar?",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Verknüpfte Boards umschalten",
"BoardComponent.add-a-group": "+ Hinzufügen einer Gruppe",
"BoardComponent.delete": "Löschen",
"BoardComponent.hidden-columns": "Versteckte Spalten",
@ -136,10 +137,20 @@
"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-empty": "ist leer",
"Filter.is-not-empty": "ist nicht leer",
"Filter.is-not-set": "ist nicht gesetzt",
"Filter.is-set": "ist gesetzt",
"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",
"FindBoardsDialog.IntroText": "Suche nach Boards",
@ -178,16 +189,16 @@
"PropertyType.CreatedTime": "Erstellzeit",
"PropertyType.Date": "Datum",
"PropertyType.Email": "E-Mail",
"PropertyType.File": "Datei oder Medien",
"PropertyType.MultiSelect": "Mehrfachauswahl",
"PropertyType.Number": "Zahl",
"PropertyType.Person": "Person",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Auswählen",
"PropertyType.Text": "Text",
"PropertyType.URL": "URL",
"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!",
@ -230,11 +241,19 @@
"Sidebar.untitled-board": "(Unbenanntes Board)",
"Sidebar.untitled-view": "(Ansicht ohne Titel)",
"SidebarCategories.BlocksMenu.Move": "Bewege nach...",
"SidebarCategories.CategoryMenu.CreateBoard": "Erstelle neues Board",
"SidebarCategories.CategoryMenu.CreateNew": "Erstelle neue Kategorie",
"SidebarCategories.CategoryMenu.Delete": "Lösche Kategorie",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in <b>{categoryName}</b> 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",
"TableComponent.add-icon": "Symbol hinzufügen",
"TableComponent.name": "Name",
"TableComponent.plus-new": "+ Neu",
@ -270,7 +289,7 @@
"View.NewCalendarTitle": "Kalenderansicht",
"View.NewGalleryTitle": "Galerie Ansicht",
"View.NewTableTitle": "Tabellenansicht",
"View.NewTemplateTitle": "Unbenannte Vorlage",
"View.NewTemplateTitle": "Unbenannt",
"View.Table": "Tabelle",
"ViewHeader.add-template": "+ Neue Vorlage",
"ViewHeader.delete-template": "Löschen",
@ -313,9 +332,9 @@
"WelcomePage.NoThanks.Text": "Nein danke, ich werde es selbst herausfinden",
"Workspace.editing-board-template": "Du bearbeitest eine Board Vorlage.",
"boardSelector.confirm-link-board": "Verknüpfe Board mit Kanal",
"boardSelector.confirm-link-board-button": "Verknüpfe Board",
"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. Du kannst die Verknüpfung eine Boards mit einem Kanal jederzeit entfernen.",
"boardSelector.confirm-link-board-subtext-with-other-channel": "Wenn du einen Kanal mit einem Board verknüpfst, werden alle Mitglieder des Kanals (aktuelle und neue) das Board bearbeiten können.{lineBreak}Dieses Board ist mit einem anderen Kanal verknüpft. Die Verknüpfung wird getrennt, wenn du es hier verknüpfst.",
"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.{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",

View File

@ -35,11 +35,9 @@
"PropertyType.CreatedTime": "Χρόνος δημιουργίας",
"PropertyType.Date": "Ημερομηνία",
"PropertyType.Email": "Email",
"PropertyType.File": "Αρχείο ή μέσο",
"PropertyType.Number": "Αριθμός",
"PropertyType.Phone": "Τηλέφωνο",
"PropertyType.Text": "Κείμενο",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Ενημερώθηκε από",
"PropertyType.UpdatedTime": "Ώρα Ενημέρωσης",
"RegistrationLink.copyLink": "Αντιγραφή συνδέσμου",

View File

@ -242,6 +242,7 @@
"Sidebar.untitled-view": "(Untitled View)",
"SidebarCategories.BlocksMenu.Move": "Move To...",
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
"SidebarCategories.CategoryMenu.CreateBoard": "Create New Board",
"SidebarCategories.CategoryMenu.Delete": "Delete Category",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in <b>{categoryName}</b> will move back to the Boards categories. You're not removed from any boards.",
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?",
@ -400,4 +401,4 @@
"tutorial_tip.ok": "Next",
"tutorial_tip.out": "Opt out of these tips.",
"tutorial_tip.seen": "Seen this before?"
}
}

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Toggle Linked Boards",
"BoardComponent.add-a-group": "+ Add a group",
"BoardComponent.delete": "Delete",
"BoardComponent.hidden-columns": "Hidden columns",
@ -19,10 +20,10 @@
"BoardTemplateSelector.add-template": "New template",
"BoardTemplateSelector.create-empty-board": "Create empty board",
"BoardTemplateSelector.delete-template": "Delete",
"BoardTemplateSelector.description": "Choose a template to help you get started. Easily customise the template to fit your needs, or create an empty board to start from scratch.",
"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.{lineBreak} Members of '\\{teamName}'\\ will have access to boards created here.",
"BoardTemplateSelector.plugin.no-content-title": "Create a Board in {teamName}",
"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",
@ -136,10 +137,20 @@
"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-empty": "is empty",
"Filter.is-not-empty": "is not empty",
"Filter.is-not-set": "is not set",
"Filter.is-set": "is set",
"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",
"FindBoardsDialog.IntroText": "Search for boards",
@ -150,6 +161,7 @@
"GroupBy.hideEmptyGroups": "Hide {count} empty groups",
"GroupBy.showHiddenGroups": "Show {count} hidden groups",
"GroupBy.ungroup": "Ungroup",
"HideBoard.MenuOption": "Hide board",
"KanbanCard.untitled": "Untitled",
"Mutator.new-board-from-template": "new board from template",
"Mutator.new-card-from-template": "new card from template",
@ -177,16 +189,16 @@
"PropertyType.CreatedTime": "Time created",
"PropertyType.Date": "Date",
"PropertyType.Email": "Email",
"PropertyType.File": "File or media",
"PropertyType.MultiSelect": "Multi select",
"PropertyType.Number": "Number",
"PropertyType.Person": "Person",
"PropertyType.Phone": "Phone",
"PropertyType.Select": "Select",
"PropertyType.Text": "Text",
"PropertyType.URL": "URL",
"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!",
@ -229,11 +241,19 @@
"Sidebar.untitled-board": "(Untitled Board)",
"Sidebar.untitled-view": "(Untitled View)",
"SidebarCategories.BlocksMenu.Move": "Move To...",
"SidebarCategories.CategoryMenu.CreateBoard": "Create New Board",
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
"SidebarCategories.CategoryMenu.Delete": "Delete Category",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in <b>{categoryName}</b> will move back to the Boards categories. You're not removed from any boards.",
"SidebarCategories.CategoryMenu.DeleteModal.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",
"TableComponent.add-icon": "Add icon",
"TableComponent.name": "Name",
"TableComponent.plus-new": "+ New",
@ -250,9 +270,16 @@
"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.",
"View.AddView": "Add view",
"View.Board": "Board",
"View.DeleteView": "Delete view",
@ -262,7 +289,7 @@
"View.NewCalendarTitle": "Calendar view",
"View.NewGalleryTitle": "Gallery view",
"View.NewTableTitle": "Table view",
"View.NewTemplateTitle": "Untitled Template",
"View.NewTemplateTitle": "Untitled",
"View.Table": "Table",
"ViewHeader.add-template": "New template",
"ViewHeader.delete-template": "Delete",
@ -306,6 +333,7 @@
"Workspace.editing-board-template": "You're editing a board template.",
"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. 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.{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",
@ -358,9 +386,14 @@
"share-board.publish": "Publish",
"share-board.share": "Share",
"shareBoard.channels-select-group": "Channels",
"shareBoard.confirm-link-public-channel": "You're adding a public channel",
"shareBoard.confirm-link-public-channel-button": "Yes, add public channel",
"shareBoard.confirm-link-public-channel-subtext": "Anyone who joins that public channel will now get 'Editor' access to the board. Are you sure you want to proceed?",
"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.",
"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.{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",
"tutorial_tip.finish_tour": "Done",

View File

@ -83,14 +83,12 @@
"PropertyType.CreatedTime": "Hora de creación",
"PropertyType.Date": "Fecha",
"PropertyType.Email": "Email",
"PropertyType.File": "Fichero o Multimedia",
"PropertyType.MultiSelect": "Selección Múltiple",
"PropertyType.Number": "Número",
"PropertyType.Person": "Persona",
"PropertyType.Phone": "Teléfono",
"PropertyType.Select": "Selector",
"PropertyType.Text": "Texto",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Última actualización por",
"PropertyType.UpdatedTime": "Hora de última actualización",
"RegistrationLink.confirmRegenerateToken": "Esto invalidará los enlaces compartidos previos. ¿Continuar?",

View File

@ -75,14 +75,12 @@
"PropertyType.CreatedTime": "Lisamise aeg",
"PropertyType.Date": "Kuupäev",
"PropertyType.Email": "E-post",
"PropertyType.File": "Fail või meedia",
"PropertyType.MultiSelect": "Mitme valimine",
"PropertyType.Number": "Number",
"PropertyType.Person": "Isik",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Vali",
"PropertyType.Text": "Tekst",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Viimati uuendati",
"PropertyType.UpdatedTime": "Viimane uuendamise aeg",
"PropertyValueElement.empty": "Tühi",

View File

@ -177,14 +177,12 @@
"PropertyType.CreatedTime": "Date de création",
"PropertyType.Date": "Date",
"PropertyType.Email": "Adresse e-mail",
"PropertyType.File": "Fichier ou média",
"PropertyType.MultiSelect": "Sélection multiple",
"PropertyType.Number": "Nombre",
"PropertyType.Person": "Personne",
"PropertyType.Phone": "Téléphone",
"PropertyType.Select": "Liste",
"PropertyType.Text": "Texte",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Dernière mise à jour par",
"PropertyType.UpdatedTime": "Date de dernière mise à jour",
"PropertyValueElement.empty": "Vide",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Uklj./Isklj. povezane ploče",
"BoardComponent.add-a-group": "+ Dodaj grupu",
"BoardComponent.delete": "Izbriši",
"BoardComponent.hidden-columns": "Skriveni stupci",
@ -21,8 +22,8 @@
"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.{lineBreak} Članovi tima „{teamName}” imat će pristup ovdje stvorenim pločama.",
"BoardTemplateSelector.plugin.no-content-title": "Stvori ploču u timu {teamName}",
"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",
@ -136,10 +137,20 @@
"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-empty": "je prazno",
"Filter.is-not-empty": "nije prazno",
"Filter.is-not-set": "nije postavljeno",
"Filter.is-set": "je postavljeno",
"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",
"FindBoardsDialog.IntroText": "Traži ploče",
@ -150,6 +161,7 @@
"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",
"Mutator.new-board-from-template": "nova ploča iz predloška",
"Mutator.new-card-from-template": "nova kartica iz predloška",
@ -177,16 +189,16 @@
"PropertyType.CreatedTime": "Vrijeme stvaranja",
"PropertyType.Date": "Datum",
"PropertyType.Email": "E-mail adresa",
"PropertyType.File": "Datoteka ili medij",
"PropertyType.MultiSelect": "Višestruki odabir",
"PropertyType.Number": "Broj",
"PropertyType.Person": "Osoba",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Odaberi",
"PropertyType.Text": "Tekst",
"PropertyType.URL": "URL",
"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!",
@ -229,11 +241,19 @@
"Sidebar.untitled-board": "(Ploča bez naslova)",
"Sidebar.untitled-view": "(Neimenovani prikaz)",
"SidebarCategories.BlocksMenu.Move": "Premjesti u …",
"SidebarCategories.CategoryMenu.CreateBoard": "Stvori novu ploču",
"SidebarCategories.CategoryMenu.CreateNew": "Stvori novu kategoriju",
"SidebarCategories.CategoryMenu.Delete": "Izbriši kategoriju",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Ploče u kategoriji <b>{categoryName}</b> 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",
"TableComponent.add-icon": "Dodaj ikonu",
"TableComponent.name": "Ime",
"TableComponent.plus-new": "+ Novo",
@ -250,9 +270,16 @@
"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.",
"View.AddView": "Dodaj prikaz",
"View.Board": "Ploča",
"View.DeleteView": "Izbriši prikaz",
@ -262,7 +289,7 @@
"View.NewCalendarTitle": "Prikaz kalendara",
"View.NewGalleryTitle": "Prikaz galerije",
"View.NewTableTitle": "Prikaz tablice",
"View.NewTemplateTitle": "Neimenovani predložak",
"View.NewTemplateTitle": "Bez naslova",
"View.Table": "Tablica",
"ViewHeader.add-template": "Novi predložak",
"ViewHeader.delete-template": "Izbriši",
@ -306,7 +333,8 @@
"Workspace.editing-board-template": "Uređuješ predložak ploče.",
"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": "Povezivanje ploče „{boardName}” s ovim kanalom dalo bi svim članovima ovog kanala pristup ploči kao „Urednik”. Stvarmo je želiš povezati?",
"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. 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.{lineBreak}Ova je ploča trenutačno povezana s drugim kanalom. Veza će se prekinuti 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",
@ -358,6 +386,14 @@
"share-board.publish": "Objavi",
"share-board.share": "Dijeli",
"shareBoard.channels-select-group": "Kanali",
"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.",
"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.{lineBreak}Ova je ploča trenutačno povezana s drugim kanalom. Veza će se prekinuti 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, ukoliko im se ne da dozvola 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",
"tutorial_tip.finish_tour": "Gotovo",

View File

@ -177,14 +177,12 @@
"PropertyType.CreatedTime": "Létrehozás ideje",
"PropertyType.Date": "Dátum",
"PropertyType.Email": "E-mail",
"PropertyType.File": "Fájl vagy média",
"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.URL": "URL",
"PropertyType.UpdatedBy": "Utoljára frissítette",
"PropertyType.UpdatedTime": "Utolsó frissítés ideje",
"PropertyValueElement.empty": "Üres",

View File

@ -63,14 +63,12 @@
"PropertyType.CreatedTime": "Waktu dibuat",
"PropertyType.Date": "Tanggal",
"PropertyType.Email": "Surel",
"PropertyType.File": "Berkas atau Media",
"PropertyType.MultiSelect": "Banyak Pilihan",
"PropertyType.Number": "Angka",
"PropertyType.Person": "Orang",
"PropertyType.Phone": "Telepon",
"PropertyType.Select": "Pilihan",
"PropertyType.Text": "Teks",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Diperbarui oleh",
"PropertyType.UpdatedTime": "Waktu diperbarui",
"RegistrationLink.confirmRegenerateToken": "Ini akan membuat tautan yang sebelumnya dibagikan tidak valid. Lanjutkan?",

View File

@ -167,14 +167,12 @@
"PropertyType.CreatedTime": "Orario di creazione",
"PropertyType.Date": "Data",
"PropertyType.Email": "Email",
"PropertyType.File": "File o Media",
"PropertyType.MultiSelect": "Selezione Multipla",
"PropertyType.Number": "Numero",
"PropertyType.Person": "Persona",
"PropertyType.Phone": "Telefono",
"PropertyType.Select": "Seleziona",
"PropertyType.Text": "Testo",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Aggiornato da",
"PropertyType.UpdatedTime": "Ora di aggiornamento",
"PropertyValueElement.empty": "Vuoto",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "リンク先ボードの表示切り替え",
"BoardComponent.add-a-group": "+ グループを追加する",
"BoardComponent.delete": "削除",
"BoardComponent.hidden-columns": "非表示",
@ -19,10 +20,10 @@
"BoardTemplateSelector.add-template": "新しいテンプレート",
"BoardTemplateSelector.create-empty-board": "空のボードを作成する",
"BoardTemplateSelector.delete-template": "削除する",
"BoardTemplateSelector.description": "手軽に始めるにはテンプレートを選択します。ニーズに合わせてテンプレートを簡単にカスタマイズしたり、空のボードを作成してゼロから始めることもできます。",
"BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。",
"BoardTemplateSelector.edit-template": "編集",
"BoardTemplateSelector.plugin.no-content-description": "サイドバーにボードを追加するには、以下のテンプレートを利用するか、空の状態から作成します。{lineBreak} \"{teamName}\"のメンバーは、作成されたボードにアクセスできます。",
"BoardTemplateSelector.plugin.no-content-title": "{teamName} にボードを作成する",
"BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。",
"BoardTemplateSelector.plugin.no-content-title": "ボードを作成する",
"BoardTemplateSelector.title": "ボードを作成する",
"BoardTemplateSelector.use-this-template": "このテンプレートを使う",
"BoardsSwitcher.Title": "ボードを探す",
@ -136,10 +137,20 @@
"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": "削除",
"FindBoardsDialog.IntroText": "ボードを検索",
@ -150,6 +161,7 @@
"GroupBy.hideEmptyGroups": "{count} 個の空のグループを隠す",
"GroupBy.showHiddenGroups": "{count} 個の非表示グループを表示する",
"GroupBy.ungroup": "グループ解除",
"HideBoard.MenuOption": "ボードを隠す",
"KanbanCard.untitled": "無題",
"Mutator.new-board-from-template": "テンプレートからの新しいボード",
"Mutator.new-card-from-template": "テンプレートから新しいカードを作成",
@ -177,16 +189,16 @@
"PropertyType.CreatedTime": "作成日時",
"PropertyType.Date": "日付",
"PropertyType.Email": "メールアドレス",
"PropertyType.File": "ファイルまたはメディア",
"PropertyType.MultiSelect": "マルチセレクト",
"PropertyType.Number": "数字",
"PropertyType.Person": "人物",
"PropertyType.Phone": "電話番号",
"PropertyType.Select": "セレクト",
"PropertyType.Text": "テキスト",
"PropertyType.URL": "URL",
"PropertyType.Unknown": "不明",
"PropertyType.UpdatedBy": "更新者",
"PropertyType.UpdatedTime": "更新日時",
"PropertyType.Url": "URL",
"PropertyValueElement.empty": "空",
"RegistrationLink.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?",
"RegistrationLink.copiedLink": "コピーしました!",
@ -203,7 +215,7 @@
"ShareBoard.copiedLink": "コピーしました!",
"ShareBoard.copyLink": "リンクをコピーする",
"ShareBoard.regenerate": "トークンを再生成する",
"ShareBoard.searchPlaceholder": "人を検索",
"ShareBoard.searchPlaceholder": "人とチャンネルを検索",
"ShareBoard.teamPermissionsText": "{teamName}チームの全員",
"ShareBoard.tokenRegenrated": "トークンが再生成されました",
"ShareBoard.userPermissionsRemoveMemberText": "メンバーを削除する",
@ -229,11 +241,19 @@
"Sidebar.untitled-board": "(無題のボード)",
"Sidebar.untitled-view": "(無題のビュー)",
"SidebarCategories.BlocksMenu.Move": "移動...",
"SidebarCategories.CategoryMenu.CreateBoard": "新規ボード作成",
"SidebarCategories.CategoryMenu.CreateNew": "新しいカテゴリを作成する",
"SidebarCategories.CategoryMenu.Delete": "カテゴリを削除する",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> にあるボードは、Boards カテゴリに戻されます。どのボードからも削除されることはありません。",
"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": "サイドバーカテゴリー",
"TableComponent.add-icon": "アイコンを追加する",
"TableComponent.name": "名前",
"TableComponent.plus-new": "+ 新規",
@ -250,9 +270,16 @@
"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": "ビューを削除",
@ -262,7 +289,7 @@
"View.NewCalendarTitle": "カレンダー表示",
"View.NewGalleryTitle": "ギャラリービュー",
"View.NewTableTitle": "テーブル表示",
"View.NewTemplateTitle": "無題のテンプレート",
"View.NewTemplateTitle": "無題",
"View.Table": "テーブル",
"ViewHeader.add-template": "新しいテンプレート",
"ViewHeader.delete-template": "削除",
@ -306,7 +333,8 @@
"Workspace.editing-board-template": "ボードのテンプレートを編集しています。",
"boardSelector.confirm-link-board": "ボードをチャンネルへリンク",
"boardSelector.confirm-link-board-button": "はい、ボードをリンクします",
"boardSelector.confirm-link-board-subtext": "\"{boardName}\" ボードをこのチャンネルにリンクすると、このチャンネルのメンバー全員にボードへの \"編集者\" アクセスを与えます。本当にリンクしますか?",
"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": "ボードを検索",
@ -358,9 +386,14 @@
"share-board.publish": "公開",
"share-board.share": "共有",
"shareBoard.channels-select-group": "Channels",
"shareBoard.confirm-link-public-channel": "公開チャンネルを追加しています",
"shareBoard.confirm-link-public-channel-button": "はい、公開チャンネルを追加します",
"shareBoard.confirm-link-public-channel-subtext": "公開チャンネルに参加したメンバー全員がボードへの \"編集者\" アクセスを持つことになりますが、本当に実行しますか?",
"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": "ボードには少なくとも1名の管理者が必要です",
"shareBoard.members-select-group": "メンバー",
"tutorial_tip.finish_tour": "完了",

View File

@ -110,14 +110,12 @@
"PropertyType.CreatedTime": "Жасалған уақыты",
"PropertyType.Date": "Даты",
"PropertyType.Email": "Email",
"PropertyType.File": "Файыл немесе Медиа",
"PropertyType.MultiSelect": "Multi таңдау",
"PropertyType.Number": "Нөмір",
"PropertyType.Person": "Тұлға",
"PropertyType.Phone": "Телефон",
"PropertyType.Select": "Таңдау",
"PropertyType.Text": "Мәтін",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Соңғы өзгерткен",
"PropertyType.UpdatedTime": "Соңғы өзгертілген уақыты",
"PropertyValueElement.empty": "Бос",

View File

@ -134,14 +134,12 @@
"PropertyType.CreatedTime": "생성 시간",
"PropertyType.Date": "날짜",
"PropertyType.Email": "전자우편",
"PropertyType.File": "파일 혹은 미디어",
"PropertyType.MultiSelect": "다중 선택",
"PropertyType.Number": "숫자",
"PropertyType.Person": "사람",
"PropertyType.Phone": "전화번호",
"PropertyType.Select": "선택",
"PropertyType.Text": "텍스트",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "최근 수정한 사람",
"PropertyType.UpdatedTime": "최근 수정 시간",
"PropertyValueElement.empty": "비어있음",

View File

@ -19,8 +19,8 @@
"BoardTemplateSelector.delete-template": "ഇല്ലാതാക്കുക",
"BoardTemplateSelector.description": "ആരംഭിക്കാൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ടെംപ്ലേറ്റ് തിരഞ്ഞെടുക്കുക. നിങ്ങളുടെ ആവശ്യങ്ങൾക്ക് അനുയോജ്യമായ രീതിയിൽ ടെംപ്ലേറ്റ് എളുപ്പത്തിൽ ഇച്ഛാനുസൃതമാക്കുക, അല്ലെങ്കിൽ ആദ്യം മുതൽ ആരംഭിക്കാൻ ഒരു ശൂന്യമായ ബോർഡ് സൃഷ്ടിക്കുക.",
"BoardTemplateSelector.edit-template": "എഡിറ്റ് ചെയ്യുക",
"BoardTemplateSelector.plugin.no-content-description": "താഴെ നിർവചിച്ചിരിക്കുന്ന ഏതെങ്കിലും ടെംപ്ലേറ്റുകൾ ഉപയോഗിച്ച് സൈഡ്ബാറിലേക്ക് ഒരു ബോർഡ് ചേർക്കുക അല്ലെങ്കിൽ ആദ്യം മുതൽ ആരംഭിക്കുക.{lineBreak} \"{teamName}\" അംഗങ്ങൾക്ക് ഇവിടെ സൃഷ്‌ടിച്ച ബോർഡുകളിലേക്ക് ആക്‌സസ് ഉണ്ടായിരിക്കും.",
"BoardTemplateSelector.plugin.no-content-title": "{teamName} എന്നതിൽ ഒരു ബോർഡ് സൃഷ്‌ടിക്കുക",
"BoardTemplateSelector.plugin.no-content-description": "ചുവടെ നിർവചിച്ചിരിക്കുന്ന ഏതെങ്കിലും ടെംപ്ലേറ്റുകൾ ഉപയോഗിച്ച് സൈഡ്ബാറിലേക്ക് ഒരു ബോർഡ് ചേർക്കുക അല്ലെങ്കിൽ ആദ്യം മുതൽ ആരംഭിക്കുക.",
"BoardTemplateSelector.plugin.no-content-title": "ഒരു ബോർഡ് ഉണ്ടാക്കുക",
"BoardTemplateSelector.title": "ഒരു ബോർഡ് ഉണ്ടാക്കുക",
"BoardTemplateSelector.use-this-template": "ഈ ടെംപ്ലേറ്റ് ഉപയോഗിക്കുക",
"BoardsSwitcher.Title": "ബോർഡുകൾ കണ്ടെത്തുക",
@ -60,6 +60,10 @@
"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": "ഈ കാർഡിന് ഒരു വിവരണമുണ്ട്",
@ -69,6 +73,8 @@
"CardDetail.add-icon": "ഐക്കൺ ചേർക്കുക",
"CardDetail.add-property": "+ ഒരു വിശേഷണം ചേർക്കുക",
"CardDetail.addCardText": "കാർഡിൽ വാക്യം ചേർക്കുക",
"CardDetail.limited-body": "ആർക്കൈവ് ചെയ്‌ത കാർഡുകൾ കാണുന്നതിനും ഓരോ ബോർഡുകൾക്കും പരിധിയില്ലാത്ത കാഴ്‌ചകൾ നേടുന്നതിനും പരിധിയില്ലാത്ത കാർഡുകൾക്കും മറ്റും ഞങ്ങളുടെ പ്രൊഫഷണൽ അല്ലെങ്കിൽ എന്റർപ്രൈസ് പ്ലാനിലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക.",
"CardDetail.limited-button": "അപ്ഗ്രേഡ്",
"CardDetail.moveContent": "കാർഡ് ഉള്ളടക്കം നീക്കുക",
"CardDetail.new-comment-placeholder": "ഒരു അഭിപ്രായം ചേർക്കുക...",
"CardDetailProperty.confirm-delete-heading": "പ്രോപ്പർട്ടി ഇല്ലാതാക്കുന്നത് സ്ഥിരീകരിക്കുക",
@ -159,14 +165,12 @@
"PropertyType.CreatedTime": "സൃഷ്ടിച്ച സമയം",
"PropertyType.Date": "തീയതി",
"PropertyType.Email": "ഇമെയിൽ",
"PropertyType.File": "ഫയൽ അല്ലെങ്കിൽ മീഡിയ",
"PropertyType.MultiSelect": "മൾട്ടി സെലക്ട്",
"PropertyType.Number": "നമ്പർ",
"PropertyType.Person": "വ്യക്തി",
"PropertyType.Phone": "ഫോൺ",
"PropertyType.Select": "തിരഞ്ഞെടുക്കുക",
"PropertyType.Text": "വാചകം",
"PropertyType.URL": "യുആർഎൽ",
"PropertyType.UpdatedBy": "അവസാനം അപ്ഡേറ്റ് ചെയ്തത്",
"PropertyType.UpdatedTime": "അവസാനം പുതുക്കിയ സമയം",
"PropertyValueElement.empty": "ശൂന്യം",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Gekoppelde borden weergeven",
"BoardComponent.add-a-group": "+ Een groep toevoegen",
"BoardComponent.delete": "Verwijderen",
"BoardComponent.hidden-columns": "Verborgen kolommen",
@ -136,10 +137,20 @@
"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-empty": "is leeg",
"Filter.is-not-empty": "is niet leeg",
"Filter.is-not-set": "is niet ingesteld",
"Filter.is-set": "is ingesteld",
"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",
"FindBoardsDialog.IntroText": "Zoeken naar borden",
@ -178,16 +189,16 @@
"PropertyType.CreatedTime": "Aangemaakt op",
"PropertyType.Date": "Datum",
"PropertyType.Email": "E-mail",
"PropertyType.File": "Bestand of media",
"PropertyType.MultiSelect": "Multiselect",
"PropertyType.Number": "Nummer",
"PropertyType.Person": "Persoon",
"PropertyType.Phone": "Telefoon",
"PropertyType.Select": "Selecteer",
"PropertyType.Text": "Tekst",
"PropertyType.URL": "URL",
"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!",
@ -230,11 +241,15 @@
"Sidebar.untitled-board": "(Titelloze bord )",
"Sidebar.untitled-view": "(Naamloze weergave)",
"SidebarCategories.BlocksMenu.Move": "Verplaatsen naar...",
"SidebarCategories.CategoryMenu.CreateBoard": "Maak een Board",
"SidebarCategories.CategoryMenu.CreateNew": "Maak een nieuwe categorie",
"SidebarCategories.CategoryMenu.Delete": "Categorie verwijderen",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Borden in <b>{categoryName}</b> 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.",
"TableComponent.add-icon": "Pictogram toevoegen",
"TableComponent.name": "Naam",
"TableComponent.plus-new": "+ Nieuw",
@ -366,7 +381,6 @@
"share-board.publish": "Publiceren",
"share-board.share": "Delen",
"shareBoard.channels-select-group": "Kanalen",
"shareBoard.confirm-link-public-channel-button": "Ja, voeg publiek kanaal toe",
"shareBoard.lastAdmin": "Besturen moeten ten minste één beheerder hebben",
"shareBoard.members-select-group": "Leden",
"tutorial_tip.finish_tour": "Klaar",

View File

@ -60,14 +60,12 @@
"PropertyType.CreatedTime": "Data de creacion",
"PropertyType.Date": "Data",
"PropertyType.Email": "Adreça e-mail",
"PropertyType.File": "Fichièr o mèdia",
"PropertyType.MultiSelect": "Seleccion multipla",
"PropertyType.Number": "Nombre",
"PropertyType.Person": "Persona",
"PropertyType.Phone": "Telefòn",
"PropertyType.Select": "Lista",
"PropertyType.Text": "Tèxt",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Darrièra actualizacion per",
"PropertyType.UpdatedTime": "Data de darrièra actualizacion",
"PropertyValueElement.empty": "Void",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Przełączanie Podlinkowanych Tablic",
"BoardComponent.add-a-group": "+ Dodaj grupę",
"BoardComponent.delete": "Usuń",
"BoardComponent.hidden-columns": "Ukryte kolumny",
@ -21,8 +22,8 @@
"BoardTemplateSelector.delete-template": "Usuń",
"BoardTemplateSelector.description": "Dodaj tablicę do paska bocznego, używając dowolnego z szablonów zdefiniowanych poniżej lub zacznij od zera.",
"BoardTemplateSelector.edit-template": "Edycja",
"BoardTemplateSelector.plugin.no-content-description": "Dodaj tablicę do paska bocznego używając jednego z szablonów zdefiniowanych poniżej lub zacznij od zera.{lineBreak} Członkowie \"{teamName}\" będą mieli dostęp do tablic utworzonych tutaj.",
"BoardTemplateSelector.plugin.no-content-title": "Utwórz tablicę w {teamName}",
"BoardTemplateSelector.plugin.no-content-description": "Dodaj tablicę do paska bocznego używając jednego z szablonów zdefiniowanych poniżej lub zacznij od zera.",
"BoardTemplateSelector.plugin.no-content-title": "Utwórz tablicę",
"BoardTemplateSelector.title": "Utwórz tablicę",
"BoardTemplateSelector.use-this-template": "Użyj tego szablonu",
"BoardsSwitcher.Title": "Znajdź Tablice",
@ -136,10 +137,20 @@
"EditableDayPicker.today": "Dzisiaj",
"Error.mobileweb": "Obsługa mobilnej strony internetowej jest obecnie we wczesnej wersji beta. Nie wszystkie funkcje mogą być dostępne.",
"Error.websocket-closed": "Połączenie websocket zamknięte, połączenie przerwane. Jeśli problem się powtarza, sprawdź konfigurację serwera lub serwera proxy.",
"Filter.contains": "zawiera",
"Filter.ends-with": "kończy się na",
"Filter.includes": "zawiera",
"Filter.is": "jest",
"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.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 filtrujący",
"FilterComponent.add-filter": "+ Dodaj filtr",
"FilterComponent.delete": "Usuń",
"FindBoardsDialog.IntroText": "Wyszukiwanie tablic",
@ -150,6 +161,7 @@
"GroupBy.hideEmptyGroups": "Ukryj {count} pustych grup",
"GroupBy.showHiddenGroups": "Pokaż {count} ukrytych grup",
"GroupBy.ungroup": "Rozgrupuj",
"HideBoard.MenuOption": "Ukryj tablicę",
"KanbanCard.untitled": "Bez tytułu",
"Mutator.new-board-from-template": "nowa tablica z szablonu",
"Mutator.new-card-from-template": "nowa karta z szablonu",
@ -177,16 +189,16 @@
"PropertyType.CreatedTime": "Czas utworzenia",
"PropertyType.Date": "Data",
"PropertyType.Email": "Email",
"PropertyType.File": "Plik lub media",
"PropertyType.MultiSelect": "Multiwybór",
"PropertyType.Number": "Numer",
"PropertyType.Person": "Osoba",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Wybierz",
"PropertyType.Text": "Tekst",
"PropertyType.URL": "URL",
"PropertyType.Unknown": "Nieznany",
"PropertyType.UpdatedBy": "Ostatnio zaktualizowane przez",
"PropertyType.UpdatedTime": "Czas ostatniej aktualizacji",
"PropertyType.Url": "URL",
"PropertyValueElement.empty": "Puste",
"RegistrationLink.confirmRegenerateToken": "Spowoduje to unieważnienie wcześniej udostępnionych linków. Kontynuować?",
"RegistrationLink.copiedLink": "Skopiowane!",
@ -229,11 +241,19 @@
"Sidebar.untitled-board": "(Tablica bez tytułu)",
"Sidebar.untitled-view": "(Widok bez Tytułu)",
"SidebarCategories.BlocksMenu.Move": "Przenieś Do...",
"SidebarCategories.CategoryMenu.CreateBoard": "Utwórz Nową Tablicę",
"SidebarCategories.CategoryMenu.CreateNew": "Utwórz Nową Kategorię",
"SidebarCategories.CategoryMenu.Delete": "Usuń Kategorię",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Tablice w <b>{categoryName}</b> 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",
"TableComponent.add-icon": "Dodaj Ikonę",
"TableComponent.name": "Nazwa",
"TableComponent.plus-new": "+ Nowy",
@ -250,9 +270,16 @@
"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.",
"View.AddView": "Dodaj widok",
"View.Board": "Tablica",
"View.DeleteView": "Usuń widok",
@ -262,7 +289,7 @@
"View.NewCalendarTitle": "Widok Kalendarza",
"View.NewGalleryTitle": "Widok galerii",
"View.NewTableTitle": "Widok tabeli",
"View.NewTemplateTitle": "Szablon bez tytułu",
"View.NewTemplateTitle": "Bez tytułu",
"View.Table": "Tabela",
"ViewHeader.add-template": "Nowy szablon",
"ViewHeader.delete-template": "Usuń",
@ -306,7 +333,8 @@
"Workspace.editing-board-template": "Edytujesz szablon tablicy.",
"boardSelector.confirm-link-board": "Połączenie tablicy z kanałem",
"boardSelector.confirm-link-board-button": "Tak, podlinkuj tablicę",
"boardSelector.confirm-link-board-subtext": "Powiązanie \"{boardName}\" tablicy z tym kanałem dałoby wszystkim członkom tego kanału \"Editor\" dostęp do tablicy. Czy na pewno chcesz to połączyć?",
"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ć. Możesz odłączyć forum od kanału w dowolnym momencie.",
"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ć.{lineBreak} Ta tablica jest obecnie połączona z innym kanałem. Zostanie ona odłączona, jeśli zdecydujesz się podłączyć ją tutaj.",
"boardSelector.create-a-board": "Utwórz tablicę",
"boardSelector.link": "Link",
"boardSelector.search-for-boards": "Wyszukiwanie tablic",
@ -358,7 +386,12 @@
"share-board.publish": "Opublikuj",
"share-board.share": "Udostępnij",
"shareBoard.channels-select-group": "Kanały",
"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. {lineBreak} Czy na pewno chcesz go odłączyć?",
"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": "Gdy połączysz kanał z tablicą, wszyscy członkowie kanału (istniejący i nowi) będą mogli go edytować.",
"shareBoard.confirm-link-channel-subtext-with-other-channel": "Kiedy połączysz kanał z tablicą, wszyscy członkowie tego kanału (istniejący i nowi) będą mogli go edytować.{lineBreak}Ta tablica jest obecnie połączona z innym kanałem. Zostanie ona usunięta, jeśli zdecydujesz się podłą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",

View File

@ -1,6 +1,6 @@
{
"BoardComponent.add-a-group": "+ Adicione um grupo",
"BoardComponent.delete": "Deletar",
"BoardComponent.delete": "Excluir",
"BoardComponent.hidden-columns": "Colunas escondidas",
"BoardComponent.hide": "Esconder",
"BoardComponent.new": "Novo",
@ -13,13 +13,13 @@
"CardDetail.addCardText": "adicionar texto ao card",
"CardDetail.moveContent": "mover conteúdo do card",
"CardDetail.new-comment-placeholder": "Adicionar um comentário...",
"CardDetailProperty.confirm-delete-subtext": "Tem certeza que quer deletar a propriedade \"{propertyName}\"? Deletando-a excluirá a propriedade de todos os cards nessa board.",
"CardDetailProperty.confirm-delete-subtext": "Tem certeza que quer excluir a propriedade \"{propertyName}\"? Deletando-a excluirá a propriedade de todos os cards nessa board.",
"CardDialog.editing-template": "Você está editando um template",
"CardDialog.nocard": "Esse card não existe ou não está acessível",
"Comment.delete": "Deletar",
"Comment.delete": "Excluir",
"CommentsList.send": "Enviar",
"ContentBlock.Delete": "Deletar",
"ContentBlock.DeleteAction": "deletar",
"ContentBlock.Delete": "Excluir",
"ContentBlock.DeleteAction": "Excluir",
"ContentBlock.addElement": "adicionar {type}",
"ContentBlock.checkbox": "caixa de seleção",
"ContentBlock.divider": "Divisor",
@ -32,20 +32,38 @@
"ContentBlock.moveDown": "Mover para baixo",
"ContentBlock.moveUp": "Mover para cima",
"ContentBlock.text": "texto",
"DeleteBoardDialog.confirm-delete": "Excluir",
"Dialog.closeDialog": "Fechar diálogo",
"EditableDayPicker.today": "Hoje",
"Error.websocket-closed": "Conexão Websocket fechada, conexão interrompida. Se isso persistir, verifique a configuração do seu servidor ou proxy da web.",
"Filter.contains": "contém",
"Filter.ends-with": "termina com",
"Filter.includes": "Inclui",
"Filter.is": "é",
"Filter.is-empty": "está vazio",
"Filter.is-not-empty": "Não está vazio",
"Filter.not-contains": "não contém",
"Filter.not-includes": "Não inclui",
"FilterByText.placeholder": "filtrar texto",
"FilterComponent.add-filter": "+ Adicionar filtro",
"FilterComponent.delete": "Deletar",
"FilterComponent.delete": "Excluir",
"FindBoardsDialog.IntroText": "Procurar por quadros",
"FindBoardsDialog.NoResultsFor": "Sem resultado para \"{searchQuery}\"",
"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",
"Mutator.new-card-from-template": "novo card à partir de um template",
"Mutator.new-template-from-card": "novo template à partir de um card",
"PropertyMenu.Delete": "Deletar",
"OnboardingTour.AddComments.Title": "Adicionar comentários",
"OnboardingTour.AddDescription.Title": "Adicionar descrição",
"OnboardingTour.AddProperties.Title": "Adicionar propriedades",
"OnboardingTour.AddView.Title": "Adicionar nova visualização",
"OnboardingTour.CopyLink.Title": "Copiar link",
"OnboardingTour.ShareBoard.Title": "Compartilhar quadro",
"PropertyMenu.Delete": "Excluir",
"PropertyMenu.changeType": "Alterar tipo da propriedade",
"PropertyMenu.typeTitle": "Tipo",
"PropertyType.Checkbox": "Caixa de seleção",
@ -53,43 +71,56 @@
"PropertyType.CreatedTime": "Horário da criação",
"PropertyType.Date": "Data",
"PropertyType.Email": "Email",
"PropertyType.File": "Arquivo ou Mídia",
"PropertyType.MultiSelect": "Seleção Múltipla",
"PropertyType.Number": "Número",
"PropertyType.Person": "Pessoa",
"PropertyType.Phone": "Telefone",
"PropertyType.Select": "Selcionar",
"PropertyType.Text": "Texto",
"PropertyType.URL": "URL",
"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.ShareInternal": "Compartilhar internamente",
"ShareBoard.Title": "Compartilhar Quadro",
"ShareBoard.confirmRegenerateToken": "Isso vai invalidar links compartilhados anteriormente. Continuar?",
"ShareBoard.copiedLink": "Copiado!",
"ShareBoard.copyLink": "Copiar link",
"ShareBoard.teamPermissionsText": "Todos no time {teamName}",
"ShareBoard.tokenRegenrated": "Token gerado novamente",
"ShareBoard.userPermissionsRemoveMemberText": "Remover membro",
"ShareBoard.userPermissionsYouText": "(Você)",
"Sidebar.about": "Sobre o Focalboard",
"Sidebar.add-board": "+ Adicionar Quadro",
"Sidebar.add-board": "+ Adicionar quadro",
"Sidebar.changePassword": "Mudar senha",
"Sidebar.delete-board": "Deletar quadro",
"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.invite-users": "Convidar usuários",
"Sidebar.logout": "Sair",
"Sidebar.random-icons": "Ícones aleatórios",
"Sidebar.set-language": "Definir linguagem",
"Sidebar.set-theme": "Definir tema",
"Sidebar.settings": "Configurações",
"Sidebar.untitled-board": "(Quadro sem nome)",
"SidebarCategories.BlocksMenu.Move": "Mover Para...",
"SidebarCategories.CategoryMenu.CreateBoard": "Criar Novo Quadro",
"SidebarCategories.CategoryMenu.CreateNew": "Criar Nova Categoria",
"SidebarCategories.CategoryMenu.Delete": "Excluir Categoria",
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Excluir esta categoria?",
"TableComponent.add-icon": "Adicionar Ícone",
"TableComponent.name": "Nome",
"TableComponent.plus-new": "+ Novo",
"TableHeaderMenu.delete": "Deletar",
"TableHeaderMenu.delete": "Excluir",
"TableHeaderMenu.duplicate": "Duplicar",
"TableHeaderMenu.hide": "Esconder",
"TableHeaderMenu.insert-left": "Inserir à esquerda",
@ -97,16 +128,16 @@
"TableHeaderMenu.sort-ascending": "Ordem ascendente",
"TableHeaderMenu.sort-descending": "Ordem descendente",
"TableRow.open": "Abrir",
"View.AddView": "Adicionar Vista",
"View.AddView": "Adicionar visualização",
"View.Board": "Quadro",
"View.DeleteView": "Deletar Vista",
"View.DuplicateView": "Duplicar Vista",
"View.NewBoardTitle": "Vista de Quadro",
"View.NewGalleryTitle": "Vista de Galeria",
"View.NewTableTitle": "Vista de Tabela",
"View.DeleteView": "Excluir visualização",
"View.DuplicateView": "Duplicar visualização",
"View.NewBoardTitle": "Visualização de Quadro",
"View.NewGalleryTitle": "Visualização de Galeria",
"View.NewTableTitle": "Visualização de Tabela",
"View.Table": "Tabela",
"ViewHeader.add-template": "+ Novo modelo",
"ViewHeader.delete-template": "Deletar",
"ViewHeader.delete-template": "Excluir",
"ViewHeader.edit-template": "Editar",
"ViewHeader.empty-card": "Card vazio",
"ViewHeader.export-board-archive": "Exportar arquivo do painel",

View File

@ -177,14 +177,12 @@
"PropertyType.CreatedTime": "Время создания",
"PropertyType.Date": "Дата",
"PropertyType.Email": "Email",
"PropertyType.File": "Файл или носитель",
"PropertyType.MultiSelect": "Многократный выбор",
"PropertyType.Number": "Номер",
"PropertyType.Person": "Персона",
"PropertyType.Phone": "Телефон",
"PropertyType.Select": "Выбрать",
"PropertyType.Text": "Текст",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Обновлено пользователем",
"PropertyType.UpdatedTime": "Время обновления",
"PropertyValueElement.empty": "Пустой",

View File

@ -110,14 +110,12 @@
"PropertyType.CreatedTime": "Vytvorené",
"PropertyType.Date": "Dátum",
"PropertyType.Email": "Email",
"PropertyType.File": "Súbor alebo médium",
"PropertyType.MultiSelect": "Viacnásobný výber",
"PropertyType.Number": "číslo",
"PropertyType.Person": "Osoba",
"PropertyType.Phone": "Telefón",
"PropertyType.Select": "Vyber",
"PropertyType.Text": "Text",
"PropertyType.URL": "URL",
"PropertyType.UpdatedBy": "Naposledy upravil",
"PropertyType.UpdatedTime": "Posledná úprava",
"PropertyValueElement.empty": "Prázdny",

View File

@ -13,15 +13,16 @@
"BoardMember.schemeNone": "Inget",
"BoardMember.schemeViewer": "Granskare",
"BoardMember.schemeViwer": "Åskådare",
"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": "Ny mall",
"BoardTemplateSelector.create-empty-board": "Skapa en tom board",
"BoardTemplateSelector.delete-template": "Ta bort",
"BoardTemplateSelector.description": "Välj en mall som hjälper dig att komma igång. Du kan enkelt anpassa mallen efter dina behov eller skapa ett tomt Board för att börja från början.",
"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.{lineBreak} Medlemmar i \"{teamName}\" kommer ha åtkomst till Board som skapas här.",
"BoardTemplateSelector.plugin.no-content-title": "Skapa en Board i {teamName}",
"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",
@ -62,6 +63,11 @@
"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",
@ -136,6 +142,7 @@
"Filter.not-includes": "inkluderar inte",
"FilterComponent.add-filter": "+ Lägg till filter",
"FilterComponent.delete": "Radera",
"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 <b>UPP/NER</b> för att bläddra. <b>RETUR</b> för att välja, <b>ESC</b> för att avbryta",
@ -143,6 +150,7 @@
"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",
"Mutator.new-board-from-template": "ny board från en mall",
"Mutator.new-card-from-template": "nytt kort från mall",
@ -170,14 +178,12 @@
"PropertyType.CreatedTime": "Skapad tid",
"PropertyType.Date": "Datum",
"PropertyType.Email": "Email",
"PropertyType.File": "Fil eller media",
"PropertyType.MultiSelect": "Flervalsalternativ",
"PropertyType.Number": "Tal",
"PropertyType.Person": "Person",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Alternativ",
"PropertyType.Text": "Text",
"PropertyType.URL": "Hyperlänk",
"PropertyType.UpdatedBy": "Senast ändrad av",
"PropertyType.UpdatedTime": "Senast uppdaterad",
"PropertyValueElement.empty": "Tom",
@ -196,6 +202,7 @@
"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",
@ -242,9 +249,16 @@
"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.",
"View.AddView": "Lägg till vy",
"View.Board": "Tavla",
"View.DeleteView": "Radera vy",
@ -254,7 +268,7 @@
"View.NewCalendarTitle": "Kalendervy",
"View.NewGalleryTitle": "Galleri vy",
"View.NewTableTitle": "Tabellvy",
"View.NewTemplateTitle": "Namnlös mall",
"View.NewTemplateTitle": "Namnlös",
"View.Table": "Tabell",
"ViewHeader.add-template": "Ny mall",
"ViewHeader.delete-template": "Radera",
@ -284,6 +298,7 @@
"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 för att få obegränsat antal visningar per board, obegränsat antal kort och mycket mer.",
"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",
@ -295,6 +310,13 @@
"WelcomePage.Heading": "Välkommen till Boards",
"WelcomePage.NoThanks.Text": "Nej tack, jag löser det själv",
"Workspace.editing-board-template": "Du redigerar en tavelmall.",
"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. 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.{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",
"calendar.month": "Månad",
"calendar.today": "IDAG",
"calendar.week": "Vecka",
@ -313,15 +335,15 @@
"error.unknown": "Ett fel inträffade.",
"generic.previous": "Föregående",
"imagePaste.upload-failed": "Vissa filer har inte laddats upp. Gränsen för filstorlek har nåtts",
"limitedCard.title": "Korten doldes",
"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",
"notification-box-card-limit-reached.close-tooltip": "Sov i 10 dagar",
"notification-box-card-limit-reached.link": "uppgradera till ett betal-abonnemang",
"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": "Din åtgärd dolde ett annat kort",
"notification-box.card-limit-reached.not-admin.text": "Om du vill komma åt arkiverade kort kontaktar du din administratör för att uppgradera till ett betal-abonnemang.",
"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}",
"register.login-button": "eller logga in om du redan har ett konto",
"register.signup-title": "Registrera dig för ett konto",

View File

@ -1,4 +1,5 @@
{
"AppBar.Tooltip": "Bağlantılı panoları aç/kapat",
"BoardComponent.add-a-group": "+ Grup ekle",
"BoardComponent.delete": "Sil",
"BoardComponent.hidden-columns": "Gizli sütunlar",
@ -12,7 +13,7 @@
"BoardMember.schemeEditor": "Düzenleyici",
"BoardMember.schemeNone": "Yok",
"BoardMember.schemeViewer": "Görüntüleyici",
"BoardMember.schemeViwer": "İzleyici",
"BoardMember.schemeViwer": "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.",
@ -63,7 +64,7 @@
"Calculations.Options.range.label": "Aralık",
"Calculations.Options.sum.displayName": "Toplam",
"Calculations.Options.sum.label": "Toplam",
"CalendarCard.untitled": "Başlıksız",
"CalendarCard.untitled": "Adlandırılmamış",
"CardActionsMenu.copiedLink": "Kopyalandı!",
"CardActionsMenu.copyLink": "Bağlantıyı kopyala",
"CardActionsMenu.delete": "Sil",
@ -75,10 +76,10 @@
"CardDetail.Following": "İzleniyor",
"CardDetail.add-content": "İçerik ekle",
"CardDetail.add-icon": "Simge ekle",
"CardDetail.add-property": "+ Alan ekle",
"CardDetail.add-property": "+ Bir özellik ekle",
"CardDetail.addCardText": "kart metni ekle",
"CardDetail.limited-body": "Arşivlenmiş kartları görüntülemek, her pano için sınırsız görüntüleme, sınırsız sayıda kart gibi daha fazla özelliğe sahip olmak için Professional ya da Enterprise tarifesine yükseltin.",
"CardDetail.limited-button": "Yükselt",
"CardDetail.limited-body": "Arşivlenmiş kartları görüntülemek, her pano için sınırsız görüntüleme, sınırsız sayıda kart gibi daha fazla özelliğe sahip olmak için 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...",
@ -136,10 +137,20 @@
"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-empty": "boş olan",
"Filter.is-not-empty": "boş olmayan",
"Filter.is-not-set": "şuna ayarlanmamış olan",
"Filter.is-set": "şuna ayarlanmış olan",
"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",
"FindBoardsDialog.IntroText": "Pano arama",
@ -151,7 +162,7 @@
"GroupBy.showHiddenGroups": "{count} gizli grubu görüntüle",
"GroupBy.ungroup": "Gruplamayı kaldır",
"HideBoard.MenuOption": "Panoyu gizle",
"KanbanCard.untitled": "Başlıksız",
"KanbanCard.untitled": "Adlandırılmamış",
"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",
@ -178,16 +189,16 @@
"PropertyType.CreatedTime": "Oluşturulma zamanı",
"PropertyType.Date": "Tarih",
"PropertyType.Email": "E-posta",
"PropertyType.File": "Dosya ya da ortam",
"PropertyType.MultiSelect": "Çoklu seçim",
"PropertyType.Number": "Sayı",
"PropertyType.Person": "Kişi",
"PropertyType.Phone": "Telefon",
"PropertyType.Select": "Seçin",
"PropertyType.Text": "Metin",
"PropertyType.URL": "Adres",
"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. Devam etmek istiyor musunuz?",
"RegistrationLink.copiedLink": "Kopyalandı!",
@ -227,14 +238,22 @@
"Sidebar.set-theme": "Tema ayarla",
"Sidebar.settings": "Ayarlar",
"Sidebar.template-from-board": "Panodan yeni kalıp",
"Sidebar.untitled-board": "(Başlıksız pano)",
"Sidebar.untitled-view": "(Adsız görünüm)",
"Sidebar.untitled-board": "(Adlandırılmamış pano)",
"Sidebar.untitled-view": "(Adlandırılmamış görünüm)",
"SidebarCategories.BlocksMenu.Move": "Şuraya taşı...",
"SidebarCategories.CategoryMenu.CreateBoard": "Yeni pano ekle",
"SidebarCategories.CategoryMenu.CreateNew": "Yeni kategori ekle",
"SidebarCategories.CategoryMenu.Delete": "Kategoriyi sił",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> İç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",
"TableComponent.add-icon": "Simge ekle",
"TableComponent.name": "Ad",
"TableComponent.plus-new": "+ Yeni",
@ -270,7 +289,7 @@
"View.NewCalendarTitle": "Takvim görünümü",
"View.NewGalleryTitle": "Galeri görünümü",
"View.NewTableTitle": "Tablo görünümü",
"View.NewTemplateTitle": "Başlıksız kalıp",
"View.NewTemplateTitle": "Adlandırılmamış",
"View.Table": "Tablo",
"ViewHeader.add-template": "Yeni kalıp",
"ViewHeader.delete-template": "Sil",
@ -290,32 +309,32 @@
"ViewHeader.select-a-template": "Bir kalıp seçin",
"ViewHeader.set-default-template": "Varsayılan olarak ata",
"ViewHeader.sort": "Sırala",
"ViewHeader.untitled": "Başlıksız",
"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": "Yükselt",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Üst tarifeye geç",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Yöneticiyi bilgilendir",
"ViewLimitDialog.Subtext.Admin": "Panoları sınırsız sayıda görüntüleyebilmek, sınırsız sayıda kart kullanmak ve diğer özellikler için Professional ya da Enterprise tarifemize yükseltin.",
"ViewLimitDialog.Subtext.Admin": "Sınırsız sayıda pano görüntüleyebilmek, sınırsız sayıda kart kullanmak ve diğer özellikler için Professional ya da Enterprise tarifemize geçin.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Tarifelerimiz hakkında ayrıntılı bilgi alın.",
"ViewLimitDialog.Subtext.RegularUser": "Panoları sınırsız sayıda görüntüleyebilmek, sınırsız sayıda kart kullanabilmek ve diğer özellikler için yöneticinizi Professional ya da Enterprise tarifesine yükseltmesi için bilgilendirin.",
"ViewLimitDialog.UpgradeImg.AltText": "yükseltme görseli",
"ViewLimitDialog.Subtext.RegularUser": "Sınırsız sayıda pano görüntüleyebilmek, sınırsız sayıda kart kullanabilmek ve diğer özellikler için Professional ya da Enterprise tarifesine geçmesi hakkında yöneticinizi 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": "Başlıksız pano",
"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",
"Workspace.editing-board-template": "Bir pano kalıbını düzenliyorsunuz.",
"boardSelector.confirm-link-board": "Panoyu kanala bağla",
"boardSelector.confirm-link-board-button": "Panoyu 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. Bir pano ile bir kanalın bağlantısını istediğiniz zaman kaldırabilirsiniz.",
"boardSelector.confirm-link-board-subtext-with-other-channel": "Bir panoyu bir kanala bağladığınızda, kanalın tüm üyeleri (var olan ve yeni) panoyu düzenleyebilir.{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.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.{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",
@ -345,10 +364,10 @@
"login.register-button": "ya da hesabınız yoksa bir hesap açın",
"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 yükselt",
"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 yükseltmesini isteyin.",
"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}",
"register.login-button": "ya da bir hesabınız varsa oturum açın",
"register.signup-title": "Hesap açın",
@ -372,7 +391,6 @@
"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.",
"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.{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-link-public-channel-button": "Evet, herkese açık kanalı ekle",
"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",

View File

@ -157,14 +157,12 @@
"PropertyType.CreatedTime": "创建时间",
"PropertyType.Date": "日期",
"PropertyType.Email": "Email",
"PropertyType.File": "文件或媒体",
"PropertyType.MultiSelect": "多选",
"PropertyType.Number": "数字",
"PropertyType.Person": "个人",
"PropertyType.Phone": "电话号码",
"PropertyType.Select": "选取",
"PropertyType.Text": "文字框",
"PropertyType.URL": "链接",
"PropertyType.UpdatedBy": "最后更新者",
"PropertyType.UpdatedTime": "上次更新时间",
"PropertyValueElement.empty": "空的",

View File

@ -139,14 +139,12 @@
"PropertyType.CreatedTime": "建立時間",
"PropertyType.Date": "日期",
"PropertyType.Email": "Email",
"PropertyType.File": "檔案或媒體",
"PropertyType.MultiSelect": "多選",
"PropertyType.Number": "數字",
"PropertyType.Person": "個人",
"PropertyType.Phone": "電話號碼",
"PropertyType.Select": "選取",
"PropertyType.Text": "文字框",
"PropertyType.URL": "超連結",
"PropertyType.UpdatedBy": "最後更新者",
"PropertyType.UpdatedTime": "最後更新時間",
"RegistrationLink.confirmRegenerateToken": "此動作將使先前分享的連結無效。確定要進行嗎?",

View File

@ -20,7 +20,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>
@ -95,7 +95,7 @@ exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Tex
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>

View File

@ -20,7 +20,7 @@ exports[`components/dialog should match snapshot 1`] = `
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>
@ -61,7 +61,7 @@ exports[`components/dialog should return dialog and click on cancel button 1`] =
>
<button
aria-label="Close dialog"
class="IconButton size--medium"
class="IconButton dialog__close size--medium"
title="Close dialog"
type="button"
>

View File

@ -63,12 +63,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
username: 'username_1',
email: '',
nickname: '',
firstname: '',
firstname: '',
lastname: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
is_guest: false,
roles: 'system_user',
}
const template1Title = 'Template 1'
@ -84,7 +85,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
},
users: {
me,
boardUsers: [me],
boardUsers: {[me.id]: me},
},
boards: {
boards: [
@ -154,6 +155,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
},
}
store = mockStateStore([], state)
jest.useRealTimers()
})
describe('not a focalboard Plugin', () => {
beforeAll(() => {
@ -399,9 +401,9 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
expect(mockedOctoClient.patchUserConfig).toBeCalledWith('user-id-1', {
updatedFields: {
'focalboard_onboardingTourStarted': '1',
'focalboard_onboardingTourStep': '0',
'focalboard_tourCategory': 'onboarding',
onboardingTourStarted: '1',
onboardingTourStep: '0',
tourCategory: 'onboarding',
},
})
})

View File

@ -19,7 +19,7 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme
import './boardTemplateSelector.scss'
import {OnboardingBoardTitle} from '../cardDetail/cardDetail'
import {IUser, UserConfigPatch, UserPropPrefix} from '../../user'
import {IUser, UserConfigPatch} from '../../user'
import {getMe, patchProps} from '../../store/users'
import {BaseTourSteps, TOUR_BASE} from '../onboardingTour'
@ -86,9 +86,9 @@ const BoardTemplateSelector = (props: Props) => {
const patch: UserConfigPatch = {
updatedFields: {
[UserPropPrefix + 'onboardingTourStarted']: '1',
[UserPropPrefix + 'onboardingTourStep']: BaseTourSteps.OPEN_A_CARD.toString(),
[UserPropPrefix + 'tourCategory']: TOUR_BASE,
onboardingTourStarted: '1',
onboardingTourStep: BaseTourSteps.OPEN_A_CARD.toString(),
tourCategory: TOUR_BASE,
},
}

View File

@ -96,16 +96,17 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
}
const me: IUser = {
id: 'user-id-1',
username: 'username_1',
id: 'user-id-1',
username: 'username_1',
nickname: '',
firstname: '',
firstname: '',
lastname: '',
email: '',
props: {},
create_at: 0,
update_at: 0,
email: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
is_guest: false,
roles: 'system_user',
}

View File

@ -111,10 +111,10 @@ describe('components/boardTemplateSelector/boardTemplateSelectorPreview', () =>
users: {
me: {
id: 'user-id',
props: {
focalboard_onboardingTourStarted: false,
},
},
myConfig: {
onboardingTourStarted: {value: false},
}
},
cards: {
templates: [],

View File

@ -9,8 +9,8 @@
background-color: rgba(var(--sidebar-text-rgb), 0.08);
color: rgba(var(--sidebar-text-rgb), 0.56);
flex: 1;
padding: 6px 8px;
gap: 6px;
padding: 6px 8px 6px 4px;
gap: 4px;
border-radius: 4px;
cursor: pointer;
height: 28px;
@ -28,9 +28,17 @@
}
.add-board-icon {
margin-left: 4px;
border-radius: 28px;
margin-left: 8px;
width: 28px;
height: 28px;
flex: 0 0 28px;
background-color: rgba(var(--sidebar-text-rgb), 0.08);
color: rgba(var(--sidebar-text-rgb), 0.72);
&:hover {
background-color: rgba(var(--sidebar-text-rgb), 0.16);
color: var(--sidebar-text);
}
}
}

Some files were not shown because too many files have changed in this diff Show More