1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-02 14:47:55 +02:00

Merge branch 'main' into GH-4476-No-card-delete-confirmation-status

This commit is contained in:
Mattermost Build 2023-03-30 01:15:27 +03:00 committed by GitHub
commit ef31f87ace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1285 additions and 327 deletions

View File

@ -15,7 +15,7 @@ env:
jobs:
ci-ubuntu-server:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:
@ -44,7 +44,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3
with:
@ -54,7 +54,7 @@ jobs:
run: cd focalboard; make server-test-${{matrix['db']}}
ci-ubuntu-webapp:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
@ -74,7 +74,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: npm ci
run: |
cd focalboard/webapp && npm ci && cd -
@ -132,7 +132,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3
@ -169,7 +169,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Set up Go
uses: actions/setup-go@v3

View File

@ -14,8 +14,7 @@ env:
jobs:
ubuntu:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
@ -34,7 +33,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -110,7 +109,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -168,7 +167,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -218,7 +217,7 @@ jobs:
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
plugin:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -238,7 +237,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go

View File

@ -13,7 +13,7 @@ env:
jobs:
down-migrations:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
@ -26,7 +26,7 @@ jobs:
golangci:
name: plugin
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
@ -48,7 +48,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: set up golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1
- name: lint

View File

@ -9,7 +9,7 @@ env:
jobs:
ubuntu:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -30,7 +30,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -106,7 +106,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -165,7 +165,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go
@ -216,7 +216,7 @@ jobs:
path: ${{ github.workspace }}/focalboard/win-wpf/dist/focalboard-win.zip
plugin-release:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -237,7 +237,7 @@ jobs:
repository: "mattermost/mattermost-server"
fetch-depth: "20"
path: "mattermost-server"
ref : "master"
ref : "b61c096497ac1f22f64b77afe58d0dd5a72b38f1"
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/focalboard/server/services/telemetry/telemetry.go

View File

@ -1,10 +1,10 @@
# This dockerfile is used to build Focalboard for Linux
# it builds all the parts inside the container and the last stage just holds the
# package that can be extracted using docker cp command
# ie
# docker build -f Dockerfile.build --no-cache -t focalboard-build:dirty .
# docker run --rm -v /tmp/dist:/tmp -d --name test focalboard-build:dirty /bin/sh -c 'sleep 1000'
# docker cp test:/dist/focalboard-server-linux-amd64.tar.gz .
# This Dockerfile is used to build Focalboard for Linux. It builds all the parts inside the image
# and the last stage just holds the package which is then copied back to the host.
#
# docker buildx build -f Dockerfile.build --no-cache --platform linux/amd64 -t focalboard-build:dirty --output out .
# docker buildx build -f Dockerfile.build --no-cache --platform linux/arm64 -t focalboard-build:dirty --output out .
#
# Afterwards the packages can be found in the ./out folder.
# build frontend
FROM node:16.3.0@sha256:ca6daf1543242acb0ca59ff425509eab7defb9452f6ae07c156893db06c7a9a4 AS frontend
@ -12,8 +12,10 @@ FROM node:16.3.0@sha256:ca6daf1543242acb0ca59ff425509eab7defb9452f6ae07c156893db
WORKDIR /webapp
COPY webapp .
RUN npm install --no-optional
RUN npm run pack
### 'CPPFLAGS="-DPNG_ARM_NEON_OPT=0"' Needed To Avoid Bug Described in: https://github.com/imagemin/optipng-bin/issues/118#issuecomment-1019838562
### Can be Removed when Ticket will be Closed
RUN CPPFLAGS="-DPNG_ARM_NEON_OPT=0" npm install --no-optional && \
npm run pack
# build backend and package
FROM golang:1.18.3@sha256:b203dc573d81da7b3176264bfa447bd7c10c9347689be40540381838d75eebef AS backend
@ -21,13 +23,13 @@ FROM golang:1.18.3@sha256:b203dc573d81da7b3176264bfa447bd7c10c9347689be405403818
COPY . .
COPY --from=frontend /webapp/pack webapp/pack
ARG TARGETARCH
# RUN apt-get update && apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev -y
RUN make server-linux
RUN make server-linux-package-docker
RUN EXCLUDE_PLUGIN=true EXCLUDE_SERVER=true EXCLUDE_ENTERPRISE=true make server-linux arch=${TARGETARCH}
RUN make server-linux-package-docker arch=${TARGETARCH}
# just hold the packages to output later
FROM alpine:3.12@sha256:c75ac27b49326926b803b9ed43bf088bc220d22556de1bc5f72d742c91398f69 AS dist
WORKDIR /dist
COPY --from=backend /go/dist/focalboard-server-linux-amd64.tar.gz .
# Copy package back to host
FROM scratch AS dist
ARG TARGETARCH
COPY --from=backend /go/dist/focalboard-server-linux-${TARGETARCH}.tar.gz .

View File

@ -63,7 +63,7 @@ endif
server-linux: setup-go-work ## Build server for Linux.
mkdir -p bin/linux
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux")
cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main
cd server; env GOOS=linux GOARCH=$(arch) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main
server-docker: setup-go-work ## Build server for Docker Architectures.
mkdir -p bin/docker
@ -101,7 +101,7 @@ server-linux-package-docker:
cp NOTICE.txt package/${PACKAGE_FOLDER}
cp webapp/NOTICE.txt package/${PACKAGE_FOLDER}/webapp-NOTICE.txt
mkdir -p dist
cd package && tar -czvf ../dist/focalboard-server-linux-amd64.tar.gz ${PACKAGE_FOLDER}
cd package && tar -czvf ../dist/focalboard-server-linux-$(arch).tar.gz ${PACKAGE_FOLDER}
rm -rf package
generate: ## Install and run code generators.

View File

@ -6,7 +6,7 @@
"support_url": "https://github.com/mattermost/focalboard/issues",
"release_notes_url": "https://github.com/mattermost/focalboard/releases",
"icon_path": "assets/starter-template-icon.svg",
"version": "7.9.0",
"version": "7.10.0",
"min_server_version": "7.2.0",
"server": {
"executables": {

View File

@ -20,7 +20,7 @@ const manifestStr = `
"support_url": "https://github.com/mattermost/focalboard/issues",
"release_notes_url": "https://github.com/mattermost/focalboard/releases",
"icon_path": "assets/starter-template-icon.svg",
"version": "7.9.0",
"version": "7.10.0",
"min_server_version": "7.2.0",
"server": {
"executables": {
@ -45,8 +45,7 @@ const manifestStr = `
"type": "bool",
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link.",
"placeholder": "",
"default": false,
"hosting": ""
"default": false
}
]
}

View File

@ -73,7 +73,7 @@ const CreateBoardFromTemplate = (props: Props) => {
let boardsAndBlocks = undefined
if (selectedBoardTemplateId === EMPTY_BOARD) {
if (templateIdRef.current === EMPTY_BOARD) {
boardsAndBlocks = await mutator.addEmptyBoard(teamId, intl)
} else {
boardsAndBlocks = await mutator.duplicateBoard(templateIdRef.current as string, ACTION_DESCRIPTION, asTemplate, undefined, undefined, teamId)

View File

@ -17,8 +17,10 @@ import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon'
import {Permission} from '../../../../webapp/src/constants'
import './rhsChannelBoardItem.scss'
import BoardPermissionGate from '../../../../webapp/src/components/permissions/boardPermissionGate'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../../../webapp/src/telemetry/telemetryClient'
import './rhsChannelBoardItem.scss'
const windowAny = (window as SuiteWindow)
@ -36,6 +38,10 @@ const RHSChannelBoardItem = (props: Props) => {
}
const handleBoardClicked = (boardID: string) => {
// send the telemetry information for the clicked board
const extraData = {teamID: team.id, board: boardID}
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelsRHSBoard, extraData)
window.open(`${windowAny.frontendBaseURL}/team/${team.id}/${boardID}`, '_blank', 'noopener')
}

View File

@ -227,7 +227,7 @@ func jsonStringResponse(w http.ResponseWriter, code int, message string) { //nol
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write(json)

View File

@ -8,6 +8,8 @@ import (
"errors"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -20,9 +22,30 @@ import (
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/shared/web"
)
var UnsafeContentTypes = [...]string{
"application/javascript",
"application/ecmascript",
"text/javascript",
"text/ecmascript",
"application/x-javascript",
"text/html",
}
var MediaContentTypes = [...]string{
"image/jpeg",
"image/png",
"image/bmp",
"image/gif",
"image/tiff",
"video/avi",
"video/mpeg",
"video/mp4",
"audio/mpeg",
"audio/wav",
}
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
@ -123,37 +146,12 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", filename)
fileInfo, err := a.app.GetFileInfo(filename)
fileInfo, fileReader, err := a.app.GetFile(board.TeamID, boardID, filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
return
}
if fileInfo != nil && fileInfo.Archived {
fileMetadata := map[string]interface{}{
"archived": true,
"name": fileInfo.Name,
"size": fileInfo.Size,
"extension": fileInfo.Extension,
}
data, jsonErr := json.Marshal(fileMetadata)
if jsonErr != nil {
a.logger.Error("failed to marshal archived file metadata", mlog.String("filename", filename), mlog.Err(jsonErr))
a.errorResponse(w, r, jsonErr)
return
}
jsonBytesResponse(w, http.StatusBadRequest, data)
return
}
fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename)
if err != nil && !errors.Is(err, app.ErrFileNotFound) {
a.errorResponse(w, r, err)
return
}
if errors.Is(err, app.ErrFileNotFound) && board.ChannelID != "" {
// prior to moving from workspaces to teams, the filepath was constructed from
// workspaceID, which is the channel ID in plugin mode.
@ -170,10 +168,74 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
}
defer fileReader.Close()
web.WriteFileResponse(filename, fileInfo.MimeType, fileInfo.Size, time.Now(), "", fileReader, false, w, r)
mimeType := ""
var fileSize int64
if fileInfo != nil {
mimeType = fileInfo.MimeType
fileSize = fileInfo.Size
}
writeFileResponse(filename, mimeType, fileSize, time.Now(), "", fileReader, false, w, r)
auditRec.Success()
}
func writeFileResponse(filename string, contentType string, contentSize int64,
lastModification time.Time, webserverMode string, fileReader io.ReadSeeker, forceDownload bool, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "private, no-cache")
w.Header().Set("X-Content-Type-Options", "nosniff")
if contentSize > 0 {
contentSizeStr := strconv.Itoa(int(contentSize))
if webserverMode == "gzip" {
w.Header().Set("X-Uncompressed-Content-Length", contentSizeStr)
} else {
w.Header().Set("Content-Length", contentSizeStr)
}
}
if contentType == "" {
contentType = "application/octet-stream"
} else {
for _, unsafeContentType := range UnsafeContentTypes {
if strings.HasPrefix(contentType, unsafeContentType) {
contentType = "text/plain"
break
}
}
}
w.Header().Set("Content-Type", contentType)
var toDownload bool
if forceDownload {
toDownload = true
} else {
isMediaType := false
for _, mediaContentType := range MediaContentTypes {
if strings.HasPrefix(contentType, mediaContentType) {
isMediaType = true
break
}
}
toDownload = !isMediaType
}
filename = url.PathEscape(filename)
if toDownload {
w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
} else {
w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename)
}
// prevent file links from being embedded in iframes
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'")
http.ServeContent(w, r, filename, lastModification, fileReader)
}
func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile
//

View File

@ -355,12 +355,15 @@ func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*mode
var oldMembers []*model.BoardMember
if patch.Type != nil || patch.ChannelID != nil {
testChannel := ""
if patch.ChannelID != nil && *patch.ChannelID == "" {
var err error
oldMembers, err = a.GetMembersForBoard(boardID)
if err != nil {
a.logger.Error("Unable to get the board members", mlog.Err(err))
}
} else if patch.ChannelID != nil && *patch.ChannelID != "" {
testChannel = *patch.ChannelID
}
board, err := a.store.GetBoard(boardID)
@ -372,7 +375,17 @@ func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*mode
}
oldChannelID = board.ChannelID
isTemplate = board.IsTemplate
if testChannel == "" {
testChannel = oldChannelID
}
if testChannel != "" {
if !a.permissions.HasPermissionToChannel(userID, testChannel, model.PermissionCreatePost) {
return nil, model.NewErrPermission("access denied to channel")
}
}
}
updatedBoard, err := a.store.PatchBoard(boardID, patch, userID)
if err != nil {
return nil, err

View File

@ -185,6 +185,7 @@ func TestPatchBoard(t *testing.T) {
// Type not null will retrieve team members
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil)
th.Store.EXPECT().GetUserByID(userID).Return(&model.User{ID: userID, Username: "UserName"}, nil)
th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return(
&model.Board{
@ -399,6 +400,104 @@ func TestPatchBoard(t *testing.T) {
require.NoError(t, err)
require.Equal(t, boardID, patchedBoard.ID)
})
t.Run("patch type channel, user without post permissions", func(t *testing.T) {
const boardID = "board_id_1"
const userID = "user_id_2"
const teamID = "team_id_1"
channelID := "myChannel"
patchType := model.BoardTypeOpen
patch := &model.BoardPatch{
Type: &patchType,
ChannelID: &channelID,
}
// Type not nil, will cause board to be reteived
// to check isTemplate
th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{
ID: boardID,
TeamID: teamID,
IsTemplate: true,
}, nil).Times(1)
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1)
_, err := th.App.PatchBoard(patch, boardID, userID)
require.Error(t, err)
})
t.Run("patch type channel, user with post permissions", func(t *testing.T) {
const boardID = "board_id_1"
const userID = "user_id_2"
const teamID = "team_id_1"
channelID := "myChannel"
patch := &model.BoardPatch{
ChannelID: &channelID,
}
// Type not nil, will cause board to be reteived
// to check isTemplate
th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{
ID: boardID,
TeamID: teamID,
}, nil).Times(2)
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1)
th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return(
&model.Board{
ID: boardID,
TeamID: teamID,
},
nil)
// Should call GetMembersForBoard 2 times
// - for WS BroadcastBoardChange
// - for AddTeamMembers check
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil).Times(2)
th.Store.EXPECT().PostMessage(utils.Anything, "", "").Times(1)
patchedBoard, err := th.App.PatchBoard(patch, boardID, userID)
require.NoError(t, err)
require.Equal(t, boardID, patchedBoard.ID)
})
t.Run("patch type remove channel, user without post permissions", func(t *testing.T) {
const boardID = "board_id_1"
const userID = "user_id_2"
const teamID = "team_id_1"
const channelID = "myChannel"
clearChannel := ""
patchType := model.BoardTypeOpen
patch := &model.BoardPatch{
Type: &patchType,
ChannelID: &clearChannel,
}
// Type not nil, will cause board to be reteived
// to check isTemplate
th.Store.EXPECT().GetBoard(boardID).Return(&model.Board{
ID: boardID,
TeamID: teamID,
IsTemplate: true,
ChannelID: channelID,
}, nil).Times(2)
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1)
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
// Should call GetMembersForBoard 2 times
// for WS BroadcastBoardChange
// for AddTeamMembers check
// We are returning the user as a direct Board Member, so BroadcastMemberDelete won't be called
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{{BoardID: boardID, UserID: userID, SchemeEditor: true}}, nil).Times(1)
_, err := th.App.PatchBoard(patch, boardID, userID)
require.Error(t, err)
})
}
func TestGetBoardCount(t *testing.T) {

View File

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/mattermost/focalboard/server/model"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/focalboard/server/utils"
@ -28,7 +29,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin
createdFilename := utils.NewID(utils.IDTypeNone)
fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
filePath := filepath.Join(teamID, rootID, fullFilename)
filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename)
fileSize, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil {
@ -45,7 +46,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin
CreateAt: now,
UpdateAt: now,
DeleteAt: 0,
Path: emptyString,
Path: filePath,
ThumbnailPath: emptyString,
PreviewPath: emptyString,
Name: filename,
@ -59,6 +60,7 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin
Content: "",
RemoteId: nil,
}
err := a.store.SaveFileInfo(fileInfo)
if err != nil {
return "", err
@ -77,6 +79,7 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) {
// will be the fileinfo id.
parts := strings.Split(filename, ".")
fileInfoID := parts[0][1:]
fileInfo, err := a.store.GetFileInfo(fileInfoID)
if err != nil {
return nil, err
@ -85,6 +88,40 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) {
return fileInfo, nil
}
func (a *App) GetFile(teamID, rootID, fileName string) (*mmModel.FileInfo, filestore.ReadCloseSeeker, error) {
fileInfo, err := a.GetFileInfo(fileName)
if err != nil && !model.IsErrNotFound(err) {
a.logger.Error("111")
return nil, nil, err
}
var filePath string
if fileInfo != nil && fileInfo.Path != "" {
filePath = fileInfo.Path
} else {
filePath = filepath.Join(teamID, rootID, fileName)
}
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to check if file exists as path. Path: %s, error: %e", filePath, err))
return nil, nil, err
}
if !exists {
return nil, nil, ErrFileNotFound
}
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err))
return nil, nil, err
}
return fileInfo, reader, nil
}
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
filePath := filepath.Join(teamID, rootID, filename)
exists, err := a.filesBackend.FileExists(filePath)

View File

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
@ -195,8 +196,8 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, string(os.PathSeparator))
assert.Equal(t, "1", paths[0])
assert.Equal(t, testBoardID, paths[1])
assert.Equal(t, "boards", paths[0])
assert.Equal(t, time.Now().Format("20060102"), paths[1])
fileName = paths[2]
return int64(10)
}
@ -219,8 +220,8 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, string(os.PathSeparator))
assert.Equal(t, "1", paths[0])
assert.Equal(t, "test-board-id", paths[1])
assert.Equal(t, "boards", paths[0])
assert.Equal(t, time.Now().Format("20060102"), paths[1])
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
return int64(10)
}
@ -243,8 +244,8 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, string(os.PathSeparator))
assert.Equal(t, "1", paths[0])
assert.Equal(t, "test-board-id", paths[1])
assert.Equal(t, "boards", paths[0])
assert.Equal(t, time.Now().Format("20060102"), paths[1])
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
return int64(10)
}
@ -304,3 +305,80 @@ func TestGetFileInfo(t *testing.T) {
assert.Nil(t, fetchedFileInfo)
})
}
func TestGetFile(t *testing.T) {
th, _ := SetupTestHelper(t)
t.Run("when FileInfo exists", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{
Id: "fileInfoID",
Path: "/path/to/file/fileName.txt",
}, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
readerFunc := func(path string) filestore.ReadCloseSeeker {
return mockedReadCloseSeek
}
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.NotNil(t, fileInfo)
assert.NotNil(t, seeker)
})
t.Run("when FileInfo doesn't exist", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
readerFunc := func(path string) filestore.ReadCloseSeeker {
return mockedReadCloseSeek
}
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.Nil(t, fileInfo)
assert.NotNil(t, seeker)
})
t.Run("when FileInfo exists but FileInfo.Path is not set", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{
Id: "fileInfoID",
Path: "",
}, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
readerFunc := func(path string) filestore.ReadCloseSeeker {
return mockedReadCloseSeek
}
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.NotNil(t, fileInfo)
assert.NotNil(t, seeker)
})
}

View File

@ -380,6 +380,8 @@ func (c *Client) GetCards(boardID string, page int, perPage int) ([]*model.Card,
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var cards []*model.Card
if err := json.NewDecoder(r.Body).Decode(&cards); err != nil {
return nil, BuildErrorResponse(r, err)
@ -398,6 +400,8 @@ func (c *Client) PatchCard(cardID string, cardPatch *model.CardPatch, disableNot
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var cardNew *model.Card
if err := json.NewDecoder(r.Body).Decode(&cardNew); err != nil {
return nil, BuildErrorResponse(r, err)
@ -412,6 +416,8 @@ func (c *Client) GetCard(cardID string) (*model.Card, *Response) {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var card *model.Card
if err := json.NewDecoder(r.Body).Decode(&card); err != nil {
return nil, BuildErrorResponse(r, err)
@ -450,6 +456,7 @@ func (c *Client) DeleteCategory(teamID, categoryID string) *Response {
return BuildErrorResponse(r, err)
}
defer closeBody(r)
return BuildResponse(r)
}
@ -1049,6 +1056,7 @@ func (c *Client) HideBoard(teamID, categoryID, boardID string) *Response {
return BuildErrorResponse(r, err)
}
defer closeBody(r)
return BuildResponse(r)
}
@ -1058,5 +1066,6 @@ func (c *Client) UnhideBoard(teamID, categoryID, boardID string) *Response {
return BuildErrorResponse(r, err)
}
defer closeBody(r)
return BuildResponse(r)
}

View File

@ -8,6 +8,7 @@ import (
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
"7.10.0",
"7.9.0",
"7.8.0",
"7.7.0",

View File

@ -21,11 +21,12 @@ const (
// query, so we want to stay safely below.
CategoryInsertBatch = 1000
TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete"
UniqueIDsMigrationKey = "UniqueIDsMigrationComplete"
CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete"
TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete"
DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete"
TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete"
UniqueIDsMigrationKey = "UniqueIDsMigrationComplete"
CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete"
TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete"
DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete"
DeDuplicateCategoryBoardTableMigrationKey = "DeDuplicateCategoryBoardTableComplete"
)
func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]*model.Block, error) {
@ -790,3 +791,102 @@ func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, err
return collation, charSet, nil
}
func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error {
// not supported for SQLite
if s.dbType == model.SqliteDBType {
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey)
if err != nil {
return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
if currentMigration >= (deDuplicateCategoryBoards + 1) {
// if the migration for which we're fixing the data is already applied,
// no need to check fix anything
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
needed, err := s.doesDuplicateCategoryBoardsExist()
if err != nil {
return err
}
if !needed {
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
}
if s.dbType == model.MysqlDBType {
return s.runMySQLDeDuplicateCategoryBoardsMigration()
} else if s.dbType == model.PostgresDBType {
return s.runPostgresDeDuplicateCategoryBoardsMigration()
}
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) {
subQuery := s.getQueryBuilder(s.db).
Select("user_id", "board_id", "count(*) AS count").
From(s.tablePrefix+"category_boards").
GroupBy("user_id", "board_id").
Having("count(*) > 1")
query := s.getQueryBuilder(s.db).
Select("COUNT(user_id)").
FromSelect(subQuery, "duplicate_dataset")
row := query.QueryRow()
count := 0
if err := row.Scan(&count); err != nil {
s.logger.Error("Error occurred reading number of duplicate records in category_boards table", mlog.Err(err))
return false, err
}
return count > 0, nil
}
func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " +
"JOIN duplicates USING(id) WHERE duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}
func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE FROM " + s.tablePrefix + "category_boards USING duplicates " +
"WHERE " + s.tablePrefix + "category_boards.id = duplicates.id AND duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}

View File

@ -22,6 +22,7 @@ func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mmModel.FileInfo) er
"extension",
"size",
"delete_at",
"path",
"archived",
).
Values(
@ -31,6 +32,7 @@ func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mmModel.FileInfo) er
fileInfo.Extension,
fileInfo.Size,
fileInfo.DeleteAt,
fileInfo.Path,
false,
)
@ -57,6 +59,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mmModel.FileInfo,
"extension",
"size",
"archived",
"path",
).
From(s.tablePrefix + "file_info").
Where(sq.Eq{"Id": id})
@ -73,6 +76,7 @@ func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mmModel.FileInfo,
&fileInfo.Extension,
&fileInfo.Size,
&fileInfo.Archived,
&fileInfo.Path,
)
if err != nil {

View File

@ -36,6 +36,7 @@ const (
uniqueIDsMigrationRequiredVersion = 14
teamLessBoardsMigrationRequiredVersion = 18
categoriesUUIDIDMigrationRequiredVersion = 20
deDuplicateCategoryBoards = 35
tempSchemaMigrationTableName = "temp_schema_migration"
)
@ -248,6 +249,15 @@ func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driv
return err
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, deDuplicateCategoryBoards); mErr != nil {
return mErr
}
currentMigrationVersion := len(appliedMigrations)
if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); mErr != nil {
return mErr
}
s.logger.Debug("== Applying all remaining migrations ====================",
mlog.Int("current_version", len(appliedMigrations)),
)
@ -309,7 +319,7 @@ func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap {
func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constraint string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -348,7 +358,7 @@ func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constra
func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -385,7 +395,7 @@ func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string,
func (s *SQLStore) genCreateIndexIfNeeded(tableName, columns string) (string, error) {
indexName := getIndexName(tableName, columns)
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
switch s.dbType {
case model.SqliteDBType:
@ -425,7 +435,7 @@ func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (st
oldTableName = addPrefixIfNeeded(oldTableName, s.tablePrefix)
newTableName = addPrefixIfNeeded(newTableName, s.tablePrefix)
normOldTableName := normalizeTablename(s.schemaName, oldTableName)
normOldTableName := s.normalizeTablename(oldTableName)
vars := map[string]string{
"schema": s.schemaName,
@ -472,7 +482,7 @@ func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (st
func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnName, dataType string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
vars := map[string]string{
"schema": s.schemaName,
@ -610,7 +620,7 @@ func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) {
func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
normTableName := s.normalizeTablename(tableName)
var query string
@ -676,8 +686,12 @@ func addPrefixIfNeeded(s, prefix string) string {
return s
}
func normalizeTablename(schemaName, tableName string) string {
if schemaName != "" && !strings.HasPrefix(tableName, schemaName+".") {
func (s *SQLStore) normalizeTablename(tableName string) string {
if s.schemaName != "" && !strings.HasPrefix(tableName, s.schemaName+".") {
schemaName := s.schemaName
if s.dbType == model.MysqlDBType {
schemaName = "`" + schemaName + "`"
}
tableName = schemaName + "." + tableName
}
return tableName

View File

@ -0,0 +1 @@
{{ addColumnIfNeeded "file_info" "path" "varchar(512)" "" }}

View File

@ -10,8 +10,9 @@ import (
// these system settings are created when running the data migrations,
// so they will be present after the tests setup.
var dataMigrationSystemSettings = map[string]string{
"UniqueIDsMigrationComplete": "true",
"CategoryUuidIdMigrationComplete": "true",
"UniqueIDsMigrationComplete": "true",
"CategoryUuidIdMigrationComplete": "true",
"DeDuplicateCategoryBoardTableComplete": "true",
}
func addBaseSettings(m map[string]string) map[string]string {

View File

@ -2,6 +2,7 @@ package utils
import (
"encoding/json"
"path"
"reflect"
"time"
@ -120,3 +121,7 @@ func DedupeStringArr(arr []string) []string {
return dedupedArr
}
func GetBaseFilePath() string {
return path.Join("boards", time.Now().Format("20060102"))
}

View File

@ -1,5 +1,15 @@
{
"AppBar.Tooltip": "اخيار الالواح المرتبطة",
"Attachment.Attachment-title": "المرفق",
"AttachmentBlock.DeleteAction": "حذف",
"AttachmentBlock.addElement": "اضافة {type}",
"AttachmentBlock.delete": "تم حذف المرفق.",
"AttachmentBlock.failed": "تعذر تحميل هذا الملف حيث تم الوصول إلى الحد الأقصى لحجم الملفات.",
"AttachmentBlock.upload": "يتم الآن تحميل المرفق.",
"AttachmentBlock.uploadSuccess": "تم تحميل المرفق.",
"AttachmentElement.delete-confirmation-dialog-button-text": "حذف",
"AttachmentElement.download": "تحميل",
"AttachmentElement.upload-percentage": "جاري التحمي... ({uploadPercent}%)",
"BoardComponent.add-a-group": "+ إضافة مجموعة",
"BoardComponent.delete": "حذف",
"BoardComponent.hidden-columns": "الأعمدة المخفية",
@ -155,9 +165,15 @@
"FindBoardsDialog.NoResultsFor": "لا يوجد نتيجة للبحث \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "اختر بحث آخر أو تأكد من الأخطاء الإملائية.",
"FindBoardsDialog.Title": "البحث عن ألواح",
"GroupBy.ungroup": "إلغاء التجميع",
"KanbanCard.untitled": "بدون عنوان",
"Mutator.new-card-from-template": "بطاقة جديدة من نموذج",
"Mutator.new-template-from-card": "نموذج جديد من بطاقة",
"OnboardingTour.AddComments.Title": "إضافة تعليقات",
"OnboardingTour.AddDescription.Title": "اضافة وصف",
"OnboardingTour.AddProperties.Title": "إضافة خواص",
"OnboardingTour.AddView.Body": "انتقل هنا لإنشاء عرض جديد لتنظيم لوحتك باستخدام تخطيطات مختلفة.",
"OnboardingTour.AddView.Title": "إضافة عرض جديد",
"OnboardingTour.CopyLink.Title": "نسخ الرابط",
"PropertyMenu.Delete": "حذف",
"PropertyMenu.changeType": "تغيير نوع الخاصية",

View File

@ -1,17 +1,81 @@
{
"Attachment.Attachment-title": "Adjunt",
"AttachmentBlock.DeleteAction": "esborra",
"AttachmentBlock.addElement": "afegir {type}",
"AttachmentBlock.delete": "Adjunt esborrat.",
"AttachmentBlock.failed": "Aquest fitxer no pot ser afegit ja que el límit de tamany de fitxer ha estat assolit.",
"AttachmentBlock.upload": "Adjunt afegint-se.",
"AttachmentBlock.uploadSuccess": "Adjunt afegit.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Esborra",
"AttachmentElement.download": "Descarrega",
"AttachmentElement.upload-percentage": "Afegint...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Afegir un grup",
"BoardComponent.delete": "Eliminar",
"BoardComponent.hidden-columns": "Columnes ocultes",
"BoardComponent.hide": "Amagar",
"BoardComponent.new": "+ Nou",
"BoardComponent.no-property": "Sense {property}",
"BoardComponent.no-property-title": "Els elements amb una propietat {property} buida anirán aquí. Aquesta col·lumna no pot elimiar-se.",
"BoardComponent.no-property-title": "Els elements amb una propietat {property} buida anirán aquí. Aquesta col·lumna no pot eliminar-se.",
"BoardComponent.show": "Mostrar",
"BoardMember.schemeAdmin": "Admin",
"BoardMember.schemeCommenter": "Comentarista",
"BoardMember.schemeEditor": "Editor",
"BoardMember.schemeNone": "Cap",
"BoardMember.schemeViewer": "Visualitzador",
"BoardPage.newVersion": "Una nova versió de Boards és disponible, clica aquí per recarregar.",
"BoardPage.syncFailed": "El tauler podria ser eliminat o revocat l'accés.",
"BoardTemplateSelector.add-template": "Crea una nova plantilla",
"BoardTemplateSelector.create-empty-board": "Crea un taulell buit",
"BoardTemplateSelector.delete-template": "Esborra",
"BoardTemplateSelector.description": "Afegeix el taulell a la barra lateral usant alguna de les plantilles definides a sota o comença des de zero.",
"BoardTemplateSelector.edit-template": "Edita",
"BoardTemplateSelector.plugin.no-content-description": "Afegeix el taulell a la barra lateral usant alguna de les plantilles definides a sota o comença des de zero.",
"BoardTemplateSelector.plugin.no-content-title": "Crea un taulell",
"BoardTemplateSelector.title": "Crea un taulell",
"BoardTemplateSelector.use-this-template": "Utilitza aquesta plantilla",
"BoardsSwitcher.Title": "Busca taulells",
"BoardsUnfurl.Updated": "Actualitzat {time}",
"Calculations.Options.average.displayName": "Promig",
"Calculations.Options.average.label": "Promig",
"Calculations.Options.countChecked.displayName": "Comprovat",
"Calculations.Options.countUniqueValue.displayName": "Únic",
"Calculations.Options.countUniqueValue.label": "Compta valors únics",
"Calculations.Options.countValue.displayName": "Valors",
"Calculations.Options.dateRange.displayName": "Rang",
"Calculations.Options.dateRange.label": "Rang",
"Calculations.Options.earliest.displayName": "Proper",
"Calculations.Options.earliest.label": "Proper",
"Calculations.Options.latest.displayName": "Últim",
"Calculations.Options.latest.label": "Últim",
"Calculations.Options.max.displayName": "Màxim",
"Calculations.Options.max.label": "Màxim",
"Calculations.Options.min.displayName": "Mínim",
"Calculations.Options.min.label": "Mínim",
"Calculations.Options.none.displayName": "Calcula",
"Calculations.Options.none.label": "Cap",
"Calculations.Options.percentChecked.displayName": "Completat",
"Calculations.Options.percentChecked.label": "Percentatge completat",
"Calculations.Options.percentUnchecked.displayName": "No finalitzat",
"Calculations.Options.percentUnchecked.label": "Percentatge no finalitzat",
"Calculations.Options.range.displayName": "Rang",
"Calculations.Options.range.label": "Rang",
"Calculations.Options.sum.displayName": "Suma",
"Calculations.Options.sum.label": "Suma",
"CalendarCard.untitled": "Sense títol",
"CardActionsMenu.copiedLink": "Copiat!",
"CardActionsMenu.copyLink": "Còpia l'enllaç",
"CardActionsMenu.delete": "Esborra",
"CardActionsMenu.duplicate": "Duplica",
"CardBadges.title-comments": "Comentaris",
"CardBadges.title-description": "Aquesta tarjeta té una descripció",
"CardDetail.Attach": "Adjunta",
"CardDetail.Follow": "Segueix",
"CardDetail.Following": "Segueix",
"CardDetail.add-content": "Afegeix contingut",
"CardDetail.add-icon": "Afegeix icona",
"CardDetail.add-property": "+ Afegeix propietat",
"CardDetail.addCardText": "afegeix text a la targeta",
"CardDetail.moveContent": "mou el contingut de la targeta",
"CardDetail.moveContent": "Mou el contingut de la targeta",
"CardDetail.new-comment-placeholder": "Afegeix un comentari...",
"CardDialog.editing-template": "Estas editant una plantilla.",
"CardDialog.nocard": "Aquesta targeta no existeix o és innaccesible.",
@ -47,18 +111,18 @@
"PropertyMenu.changeType": "Canviar el tipus de propietat",
"PropertyMenu.typeTitle": "Tipus",
"PropertyType.Checkbox": "casella de verificació",
"PropertyType.CreatedBy": "Creat Per",
"PropertyType.CreatedBy": "Creada per",
"PropertyType.CreatedTime": "Moment de creació",
"PropertyType.Date": "Data",
"PropertyType.Email": "Correu electrònic",
"PropertyType.MultiSelect": "Multi selecció",
"PropertyType.MultiSelect": "Selecció múltiple",
"PropertyType.Number": "Número",
"PropertyType.Person": "Persona",
"PropertyType.Phone": "Telèfon",
"PropertyType.Select": "Selecciona",
"PropertyType.Text": "Text",
"PropertyType.UpdatedBy": "Actualitzat per",
"PropertyType.UpdatedTime": "Moment de actualització",
"PropertyType.UpdatedBy": "Última actualització feta per",
"PropertyType.UpdatedTime": "Moment d'actualització",
"RegistrationLink.confirmRegenerateToken": "Això invalidarà enllaços compartits anteriorment. Continuar?",
"RegistrationLink.copiedLink": "Copiat!",
"RegistrationLink.copyLink": "Copiar enllaç",
@ -113,7 +177,7 @@
"ViewHeader.group-by": "Agrupar per: {property}",
"ViewHeader.new": "Nou",
"ViewHeader.properties": "Propietats",
"ViewHeader.search-text": "Cercar text",
"ViewHeader.search-text": "Cerca tarjetes",
"ViewHeader.select-a-template": "Selecciona una plantilla",
"ViewHeader.sort": "Ordenar",
"ViewHeader.untitled": "Sense títol",

View File

@ -1,4 +1,15 @@
{
"AppBar.Tooltip": "Alternar tableros vinculados",
"Attachment.Attachment-title": "Archivos adjuntos",
"AttachmentBlock.DeleteAction": "borrar",
"AttachmentBlock.addElement": "agregar {type}",
"AttachmentBlock.delete": "Archivo adjunto eliminado.",
"AttachmentBlock.failed": "Este archivo no puede subirse debido a que excede el límite de tamaño de archivo.",
"AttachmentBlock.upload": "Subiendo archivo adjunto.",
"AttachmentBlock.uploadSuccess": "Archivo adjunto subido.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Borrar",
"AttachmentElement.download": "Descargar",
"AttachmentElement.upload-percentage": "Subiendo...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Añadir un grupo",
"BoardComponent.delete": "Borrar",
"BoardComponent.hidden-columns": "Columnas Ocultas",
@ -10,21 +21,39 @@
"BoardMember.schemeAdmin": "Administrador",
"BoardMember.schemeEditor": "Editor",
"BoardMember.schemeNone": "Ninguno",
"BoardPage.newVersion": "Una nueva versión de Board está disponible, haz click aquí para recargar.",
"BoardPage.syncFailed": "El tablero puede estar eliminado o el acceso fue revocado.",
"BoardTemplateSelector.add-template": "Nueva plantilla",
"BoardTemplateSelector.create-empty-board": "Crear pizarra vacía",
"BoardTemplateSelector.delete-template": "Suprimir",
"BoardMember.schemeViewer": "Visualizador",
"BoardMember.unlinkChannel": "Desvincular",
"BoardPage.newVersion": "Una nueva versión de Boards está disponible, haz clic aquí para recargar.",
"BoardPage.syncFailed": "El tablero puede haber sido eliminado o el acceso revocado.",
"BoardTemplateSelector.add-template": "Crear nueva plantilla",
"BoardTemplateSelector.create-empty-board": "Crear un tablero vacío",
"BoardTemplateSelector.delete-template": "Eliminar",
"BoardTemplateSelector.description": "Agregar un tablero a la barra lateral usando alguna de las plantillas definidas a continuación o empezar desde cero.",
"BoardTemplateSelector.edit-template": "Editar",
"BoardTemplateSelector.plugin.no-content-description": "Agregar un tablero a la barra lateral usando alguna de las plantillas definidas a continuación o empezar desde cero.",
"BoardTemplateSelector.plugin.no-content-title": "Crear un tablero",
"BoardTemplateSelector.title": "Crear un tablero",
"BoardTemplateSelector.use-this-template": "Utiliza esta plantilla",
"BoardsSwitcher.Title": "Encontrar tableros",
"BoardsUnfurl.Limited": "Los detalles adicionales están ocultos debido a que la tarjeta ha sido archivada",
"BoardsUnfurl.Updated": "Actualizado {time}",
"Calculations.Options.count.displayName": "Cantidad",
"Calculations.Options.count.label": "Cantidad",
"Calculations.Options.average.displayName": "Promedio",
"Calculations.Options.average.label": "Promedio",
"Calculations.Options.count.displayName": "Contar",
"Calculations.Options.count.label": "Contar",
"Calculations.Options.countChecked.displayName": "Marcado",
"Calculations.Options.countChecked.label": "Contar marcados",
"Calculations.Options.countUnchecked.displayName": "Deseleccionado",
"Calculations.Options.countUnchecked.label": "Contar no marcados",
"Calculations.Options.countUniqueValue.displayName": "Único",
"Calculations.Options.countUniqueValue.label": "Contar valores únicos",
"Calculations.Options.countValue.displayName": "Valores",
"Calculations.Options.dateRange.displayName": "Rango",
"Calculations.Options.dateRange.label": "Rango",
"Calculations.Options.earliest.displayName": "Más antiguo",
"Calculations.Options.earliest.label": "Más antiguo",
"Calculations.Options.latest.displayName": "Último",
"Calculations.Options.latest.label": "Último",
"Calculations.Options.max.displayName": "Máx",
"Calculations.Options.max.label": "Máx",
"Calculations.Options.median.displayName": "Mediana",
@ -34,17 +63,54 @@
"Calculations.Options.none.displayName": "Calcular",
"Calculations.Options.none.label": "Ninguna",
"Calculations.Options.percentChecked.displayName": "Marcado",
"Calculations.Options.percentChecked.label": "Porcentaje marcado",
"Calculations.Options.percentUnchecked.displayName": "Desmarcado",
"Calculations.Options.percentUnchecked.label": "Porcentaje desmarcado",
"Calculations.Options.range.displayName": "Rango",
"Calculations.Options.range.label": "Rango",
"Calculations.Options.sum.displayName": "Suma",
"Calculations.Options.sum.label": "Suma",
"CalendarCard.untitled": "Sin título",
"CardActionsMenu.copiedLink": "¡Copiado!",
"CardActionsMenu.copyLink": "Copiar hipervínculo",
"CardActionsMenu.delete": "Eliminar",
"CardActionsMenu.duplicate": "Duplicar",
"CardBadges.title-checkboxes": "Casillas de verificación",
"CardBadges.title-comments": "Comentarios",
"CardBadges.title-description": "Esta tarjeta tiene una descripción",
"CardDetail.Attach": "Adjuntar",
"CardDetail.Follow": "Seguir",
"CardDetail.Following": "Siguiendo",
"CardDetail.add-content": "Añadir contenido",
"CardDetail.add-icon": "Añadir icono",
"CardDetail.add-property": "+ Añadir propiedad",
"CardDetail.addCardText": "añade texto a la tarjeta",
"CardDetail.moveContent": "mover contenido de la tarjeta",
"CardDetail.addCardText": "agregar texto a la tarjeta",
"CardDetail.limited-body": "Mejorar a nuestro plan Professional o Enterprise.",
"CardDetail.limited-button": "Mejorar",
"CardDetail.limited-title": "Esta tarjeta está oculta",
"CardDetail.moveContent": "Mover contenido de la tarjeta",
"CardDetail.new-comment-placeholder": "Añadir un comentario...",
"CardDetailProperty.confirm-delete-subtext": "¿Estas seguro de que quieres eliminar la propiedad \"{nombre de la propiedad}\"? Al eliminarla se borrará la propiedad de todas las tarjetas de este tablero.",
"CardDetailProperty.property-deleted": "¡{nombre de la propiedad} ha sido eliminado exitosamente!",
"CardDetailProperty.confirm-delete-heading": "Confirmar eliminación de la propiedad",
"CardDetailProperty.confirm-delete-subtext": "¿Estás seguro de que quieres eliminar la propiedad \"{propertyName}\"? Al eliminarla también se removerá la propiedad en todas las tarjetas de este tablero.",
"CardDetailProperty.confirm-property-name-change-subtext": "¿Estás seguro de que quieres cambiar la propiedad \"{propertyName}\" {customText}? Esto puede afectar a los valores en {numOfCards} tarjeta(s) en este tablero, lo que puede resultar en una pérdida de datos.",
"CardDetailProperty.confirm-property-type-change": "Confirmar cambio de tipo de la propiedad",
"CardDetailProperty.delete-action-button": "Eliminar",
"CardDetailProperty.property-change-action-button": "Modificar propiedad",
"CardDetailProperty.property-changed": "¡Propiedad modificada exitosamente!",
"CardDetailProperty.property-deleted": "¡La propiedad {propertyName} ha sido eliminada exitosamente!",
"CardDetial.limited-link": "Aprende más sobre nuestros planes.",
"CardDialog.delete-confirmation-dialog-attachment": "Confirmar eliminación del archivo adjunto",
"CardDialog.delete-confirmation-dialog-button-text": "Eliminar",
"CardDialog.delete-confirmation-dialog-heading": "Confirmar eliminación de la tarjeta",
"CardDialog.editing-template": "Estás editando una plantilla.",
"CardDialog.nocard": "Esta tarjeta no existe o es inaccesible.",
"Categories.CreateCategoryDialog.CancelText": "Cancelar",
"Categories.CreateCategoryDialog.CreateText": "Crear",
"Categories.CreateCategoryDialog.Placeholder": "Pon nombre a la categoría",
"Categories.CreateCategoryDialog.UpdateText": "Actualizar",
"CenterPanel.Login": "Ingresar",
"CenterPanel.Share": "Compartir",
"ChannelIntro.CreateBoard": "Crear un tablero",
"ColorOption.selectColor": "Seleccionar {color} Color",
"Comment.delete": "Borrar",
"CommentsList.send": "Enviar",

View File

@ -1,14 +1,42 @@
{
"AppBar.Tooltip": "Veksle lenkede tavler",
"Attachment.Attachment-title": "Vedlegg",
"AttachmentBlock.DeleteAction": "slett",
"AttachmentBlock.addElement": "legg til {type}",
"AttachmentBlock.delete": "Vedlegg slettet.",
"AttachmentBlock.failed": "Denne filen kunne ikke lastes opp fordi størrelsesgrensen er nådd.",
"AttachmentBlock.upload": "Vedlegg lastes opp.",
"AttachmentBlock.uploadSuccess": "Vedlegg lastet opp.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Slett",
"AttachmentElement.download": "Last ned",
"AttachmentElement.upload-percentage": "Laster opp ...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Legg til gruppe",
"BoardComponent.delete": "Slett",
"BoardComponent.hidden-columns": "Skjulte kolonner",
"BoardComponent.hide": "Skjul",
"BoardComponent.new": "+ Ny",
"BoardComponent.no-property": "Ingen {property}",
"BoardComponent.no-property-title": "Elementer med en tom {property} område kommer hit. Denne kolonnen kan ikke fjernes.",
"BoardComponent.no-property-title": "Elementer med tom {property} atributt legges her. Denne kolonnen kan ikke fjernes.",
"BoardComponent.show": "Vis",
"BoardMember.schemeAdmin": "Admin",
"BoardMember.schemeCommenter": "Kommentator",
"BoardMember.schemeEditor": "Redaktør",
"BoardMember.schemeNone": "Ingen",
"BoardMember.schemeViewer": "Viser",
"BoardMember.unlinkChannel": "Fjern lenke",
"BoardPage.newVersion": "En ny versjon av Boards er tilgjengelig, klikk her for å laste inn på nytt.",
"BoardPage.syncFailed": "Tavle kan slettes eller adgangen trekkes tilbake.",
"BoardTemplateSelector.add-template": "Lag ny mal",
"BoardTemplateSelector.create-empty-board": "Opprett tom tavle",
"BoardTemplateSelector.delete-template": "Slett",
"BoardTemplateSelector.description": "Legg til en tavle til sidestolpen med hvilken mal du vil fra listen under, eller start med en helt tom tavle.",
"BoardTemplateSelector.edit-template": "Rediger",
"BoardTemplateSelector.plugin.no-content-description": "Legg til en tavle i sidestolpen med hvilken mal du vil, eller start med en tom tavle.",
"BoardTemplateSelector.plugin.no-content-title": "Lag ny tavle",
"BoardTemplateSelector.title": "Lag ny tavle",
"BoardTemplateSelector.use-this-template": "Bruk denne malen",
"BoardsSwitcher.Title": "Finn tavle",
"BoardsUnfurl.Limited": "Flere detaljer er skjult fordi kortet er arkivert",
"BoardsUnfurl.Remainder": "+{remainder} mer",
"BoardsUnfurl.Updated": "Oppdatert {time}",
"Calculations.Options.average.displayName": "Gjennomsnitt",
@ -16,11 +44,142 @@
"Calculations.Options.count.displayName": "Antall",
"Calculations.Options.count.label": "Antall",
"Calculations.Options.countChecked.displayName": "Avkrysset",
"Calculations.Options.countChecked.label": "Antall avsjekket",
"Calculations.Options.countChecked.label": "Antall valgt",
"Calculations.Options.countUnchecked.displayName": "Ikke avmerket",
"Calculations.Options.countUnchecked.label": "Antall Ikke Avmerket",
"Calculations.Options.countUnchecked.label": "Antall ikke valgt",
"Calculations.Options.countUniqueValue.displayName": "Unik",
"Calculations.Options.countUniqueValue.label": "Antall Unike Verdier",
"Calculations.Options.countUniqueValue.label": "Antall unike verdier",
"Calculations.Options.countValue.displayName": "Verdier",
"Calculations.Options.countValue.label": "Antall Verdier"
"Calculations.Options.countValue.label": "Antall verdier",
"Calculations.Options.dateRange.displayName": "Tidsrom",
"Calculations.Options.dateRange.label": "Tidsrom",
"Calculations.Options.earliest.displayName": "Tiligst",
"Calculations.Options.earliest.label": "Tiligst",
"Calculations.Options.latest.displayName": "Senest",
"Calculations.Options.latest.label": "Senest",
"Calculations.Options.max.displayName": "Maks",
"Calculations.Options.max.label": "Maks",
"Calculations.Options.median.displayName": "Median",
"Calculations.Options.median.label": "Median",
"Calculations.Options.min.displayName": "Min",
"Calculations.Options.min.label": "Min",
"Calculations.Options.none.displayName": "Kalkulèr",
"Calculations.Options.none.label": "Ingen",
"Calculations.Options.percentChecked.displayName": "Valgt",
"Calculations.Options.percentChecked.label": "Prosent valgt",
"Calculations.Options.percentUnchecked.displayName": "Ikke valgt",
"Calculations.Options.percentUnchecked.label": "Prosent ikke valgt",
"Calculations.Options.range.displayName": "Tidsrom",
"Calculations.Options.range.label": "Tidsrom",
"Calculations.Options.sum.displayName": "Sum",
"Calculations.Options.sum.label": "Sum",
"CalendarCard.untitled": "Uten navn",
"CardActionsMenu.copiedLink": "Kopiert!",
"CardActionsMenu.copyLink": "Kopier lenke",
"CardActionsMenu.delete": "Slett",
"CardActionsMenu.duplicate": "Dupliser",
"CardBadges.title-checkboxes": "Avkrysningsbokser",
"CardBadges.title-comments": "Kommentarer",
"CardBadges.title-description": "Dette kortet har en beskrivelsestekst",
"CardDetail.Attach": "Legg ved",
"CardDetail.Follow": "Følg",
"CardDetail.Following": "Følger",
"CardDetail.add-content": "Legg til innhold",
"CardDetail.add-icon": "Legg til ikon",
"CardDetail.add-property": "+ Legg til en verdi",
"CardDetail.addCardText": "legg inn tekst i kortet",
"CardDetail.limited-body": "Oppgrader til vår profesjonelle eller bedriftsplan.",
"CardDetail.limited-button": "Oppgrader",
"CardDetail.limited-title": "Dette kortet er skjult",
"CardDetail.moveContent": "Flytt innholdet",
"CardDetail.new-comment-placeholder": "Legg til kommentar ...",
"CardDetailProperty.confirm-delete-heading": "Bekreft sletting av verdi",
"CardDetailProperty.confirm-delete-subtext": "Er du sikker på at du vil slette verdien \"{propertyName}\"? Dette vil fjerne verdien fra alle kortene på denne tavlen.",
"CardDetailProperty.confirm-property-name-change-subtext": "Er du sikker på at du vil endre verdien \"{propertyName}\" {customText}? Dette vil påvirke verdien på {numOfCards} kort på denne tavlen, og kan forårsake at du mister informasjon.",
"CardDetailProperty.confirm-property-type-change": "Bekreft endring av verditype",
"CardDetailProperty.delete-action-button": "Slett",
"CardDetailProperty.property-change-action-button": "Endre verdi",
"CardDetailProperty.property-changed": "Verdi endret!",
"CardDetailProperty.property-deleted": "Fjernet {propertyName}!",
"CardDetailProperty.property-name-change-subtext": "type fra \"{oldPropType}\" til \"{newPropType}\"",
"CardDetial.limited-link": "Lær mer om våre planer.",
"CardDialog.delete-confirmation-dialog-attachment": "Bekreft sletting av vedlegg",
"CardDialog.delete-confirmation-dialog-button-text": "Slett",
"CardDialog.delete-confirmation-dialog-heading": "Bekreft sletting av kort",
"CardDialog.editing-template": "Du redigerer en mal.",
"CardDialog.nocard": "Dette kortet eksisterer ikke eller du har ikke tilgang.",
"Categories.CreateCategoryDialog.CancelText": "Avbryt",
"Categories.CreateCategoryDialog.CreateText": "Opprett",
"Categories.CreateCategoryDialog.Placeholder": "Navngi kategorien",
"Categories.CreateCategoryDialog.UpdateText": "Oppdater",
"CenterPanel.Login": "Logg inn",
"CenterPanel.Share": "Del",
"ChannelIntro.CreateBoard": "Opprett tavle",
"CloudMessage.cloud-server": "Få din egen gratis skytjener.",
"ColorOption.selectColor": "Velg {color} farge",
"Comment.delete": "Slett",
"CommentsList.send": "Send",
"ConfirmPerson.empty": "Tom",
"ConfirmPerson.search": "Søk ...",
"ConfirmationDialog.cancel-action": "Avbryt",
"ConfirmationDialog.confirm-action": "Bekreft",
"ContentBlock.Delete": "Slett",
"ContentBlock.DeleteAction": "slett",
"ContentBlock.addElement": "legg til {type}",
"ContentBlock.checkbox": "avkrysningsboks",
"ContentBlock.divider": "avdeler",
"ContentBlock.editCardCheckbox": "krysset-boks",
"ContentBlock.editCardCheckboxText": "rediger kort tekst",
"ContentBlock.editCardText": "rediger kort tekst",
"ContentBlock.editText": "Rediger tekst ...",
"ContentBlock.image": "bilde",
"ContentBlock.insertAbove": "Sett inn over",
"ContentBlock.moveBlock": "flytt kort innhold",
"ContentBlock.moveDown": "Flytt ned",
"ContentBlock.moveUp": "Flytt opp",
"ContentBlock.text": "tekst",
"DateRange.clear": "Tøm",
"DateRange.empty": "Tom",
"DateRange.endDate": "Sluttdato",
"DateRange.today": "I dag",
"DeleteBoardDialog.confirm-cancel": "Avbryt",
"DeleteBoardDialog.confirm-delete": "Slett",
"DeleteBoardDialog.confirm-info": "Er du sikker på at du vil slette tavlen \"{boardTitle}\"? Dette vil slette alle kortene på tavlen.",
"DeleteBoardDialog.confirm-info-template": "Er du sikker på at du vil slette tavlemalen \"{boardTitle}\"?",
"DeleteBoardDialog.confirm-tite": "Bekreft sletting av tavle",
"DeleteBoardDialog.confirm-tite-template": "Bekreft sletting av tavlemal",
"Dialog.closeDialog": "Lukk",
"EditableDayPicker.today": "I dag",
"Error.mobileweb": "Støtte for bruk i nettleser på mobil er i tidlig beta. Alt vil ikke fungere.",
"Error.websocket-closed": "Problemer med kobling til tjeneren. Sjekk konfigurasjonen hvis problemet vedvarer.",
"Filter.contains": "inneholder",
"Filter.ends-with": "ender med",
"Filter.includes": "inkluderer",
"Filter.is": "er",
"Filter.is-empty": "er tom",
"Filter.is-not-empty": "er ikke tom",
"Filter.is-not-set": "er ikke satt",
"Filter.is-set": "er satt",
"Filter.not-contains": "inkluderer ikke",
"Filter.not-ends-with": "ender ikke med",
"Filter.not-includes": "inkluderer ikke",
"Filter.not-starts-with": "starter ikke med",
"Filter.starts-with": "starter med",
"FilterByText.placeholder": "filtrer tekst",
"FilterComponent.add-filter": "+ Nytt filter",
"FilterComponent.delete": "Slett",
"FilterValue.empty": "(tom)",
"FindBoardsDialog.IntroText": "Søk etter tavle",
"FindBoardsDialog.NoResultsFor": "Ingen resultat for \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Sjekk stavingen eller søk på noe annet.",
"FindBoardsDialog.SubTitle": "Skriv for å finne en tavle. Bruk <b>opp/ned</b> for å navigere. <b>Enter</b> for å velge, eller <b>Esc</b> for å avbryte",
"FindBoardsDialog.Title": "Finn tavle",
"GroupBy.hideEmptyGroups": "Skjul {count} tomme grupper",
"GroupBy.showHiddenGroups": "Vis {count} tomme grupper",
"GroupBy.ungroup": "Fjern fra gruppe",
"HideBoard.MenuOption": "Skjul tavlen",
"KanbanCard.untitled": "Uten navn",
"Mutator.new-board-from-template": "ny tavle fra mal",
"Mutator.new-card-from-template": "nytt kort fra mal",
"Mutator.new-template-from-card": "ny mal fra kort"
}

View File

@ -1,5 +1,12 @@
{
"AppBar.Tooltip": "Ativar Boards vinculados",
"AppBar.Tooltip": "Ativar boards vinculados",
"Attachment.Attachment-title": "Anexo",
"AttachmentBlock.DeleteAction": "apagar",
"AttachmentBlock.addElement": "adicionar {type}",
"AttachmentBlock.delete": "Anexo apagado.",
"AttachmentBlock.uploadSuccess": "Anexo enviado.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Apagar",
"AttachmentElement.upload-percentage": "Enviando...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Adicione um grupo",
"BoardComponent.delete": "Excluir",
"BoardComponent.hidden-columns": "Colunas ocultas",
@ -16,8 +23,8 @@
"BoardMember.unlinkChannel": "Desvincular",
"BoardPage.newVersion": "Uma nova versão do Boards está disponível, clique aqui para recarregar.",
"BoardPage.syncFailed": "O Board pode ter sido excluído ou o acesso revogado.",
"BoardTemplateSelector.add-template": "Novo modelo",
"BoardTemplateSelector.create-empty-board": "Criar board vazio",
"BoardTemplateSelector.add-template": "Criar novo modelo",
"BoardTemplateSelector.create-empty-board": "Criar um board vazio",
"BoardTemplateSelector.delete-template": "Excluir",
"BoardTemplateSelector.description": "Adicione um quadro à barra lateral usando qualquer um dos modelos definidos abaixo ou comece do zero.",
"BoardTemplateSelector.edit-template": "Editar",
@ -25,7 +32,7 @@
"BoardTemplateSelector.plugin.no-content-title": "Criar um board",
"BoardTemplateSelector.title": "Criar um board",
"BoardTemplateSelector.use-this-template": "Use este template",
"BoardsSwitcher.Title": "Encontrar Boards",
"BoardsSwitcher.Title": "Encontrar boards",
"BoardsUnfurl.Limited": "Detalhes adicionais estão ocultos devido ao cartão ter sido arquivado",
"BoardsUnfurl.Remainder": "+{remainder} mais",
"BoardsUnfurl.Updated": "Atualizado {time}",
@ -71,6 +78,7 @@
"CardBadges.title-checkboxes": "Caixa de seleção",
"CardBadges.title-comments": "Comentários",
"CardBadges.title-description": "Este cartão tem uma descrição",
"CardDetail.Attach": "Anexar",
"CardDetail.Follow": "Seguir",
"CardDetail.Following": "Seguindo",
"CardDetail.add-content": "Adicionar conteúdo",
@ -102,10 +110,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Atualizar",
"CenterPanel.Login": "Login",
"CenterPanel.Share": "Compartilhar",
"ChannelIntro.CreateBoard": "Criar um board",
"CloudMessage.cloud-server": "Obtenha seu próprio cloud server de graça.",
"ColorOption.selectColor": "Selecione {color} Cor",
"Comment.delete": "Excluir",
"CommentsList.send": "Enviar",
"ConfirmPerson.empty": "Vazio",
"ConfirmPerson.search": "Buscar...",
"ConfirmationDialog.cancel-action": "Cancelar",
"ConfirmationDialog.confirm-action": "Confirmar",
"ContentBlock.Delete": "Excluir",
@ -181,6 +192,7 @@
"OnboardingTour.ShareBoard.Body": "Você pode compartilhar seu board internament, com seu time, ou public para permitir visibilidade fora da sua organização.",
"OnboardingTour.ShareBoard.Title": "Compartilhar quadro",
"PersonProperty.board-members": "Membros do Board",
"PersonProperty.me": "Eu",
"PersonProperty.non-board-members": "Não membros do board",
"PropertyMenu.Delete": "Excluir",
"PropertyMenu.changeType": "Alterar tipo da propriedade",
@ -235,6 +247,7 @@
"Sidebar.import-archive": "Importar arquivo",
"Sidebar.invite-users": "Convidar usuários",
"Sidebar.logout": "Sair",
"Sidebar.new-category.badge": "Novo",
"Sidebar.no-boards-in-category": "Nenhum board",
"Sidebar.product-tour": "Tour pelo produto",
"Sidebar.random-icons": "Ícones aleatórios",
@ -257,6 +270,7 @@
"SidebarTour.SidebarCategories.Body": "Todos seus boards agora são organizados sob sua nova barra lateral. Não é mais necessárioa alternar entre espaços de trabalho. Categorias personalizadas em suas estações prévias de trabalho foram automaticamente criadas para você como parte do seu upgrade para v7.2. Estas podem ser removidas ou editadas de acordo com a sua preferência.",
"SidebarTour.SidebarCategories.Link": "Saiba mais",
"SidebarTour.SidebarCategories.Title": "Categorias de barra lateral",
"SiteStats.total_boards": "Total de boards",
"TableComponent.add-icon": "Adicionar Ícone",
"TableComponent.name": "Nome",
"TableComponent.plus-new": "+ Novo",
@ -267,6 +281,7 @@
"TableHeaderMenu.insert-right": "Inserir à direita",
"TableHeaderMenu.sort-ascending": "Ordem ascendente",
"TableHeaderMenu.sort-descending": "Ordem descendente",
"TableRow.MoreOption": "Mais ações",
"TableRow.open": "Abrir",
"TopBar.give-feedback": "Dar feedback",
"URLProperty.copiedLink": "Copiado!",
@ -348,6 +363,7 @@
"calendar.month": "Mês",
"calendar.today": "HOJE",
"calendar.week": "Semana",
"centerPanel.unknown-user": "Usuário desconhecido",
"cloudMessage.learn-more": "Saiba mais",
"createImageBlock.failed": "Não foi possível enviar o arquivo. Limite de tamanho alcançado.",
"default-properties.badges": "Comentários e descrição",
@ -369,6 +385,7 @@
"login.log-in-button": "Entrar",
"login.log-in-title": "Entrar",
"login.register-button": "ou criar uma conta se você ainda não tiver uma",
"new_channel_modal.create_board.select_template_placeholder": "Selecionar um modelo",
"notification-box-card-limit-reached.close-tooltip": "Soneca por 10 dias",
"notification-box-card-limit-reached.contact-link": "notificar seu admin",
"notification-box-card-limit-reached.link": "Atualizar para um plano pago",
@ -388,14 +405,14 @@
"rhs-boards.dm": "DM",
"rhs-boards.gm": "GM",
"rhs-boards.header.dm": "esta Direct Message",
"rhs-boards.header.gm": "Este Gruop Message",
"rhs-boards.header.gm": "esta mensagem de grupo",
"rhs-boards.last-update-at": "Última atualização em: {datetime}",
"rhs-boards.link-boards-to-channel": "Vincular boards para {channelName}",
"rhs-boards.linked-boards": "Boards vinculados",
"rhs-boards.no-boards-linked-to-channel": "Nenhum board está vinculado a {channelName} ainda",
"rhs-boards.no-boards-linked-to-channel-description": "Boards é uma ferramenta de gerenciamento de projeto que ajuda a definir, organizar, rastrear e gerenciar o trabalho entre times, usando uma visualização de quadro estilo Kaban familiar.",
"rhs-boards.unlink-board": "Desvincular board",
"rhs-boards.unlink-board1": "Desvincular board Hello",
"rhs-boards.unlink-board1": "Desvincular board",
"rhs-channel-boards-header.title": "Boards",
"share-board.publish": "Publicar",
"share-board.share": "Compartilhar",

View File

@ -2,14 +2,14 @@
"AppBar.Tooltip": "Переключить связанные доски",
"Attachment.Attachment-title": "Вложение",
"AttachmentBlock.DeleteAction": "Удалить",
"AttachmentBlock.addElement": "добавить",
"AttachmentBlock.delete": "Вложение успешно удалено.",
"AttachmentBlock.failed": "Не удалось загрузить файл. Достигнут предел размера вложения.",
"AttachmentBlock.addElement": "добавить {type}",
"AttachmentBlock.delete": "Вложение удалено.",
"AttachmentBlock.failed": "Не удалось загрузить файл, так как превышена квота на размер файла.",
"AttachmentBlock.upload": "Загрузка вложения.",
"AttachmentBlock.uploadSuccess": "Вложение успешно загружено.",
"AttachmentBlock.uploadSuccess": "Вложение загружено.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Удалить",
"AttachmentElement.download": "Скачать",
"AttachmentElement.upload-percentage": "Загрузка",
"AttachmentElement.upload-percentage": "Загрузка...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Добавить группу",
"BoardComponent.delete": "Удалить",
"BoardComponent.hidden-columns": "Скрытые столбцы",
@ -31,12 +31,12 @@
"BoardTemplateSelector.delete-template": "Удалить",
"BoardTemplateSelector.description": "Добавьте доску на боковую панель, используя любой из шаблонов, описанных ниже, или начните с нуля.",
"BoardTemplateSelector.edit-template": "Изменить",
"BoardTemplateSelector.plugin.no-content-description": "Добавьте доску на боковую панель, используя любой из указанных ниже шаблонов, или начните с нуля.{lineBreak} Участники \"{teamName}\" будут иметь доступ к созданным здесь доскам.",
"BoardTemplateSelector.plugin.no-content-description": "Добавьте доску на боковую панель, используя любой из указанных ниже шаблонов, или начните с нуля.",
"BoardTemplateSelector.plugin.no-content-title": "Создать доску",
"BoardTemplateSelector.title": "Создать доску",
"BoardTemplateSelector.use-this-template": "Использовать этот шаблон",
"BoardsSwitcher.Title": "Найти доски",
"BoardsUnfurl.Limited": "Информация скрыта в связи с тем, что карточка находится в архиве",
"BoardsUnfurl.Limited": "Информация скрыта, потому что карточка находится в архиве",
"BoardsUnfurl.Remainder": "+{remainder} ещё",
"BoardsUnfurl.Updated": "Обновлено {time}",
"Calculations.Options.average.displayName": "Среднее",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "{propertyName} успешно удалено!",
"CardDetailProperty.property-name-change-subtext": "тип из \"{oldPropType}\" в \"{newPropType}\"",
"CardDetial.limited-link": "Узнайте больше о наших планах.",
"CardDialog.delete-confirmation-dialog-attachment": "Подтвердите удаление вложения!",
"CardDialog.delete-confirmation-dialog-attachment": "Подтвердите удаление вложения",
"CardDialog.delete-confirmation-dialog-button-text": "Удалить",
"CardDialog.delete-confirmation-dialog-heading": "Подтвердите удаление карточки!",
"CardDialog.delete-confirmation-dialog-heading": "Подтвердите удаление карточки",
"CardDialog.editing-template": "Вы редактируете шаблон.",
"CardDialog.nocard": "Эта карточка не существует или недоступна.",
"Categories.CreateCategoryDialog.CancelText": "Отмена",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Обновить",
"CenterPanel.Login": "Логин",
"CenterPanel.Share": "Поделиться",
"ChannelIntro.CreateBoard": "Создать доску",
"CloudMessage.cloud-server": "Получите свой бесплатный облачный сервер.",
"ColorOption.selectColor": "Выберите цвет {color}",
"Comment.delete": "Удалить",
"CommentsList.send": "Отправить",
"ConfirmPerson.empty": "Пусто",
"ConfirmPerson.search": "Поиск...",
"ConfirmationDialog.cancel-action": "Отмена",
"ConfirmationDialog.confirm-action": "Подтвердить",
"ContentBlock.Delete": "Удалить",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "фильтровать текст",
"FilterComponent.add-filter": "+ Добавить фильтр",
"FilterComponent.delete": "Удалить",
"FilterValue.empty": "(пусто)",
"FindBoardsDialog.IntroText": "Поиск досок",
"FindBoardsDialog.NoResultsFor": "Нет результатов для \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Проверьте правильность написания или попробуйте другой запрос.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Добавить комментарии",
"OnboardingTour.AddDescription.Body": "Добавьте описание к своей карточке, чтобы Ваши коллеги по команде знали, о чем эта карточка.",
"OnboardingTour.AddDescription.Title": "Добавить описание",
"OnboardingTour.AddProperties.Body": "Добавляйте различные свойства карточкам, чтобы сделать их более мощными!",
"OnboardingTour.AddProperties.Body": "Добавляйте различные свойства карточкам, чтобы сделать их более значительными.",
"OnboardingTour.AddProperties.Title": "Добавить свойства",
"OnboardingTour.AddView.Body": "Перейдите сюда, чтобы создать новый вид для организации доски с использованием различных макетов.",
"OnboardingTour.AddView.Title": "Добавить новый вид",
@ -284,6 +288,7 @@
"TableHeaderMenu.insert-right": "Вставить справа",
"TableHeaderMenu.sort-ascending": "Сортировать по возрастанию",
"TableHeaderMenu.sort-descending": "Сортировать по убыванию",
"TableRow.DuplicateCard": "дублировать карточку",
"TableRow.MoreOption": "Больше действий",
"TableRow.open": "Открыть",
"TopBar.give-feedback": "Дать обратную связь",
@ -351,10 +356,13 @@
"WelcomePage.Explore.Button": "Исследовать",
"WelcomePage.Heading": "Добро пожаловать на Доски",
"WelcomePage.NoThanks.Text": "Нет спасибо, сам разберусь",
"WelcomePage.StartUsingIt.Text": "Начать пользоваться",
"Workspace.editing-board-template": "Вы редактируете шаблон доски.",
"badge.guest": "Гость",
"boardSelector.confirm-link-board": "Привязать доску к каналу",
"boardSelector.confirm-link-board-button": "Да, ссылка доски",
"boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с этим каналом даст всем участникам этого канала доступ на редактирование доски. Вы уверены, что хотите связать это?",
"boardSelector.confirm-link-board-subtext": "Связывание доски \"{boardName}\" с каналом даст всем участникам канала доступ на редактирование доски. Вы можете в любое время отвязать доску о канала.",
"boardSelector.confirm-link-board-subtext-with-other-channel": "Привязка \"{boardName}\" с каналом приведет к возможности её редактирования всеми участниками канала (существующими и новыми). Кроме гостей канала.{lineBreak} Эта доска сейчас связана с другим каналом. Он будет отключен, если вы решите изменить привязку.",
"boardSelector.create-a-board": "Создать доску",
"boardSelector.link": "Ссылка",
"boardSelector.search-for-boards": "Поиск досок",
@ -363,6 +371,8 @@
"calendar.month": "Месяц",
"calendar.today": "СЕГОДНЯ",
"calendar.week": "Неделя",
"centerPanel.undefined": "Отсутствует {propertyName}",
"centerPanel.unknown-user": "Неизвестный пользователь",
"cloudMessage.learn-more": "Учить больше",
"createImageBlock.failed": "Не удалось загрузить файл. Достигнут предел размера файла.",
"default-properties.badges": "Комментарии и описание",
@ -377,11 +387,15 @@
"error.team-undefined": "Не корректная команда.",
"error.unknown": "Произошла ошибка.",
"generic.previous": "Предыдущий",
"imagePaste.upload-failed": "Некоторые файлы не загружены. Достигнут предел размера файла",
"imagePaste.upload-failed": "Некоторые файлы не загружены из-за превышения квоты на размер файла.",
"limitedCard.title": "Карточки скрыты",
"login.log-in-button": "Вход в систему",
"login.log-in-title": "Вход в систему",
"login.register-button": "или создать аккаунт, если у Вас его нет",
"new_channel_modal.create_board.empty_board_description": "Создать новую пустую доску",
"new_channel_modal.create_board.empty_board_title": "Пустая доска",
"new_channel_modal.create_board.select_template_placeholder": "Выбрать шаблон",
"new_channel_modal.create_board.title": "Создать доску для этого канала",
"notification-box-card-limit-reached.close-tooltip": "Отложить на 10 дней",
"notification-box-card-limit-reached.contact-link": "уведомить Вашего администратора",
"notification-box-card-limit-reached.link": "Перейти на платный тариф",
@ -389,13 +403,18 @@
"notification-box-cards-hidden.title": "Это действие скрыло другую карточку",
"notification-box.card-limit-reached.not-admin.text": "Чтобы получить доступ к архивным карточкам, Вы можете {contactLink} перейти на платный тариф.",
"notification-box.card-limit-reached.text": "Достигнут лимит карточки, чтобы просмотреть старые карточки, {link}",
"person.add-user-to-board": "Добавить {username} на доску",
"person.add-user-to-board-confirm-button": "Добавить доску",
"person.add-user-to-board-permissions": "Разрешения",
"person.add-user-to-board-question": "Вы хотите добавить {username} на доску?",
"register.login-button": "или войти в систему, если у вас уже есть аккаунт",
"register.signup-title": "Зарегистрируйте свой аккаунт",
"rhs-board-non-admin-msg": "Вы не являетесь администратором этой доски",
"rhs-boards.add": "Добавить",
"rhs-boards.last-update-at": "Последнее обновление: {datetime}",
"rhs-boards.link-boards-to-channel": "Связать доски с {channelName}",
"rhs-boards.linked-boards": "Связанные доски",
"rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски.",
"rhs-boards.no-boards-linked-to-channel": "К каналу {channelName} пока не подключены доски",
"rhs-boards.no-boards-linked-to-channel-description": "Доски — это инструмент управления проектами, который помогает определять, организовывать, отслеживать и управлять работой между командами, используя знакомое представление доски Канбан.",
"rhs-boards.unlink-board": "Отвязать доску",
"rhs-channel-boards-header.title": "Доски",

View File

@ -1,14 +1,40 @@
{
"Attachment.Attachment-title": "Príloha",
"AttachmentBlock.DeleteAction": "odstrániť",
"AttachmentBlock.delete": "Príloha odstránená.",
"AttachmentBlock.failed": "Tento súbor nebol nahratý, pretože presiahol veľkostný limit.",
"AttachmentBlock.upload": "Príloha sa nahráva.",
"AttachmentBlock.uploadSuccess": "Príloha bola nahratá.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Odstrániť",
"AttachmentElement.download": "Stiahnuť",
"AttachmentElement.upload-percentage": "Nahrávam... ({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Pridaj skupinu",
"BoardComponent.delete": "Mazať",
"BoardComponent.hidden-columns": "Skryté stľpce",
"BoardComponent.delete": "Odstrániť",
"BoardComponent.hidden-columns": "Skryté stĺpce",
"BoardComponent.hide": "Skryť",
"BoardComponent.new": "+ Nový",
"BoardComponent.no-property": "žiadna {property}",
"BoardComponent.no-property-title": "Položky s prázdnou {property} pôjdu tu. Tento stĺpec nemožno vymazať.",
"BoardComponent.show": "Ukáž",
"BoardMember.schemeAdmin": "Administrátor",
"BoardMember.schemeCommenter": "Komentátor",
"BoardMember.schemeEditor": "Editor",
"BoardMember.schemeNone": "Žiadny",
"BoardMember.schemeViewer": "Sledovateľ",
"BoardMember.unlinkChannel": "Odpojiť",
"BoardPage.newVersion": "Nová verzia je dostupná, kliknite tu pre znovu načítanie.",
"BoardPage.syncFailed": "Nástenka môže byť vymazaná, alebo prístup odobraný.",
"BoardPage.syncFailed": "Nástenka môže byť vymazaná alebo prístup odobraný.",
"BoardTemplateSelector.add-template": "Vytvoriť novú šablónu",
"BoardTemplateSelector.create-empty-board": "Vytvoriť prázdnu nástenku",
"BoardTemplateSelector.delete-template": "Odstrániť",
"BoardTemplateSelector.description": "Pridajte nástenku do bočného panelu pomocou ktorýchkoľvek šablón definovaných dole alebo začnite od začiatku.",
"BoardTemplateSelector.edit-template": "Upraviť",
"BoardTemplateSelector.plugin.no-content-description": "Pridajte nástenku do bočného panelu pomocou ktorýchkoľvek šablón dole alebo začnite od začiatku.",
"BoardTemplateSelector.plugin.no-content-title": "Vytvoriť nástenku",
"BoardTemplateSelector.title": "Vytvoriť nástenku",
"BoardTemplateSelector.use-this-template": "Použiť túto šablónu",
"BoardsSwitcher.Title": "Hľadať nástenky",
"BoardsUnfurl.Limited": "Ďalšie detaily sú skryté, pretože je karta archivovaná",
"BoardsUnfurl.Remainder": "+{remainder} viac",
"BoardsUnfurl.Updated": "Upravené {time}",
"Calculations.Options.average.displayName": "Priemer",
@ -16,13 +42,13 @@
"Calculations.Options.count.displayName": "Počet",
"Calculations.Options.count.label": "Počet",
"Calculations.Options.countChecked.displayName": "Označené",
"Calculations.Options.countChecked.label": "Spočítaj označené",
"Calculations.Options.countChecked.label": "Počítať označené",
"Calculations.Options.countUnchecked.displayName": "Neoznačené",
"Calculations.Options.countUnchecked.label": "Spočítaj neoznačené",
"Calculations.Options.countUnchecked.label": "Počítať neoznačené",
"Calculations.Options.countUniqueValue.displayName": "Unikátne",
"Calculations.Options.countUniqueValue.label": "Spočítaj unikátne hodnoty",
"Calculations.Options.countUniqueValue.label": "Počítať unikátne hodnoty",
"Calculations.Options.countValue.displayName": "Hodnoty",
"Calculations.Options.countValue.label": "Spočítaj hodnoty",
"Calculations.Options.countValue.label": "Počítať hodnotu",
"Calculations.Options.dateRange.displayName": "Rozsah",
"Calculations.Options.dateRange.label": "Rozsah",
"Calculations.Options.earliest.displayName": "Prvý",
@ -31,76 +57,130 @@
"Calculations.Options.latest.label": "Posledný",
"Calculations.Options.max.displayName": "Max",
"Calculations.Options.max.label": "Max",
"Calculations.Options.median.displayName": "Median",
"Calculations.Options.median.label": "Median",
"Calculations.Options.median.displayName": "Medián",
"Calculations.Options.median.label": "Medián",
"Calculations.Options.min.displayName": "Min",
"Calculations.Options.min.label": "Min",
"Calculations.Options.none.displayName": "Vypočítaj",
"Calculations.Options.none.displayName": "Vypočítať",
"Calculations.Options.none.label": "Nič",
"Calculations.Options.percentChecked.displayName": "Skontrolované",
"Calculations.Options.percentChecked.label": "Percent Skontrolovaných",
"Calculations.Options.percentChecked.displayName": "Označené",
"Calculations.Options.percentChecked.label": "Percent skontrolovaných",
"Calculations.Options.percentUnchecked.displayName": "Neskontrolované",
"Calculations.Options.percentUnchecked.label": "Percent neskontrolovaných",
"Calculations.Options.range.displayName": "Rozsah",
"Calculations.Options.range.label": "Rozsah",
"Calculations.Options.sum.displayName": "Súčet",
"Calculations.Options.sum.label": "Súčet",
"CardDetail.Follow": "Sleduj",
"CardDetail.Following": "Sledujúce",
"CardDetail.add-content": "Pridaj obsah",
"CardDetail.add-icon": "Pridaj ikonu",
"CardDetail.add-property": "+ Pridaj vlastnosť",
"CardDetail.addCardText": "Pridaj text karty",
"CardDetail.moveContent": "presuň obsah karty",
"CardDetail.new-comment-placeholder": "Pridaj komentár ...",
"CardDetailProperty.confirm-delete-heading": "Potvrď vymazanie vlastnosti",
"CardDetailProperty.confirm-delete-subtext": "Skutočne chcete vymazať \"{propertyName}\"? Mazaním ju odstránite zo všetkých kariet na tabuli.",
"CardDetailProperty.confirm-property-name-change-subtext": "Skutočne chcete vymazať \"{propertyName}\" {customText}? Ovplyvní to hodnoty na {numOfCards} kartách na tabuli, a môže viesť k strate dát.",
"CardDetailProperty.confirm-property-type-change": "Potvrď zmenu typu vlastnosti!",
"CalendarCard.untitled": "Bez názvu",
"CardActionsMenu.copiedLink": "Skopírované!",
"CardActionsMenu.copyLink": "Skopírovať odkaz",
"CardActionsMenu.delete": "Odstrániť",
"CardActionsMenu.duplicate": "Duplikovať",
"CardBadges.title-checkboxes": "Začiarkávacie políčka",
"CardBadges.title-comments": "Komentáre",
"CardBadges.title-description": "Táto karta má popis",
"CardDetail.Attach": "Priložiť",
"CardDetail.Follow": "Sledovať",
"CardDetail.Following": "Sledujúci",
"CardDetail.add-content": "Pridať obsah",
"CardDetail.add-icon": "Pridať ikonu",
"CardDetail.add-property": "+ Pridať vlastnosť",
"CardDetail.addCardText": "pridať text karty",
"CardDetail.limited-body": "Vylepšiť na náš Professional alebo Enterprise plán.",
"CardDetail.limited-button": "Zmeniť plán",
"CardDetail.limited-title": "Táto karta je skrytá",
"CardDetail.moveContent": "Presunúť obsah karty",
"CardDetail.new-comment-placeholder": "Pridať komentár...",
"CardDetailProperty.confirm-delete-heading": "Potvrdiť vymazanie vlastnosti",
"CardDetailProperty.confirm-delete-subtext": "Skutočne chcete vymazať hodnotu \"{propertyName}\"? Bude odstránená zo všetkých kariet na tejto tabuli.",
"CardDetailProperty.confirm-property-name-change-subtext": "Skutočne chcete zmeniť hodnotu \"{propertyName}\" {customText}? Ovplyvní to {numOfCards} kariet na tabuli a môže viesť k strate dát.",
"CardDetailProperty.confirm-property-type-change": "Potvrdiť zmenu typu vlastnosti",
"CardDetailProperty.delete-action-button": "Odstrániť",
"CardDetailProperty.property-change-action-button": "Zmeniť vlastnosť",
"CardDetailProperty.property-changed": "Zmena vlastnosti úspešná!",
"CardDetailProperty.property-deleted": "Mazanie {propertyName} úspešné!",
"CardDetailProperty.property-name-change-subtext": "typ od \"{oldPropType}\" do \"{newPropType}\"",
"CardDialog.editing-template": "Editujete template.",
"CardDialog.nocard": "Karta neexistuje alebo je neprístupná.",
"ColorOption.selectColor": "Vyber {color} farbu",
"CardDetailProperty.property-deleted": "Odstránenie {propertyName} úspešné!",
"CardDetailProperty.property-name-change-subtext": "typ z \"{oldPropType}\" na \"{newPropType}\"",
"CardDetial.limited-link": "Dozvedieť sa viac o našich plánoch.",
"CardDialog.delete-confirmation-dialog-attachment": "Potvrdiť odstránenie prílohy",
"CardDialog.delete-confirmation-dialog-button-text": "Odstrániť",
"CardDialog.delete-confirmation-dialog-heading": "Potvrdiť odstránenie karty",
"CardDialog.editing-template": "Upravujete šablónu.",
"CardDialog.nocard": "Táto karta neexistuje alebo nie je prístupná.",
"Categories.CreateCategoryDialog.CancelText": "Zrušiť",
"Categories.CreateCategoryDialog.CreateText": "Vytvoriť",
"Categories.CreateCategoryDialog.Placeholder": "Nazvite Vašu kategóriu",
"Categories.CreateCategoryDialog.UpdateText": "Zmeniť",
"CenterPanel.Login": "Prihlásiť sa",
"CenterPanel.Share": "Zdieľať",
"ChannelIntro.CreateBoard": "Vytvoriť nástenku",
"CloudMessage.cloud-server": "Získajte vlastný cloudový server zadarmo.",
"ColorOption.selectColor": "Vyberte {color} farbu",
"Comment.delete": "Odstrániť",
"CommentsList.send": "Poslať",
"CommentsList.send": "Odoslať",
"ConfirmPerson.empty": "Prázdne",
"ConfirmPerson.search": "Vyhľadať...",
"ConfirmationDialog.cancel-action": "Zrušiť",
"ConfirmationDialog.confirm-action": "Potvrdiť",
"ContentBlock.Delete": "Odstrániť",
"ContentBlock.DeleteAction": "Odstrániť",
"ContentBlock.addElement": "pridaj {type}",
"ContentBlock.checkbox": "checkbox",
"ContentBlock.DeleteAction": "odstrániť",
"ContentBlock.addElement": "pridať {type}",
"ContentBlock.checkbox": "začiarkávacie pole",
"ContentBlock.divider": "oddeľovač",
"ContentBlock.editCardCheckbox": "označený-checkbox",
"ContentBlock.editCardCheckbox": "Začiarknuté pole",
"ContentBlock.editCardCheckboxText": "upraviť text karty",
"ContentBlock.editCardText": "upraviť text karty",
"ContentBlock.editText": "Upraviť text...",
"ContentBlock.image": "obrázok",
"ContentBlock.insertAbove": "vlož nad",
"ContentBlock.moveDown": "Presuň dole",
"ContentBlock.moveUp": "Presuň hore",
"ContentBlock.insertAbove": "Vložiť nad",
"ContentBlock.moveBlock": "presunúť obsah karty",
"ContentBlock.moveDown": "Presunúť dole",
"ContentBlock.moveUp": "Presunúť hore",
"ContentBlock.text": "text",
"DateRange.clear": "Vyčistiť",
"DateRange.empty": "Prázdny",
"DateRange.endDate": "Koncový dátum",
"DateRange.today": "Dnes",
"DeleteBoardDialog.confirm-cancel": "Zrušiť",
"DeleteBoardDialog.confirm-delete": "Odstrániť",
"DeleteBoardDialog.confirm-info": "Naozaj chcete odstrániť nástenku “{boardTitle}”? Odstránením vymažete všetky karty na tabuli.",
"DeleteBoardDialog.confirm-info-template": "Naozaj chcete odstrániť nástenkovú šablónu \"{boardTitle}\"?",
"DeleteBoardDialog.confirm-tite": "Potvrďte odstránenie nástenky",
"Dialog.closeDialog": "Zavrieť dialog",
"DeleteBoardDialog.confirm-tite-template": "Potvrdiť odstránenie šablóny nástenky",
"Dialog.closeDialog": "Zatvoriť dialógové okno",
"EditableDayPicker.today": "Dnes",
"Error.mobileweb": "Mobile web support is currently in early beta. Not all functionality may be present.",
"Error.websocket-closed": "Websocket pripojenie zlyhalo - prerušené. Skontrolujte konfiguráciu servera ak problém pretrváva.",
"Error.mobileweb": "Podpora pre mobilné prehliadače je v skorej bete. Niektoré funkcionality môžu chýbať.",
"Error.websocket-closed": "Websocket pripojenie zlyhalo - bolo prerušené. Pokiaľ problém pretrváva, skontrolujte konfiguráciu servera.",
"Filter.contains": "obsahuje",
"Filter.ends-with": "končí s",
"Filter.includes": "zahŕňa",
"Filter.is": "je",
"Filter.is-empty": "je prázdny",
"Filter.is-not-empty": "nie je prázdny",
"Filter.is-not-set": "nie je nastavený",
"Filter.is-set": "je nastavený",
"Filter.not-contains": "neobsahuje",
"Filter.not-ends-with": "nekončí s",
"Filter.not-includes": "nezahŕňa",
"Filter.not-starts-with": "nezačína s",
"Filter.starts-with": "začína s",
"FilterByText.placeholder": "text filtra",
"FilterComponent.add-filter": "+ Pridaj filter",
"FilterComponent.delete": "Odstrániť",
"FilterValue.empty": "(prázdny)",
"FindBoardsDialog.IntroText": "Vyhľadať nástenky",
"FindBoardsDialog.NoResultsFor": "Žiadne výsledky pre \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Skontrolujte pravopis alebo vyskúšajte iný pojem.",
"FindBoardsDialog.SubTitle": "Nájdite nástenku písaním. Použite <b>HORE/DOLE</b> na prehliadanie, <b>ENTER</b> na vybratie a <b>ESC</b> na zrušenie",
"FindBoardsDialog.Title": "Nájsť nástenky",
"GroupBy.hideEmptyGroups": "Skryť {count} prázdnych skupín",
"GroupBy.showHiddenGroups": "Zobraziť {count} prázdnych skupín",
"GroupBy.ungroup": "Zrušiť zoskupenie",
"KanbanCard.untitled": "Nepomenované",
"Mutator.new-card-from-template": "nová karta z template-u",
"Mutator.new-template-from-card": "nový template z karty",
"HideBoard.MenuOption": "Skryť nástenku",
"KanbanCard.untitled": "Bez názvu",
"MentionSuggestion.is-not-board-member": "(nie je členom nástenky)",
"Mutator.new-board-from-template": "nová nástenka zo šablóny",
"Mutator.new-card-from-template": "nová karta zo šablóny",
"Mutator.new-template-from-card": "nová šablóna z karty",
"PropertyMenu.Delete": "Odstrániť",
"PropertyMenu.changeType": "Zmeniť vlastnosť",
"PropertyMenu.selectType": "Vybrať vlastnosť",

View File

@ -1,4 +1,7 @@
{
"AppBar.Tooltip": "Chuyển sang các bảng đã liên kết",
"Attachment.Attachment-title": "Đính kèm",
"AttachmentBlock.DeleteAction": "xóa",
"BoardComponent.add-a-group": "+ Thêm nhóm",
"BoardComponent.delete": "Xóa",
"BoardComponent.hidden-columns": "Cột ẩn",

View File

@ -1,12 +1,12 @@
{
"name": "focalboard",
"version": "7.9.0",
"version": "7.10.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "focalboard",
"version": "7.9.0",
"version": "7.10.0",
"dependencies": {
"@draft-js-plugins/editor": "^4.1.2",
"@draft-js-plugins/emoji": "^4.6.0",

View File

@ -1,6 +1,6 @@
{
"name": "focalboard",
"version": "7.9.0",
"version": "7.10.0",
"private": true,
"description": "",
"scripts": {

View File

@ -10,7 +10,7 @@ exports[`components/sidebar/GlobalHeader header menu should match snapshot 1`] =
/>
<a
class="GlobalHeaderComponent__button help-button"
href="https://www.focalboard.com/fwlink/doc-boards.html?v=7.9.0"
href="https://www.focalboard.com/fwlink/doc-boards.html?v=7.10.0"
rel="noreferrer"
target="_blank"
>

View File

@ -51,9 +51,9 @@ exports[`components/sidebarSidebar dont show hidden boards 1`] = `
>
<div
class="version"
title="v7.9.0"
title="v7.10.0"
>
v7.9.0
v7.10.0
</div>
</div>
</div>
@ -252,9 +252,9 @@ exports[`components/sidebarSidebar should assign default category if current boa
>
<div
class="version"
title="v7.9.0"
title="v7.10.0"
>
v7.9.0
v7.10.0
</div>
</div>
</div>
@ -508,9 +508,9 @@ exports[`components/sidebarSidebar shouldnt do any category assignment is board
>
<div
class="version"
title="v7.9.0"
title="v7.10.0"
>
v7.9.0
v7.10.0
</div>
</div>
</div>
@ -919,9 +919,9 @@ exports[`components/sidebarSidebar sidebar hidden 1`] = `
>
<div
class="version"
title="v7.9.0"
title="v7.10.0"
>
v7.9.0
v7.10.0
</div>
</div>
</div>
@ -1213,9 +1213,9 @@ exports[`components/sidebarSidebar some categories hidden 1`] = `
>
<div
class="version"
title="v7.9.0"
title="v7.10.0"
>
v7.9.0
v7.10.0
</div>
</div>
</div>

View File

@ -6,6 +6,10 @@
margin-top: 0;
}
.dialog {
color: rgba(var(--center-channel-color-rgb));
}
.octo-sidebar-item {
display: flex;
flex-direction: row;

View File

@ -203,6 +203,7 @@
width: inherit;
}
.MultiPerson.octo-propertyvalue,
.Person.octo-propertyvalue,
.DateRange.octo-propertyvalue {
overflow: unset;

View File

@ -37,8 +37,8 @@ class Constants {
static readonly titleColumnId = '__title'
static readonly badgesColumnId = '__badges'
static readonly versionString = '7.9.0'
static readonly versionDisplayString = 'Mar 2023'
static readonly versionString = '7.10.0'
static readonly versionDisplayString = 'Apr 2023'
static readonly archiveHelpPage = 'https://docs.mattermost.com/boards/migrate-to-boards.html'
static readonly imports = [

View File

@ -18,32 +18,40 @@ exports[`pages/welcome Welcome Page shows Explore Page 1`] = `
>
Boards is a project management tool that helps define, organize, track, and manage work across teams using a familiar Kanban board view.
</div>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large"
src="test-file-stub"
/>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small"
src="test-file-stub"
/>
<button
class="Button filled size--large"
type="button"
<div
class="WelcomePage__content"
>
<span>
Take a tour
</span>
<i
class="CompassIcon icon-chevron-right Icon Icon--right"
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large"
src="test-file-stub"
/>
</button>
<a
class="skip"
>
No thanks, I'll figure it out myself
</a>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small"
src="test-file-stub"
/>
<div
class="WelcomePage__buttons"
>
<button
class="Button filled size--large"
type="button"
>
<span>
Take a tour
</span>
<i
class="CompassIcon icon-chevron-right Icon Icon--right"
/>
</button>
<a
class="skip"
>
No thanks, I'll figure it out myself
</a>
</div>
</div>
</div>
</div>
</div>
@ -67,32 +75,40 @@ exports[`pages/welcome Welcome Page shows Explore Page with subpath 1`] = `
>
Boards is a project management tool that helps define, organize, track, and manage work across teams using a familiar Kanban board view.
</div>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large"
src="test-file-stub"
/>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small"
src="test-file-stub"
/>
<button
class="Button filled size--large"
type="button"
<div
class="WelcomePage__content"
>
<span>
Take a tour
</span>
<i
class="CompassIcon icon-chevron-right Icon Icon--right"
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--large"
src="test-file-stub"
/>
</button>
<a
class="skip"
>
No thanks, I'll figure it out myself
</a>
<img
alt="Boards Welcome Image"
class="WelcomePage__image WelcomePage__image--small"
src="test-file-stub"
/>
<div
class="WelcomePage__buttons"
>
<button
class="Button filled size--large"
type="button"
>
<span>
Take a tour
</span>
<i
class="CompassIcon icon-chevron-right Icon Icon--right"
/>
</button>
<a
class="skip"
>
No thanks, I'll figure it out myself
</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -10,6 +10,26 @@
@media (max-height: 768px) {
justify-content: flex-start;
height: auto;
padding-top: 40px;
}
.WelcomePage__content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
@media (max-height: 800px) {
flex-direction: column-reverse;
margin-top: 16px;
}
}
.WelcomePage__buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
> div {
@ -34,7 +54,6 @@
}
.skip {
margin-top: 12px;
color: rgba(var(--link-color-rgb), 1);
cursor: pointer;

View File

@ -127,59 +127,63 @@ const WelcomePage = () => {
/>
</div>
{/* This image will be rendered on large screens over 2000px */}
<img
src={Utils.buildURL(BoardWelcomePNG, true)}
className='WelcomePage__image WelcomePage__image--large'
alt='Boards Welcome Image'
/>
<div className='WelcomePage__content'>
{/* This image will be rendered on large screens over 2000px */}
<img
src={Utils.buildURL(BoardWelcomePNG, true)}
className='WelcomePage__image WelcomePage__image--large'
alt='Boards Welcome Image'
/>
{/* This image will be rendered on small screens below 2000px */}
<img
src={Utils.buildURL(BoardWelcomeSmallPNG, true)}
className='WelcomePage__image WelcomePage__image--small'
alt='Boards Welcome Image'
/>
{/* This image will be rendered on small screens below 2000px */}
<img
src={Utils.buildURL(BoardWelcomeSmallPNG, true)}
className='WelcomePage__image WelcomePage__image--small'
alt='Boards Welcome Image'
/>
{me?.is_guest !== true &&
<Button
onClick={startTour}
filled={true}
size='large'
icon={
<CompassIcon
icon='chevron-right'
className='Icon Icon--right'
/>}
rightIcon={true}
>
<FormattedMessage
id='WelcomePage.Explore.Button'
defaultMessage='Take a tour'
/>
</Button>}
<div className='WelcomePage__buttons'>
{me?.is_guest !== true &&
<Button
onClick={startTour}
filled={true}
size='large'
icon={
<CompassIcon
icon='chevron-right'
className='Icon Icon--right'
/>}
rightIcon={true}
>
<FormattedMessage
id='WelcomePage.Explore.Button'
defaultMessage='Take a tour'
/>
</Button>}
{me?.is_guest !== true &&
<a
className='skip'
onClick={skipTour}
>
<FormattedMessage
id='WelcomePage.NoThanks.Text'
defaultMessage="No thanks, I'll figure it out myself"
/>
</a>}
{me?.is_guest === true &&
<Button
onClick={skipTour}
filled={true}
size='large'
>
<FormattedMessage
id='WelcomePage.StartUsingIt.Text'
defaultMessage='Start using it'
/>
</Button>}
{me?.is_guest !== true &&
<a
className='skip'
onClick={skipTour}
>
<FormattedMessage
id='WelcomePage.NoThanks.Text'
defaultMessage="No thanks, I'll figure it out myself"
/>
</a>}
{me?.is_guest === true &&
<Button
onClick={skipTour}
filled={true}
size='large'
>
<FormattedMessage
id='WelcomePage.StartUsingIt.Text'
defaultMessage='Start using it'
/>
</Button>}
</div>
</div>
</div>
</div>
)

View File

@ -34,6 +34,23 @@ exports[`properties/dateRange handle clear 1`] = `
</div>
`;
exports[`properties/dateRange returns component with new date after prop change 1`] = `
<div>
<div
class="DateRange octo-propertyvalue"
>
<button
class="Button"
type="button"
>
<span>
June 15
</span>
</button>
</div>
</div>
`;
exports[`properties/dateRange returns default correctly 1`] = `
<div>
<div

View File

@ -315,4 +315,36 @@ describe('properties/dateRange', () => {
expect(mockedMutator.changePropertyValue).toHaveBeenCalledWith(board.id, card, propertyTemplate.id, JSON.stringify({from: today}))
})
test('returns component with new date after prop change', () => {
const component = wrapIntl(
<DateProp
property={new DateProperty()}
propertyValue=''
showEmptyPlaceholder={false}
readOnly={false}
board={{...board}}
card={{...card}}
propertyTemplate={propertyTemplate}
/>,
)
const {container, rerender} = render(component)
rerender(
wrapIntl(
<DateProp
property={new DateProperty()}
propertyValue={'{"from": ' + June15.getTime().toString() + '}'}
showEmptyPlaceholder={false}
readOnly={false}
board={{...board}}
card={{...card}}
propertyTemplate={propertyTemplate}
/>,
),
)
expect(container).toMatchSnapshot()
})
})

View File

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo, useState, useCallback} from 'react'
import React, {useMemo, useState, useCallback, useEffect} from 'react'
import {useIntl} from 'react-intl'
import {DateUtils} from 'react-day-picker'
import MomentLocaleUtils from 'react-day-picker/moment'
@ -58,6 +58,12 @@ function DateRange(props: PropertyProps): JSX.Element {
const [value, setValue] = useState(propertyValue)
const intl = useIntl()
useEffect(() => {
if (value !== propertyValue) {
setValue(propertyValue)
}
}, [propertyValue, setValue])
const onChange = useCallback((newValue) => {
if (value !== newValue) {
setValue(newValue)

View File

@ -35,11 +35,14 @@ const URLProperty = (props: PropertyProps): JSX.Element => {
if (value !== (props.card.fields.properties[props.propertyTemplate?.id || ''] || '')) {
mutator.changePropertyValue(props.board.id, props.card, props.propertyTemplate?.id || '', value)
}
}, [props.card, props.propertyTemplate, value])
}, [props.board.id, props.card, props.propertyTemplate?.id, value])
const saveTextPropertyRef = useRef<() => void>(saveTextProperty)
saveTextPropertyRef.current = saveTextProperty
if (props.readOnly) {
saveTextPropertyRef.current = () => null
} else {
saveTextPropertyRef.current = saveTextProperty
}
useEffect(() => {
return () => {
saveTextPropertyRef.current && saveTextPropertyRef.current()

View File

@ -50,6 +50,7 @@ export const TelemetryActions = {
LimitCardLimitReached: 'limit_cardLimitReached',
LimitCardLimitLinkOpen: 'limit_cardLimitLinkOpen',
VersionMoreInfo: 'version_more_info',
ClickChannelsRHSBoard: 'click_board_in_channels_RHS',
}
interface IEventProps {