mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-02 14:47:55 +02:00
Merge branch 'mattermost:main' into patch-1
This commit is contained in:
commit
193d8dda7a
@ -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 .
|
||||
|
4
Makefile
4
Makefile
@ -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.
|
||||
|
@ -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": {
|
||||
|
5
mattermost-plugin/server/manifest.go
generated
5
mattermost-plugin/server/manifest.go
generated
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -109,6 +109,104 @@ exports[`components/rhsChannelBoards renders the RHS for channel boards 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders the RHS for channel boards, no add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards"
|
||||
>
|
||||
<div
|
||||
class="rhs-boards-header"
|
||||
>
|
||||
<span
|
||||
class="linked-boards"
|
||||
>
|
||||
Linked boards
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="rhs-boards-list"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 08, 2022, 8:10 PM
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="RHSChannelBoardItem"
|
||||
>
|
||||
<div
|
||||
class="board-info"
|
||||
>
|
||||
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Untitled board
|
||||
</span>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="IconButton"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="description"
|
||||
/>
|
||||
<div
|
||||
class="date"
|
||||
>
|
||||
Last update at: July 08, 2022, 8:10 PM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
@ -144,3 +242,31 @@ exports[`components/rhsChannelBoards renders with empty list of boards 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/rhsChannelBoards renders with empty list of boards, cannot add 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="RHSChannelBoards empty"
|
||||
>
|
||||
<h2>
|
||||
No boards are linked to Channel Name yet
|
||||
</h2>
|
||||
<div
|
||||
class="empty-paragraph"
|
||||
>
|
||||
Boards is a project management tool that helps define, organize, track and manage work across teams, using a familiar kanban board view.
|
||||
</div>
|
||||
<div
|
||||
class="boards-screenshots"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {act, render} from '@testing-library/react'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
import {mocked} from 'jest-mock'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
@ -44,6 +44,7 @@ describe('components/rhsChannelBoards', () => {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id',
|
||||
permissions: ['create_post']
|
||||
},
|
||||
},
|
||||
language: {
|
||||
@ -89,7 +90,8 @@ describe('components/rhsChannelBoards', () => {
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@ -107,6 +109,45 @@ describe('components/rhsChannelBoards', () => {
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders the RHS for channel boards, no add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Add')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with empty list of boards, cannot add', async () => {
|
||||
const localState = {...state, users: {me:{id: 'user-id'}}, boards: {...state.boards, boards: {}}}
|
||||
const store = mockStateStore([thunk], localState)
|
||||
|
||||
let container: Element | DocumentFragment | null = null
|
||||
await act(async () => {
|
||||
const result = render(wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<RHSChannelBoards/>
|
||||
</ReduxProvider>
|
||||
))
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = screen.queryByText('Link boards to Channel Name')
|
||||
expect(buttonElement).toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -49,7 +49,7 @@ const RHSChannelBoards = () => {
|
||||
dispatch(loadMyBoardsMemberships()),
|
||||
dispatch(fetchMe()),
|
||||
]).then(() => setDataLoaded(true))
|
||||
}, [])
|
||||
}, [currentChannel?.id])
|
||||
|
||||
useWebsockets(teamId || '', (wsClient: WSClient) => {
|
||||
const onChangeBoardHandler = (_: WSClient, boards: Board[]): void => {
|
||||
@ -117,6 +117,7 @@ const RHSChannelBoards = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className='boards-screenshots'><img src={Utils.buildURL(boardsScreenshots, true)}/></div>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
emphasis='primary'
|
||||
@ -128,6 +129,7 @@ const RHSChannelBoards = () => {
|
||||
values={{channelName: channelName}}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -143,6 +145,7 @@ const RHSChannelBoards = () => {
|
||||
defaultMessage='Linked boards'
|
||||
/>
|
||||
</span>
|
||||
{me?.permissions?.find((s) => s === 'create_post') &&
|
||||
<Button
|
||||
onClick={() => dispatch(setLinkToChannel(currentChannel.id))}
|
||||
icon={<AddIcon/>}
|
||||
@ -153,6 +156,7 @@ const RHSChannelBoards = () => {
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div className='rhs-boards-list'>
|
||||
{channelBoards.map((b) => (
|
||||
|
@ -249,6 +249,7 @@ export default class Plugin {
|
||||
if (lastViewedChannel !== currentChannel && currentChannel) {
|
||||
localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel)
|
||||
lastViewedChannel = currentChannel
|
||||
octoClient.channelId = currentChannel
|
||||
const currentChannelObj = mmStore.getState().entities.channels.channels[lastViewedChannel]
|
||||
store.dispatch(setChannel(currentChannelObj))
|
||||
}
|
||||
|
@ -2,6 +2,10 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.focalboard-body .RightControlsContainer-eacbOh {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.focalboard-body .feature-global-header>header {
|
||||
z-index: 1000;
|
||||
|
||||
|
@ -132,11 +132,18 @@ if (TARGET_IS_PRODUCT) {
|
||||
const sharedObject = {};
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
// Set both versions to false so that the version of this module provided by the web app will be used
|
||||
sharedObject[packageName] = {
|
||||
requiredVersion: false,
|
||||
|
||||
// Ensure only one copy of this package is ever loaded
|
||||
singleton: true,
|
||||
version: false,
|
||||
|
||||
// Set this to false to prevent Webpack from packaging any "fallback" version of this package so that
|
||||
// only the version provided by the web app will be used
|
||||
import: false,
|
||||
|
||||
// Set these to false so that any version provided by the web app will be accepted
|
||||
requiredVersion: false,
|
||||
version: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
@ -17,6 +20,7 @@ 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"
|
||||
)
|
||||
|
||||
// FileUploadResponse is the response to a file upload
|
||||
@ -119,37 +123,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.
|
||||
@ -166,7 +145,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
defer fileReader.Close()
|
||||
http.ServeContent(w, r, filename, time.Now(), fileReader)
|
||||
web.WriteFileResponse(filename, fileInfo.MimeType, fileInfo.Size, time.Now(), "", fileReader, false, w, r)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
|
@ -206,6 +206,11 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: allow_admin
|
||||
// in: path
|
||||
// description: allows admin users to join private boards
|
||||
// required: false
|
||||
// type: boolean
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -222,6 +227,9 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
query := r.URL.Query()
|
||||
allowAdmin := query.Has("allow_admin")
|
||||
|
||||
userID := getUserID(r)
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r, model.NewErrBadRequest("missing user ID"))
|
||||
@ -234,10 +242,15 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
if board.Type != model.BoardTypeOpen {
|
||||
if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
|
||||
return
|
||||
}
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
|
||||
@ -257,7 +270,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
newBoardMember := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin,
|
||||
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin,
|
||||
SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor,
|
||||
SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter,
|
||||
SchemeViewer: board.MinimumRole == model.BoardRoleViewer,
|
||||
|
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -15,6 +16,7 @@ func (a *API) registerTeamsRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST")
|
||||
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
|
||||
}
|
||||
|
||||
@ -257,3 +259,106 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("userCount", len(users))
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /teams/{teamID}/users getTeamUsersByID
|
||||
//
|
||||
// Returns a user[]
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Team ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: []UserIDs to return
|
||||
// required: true
|
||||
// type: []string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/User"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
requestBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var userIDs []string
|
||||
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
userID := getUserID(r)
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
|
||||
return
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
var error error
|
||||
|
||||
if len(userIDs) == 0 {
|
||||
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
|
||||
return
|
||||
}
|
||||
|
||||
if userIDs[0] == model.SingleUser {
|
||||
ws, _ := a.app.GetRootTeam()
|
||||
now := utils.GetMillis()
|
||||
user := &model.User{
|
||||
ID: model.SingleUser,
|
||||
Username: model.SingleUser,
|
||||
Email: model.SingleUser,
|
||||
CreateAt: ws.UpdateAt,
|
||||
UpdateAt: now,
|
||||
}
|
||||
users = append(users, user)
|
||||
} else {
|
||||
users, error = a.app.GetUsersList(userIDs)
|
||||
if error != nil {
|
||||
a.errorResponse(w, r, error)
|
||||
return
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usersList, err := json.Marshal(users)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonStringResponse(w, http.StatusOK, string(usersList))
|
||||
auditRec.Success()
|
||||
}
|
||||
|
@ -107,6 +107,17 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Team ID
|
||||
// required: false
|
||||
// type: string
|
||||
// - name: channelID
|
||||
// in: path
|
||||
// description: Channel ID
|
||||
// required: false
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -118,6 +129,9 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
query := r.URL.Query()
|
||||
teamID := query.Get("teamID")
|
||||
channelID := query.Get("channelID")
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
@ -146,6 +160,16 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionCreatePost.Id)
|
||||
}
|
||||
|
||||
userData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/notify"
|
||||
@ -309,14 +310,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e
|
||||
|
||||
for i := range copiedBlocks {
|
||||
block := copiedBlocks[i]
|
||||
fileName := ""
|
||||
isOk := false
|
||||
|
||||
fileName, ok := block.Fields["fileId"]
|
||||
if !ok || fileName == "" {
|
||||
continue // doesn't have a file attachment
|
||||
switch block.Type {
|
||||
case model.TypeImage:
|
||||
fileName, isOk = block.Fields["fileId"].(string)
|
||||
if !isOk || fileName == "" {
|
||||
continue
|
||||
}
|
||||
case model.TypeAttachment:
|
||||
fileName, isOk = block.Fields["attachmentId"].(string)
|
||||
if !isOk || fileName == "" {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// create unique filename in case we are copying cards within the same board.
|
||||
ext := filepath.Ext(fileName.(string))
|
||||
ext := filepath.Ext(fileName)
|
||||
destFilename := utils.NewID(utils.IDTypeNone) + ext
|
||||
|
||||
if destBoardID == "" || block.BoardID != destBoardID {
|
||||
@ -328,7 +341,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e
|
||||
destTeamID = destBoard.TeamID
|
||||
}
|
||||
|
||||
sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName.(string))
|
||||
sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName)
|
||||
destinationFilePath := filepath.Join(destTeamID, block.BoardID, destFilename)
|
||||
|
||||
a.logger.Debug(
|
||||
@ -345,8 +358,25 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
if block.Type == model.TypeAttachment {
|
||||
block.Fields["attachmentId"] = destFilename
|
||||
parts := strings.Split(fileName, ".")
|
||||
fileInfoID := parts[0][1:]
|
||||
fileInfo, err := a.store.GetFileInfo(fileInfoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CopyCardFiles: cannot retrieve original fileinfo: %w", err)
|
||||
}
|
||||
newParts := strings.Split(destFilename, ".")
|
||||
newFileID := newParts[0][1:]
|
||||
fileInfo.Id = newFileID
|
||||
err = a.store.SaveFileInfo(fileInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err)
|
||||
}
|
||||
} else {
|
||||
block.Fields["fileId"] = destFilename
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -202,15 +202,23 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
||||
blockPatches := make([]model.BlockPatch, 0)
|
||||
|
||||
for _, block := range bab.Blocks {
|
||||
if fileID, ok := block.Fields["fileId"]; ok {
|
||||
fieldName := ""
|
||||
if block.Type == model.TypeImage {
|
||||
fieldName = "fileId"
|
||||
} else if block.Type == model.TypeAttachment {
|
||||
fieldName = "attachmentId"
|
||||
}
|
||||
if fieldName != "" {
|
||||
if fieldID, ok := block.Fields[fieldName]; ok {
|
||||
blockIDs = append(blockIDs, block.ID)
|
||||
blockPatches = append(blockPatches, model.BlockPatch{
|
||||
UpdatedFields: map[string]interface{}{
|
||||
"fileId": fileID,
|
||||
fieldName: fieldID,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs)))
|
||||
|
||||
if len(blockIDs) != 0 {
|
||||
@ -347,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)
|
||||
@ -364,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
|
||||
@ -494,11 +515,48 @@ func (a *App) DeleteBoard(boardID, userID string) error {
|
||||
}
|
||||
|
||||
func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForBoard(boardID)
|
||||
members, err := a.store.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if err != nil && !model.IsErrNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if board != nil {
|
||||
for i, m := range members {
|
||||
if !m.SchemeAdmin {
|
||||
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
members[i].SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForUser(userID)
|
||||
members, err := a.store.GetMembersForUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, m := range members {
|
||||
if !m.SchemeAdmin {
|
||||
board, err := a.store.GetBoard(m.BoardID)
|
||||
if err != nil && !model.IsErrNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if board != nil {
|
||||
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
// if system/team admin
|
||||
members[i].SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) {
|
||||
@ -528,6 +586,14 @@ func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !newMember.SchemeAdmin {
|
||||
if board != nil {
|
||||
if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
newMember.SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !board.IsTemplate {
|
||||
if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil {
|
||||
return nil, err
|
||||
|
@ -127,6 +127,7 @@ func TestAddMemberToBoard(t *testing.T) {
|
||||
},
|
||||
}, nil).Times(2)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil)
|
||||
th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
addedBoardMember, err := th.App.AddMemberToBoard(boardMember)
|
||||
require.NoError(t, err)
|
||||
@ -180,10 +181,11 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil)
|
||||
th.Store.EXPECT().GetUserByID(userID).Return(&model.User{ID: userID, Username: "UserName"}, nil)
|
||||
|
||||
th.Store.EXPECT().PatchBoard(boardID, patch, userID).Return(
|
||||
&model.Board{
|
||||
@ -218,7 +220,7 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil)
|
||||
@ -256,7 +258,7 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
@ -294,7 +296,7 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
@ -332,7 +334,10 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(3)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
@ -370,7 +375,11 @@ func TestPatchBoard(t *testing.T) {
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
ChannelID: "",
|
||||
}, nil).Times(1)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
@ -391,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) {
|
||||
@ -566,3 +673,99 @@ func TestDuplicateBoard(t *testing.T) {
|
||||
assert.NotNil(t, members)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMembersForBoard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
const boardID = "board_id_1"
|
||||
const userID = "user_id_1"
|
||||
const teamID = "team_id_1"
|
||||
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{
|
||||
{
|
||||
BoardID: boardID,
|
||||
UserID: userID,
|
||||
SchemeEditor: true,
|
||||
},
|
||||
}, nil).Times(3)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1)
|
||||
t.Run("-base case", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2)
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
t.Run("-team check false ", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
t.Run("-team check true", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMembersForUser(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
const boardID = "board_id_1"
|
||||
const userID = "user_id_1"
|
||||
const teamID = "team_id_1"
|
||||
|
||||
th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{
|
||||
{
|
||||
BoardID: boardID,
|
||||
UserID: userID,
|
||||
SchemeEditor: true,
|
||||
},
|
||||
}, nil).Times(3)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(nil, nil)
|
||||
t.Run("-base case", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
t.Run("-team check false ", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
t.Run("-team check true", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ func TestGetUserCategoryBoards(t *testing.T) {
|
||||
Synthetic: false,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
|
||||
|
||||
categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id")
|
||||
@ -151,6 +152,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
||||
Synthetic: true,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
|
||||
existingCategoryBoards := []model.CategoryBoards{}
|
||||
boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards)
|
||||
@ -195,6 +197,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
||||
Synthetic: false,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
|
||||
@ -244,6 +247,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
||||
Synthetic: true,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ import (
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/metrics"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||
mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks"
|
||||
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
|
||||
"github.com/mattermost/focalboard/server/services/store/mockstore"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
@ -23,6 +26,7 @@ type TestHelper struct {
|
||||
Store *mockstore.MockStore
|
||||
FilesBackend *mocks.FileBackend
|
||||
logger mlog.LoggerIFace
|
||||
API *mmpermissionsMocks.MockAPI
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
@ -37,6 +41,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
webhook := webhook.NewClient(&cfg, logger)
|
||||
metricsService := metrics.NewMetrics(metrics.InstanceInfo{})
|
||||
|
||||
mockStore := permissionsMocks.NewMockStore(ctrl)
|
||||
mockAPI := mmpermissionsMocks.NewMockAPI(ctrl)
|
||||
permissions := mmpermissions.New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError))
|
||||
|
||||
appServices := Services{
|
||||
Auth: auth,
|
||||
Store: store,
|
||||
@ -45,6 +53,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
Metrics: metricsService,
|
||||
Logger: logger,
|
||||
SkipTemplateInit: true,
|
||||
Permissions: permissions,
|
||||
}
|
||||
app2 := New(&cfg, wsserver, appServices)
|
||||
|
||||
@ -60,5 +69,6 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
Store: store,
|
||||
FilesBackend: filesBackend,
|
||||
logger: logger,
|
||||
API: mockAPI,
|
||||
}, tearDown
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
||||
nil, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2)
|
||||
th.Store.EXPECT().GetMembersForBoard("board_id_2").Return([]*model.BoardMember{}, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(2)
|
||||
th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil)
|
||||
|
||||
|
@ -10,7 +10,20 @@ func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, erro
|
||||
}
|
||||
|
||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) {
|
||||
return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName)
|
||||
users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mmModel.Preference, error) {
|
||||
@ -50,7 +63,18 @@ func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) {
|
||||
}
|
||||
|
||||
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) {
|
||||
return a.store.SearchUserChannels(teamID, userID, query)
|
||||
channels, err := a.store.SearchUserChannels(teamID, userID, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var writeableChannels []*mmModel.Channel
|
||||
for _, channel := range channels {
|
||||
if a.permissions.HasPermissionToChannel(userID, channel.Id, model.PermissionCreatePost) {
|
||||
writeableChannels = append(writeableChannels, channel)
|
||||
}
|
||||
}
|
||||
return writeableChannels, nil
|
||||
}
|
||||
|
||||
func (a *App) GetChannel(teamID string, channelID string) (*mmModel.Channel, error) {
|
||||
|
85
server/app/user_test.go
Normal file
85
server/app/user_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
|
||||
teamID := "team-id-1"
|
||||
userID := "user-id-1"
|
||||
|
||||
t.Run("return empty users", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{}, nil)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(users))
|
||||
})
|
||||
|
||||
t.Run("return user", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, 0, len(users[0].Permissions))
|
||||
})
|
||||
|
||||
t.Run("return team admin", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id)
|
||||
})
|
||||
|
||||
t.Run("return system admin", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(true).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id)
|
||||
assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id)
|
||||
})
|
||||
|
||||
t.Run("test user channels", func(t *testing.T) {
|
||||
channelID := "Channel1"
|
||||
th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(true).Times(1)
|
||||
|
||||
channels, err := th.App.SearchUserChannels(teamID, userID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(channels))
|
||||
})
|
||||
|
||||
t.Run("test user channels- no permissions", func(t *testing.T) {
|
||||
channelID := "Channel1"
|
||||
th.Store.EXPECT().SearchUserChannels(teamID, userID, "").Return([]*mmModel.Channel{{Id: channelID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToChannel(userID, channelID, model.PermissionCreatePost).Return(false).Times(1)
|
||||
|
||||
channels, err := th.App.SearchUserChannels(teamID, userID, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(channels))
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -78,6 +78,9 @@ func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmMod
|
||||
}
|
||||
|
||||
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
||||
if permission.Id == model.PermissionManageTeam.Id {
|
||||
return false
|
||||
}
|
||||
if userID == userNoTeamMember {
|
||||
return false
|
||||
}
|
||||
@ -88,7 +91,7 @@ func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string
|
||||
}
|
||||
|
||||
func (*FakePermissionPluginAPI) HasPermissionToChannel(userID string, channelID string, permission *mmModel.Permission) bool {
|
||||
return channelID == "valid-channel-id"
|
||||
return channelID == "valid-channel-id" || channelID == "valid-channel-id-2"
|
||||
}
|
||||
|
||||
func getTestConfig() (*config.Configuration, error) {
|
||||
|
@ -6,7 +6,10 @@ import (
|
||||
|
||||
var (
|
||||
PermissionViewTeam = mmModel.PermissionViewTeam
|
||||
PermissionManageTeam = mmModel.PermissionManageTeam
|
||||
PermissionManageSystem = mmModel.PermissionManageSystem
|
||||
PermissionReadChannel = mmModel.PermissionReadChannel
|
||||
PermissionCreatePost = mmModel.PermissionCreatePost
|
||||
PermissionViewMembers = mmModel.PermissionViewMembers
|
||||
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
|
||||
PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel
|
||||
|
@ -66,6 +66,9 @@ type User struct {
|
||||
// required: true
|
||||
IsGuest bool `json:"is_guest"`
|
||||
|
||||
// Special Permissions the user may have
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
|
||||
Roles string `json:"roles"`
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -189,6 +189,9 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac
|
||||
// comment add/delete
|
||||
attachment.Fields = appendCommentChanges(attachment.Fields, cardDiff)
|
||||
|
||||
// File Attachment add/delete
|
||||
attachment.Fields = appendAttachmentChanges(attachment.Fields, cardDiff)
|
||||
|
||||
// content/description changes
|
||||
attachment.Fields = appendContentChanges(attachment.Fields, cardDiff, opts.Logger)
|
||||
|
||||
@ -264,6 +267,31 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendAttachmentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
|
||||
for _, child := range cardDiff.Diffs {
|
||||
if child.BlockType == model.TypeAttachment {
|
||||
var format string
|
||||
var msg string
|
||||
if child.NewBlock != nil && child.OldBlock == nil {
|
||||
format = "Added an attachment: **`%s`**"
|
||||
msg = child.NewBlock.Title
|
||||
} else {
|
||||
format = "Removed ~~`%s`~~ attachment"
|
||||
msg = stripNewlines(child.OldBlock.Title)
|
||||
}
|
||||
|
||||
if format != "" {
|
||||
fields = append(fields, &mm_model.SlackAttachmentField{
|
||||
Short: false,
|
||||
Title: "Changed by " + makeAuthorsList(child.Authors, "unknown_user"), // TODO: localize this when server has i18n
|
||||
Value: fmt.Sprintf(format, msg),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField {
|
||||
for _, child := range cardDiff.Diffs {
|
||||
var opAdd, opDelete bool
|
||||
@ -316,7 +344,7 @@ func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("appendContentChanges",
|
||||
logger.Trace("appendContentChanges",
|
||||
mlog.String("type", string(child.BlockType)),
|
||||
mlog.String("opString", opString),
|
||||
mlog.String("oldTitle", oldTitle),
|
||||
|
@ -31,6 +31,9 @@ func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel
|
||||
if userID == "" || teamID == "" || permission == nil {
|
||||
return false
|
||||
}
|
||||
if permission.Id == model.PermissionManageTeam.Id {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,11 @@ func TestHasPermissionToTeam(t *testing.T) {
|
||||
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("no users have PermissionManageTeam on teams", func(t *testing.T) {
|
||||
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageTeam)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHasPermissionToBoard(t *testing.T) {
|
||||
@ -141,4 +146,27 @@ func TestHasPermissionToBoard(t *testing.T) {
|
||||
|
||||
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("Manage Team Permission ", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionViewBoard,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
}
|
||||
|
@ -58,6 +58,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
if !member.SchemeAdmin {
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam).
|
||||
Return(roleName == "elevated-admin").
|
||||
Times(1)
|
||||
}
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
@ -80,6 +87,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board
|
||||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
if !member.SchemeAdmin {
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam).
|
||||
Return(roleName == "elevated-admin").
|
||||
Times(1)
|
||||
}
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
|
@ -82,7 +82,6 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
return false
|
||||
}
|
||||
|
||||
member, err := s.store.GetMemberForBoard(boardID, userID)
|
||||
if model.IsErrNotFound(err) {
|
||||
return false
|
||||
@ -107,6 +106,13 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
member.SchemeViewer = true
|
||||
}
|
||||
|
||||
// Admins become member of boards, but get minimal role
|
||||
// if they are a System/Team Admin (model.PermissionManageTeam)
|
||||
// elevate their permissions
|
||||
if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
|
@ -219,4 +219,25 @@ func TestHasPermissionToBoard(t *testing.T) {
|
||||
|
||||
th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("elevate board viewer permissions", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionViewBoard,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{}
|
||||
th.checkBoardPermissions("elevated-admin", member, teamID, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ const (
|
||||
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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)),
|
||||
)
|
||||
|
@ -0,0 +1 @@
|
||||
SELECT 1;
|
@ -0,0 +1 @@
|
||||
{{ addColumnIfNeeded "file_info" "path" "varchar(512)" "" }}
|
@ -4,11 +4,13 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/morph/models"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// EnsureSchemaMigrationFormat checks the schema migrations table
|
||||
@ -21,6 +23,7 @@ func (s *SQLStore) EnsureSchemaMigrationFormat() error {
|
||||
}
|
||||
|
||||
if !migrationNeeded {
|
||||
s.logger.Info("Schema migration table is correct format")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -105,8 +108,8 @@ func filterMigrations(migrations []*models.Migration, legacySchemaVersion uint32
|
||||
}
|
||||
|
||||
func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) {
|
||||
// Check if `dirty` column exists on schema version table.
|
||||
// This column exists only for the old schema version table.
|
||||
// Check if `name` column exists on schema version table.
|
||||
// This column exists only for the new schema version table.
|
||||
|
||||
// SQLite needs a bit of a special handling
|
||||
if s.dbType == model.SqliteDBType {
|
||||
@ -114,22 +117,46 @@ func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) {
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder(s.db).
|
||||
Select("count(*)").
|
||||
Select("COLUMN_NAME").
|
||||
From("information_schema.COLUMNS").
|
||||
Where(sq.Eq{
|
||||
"TABLE_NAME": s.tablePrefix + "schema_migrations",
|
||||
"COLUMN_NAME": "dirty",
|
||||
})
|
||||
|
||||
row := query.QueryRow()
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
s.logger.Error("failed to check for columns of schema_migrations table", mlog.Err(err))
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch columns in schema_migrations table", mlog.Err(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count == 1, nil
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
data := []string{}
|
||||
for rows.Next() {
|
||||
var columnName string
|
||||
|
||||
err := rows.Scan(&columnName)
|
||||
if err != nil {
|
||||
s.logger.Error("error scanning rows from schema_migrations table definition", mlog.Err(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
data = append(data, columnName)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
// if no data then table does not exist and therefore a schema migration is not needed.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, columnName := range data {
|
||||
// look for a column named 'name', if found then no migration is needed
|
||||
if strings.ToLower(columnName) == "name" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) {
|
||||
@ -145,18 +172,27 @@ func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) {
|
||||
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
const (
|
||||
idxCid = iota
|
||||
idxName
|
||||
idxType
|
||||
idxNotnull
|
||||
idxDfltValue
|
||||
idxPk
|
||||
)
|
||||
|
||||
data := [][]*string{}
|
||||
for rows.Next() {
|
||||
// PRAGMA returns 6 columns
|
||||
row := make([]*string, 6)
|
||||
|
||||
err := rows.Scan(
|
||||
&row[0],
|
||||
&row[1],
|
||||
&row[2],
|
||||
&row[3],
|
||||
&row[4],
|
||||
&row[5],
|
||||
&row[idxCid],
|
||||
&row[idxName],
|
||||
&row[idxType],
|
||||
&row[idxNotnull],
|
||||
&row[idxDfltValue],
|
||||
&row[idxPk],
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Error("error scanning rows from SQLite schema_migrations table definition", mlog.Err(err))
|
||||
@ -166,15 +202,19 @@ func (s *SQLStore) isSchemaMigrationNeededSQLite() (bool, error) {
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
nameColumnFound := false
|
||||
if len(data) == 0 {
|
||||
// if no data then table does not exist and therefore a schema migration is not needed.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, row := range data {
|
||||
if len(row) >= 2 && *row[1] == "dirty" {
|
||||
nameColumnFound = true
|
||||
break
|
||||
// look for a column named 'name', if found then no migration is needed
|
||||
if len(row) >= 2 && strings.ToLower(*row[idxName]) == "name" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nameColumnFound, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getLegacySchemaVersion() (uint32, error) {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
var dataMigrationSystemSettings = map[string]string{
|
||||
"UniqueIDsMigrationComplete": "true",
|
||||
"CategoryUuidIdMigrationComplete": "true",
|
||||
"DeDuplicateCategoryBoardTableComplete": "true",
|
||||
}
|
||||
|
||||
func addBaseSettings(m map[string]string) map[string]string {
|
||||
|
@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -119,9 +120,11 @@ func (cn *CallbackQueue) exec(f CallbackFunc) {
|
||||
// don't let a panic in the callback exit the thread.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
cn.logger.Error("CallbackQueue callback panic",
|
||||
mlog.String("name", cn.name),
|
||||
mlog.Any("panic", r),
|
||||
mlog.String("stack", string(stack)),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
@ -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"))
|
||||
}
|
||||
|
@ -454,7 +454,7 @@ func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[st
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block *model.Block) {
|
||||
pa.logger.Debug("BroadcastingBlockChange",
|
||||
pa.logger.Trace("BroadcastingBlockChange",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", block.BoardID),
|
||||
mlog.String("blockID", block.ID),
|
||||
|
@ -98,13 +98,9 @@ describe('Card URL Property', () => {
|
||||
const addView = (type: ViewType) => {
|
||||
cy.log(`**Add ${type} view**`)
|
||||
|
||||
// Intercept and wait for getUser request because it is the last one in the effects for BoardPage
|
||||
// After this last request the BoardPage component will not have additional rerenders
|
||||
cy.intercept('POST', '/api/v2/users').as('getUser')
|
||||
cy.findByRole('button', {name: 'View menu'}).click()
|
||||
cy.findByText('Add view').realHover()
|
||||
cy.findByRole('button', {name: type}).click()
|
||||
cy.wait('@getUser')
|
||||
cy.findByRole('textbox', {name: `${type} view`}).should('exist')
|
||||
}
|
||||
|
||||
|
@ -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": "تغيير نوع الخاصية",
|
||||
|
@ -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",
|
||||
|
@ -395,6 +395,10 @@
|
||||
"login.log-in-button": "Log in",
|
||||
"login.log-in-title": "Log in",
|
||||
"login.register-button": "or create an account if you don't have one",
|
||||
"new_channel_modal.create_board.empty_board_description": "Create a new empty board",
|
||||
"new_channel_modal.create_board.empty_board_title": "Empty board",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Select a template",
|
||||
"new_channel_modal.create_board.title": "Create a board for this channel",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days",
|
||||
"notification-box-card-limit-reached.contact-link": "Contact your adminstrator",
|
||||
"notification-box-card-limit-reached.link": "Upgrade to a paid plan",
|
||||
|
@ -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",
|
||||
|
@ -41,16 +41,16 @@
|
||||
"BoardsUnfurl.Updated": "Aktulaizirano {time}",
|
||||
"Calculations.Options.average.displayName": "Prosjek",
|
||||
"Calculations.Options.average.label": "Prosjek",
|
||||
"Calculations.Options.count.displayName": "Zbroj",
|
||||
"Calculations.Options.count.label": "Zbroj",
|
||||
"Calculations.Options.countChecked.displayName": "Provjereno",
|
||||
"Calculations.Options.countChecked.label": "Zbroj provjeren",
|
||||
"Calculations.Options.countUnchecked.displayName": "Neprovjereno",
|
||||
"Calculations.Options.countUnchecked.label": "Zbroj neprovjeren",
|
||||
"Calculations.Options.count.displayName": "Broji",
|
||||
"Calculations.Options.count.label": "Broji",
|
||||
"Calculations.Options.countChecked.displayName": "Označeno",
|
||||
"Calculations.Options.countChecked.label": "Broji označene",
|
||||
"Calculations.Options.countUnchecked.displayName": "Neoznačeno",
|
||||
"Calculations.Options.countUnchecked.label": "Broji neoznačene",
|
||||
"Calculations.Options.countUniqueValue.displayName": "Jedinstveno",
|
||||
"Calculations.Options.countUniqueValue.label": "Broji jedinstvene vrijednosti",
|
||||
"Calculations.Options.countValue.displayName": "Vrijednosti",
|
||||
"Calculations.Options.countValue.label": "Vrijednost zbroja",
|
||||
"Calculations.Options.countValue.label": "Broji vrijednost",
|
||||
"Calculations.Options.dateRange.displayName": "Raspon",
|
||||
"Calculations.Options.dateRange.label": "Raspon",
|
||||
"Calculations.Options.earliest.displayName": "Najraniji",
|
||||
@ -395,6 +395,10 @@
|
||||
"login.log-in-button": "Prijavi se",
|
||||
"login.log-in-title": "Prijavi se",
|
||||
"login.register-button": "ili stvori račun, ako ga još nemaš",
|
||||
"new_channel_modal.create_board.empty_board_description": "Stvori novu praznu ploču",
|
||||
"new_channel_modal.create_board.empty_board_title": "Prazna ploča",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Odaberi predložak",
|
||||
"new_channel_modal.create_board.title": "Stvori ploču za ovaj kanal",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Postavi pripravno stanje na 10 dana",
|
||||
"notification-box-card-limit-reached.contact-link": "obavijesti svog administratora",
|
||||
"notification-box-card-limit-reached.link": "Nadogradi na plaćenu tarifu",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"AppBar.Tooltip": "リンク先ボードの表示切り替え",
|
||||
"AppBar.Tooltip": "リンク先Boardの切替え",
|
||||
"Attachment.Attachment-title": "添付する",
|
||||
"AttachmentBlock.DeleteAction": "削除",
|
||||
"AttachmentBlock.addElement": "{type} を追加",
|
||||
@ -24,18 +24,18 @@
|
||||
"BoardMember.schemeNone": "なし",
|
||||
"BoardMember.schemeViewer": "閲覧者",
|
||||
"BoardMember.unlinkChannel": "リンク解除",
|
||||
"BoardPage.newVersion": "ボードの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。",
|
||||
"BoardPage.syncFailed": "ボードが削除されたか、アクセスが取り消されました。",
|
||||
"BoardPage.newVersion": "Boardsの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。",
|
||||
"BoardPage.syncFailed": "Boardが削除されたか、アクセスが取り消されました。",
|
||||
"BoardTemplateSelector.add-template": "テンプレート新規作成",
|
||||
"BoardTemplateSelector.create-empty-board": "空のボードを作成",
|
||||
"BoardTemplateSelector.create-empty-board": "空のBoardを作成",
|
||||
"BoardTemplateSelector.delete-template": "削除する",
|
||||
"BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。",
|
||||
"BoardTemplateSelector.description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。",
|
||||
"BoardTemplateSelector.edit-template": "編集",
|
||||
"BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにボードを追加できます。",
|
||||
"BoardTemplateSelector.plugin.no-content-title": "ボードを作成する",
|
||||
"BoardTemplateSelector.title": "ボードを作成する",
|
||||
"BoardTemplateSelector.plugin.no-content-description": "以下のテンプレートを使用するか、空の状態から作成することで、サイドバーにBoardを追加できます。",
|
||||
"BoardTemplateSelector.plugin.no-content-title": "Boardを作成する",
|
||||
"BoardTemplateSelector.title": "Boardを作成する",
|
||||
"BoardTemplateSelector.use-this-template": "このテンプレートを使う",
|
||||
"BoardsSwitcher.Title": "ボード検索",
|
||||
"BoardsSwitcher.Title": "Board検索",
|
||||
"BoardsUnfurl.Limited": "カードがアーカイブされているため詳細は表示されません",
|
||||
"BoardsUnfurl.Remainder": "残り +{remainder}",
|
||||
"BoardsUnfurl.Updated": "更新日時 {time}",
|
||||
@ -94,8 +94,8 @@
|
||||
"CardDetail.moveContent": "カード内容の移動",
|
||||
"CardDetail.new-comment-placeholder": "コメントを追加する...",
|
||||
"CardDetailProperty.confirm-delete-heading": "プロパティの削除を確定する",
|
||||
"CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このボードのすべてのカードからそのプロパティが削除されます。",
|
||||
"CardDetailProperty.confirm-property-name-change-subtext": "本当にプロパティ \"{propertyName}\" の \"{customText}\" に変更しますか? これは、このボードの{numOfCards}カード全体の値に影響し、データの損失につながる恐れがあります。",
|
||||
"CardDetailProperty.confirm-delete-subtext": "本当にプロパティ \"{propertyName}\" を削除しますか? 削除すると、このBoardのすべてのカードからそのプロパティが削除されます。",
|
||||
"CardDetailProperty.confirm-property-name-change-subtext": "本当にプロパティ \"{propertyName}\" の \"{customText}\" に変更しますか? これは、このBoardの{numOfCards}カード全体の値に影響し、データの損失につながる恐れがあります。",
|
||||
"CardDetailProperty.confirm-property-type-change": "プロパティ種別の変更を確定する",
|
||||
"CardDetailProperty.delete-action-button": "削除",
|
||||
"CardDetailProperty.property-change-action-button": "プロパティの変更",
|
||||
@ -114,7 +114,7 @@
|
||||
"Categories.CreateCategoryDialog.UpdateText": "更新",
|
||||
"CenterPanel.Login": "ログイン",
|
||||
"CenterPanel.Share": "共有",
|
||||
"ChannelIntro.CreateBoard": "ボードを作成する",
|
||||
"ChannelIntro.CreateBoard": "Boardを作成する",
|
||||
"CloudMessage.cloud-server": "専用の無料クラウドサーバーを入手する。",
|
||||
"ColorOption.selectColor": "{color} 色を選択",
|
||||
"Comment.delete": "削除",
|
||||
@ -144,10 +144,10 @@
|
||||
"DateRange.today": "今日",
|
||||
"DeleteBoardDialog.confirm-cancel": "キャンセル",
|
||||
"DeleteBoardDialog.confirm-delete": "削除",
|
||||
"DeleteBoardDialog.confirm-info": "本当にボード \"{boardTitle}\" を削除しますか? 削除すると、このボードのすべてのカードが削除されます。",
|
||||
"DeleteBoardDialog.confirm-info-template": "ボードテンプレート \"{boardTitle}\" を本当に削除しますか?",
|
||||
"DeleteBoardDialog.confirm-tite": "ボードの削除を確定する",
|
||||
"DeleteBoardDialog.confirm-tite-template": "ボードテンプレートの削除を確定する",
|
||||
"DeleteBoardDialog.confirm-info": "本当にBoard \"{boardTitle}\" を削除しますか? 削除すると、このBoardのすべてのカードが削除されます。",
|
||||
"DeleteBoardDialog.confirm-info-template": "Boardテンプレート \"{boardTitle}\" を本当に削除しますか?",
|
||||
"DeleteBoardDialog.confirm-tite": "Boardの削除を確定する",
|
||||
"DeleteBoardDialog.confirm-tite-template": "Boardテンプレートの削除を確定する",
|
||||
"Dialog.closeDialog": "ダイアログを閉じる",
|
||||
"EditableDayPicker.today": "今日",
|
||||
"Error.mobileweb": "モバイルウェブのサポートは現在、初期ベータ版です。一部の機能が利用できない場合があります。",
|
||||
@ -169,18 +169,18 @@
|
||||
"FilterComponent.add-filter": "+ フィルターを追加する",
|
||||
"FilterComponent.delete": "削除",
|
||||
"FilterValue.empty": "(空)",
|
||||
"FindBoardsDialog.IntroText": "ボードを検索",
|
||||
"FindBoardsDialog.IntroText": "Boardを検索",
|
||||
"FindBoardsDialog.NoResultsFor": "\"{searchQuery}\"に対する結果はありません",
|
||||
"FindBoardsDialog.NoResultsSubtext": "スペルを確認し、再度検索してください。",
|
||||
"FindBoardsDialog.SubTitle": "ボードを検索するために文字を入力してください。<b>UP/DOWN</b>で閲覧、<b>ENTER</b>で選択、<b>ESC</b>でキャンセル",
|
||||
"FindBoardsDialog.Title": "ボードを探す",
|
||||
"FindBoardsDialog.SubTitle": "Boardを検索するために文字を入力してください。<b>UP/DOWN</b>で閲覧、<b>ENTER</b>で選択、<b>ESC</b>でキャンセル",
|
||||
"FindBoardsDialog.Title": "Boardを探す",
|
||||
"GroupBy.hideEmptyGroups": "{count} 個の空のグループを隠す",
|
||||
"GroupBy.showHiddenGroups": "{count} 個の非表示グループを表示する",
|
||||
"GroupBy.ungroup": "グループ解除",
|
||||
"HideBoard.MenuOption": "ボードを隠す",
|
||||
"HideBoard.MenuOption": "Boardを隠す",
|
||||
"KanbanCard.untitled": "無題",
|
||||
"MentionSuggestion.is-not-board-member": "(not board member)",
|
||||
"Mutator.new-board-from-template": "テンプレートからの新しいボード",
|
||||
"Mutator.new-board-from-template": "テンプレートからの新しいBoard",
|
||||
"Mutator.new-card-from-template": "テンプレートから新しいカードを作成",
|
||||
"Mutator.new-template-from-card": "カードから新しいテンプレートを作成",
|
||||
"OnboardingTour.AddComments.Body": "問題にコメントしたり、仲間のMattermostユーザーの注意を引くために@メンションすることもできます。",
|
||||
@ -189,14 +189,14 @@
|
||||
"OnboardingTour.AddDescription.Title": "説明を追加する",
|
||||
"OnboardingTour.AddProperties.Body": "カードに様々なプロパティを追加することで、より便利になります。",
|
||||
"OnboardingTour.AddProperties.Title": "プロパティを追加する",
|
||||
"OnboardingTour.AddView.Body": "異なるレイアウトでボードを整理するための新しいビューを作成するには、ここに移動します。",
|
||||
"OnboardingTour.AddView.Body": "異なるレイアウトでBoardを整理するための新しいビューを作成するには、ここに移動します。",
|
||||
"OnboardingTour.AddView.Title": "新しいビューを追加する",
|
||||
"OnboardingTour.CopyLink.Body": "リンクをコピーしてチャンネル、ダイレクトメッセージ、グループメッセージに貼り付けることで、カードをチームメイトと共有することができます。",
|
||||
"OnboardingTour.CopyLink.Title": "リンクをコピー",
|
||||
"OnboardingTour.OpenACard.Body": "カードを開き、あなたの仕事を整理するのに役立つボードの便利な使い方を探ってみてください。",
|
||||
"OnboardingTour.OpenACard.Body": "カードを開き、あなたの仕事を整理するのに役立つBoardの便利な使い方を探ってみてください。",
|
||||
"OnboardingTour.OpenACard.Title": "カードを開く",
|
||||
"OnboardingTour.ShareBoard.Body": "作成したボードは、社内やチーム内で共有することも、組織外から見えるように公開することも可能です。",
|
||||
"OnboardingTour.ShareBoard.Title": "ボードを共有する",
|
||||
"OnboardingTour.ShareBoard.Body": "作成したBoardは、社内やチーム内で共有することも、組織外から見えるように公開することも可能です。",
|
||||
"OnboardingTour.ShareBoard.Title": "Boardを共有",
|
||||
"PersonProperty.board-members": "Board members",
|
||||
"PersonProperty.me": "私",
|
||||
"PersonProperty.non-board-members": "Not board members",
|
||||
@ -231,7 +231,7 @@
|
||||
"ShareBoard.PublishTitle": "Web上へ公開する",
|
||||
"ShareBoard.ShareInternal": "内部で共有する",
|
||||
"ShareBoard.ShareInternalDescription": "権限のあるユーザーは、このリンクを使用することができます。",
|
||||
"ShareBoard.Title": "ボードを共有する",
|
||||
"ShareBoard.Title": "Boardを共有",
|
||||
"ShareBoard.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?",
|
||||
"ShareBoard.copiedLink": "コピーしました!",
|
||||
"ShareBoard.copyLink": "リンクをコピー",
|
||||
@ -244,40 +244,40 @@
|
||||
"ShareTemplate.Title": "テンプレートを共有する",
|
||||
"ShareTemplate.searchPlaceholder": "人を検索",
|
||||
"Sidebar.about": "Focalboardについて",
|
||||
"Sidebar.add-board": "+ ボードを追加する",
|
||||
"Sidebar.add-board": "+ Boardを追加",
|
||||
"Sidebar.changePassword": "パスワードを変更する",
|
||||
"Sidebar.delete-board": "ボードを削除",
|
||||
"Sidebar.duplicate-board": "ボードを複製する",
|
||||
"Sidebar.delete-board": "Boardを削除",
|
||||
"Sidebar.duplicate-board": "Boardを複製する",
|
||||
"Sidebar.export-archive": "エクスポート",
|
||||
"Sidebar.import": "インポート",
|
||||
"Sidebar.import-archive": "インポート",
|
||||
"Sidebar.invite-users": "ユーザーを招待する",
|
||||
"Sidebar.logout": "ログアウト",
|
||||
"Sidebar.new-category.badge": "新規",
|
||||
"Sidebar.new-category.drag-boards-cta": "ここにボードをドラッグ...",
|
||||
"Sidebar.no-boards-in-category": "カテゴリ内にボードがありません",
|
||||
"Sidebar.new-category.drag-boards-cta": "ここにBoardをドラッグ...",
|
||||
"Sidebar.no-boards-in-category": "カテゴリ内にBoardがありません",
|
||||
"Sidebar.product-tour": "プロダクトツアー",
|
||||
"Sidebar.random-icons": "ランダムアイコン",
|
||||
"Sidebar.set-language": "言語設定",
|
||||
"Sidebar.set-theme": "テーマ設定",
|
||||
"Sidebar.settings": "設定",
|
||||
"Sidebar.template-from-board": "ボードからの新しいテンプレート",
|
||||
"Sidebar.untitled-board": "(無題のボード)",
|
||||
"Sidebar.template-from-board": "Boardからの新しいテンプレート",
|
||||
"Sidebar.untitled-board": "(無題のBoard)",
|
||||
"Sidebar.untitled-view": "(無題のビュー)",
|
||||
"SidebarCategories.BlocksMenu.Move": "移動...",
|
||||
"SidebarCategories.CategoryMenu.CreateNew": "新しいカテゴリを作成する",
|
||||
"SidebarCategories.CategoryMenu.Delete": "カテゴリを削除する",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> にあるボードは、Boards カテゴリに戻されます。どのボードからも削除されることはありません。",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> にあるBoardは、Boards カテゴリに戻されます。どのBoardからも削除されることはありません。",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "このカテゴリを削除しますか?",
|
||||
"SidebarCategories.CategoryMenu.Update": "カテゴリ名を変更する",
|
||||
"SidebarTour.ManageCategories.Body": "カスタムカテゴリーを作成し、管理することができます。カテゴリはユーザーごとに設定されるため、ボードを自分のカテゴリに移動しても、同じボードを使用している他のメンバーには影響がありません。",
|
||||
"SidebarTour.ManageCategories.Body": "カスタムカテゴリーを作成し、管理することができます。カテゴリはユーザーごとに設定されるため、Boardを自分のカテゴリに移動しても、同じBoardを使用している他のメンバーには影響がありません。",
|
||||
"SidebarTour.ManageCategories.Title": "カテゴリー管理",
|
||||
"SidebarTour.SearchForBoards.Body": "ボード切り替え(Cmd/Ctrl + K)により、素早くボードを検索し、サイドバーに追加することができます。",
|
||||
"SidebarTour.SearchForBoards.Title": "ボードを検索",
|
||||
"SidebarTour.SidebarCategories.Body": "すべてのボードが新しいサイドバーの下に整理されました。もう、ワークスペースを切り替える必要はありません。v7.2へのアップグレードに伴い、以前のワークスペースに基づいたカスタムカテゴリーが自動的に作成されている場合があります。これらは、お好みで削除したり編集することができます。",
|
||||
"SidebarTour.SearchForBoards.Body": "Board切替(Cmd/Ctrl + K)により、素早くBoardを検索し、サイドバーに追加することができます。",
|
||||
"SidebarTour.SearchForBoards.Title": "Boardを検索",
|
||||
"SidebarTour.SidebarCategories.Body": "すべてのBoardが新しいサイドバーの下に整理されました。もう、ワークスペースを切り替える必要はありません。v7.2へのアップグレードに伴い、以前のワークスペースに基づいたカスタムカテゴリーが自動的に作成されている場合があります。これらは、お好みで削除したり編集することができます。",
|
||||
"SidebarTour.SidebarCategories.Link": "詳細",
|
||||
"SidebarTour.SidebarCategories.Title": "サイドバーカテゴリー",
|
||||
"SiteStats.total_boards": "ボード数",
|
||||
"SiteStats.total_boards": "Board総数",
|
||||
"SiteStats.total_cards": "カード数",
|
||||
"TableComponent.add-icon": "アイコンを追加する",
|
||||
"TableComponent.name": "名前",
|
||||
@ -307,11 +307,11 @@
|
||||
"ValueSelectorLabel.openMenu": "メニューを開く",
|
||||
"VersionMessage.help": "このバージョンの新機能を確認する。",
|
||||
"View.AddView": "ビューを追加",
|
||||
"View.Board": "ボード",
|
||||
"View.Board": "Board",
|
||||
"View.DeleteView": "ビューを削除",
|
||||
"View.DuplicateView": "ビューを複製",
|
||||
"View.Gallery": "ギャラリー",
|
||||
"View.NewBoardTitle": "ボード表示",
|
||||
"View.NewBoardTitle": "Board表示",
|
||||
"View.NewCalendarTitle": "カレンダー表示",
|
||||
"View.NewGalleryTitle": "ギャラリービュー",
|
||||
"View.NewTableTitle": "テーブル表示",
|
||||
@ -323,7 +323,7 @@
|
||||
"ViewHeader.display-by": "表示対象: {property}",
|
||||
"ViewHeader.edit-template": "編集",
|
||||
"ViewHeader.empty-card": "空のカード",
|
||||
"ViewHeader.export-board-archive": "ボードアーカイブのエクスポート",
|
||||
"ViewHeader.export-board-archive": "Boardアーカイブのエクスポート",
|
||||
"ViewHeader.export-complete": "エクスポートが完了しました!",
|
||||
"ViewHeader.export-csv": "CSVエクスポート",
|
||||
"ViewHeader.export-failed": "エクスポートが失敗しました!",
|
||||
@ -339,7 +339,7 @@
|
||||
"ViewHeader.untitled": "無題",
|
||||
"ViewHeader.view-header-menu": "ヘッダーメニューを見る",
|
||||
"ViewHeader.view-menu": "メニューを見る",
|
||||
"ViewLimitDialog.Heading": "ボードごとのビュー数制限に達しました",
|
||||
"ViewLimitDialog.Heading": "Boardごとのビュー数制限に達しました",
|
||||
"ViewLimitDialog.PrimaryButton.Title.Admin": "アップグレード",
|
||||
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "管理者に通知する",
|
||||
"ViewLimitDialog.Subtext.Admin": "ProfessionalプランまたはEnterpriseプランにアップグレードしてください。",
|
||||
@ -352,22 +352,22 @@
|
||||
"ViewTitle.random-icon": "ランダム",
|
||||
"ViewTitle.remove-icon": "アイコンを削除する",
|
||||
"ViewTitle.show-description": "説明を表示",
|
||||
"ViewTitle.untitled-board": "無題のボード",
|
||||
"ViewTitle.untitled-board": "無題のBoard",
|
||||
"WelcomePage.Description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、整理、追跡、管理するためのプロジェクト管理ツールです。",
|
||||
"WelcomePage.Explore.Button": "ツアーに参加する",
|
||||
"WelcomePage.Heading": "ボードへようこそ",
|
||||
"WelcomePage.Heading": "Boardへようこそ",
|
||||
"WelcomePage.NoThanks.Text": "いいえ、自分で調べます",
|
||||
"WelcomePage.StartUsingIt.Text": "利用を開始する",
|
||||
"Workspace.editing-board-template": "ボードのテンプレートを編集しています。",
|
||||
"Workspace.editing-board-template": "Boardのテンプレートを編集しています。",
|
||||
"badge.guest": "ゲスト",
|
||||
"boardSelector.confirm-link-board": "ボードをチャンネルへリンク",
|
||||
"boardSelector.confirm-link-board-button": "はい、ボードをリンクします",
|
||||
"boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。ボードとチャンネルのリンク解除はいつでも可能です。",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このボードは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。",
|
||||
"boardSelector.create-a-board": "ボードを作成",
|
||||
"boardSelector.confirm-link-board": "Boardをチャンネルへリンク",
|
||||
"boardSelector.confirm-link-board-button": "はい、Boardをリンクします",
|
||||
"boardSelector.confirm-link-board-subtext": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。Boardとチャンネルのリンク解除はいつでも可能です。",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "\"{boardName}\" をチャンネルにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。",
|
||||
"boardSelector.create-a-board": "Boardを作成",
|
||||
"boardSelector.link": "リンク",
|
||||
"boardSelector.search-for-boards": "ボードを検索",
|
||||
"boardSelector.title": "ボードをリンク",
|
||||
"boardSelector.search-for-boards": "Boardを検索",
|
||||
"boardSelector.title": "Boardをリンク",
|
||||
"boardSelector.unlink": "リンク解除",
|
||||
"calendar.month": "月",
|
||||
"calendar.today": "今日",
|
||||
@ -380,64 +380,68 @@
|
||||
"default-properties.title": "タイトル",
|
||||
"error.back-to-home": "ホームへ戻る",
|
||||
"error.back-to-team": "チームに戻る",
|
||||
"error.board-not-found": "ボードが見つかりませんでした。",
|
||||
"error.board-not-found": "Boardが見つかりませんでした。",
|
||||
"error.go-login": "ログイン",
|
||||
"error.invalid-read-only-board": "このボードにアクセスできません。アクセスするにはログインしてください。",
|
||||
"error.not-logged-in": "セッションの有効期限が切れているか、ログインしていない可能性があります。ボードにアクセスするには再度ログインしてください。",
|
||||
"error.invalid-read-only-board": "このBoardにアクセスできません。アクセスするにはBoardsにログインしてください。",
|
||||
"error.not-logged-in": "セッションの有効期限が切れているか、ログインしていない可能性があります。Boardsにアクセスするには再度ログインしてください。",
|
||||
"error.page.title": "申し訳ありませんが、何か問題が発生しました",
|
||||
"error.team-undefined": "有効なチームではありません。",
|
||||
"error.unknown": "エラーが発生しました。",
|
||||
"generic.previous": "前へ",
|
||||
"guest-no-board.subtitle": "あなたはまだこのチームのどのボードにもアクセスできません。誰かがあなたをボードに追加するまでお待ちください。",
|
||||
"guest-no-board.title": "まだボードはありません",
|
||||
"guest-no-board.subtitle": "あなたはまだこのチームのどのBoardにもアクセスできません。誰かがあなたをBoardに追加するまでお待ちください。",
|
||||
"guest-no-board.title": "まだBoardsはありません",
|
||||
"imagePaste.upload-failed": "ファイルサイズの制限に達しているため、一部のファイルをアップロードできませんでした。",
|
||||
"limitedCard.title": "非表示カード",
|
||||
"login.log-in-button": "ログイン",
|
||||
"login.log-in-title": "ログイン",
|
||||
"login.register-button": "アカウントをお持ちでない方はアカウントを作成してください",
|
||||
"new_channel_modal.create_board.empty_board_description": "空のBoardを新規作成する",
|
||||
"new_channel_modal.create_board.empty_board_title": "空のBoard",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "テンプレートを選択",
|
||||
"new_channel_modal.create_board.title": "このチャンネル用のBoardを作成する",
|
||||
"notification-box-card-limit-reached.close-tooltip": "10日間のスヌーズ",
|
||||
"notification-box-card-limit-reached.contact-link": "管理者に通知する",
|
||||
"notification-box-card-limit-reached.link": "有料プランへのアップグレード",
|
||||
"notification-box-card-limit-reached.title": "ボードから {cards} カードが非表示になっています",
|
||||
"notification-box-card-limit-reached.title": "Boardから {cards} カードが非表示になっています",
|
||||
"notification-box-cards-hidden.title": "このアクションにより他のカードが非表示になります",
|
||||
"notification-box.card-limit-reached.not-admin.text": "アーカイブされたカードにアクセスするには、{contactLink}から有料プランにアップグレードしてください。",
|
||||
"notification-box.card-limit-reached.text": "カード数の制限に達しました。古いカードを閲覧するには、{link}",
|
||||
"person.add-user-to-board": "{username} をボードに追加",
|
||||
"person.add-user-to-board-confirm-button": "ボードに追加",
|
||||
"person.add-user-to-board": "{username} をBoardに追加",
|
||||
"person.add-user-to-board-confirm-button": "Boardに追加",
|
||||
"person.add-user-to-board-permissions": "権限",
|
||||
"person.add-user-to-board-question": "{username} をボードに追加しますか?",
|
||||
"person.add-user-to-board-warning": "{username} はボードのメンバーではないので、それに関する通知を受け取ることはありません。",
|
||||
"person.add-user-to-board-question": "{username} をBoardに追加しますか?",
|
||||
"person.add-user-to-board-warning": "{username} はBoardのメンバーではないので、それに関する通知を受け取ることはありません。",
|
||||
"register.login-button": "または、すでにアカウントをお持ちの方はログインしてください",
|
||||
"register.signup-title": "アカウント登録",
|
||||
"rhs-board-non-admin-msg": "あなたはボードの管理者ではありません",
|
||||
"rhs-board-non-admin-msg": "あなたはBoardの管理者ではありません",
|
||||
"rhs-boards.add": "追加",
|
||||
"rhs-boards.dm": "DM",
|
||||
"rhs-boards.gm": "GM",
|
||||
"rhs-boards.header.dm": "このダイレクトメッセージ",
|
||||
"rhs-boards.header.gm": "このグループメッセージ",
|
||||
"rhs-boards.last-update-at": "最終更新: {datetime}",
|
||||
"rhs-boards.link-boards-to-channel": "ボードを{channelName}へリンクする",
|
||||
"rhs-boards.linked-boards": "リンク済みボード",
|
||||
"rhs-boards.no-boards-linked-to-channel": "{channelName}にリンクされたボードはまだありません",
|
||||
"rhs-boards.link-boards-to-channel": "Boardsを{channelName}へリンクする",
|
||||
"rhs-boards.linked-boards": "リンク済みBoards",
|
||||
"rhs-boards.no-boards-linked-to-channel": "{channelName}にリンクされたBoardsはまだありません",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "Boardsは、よく知られたKanban形式のビューを使用して、チーム全体の作業を定義、生理、追跡、管理するためのプロジェクト管理ツールです。",
|
||||
"rhs-boards.unlink-board": "ボードのリンクを解除",
|
||||
"rhs-boards.unlink-board1": "ボードをリンク解除",
|
||||
"rhs-channel-boards-header.title": "ボード",
|
||||
"rhs-boards.unlink-board": "Boardのリンクを解除",
|
||||
"rhs-boards.unlink-board1": "Boardのリンクを解除",
|
||||
"rhs-channel-boards-header.title": "Boards",
|
||||
"share-board.publish": "公開",
|
||||
"share-board.share": "共有",
|
||||
"shareBoard.channels-select-group": "Channels",
|
||||
"shareBoard.confirm-change-team-role.body": "このボードで \"{role}\" より弱い権限のユーザー全員が <b>{role}</b> に昇格します。本当にボードの最低限のロールを変更しますか?",
|
||||
"shareBoard.confirm-change-team-role.body": "このBoardで \"{role}\" より弱い権限のユーザー全員が <b>{role}</b> に昇格します。本当にBoardの最低限のロールを変更しますか?",
|
||||
"shareBoard.confirm-change-team-role.confirmBtnText": "最低限のロールを変更",
|
||||
"shareBoard.confirm-change-team-role.title": "最低限のロールを変更",
|
||||
"shareBoard.confirm-link-channel": "ボードをチャンネルへリンク",
|
||||
"shareBoard.confirm-link-channel": "Boardをチャンネルへリンク",
|
||||
"shareBoard.confirm-link-channel-button": "チャンネルにリンク",
|
||||
"shareBoard.confirm-link-channel-button-with-other-channel": "リンク解除とリンクはこちら",
|
||||
"shareBoard.confirm-link-channel-subtext": "チャンネルをボードにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。",
|
||||
"shareBoard.confirm-link-channel-subtext-with-other-channel": "チャンネルをボードにリンクすると、チャンネルの(既存/新規)メンバー全員がボードを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このボードは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。",
|
||||
"shareBoard.confirm-unlink.body": "ボードからチャンネルのリンクを解除すると、別途権限を付与されない限り、チャンネルの(既存/新規)メンバー全員がボードへアクセスできなくなります。",
|
||||
"shareBoard.confirm-link-channel-subtext": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。",
|
||||
"shareBoard.confirm-link-channel-subtext-with-other-channel": "チャンネルをBoardにリンクすると、チャンネルの(既存/新規)メンバー全員がBoardを編集できるようになります。ただし、ゲストユーザーは除外されます。{lineBreak} このBoardは現在他のチャンネルにリンクされています。ここにリンクさせると、他のチャンネルとのリンクは解除されます。",
|
||||
"shareBoard.confirm-unlink.body": "Boardからチャンネルへのリンクを解除すると、別途権限を付与されない限り、チャンネルの(既存/新規)メンバー全員がBoardへアクセスできなくなります。",
|
||||
"shareBoard.confirm-unlink.confirmBtnText": "チャンネルとのリンクを解除",
|
||||
"shareBoard.confirm-unlink.title": "ボードからチャンネルへのリンクを解除する",
|
||||
"shareBoard.lastAdmin": "ボードには少なくとも1名の管理者が必要です",
|
||||
"shareBoard.confirm-unlink.title": "Boardからチャンネルへのリンクを解除する",
|
||||
"shareBoard.lastAdmin": "Boardsには少なくとも1名の管理者が必要です",
|
||||
"shareBoard.members-select-group": "メンバー",
|
||||
"shareBoard.unknown-channel-display-name": "不明なチャンネル",
|
||||
"tutorial_tip.finish_tour": "完了",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -395,6 +395,10 @@
|
||||
"login.log-in-button": "Aanmelden",
|
||||
"login.log-in-title": "Aanmelden",
|
||||
"login.register-button": "of maak een account aan als je er nog geen hebt",
|
||||
"new_channel_modal.create_board.empty_board_description": "Maak een nieuw leeg bord",
|
||||
"new_channel_modal.create_board.empty_board_title": "Leeg bord",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Kies een sjabloon",
|
||||
"new_channel_modal.create_board.title": "Maak een bord voor dit kanaal",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Snooze voor 10 dagen",
|
||||
"notification-box-card-limit-reached.contact-link": "breng je beheerder op de hoogte",
|
||||
"notification-box-card-limit-reached.link": "Upgrade naar een betaald plan",
|
||||
|
@ -395,6 +395,10 @@
|
||||
"login.log-in-button": "Zaloguj się",
|
||||
"login.log-in-title": "Zaloguj się",
|
||||
"login.register-button": "lub załóż konto, jeśli jeszcze go nie masz",
|
||||
"new_channel_modal.create_board.empty_board_description": "Utwórz nową pustą tablicę",
|
||||
"new_channel_modal.create_board.empty_board_title": "Wyczyść tablicę",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Wybierz szablon",
|
||||
"new_channel_modal.create_board.title": "Utwórz tablicę dla tego kanału",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Uśpij na 10 dni",
|
||||
"notification-box-card-limit-reached.contact-link": "powiadom swojego administratora",
|
||||
"notification-box-card-limit-reached.link": "Uaktualnienie do planu płatnego",
|
||||
|
12
webapp/i18n/pt.json
Normal file
12
webapp/i18n/pt.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"AppBar.Tooltip": "Alternar quadros vinculados",
|
||||
"Attachment.Attachment-title": "Anexo",
|
||||
"AttachmentBlock.DeleteAction": "Apagar",
|
||||
"AttachmentBlock.addElement": "Adicionar {tipo}",
|
||||
"AttachmentBlock.delete": "Anexo apagado.",
|
||||
"AttachmentBlock.failed": "Este arquivo não pôde ser carregado pois ultrapassou o tamanho limite.",
|
||||
"AttachmentBlock.upload": "Carregando anexo.",
|
||||
"AttachmentBlock.uploadSuccess": "Anexo carregado.",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "Apagar",
|
||||
"AttachmentElement.download": "Baixar"
|
||||
}
|
@ -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",
|
||||
|
@ -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": "Доски",
|
||||
|
@ -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ť",
|
||||
|
@ -277,7 +277,7 @@
|
||||
"SidebarTour.SidebarCategories.Body": "Alla dina boards är nu organiserade i ditt nya sidofält. Du behöver inte längre växla mellan olika arbetsområden. Anpassade kategorier baserade på dina tidigare arbetsytor kan ha skapats automatiskt för dig som en del av din uppgradering av v7.2. Dessa kan tas bort eller redigeras enligt dina önskemål.",
|
||||
"SidebarTour.SidebarCategories.Link": "Mer information",
|
||||
"SidebarTour.SidebarCategories.Title": "Kategorier i sidoomenyn",
|
||||
"SiteStats.total_boards": "Totalt antal boards",
|
||||
"SiteStats.total_boards": "Totalt antal tavlor",
|
||||
"SiteStats.total_cards": "Totalt antal kort",
|
||||
"TableComponent.add-icon": "Lägg till ikon",
|
||||
"TableComponent.name": "Namn",
|
||||
@ -395,6 +395,10 @@
|
||||
"login.log-in-button": "Logga in",
|
||||
"login.log-in-title": "Logga in",
|
||||
"login.register-button": "eller skapa ett konto om du inte redan har ett",
|
||||
"new_channel_modal.create_board.empty_board_description": "Skapa en ny tom tavla",
|
||||
"new_channel_modal.create_board.empty_board_title": "Tom tavla",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "Välj en mall",
|
||||
"new_channel_modal.create_board.title": "Skapa en tavla för den här kanalen",
|
||||
"notification-box-card-limit-reached.close-tooltip": "Sov i 10 dagar",
|
||||
"notification-box-card-limit-reached.contact-link": "notifiera din administratör",
|
||||
"notification-box-card-limit-reached.link": "Uppgradera till ett betal-abonnemang",
|
||||
|
@ -4,9 +4,9 @@
|
||||
"AttachmentBlock.DeleteAction": "видалити",
|
||||
"AttachmentBlock.addElement": "додати {type}",
|
||||
"AttachmentBlock.delete": "Прикріплення успішно видалено.",
|
||||
"AttachmentBlock.failed": "Неможливо завантажити файл. Досягнуто ліміт розміру прикріпленного файлу.",
|
||||
"AttachmentBlock.failed": "Не вдалося завантажити цей файл, оскільки досягнуто обмеження розміру файлу.",
|
||||
"AttachmentBlock.upload": "Прикріплення завантажуються.",
|
||||
"AttachmentBlock.uploadSuccess": "Прикріплення завантажені успішно.",
|
||||
"AttachmentBlock.uploadSuccess": "Вкладення завантажено.",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "Видалити",
|
||||
"AttachmentElement.download": "Завантажити",
|
||||
"AttachmentElement.upload-percentage": "Завантаження...({uploadPercent}%)",
|
||||
@ -16,13 +16,13 @@
|
||||
"BoardComponent.hide": "Приховати",
|
||||
"BoardComponent.new": "+ Створити",
|
||||
"BoardComponent.no-property": "Немає {property}",
|
||||
"BoardComponent.no-property-title": "Елементи з порожнім {property} полем потраплять сюди. Цей стовпець неможливо видалити.",
|
||||
"BoardComponent.no-property-title": "Елементи з порожнім полем {property} потраплять сюди. Цей стовпець неможливо видалити.",
|
||||
"BoardComponent.show": "Показати",
|
||||
"BoardMember.schemeAdmin": "Адміністратор",
|
||||
"BoardMember.schemeCommenter": "Коментатор",
|
||||
"BoardMember.schemeEditor": "Редактор",
|
||||
"BoardMember.schemeNone": "Жоден",
|
||||
"BoardMember.schemeViewer": "Глядач",
|
||||
"BoardMember.schemeViewer": "Спостерігач",
|
||||
"BoardMember.unlinkChannel": "Від’єднати",
|
||||
"BoardPage.newVersion": "Доступна оновлена версія Панелі, тицьни тут щоб оновити.",
|
||||
"BoardPage.syncFailed": "Можливо Панель видалено або права анульовано.",
|
||||
@ -44,8 +44,205 @@
|
||||
"Calculations.Options.count.displayName": "Кількість",
|
||||
"Calculations.Options.count.label": "Кількість",
|
||||
"Calculations.Options.countChecked.displayName": "Перевірено",
|
||||
"Calculations.Options.countChecked.label": "Кількість перевірено",
|
||||
"Calculations.Options.countUnchecked.displayName": "Не перевірено",
|
||||
"Calculations.Options.countUnchecked.label": "Підрахунок не перевірено",
|
||||
"Calculations.Options.countUniqueValue.displayName": "Унікальний",
|
||||
"Calculations.Options.countUniqueValue.label": "Підрахувати унікальні значення",
|
||||
"Calculations.Options.countValue.displayName": "Значення",
|
||||
"Calculations.Options.countValue.label": "Розрахунок значення",
|
||||
"Calculations.Options.dateRange.displayName": "Діапазон",
|
||||
"Calculations.Options.dateRange.label": "Діапазон",
|
||||
"Calculations.Options.earliest.displayName": "Найраніший",
|
||||
"Calculations.Options.earliest.label": "Найраніший",
|
||||
"Calculations.Options.latest.displayName": "Останній",
|
||||
"Calculations.Options.latest.label": "Останній",
|
||||
"Calculations.Options.max.displayName": "Макс",
|
||||
"Calculations.Options.max.label": "Макс",
|
||||
"Calculations.Options.median.displayName": "Медіана",
|
||||
"Calculations.Options.median.label": "Медіана",
|
||||
"Calculations.Options.min.displayName": "Мін",
|
||||
"Calculations.Options.min.label": "Мін",
|
||||
"Calculations.Options.none.displayName": "Обчислити",
|
||||
"Calculations.Options.none.label": "Жодного",
|
||||
"Calculations.Options.percentChecked.displayName": "Перевірено",
|
||||
"Calculations.Options.percentChecked.label": "Відсоток перевірено",
|
||||
"Calculations.Options.percentUnchecked.displayName": "Не перевірено",
|
||||
"Calculations.Options.percentUnchecked.label": "Відсоток не перевірено",
|
||||
"Calculations.Options.range.displayName": "Діапазон",
|
||||
"Calculations.Options.range.label": "Діапазон",
|
||||
"Calculations.Options.sum.displayName": "Сума",
|
||||
"Calculations.Options.sum.label": "Сума",
|
||||
"CalendarCard.untitled": "Без назви",
|
||||
"CardActionsMenu.copiedLink": "Скопійовано!",
|
||||
"CardActionsMenu.copyLink": "Копіювати посилання",
|
||||
"CardActionsMenu.delete": "Видалити",
|
||||
"CardActionsMenu.duplicate": "Дублювати",
|
||||
"CardBadges.title-checkboxes": "Прапорці",
|
||||
"CardBadges.title-comments": "Коментарі",
|
||||
"CardBadges.title-description": "Ця картка має опис",
|
||||
"CardDetail.Attach": "Прикріпити",
|
||||
"CardDetail.Follow": "Слідкувати",
|
||||
"CardDetail.Following": "Відслідковувати",
|
||||
"CardDetail.add-content": "Додайте вміст",
|
||||
"CardDetail.add-icon": "Додати значок",
|
||||
"CardDetail.add-property": "+ Додати властивість",
|
||||
"CardDetail.addCardText": "додати текст картки",
|
||||
"CardDetail.limited-body": "Перейдіть на наш план Professional або Enterprise.",
|
||||
"CardDetail.limited-button": "Оновлення",
|
||||
"CardDetail.limited-title": "Ця прихована картка",
|
||||
"CardDetail.moveContent": "Перемістити вміст картки",
|
||||
"CardDetail.new-comment-placeholder": "Додати коментар...",
|
||||
"CardDetailProperty.confirm-delete-heading": "Підтвердьте видалення властивості",
|
||||
"CardDetailProperty.confirm-delete-subtext": "Ви впевнені, що хочете видалити властивість \"{propertyName}\"? При видаленні властивість буде видалено з усіх карток на цій дошці.",
|
||||
"CardDetailProperty.confirm-property-name-change-subtext": "Ви дійсно хочете змінити властивість \"{propertyName}\" {customText}? Це вплине на значення(-я) на {numOfCards} картці(-ах) на цій дошці і може призвести до втрати даних.",
|
||||
"CardDetailProperty.confirm-property-type-change": "Підтвердити зміну типу власності",
|
||||
"CardDetailProperty.delete-action-button": "Видалити",
|
||||
"CardDetailProperty.property-change-action-button": "Змінити властивість",
|
||||
"CardDetailProperty.property-changed": "Властивість змінена успішно!",
|
||||
"CardDetailProperty.property-deleted": "{propertyName} успішно видалено!",
|
||||
"CardDetailProperty.property-name-change-subtext": "тип з \"{oldPropType}\" в \"{newPropType}\"",
|
||||
"CardDetial.limited-link": "Дізнайтеся більше про наші плани.",
|
||||
"CardDialog.delete-confirmation-dialog-attachment": "Підтвердити видалення вкладення",
|
||||
"CardDialog.delete-confirmation-dialog-button-text": "Видалити",
|
||||
"CardDialog.delete-confirmation-dialog-heading": "Підтвердити видалення картки",
|
||||
"CardDialog.editing-template": "Ви редагуєте шаблон.",
|
||||
"CardDialog.nocard": "Ця картка не існує або недоступна.",
|
||||
"Categories.CreateCategoryDialog.CancelText": "Скасувати",
|
||||
"Categories.CreateCategoryDialog.CreateText": "Створити",
|
||||
"Categories.CreateCategoryDialog.Placeholder": "Назвіть свою категорію",
|
||||
"Categories.CreateCategoryDialog.UpdateText": "Оновити",
|
||||
"CenterPanel.Login": "Логін",
|
||||
"CenterPanel.Share": "Поділитися",
|
||||
"ChannelIntro.CreateBoard": "Створити дошку",
|
||||
"CloudMessage.cloud-server": "Отримайте власний безкоштовний хмарний сервер.",
|
||||
"ColorOption.selectColor": "Виберіть колір {color}",
|
||||
"Comment.delete": "Видалити",
|
||||
"CommentsList.send": "Надіслати",
|
||||
"ConfirmPerson.empty": "Порожній",
|
||||
"ConfirmPerson.search": "Пошук...",
|
||||
"ConfirmationDialog.cancel-action": "Скасувати",
|
||||
"ConfirmationDialog.confirm-action": "Підтвердити",
|
||||
"ContentBlock.Delete": "Видалити",
|
||||
"ContentBlock.DeleteAction": "видалити",
|
||||
"ContentBlock.addElement": "додати {type}",
|
||||
"ContentBlock.checkbox": "прапорець",
|
||||
"ContentBlock.divider": "роздільник",
|
||||
"ContentBlock.editCardCheckbox": "позначений прапорець",
|
||||
"ContentBlock.editCardCheckboxText": "редагувати текст картки",
|
||||
"ContentBlock.editCardText": "редагувати текст картки",
|
||||
"ContentBlock.editText": "Редагувати текст...",
|
||||
"ContentBlock.image": "зображення",
|
||||
"ContentBlock.insertAbove": "Вставте вище",
|
||||
"ContentBlock.moveBlock": "перемістити вміст картки",
|
||||
"ContentBlock.moveDown": "Опустити",
|
||||
"ContentBlock.moveUp": "Підняти",
|
||||
"ContentBlock.text": "текст",
|
||||
"DateRange.clear": "Очистити",
|
||||
"DateRange.empty": "Пусто",
|
||||
"DateRange.endDate": "Дата закінчення",
|
||||
"DateRange.today": "Сьогодні",
|
||||
"DeleteBoardDialog.confirm-cancel": "Скасувати",
|
||||
"DeleteBoardDialog.confirm-delete": "Видалити",
|
||||
"DeleteBoardDialog.confirm-info": "Ви впевнені, що хочете видалити дошку “{boardTitle}”? Видалення призведе до видалення всіх карток на дошці.",
|
||||
"DeleteBoardDialog.confirm-info-template": "Ви впевнені, що хочете видалити шаблон дошки «{boardTitle}»?",
|
||||
"DeleteBoardDialog.confirm-tite": "Підтвердьте видалення дошки",
|
||||
"DeleteBoardDialog.confirm-tite-template": "Підтвердьте видалення шаблону дошки",
|
||||
"Dialog.closeDialog": "Закрити діалогове вікно",
|
||||
"EditableDayPicker.today": "Сьогодні",
|
||||
"Error.mobileweb": "Мобільна веб-підтримка зараз знаходиться на ранній стадії бета-тестування. Не всі функції можуть бути присутніми.",
|
||||
"Error.websocket-closed": "З'єднання через веб-сокет закрито, з'єднання перервано. Якщо це продовжується й далі, перевірте конфігурацію сервера або веб-проксі.",
|
||||
"Filter.contains": "містить",
|
||||
"Filter.ends-with": "закінчується на",
|
||||
"Filter.includes": "включає в себе",
|
||||
"Filter.is": "є",
|
||||
"Filter.is-empty": "пусто",
|
||||
"Filter.is-not-empty": "не порожній",
|
||||
"Filter.is-not-set": "не встановлено",
|
||||
"Filter.is-set": "встановлено",
|
||||
"Filter.not-contains": "не містить",
|
||||
"Filter.not-ends-with": "не закінчується",
|
||||
"Filter.not-includes": "не включає",
|
||||
"Filter.not-starts-with": "не починається з",
|
||||
"Filter.starts-with": "починається з",
|
||||
"FilterByText.placeholder": "фільтрувати текст",
|
||||
"FilterComponent.add-filter": "+ Додати фільтр",
|
||||
"FilterComponent.delete": "Видалити",
|
||||
"FilterValue.empty": "(порожній)",
|
||||
"FindBoardsDialog.IntroText": "Пошук дощок",
|
||||
"FindBoardsDialog.NoResultsFor": "Немає результатів для \"{searchQuery}\"",
|
||||
"FindBoardsDialog.NoResultsSubtext": "Перевірте правильність написання або спробуйте інший запит.",
|
||||
"FindBoardsDialog.SubTitle": "Введіть, щоб знайти дошку. Використовуйте <b>ВГОРУ/ВНИЗ</b> для перегляду. <b>ENTER</b>, щоб вибрати, <b>ESC</b>, щоб закрити",
|
||||
"FindBoardsDialog.Title": "Знайти дошки",
|
||||
"GroupBy.hideEmptyGroups": "Сховати {count} порожні групи",
|
||||
"GroupBy.showHiddenGroups": "Показати {count} прихованих груп",
|
||||
"GroupBy.ungroup": "Розгрупувати",
|
||||
"HideBoard.MenuOption": "Сховати дошку",
|
||||
"KanbanCard.untitled": "Без назви",
|
||||
"MentionSuggestion.is-not-board-member": "(не член правління)",
|
||||
"Mutator.new-board-from-template": "нова дошка з шаблону",
|
||||
"Mutator.new-card-from-template": "нова картка із шаблону",
|
||||
"Mutator.new-template-from-card": "новий шаблон із картки",
|
||||
"OnboardingTour.AddComments.Body": "Ви можете коментувати проблеми та навіть @згадувати інших користувачів Mattermost, щоб привернути їх увагу.",
|
||||
"OnboardingTour.AddComments.Title": "Додати коментарі",
|
||||
"OnboardingTour.AddDescription.Body": "Додайте опис до картки, щоб ваші товариші по команді знали, про що йде мова.",
|
||||
"OnboardingTour.AddDescription.Title": "Додайте опис",
|
||||
"OnboardingTour.AddProperties.Body": "Додайте карткам різні властивості, щоб зробити їх потужнішими.",
|
||||
"OnboardingTour.AddProperties.Title": "Додайте властивості",
|
||||
"OnboardingTour.AddView.Body": "Перейдіть сюди, щоб створити новий вид для організації дошки за допомогою різних макетів.",
|
||||
"OnboardingTour.AddView.Title": "Додайте новий вид",
|
||||
"OnboardingTour.CopyLink.Body": "Ви можете поділитися своїми картками з товаришами по команді, скопіювавши посилання та вставивши його в канал, пряме або групове повідомлення.",
|
||||
"OnboardingTour.CopyLink.Title": "Копіювати посилання",
|
||||
"OnboardingTour.OpenACard.Body": "Відкрийте картку, щоб дослідити потужні способи, за допомогою яких дошки можуть допомогти вам організувати вашу роботу.",
|
||||
"OnboardingTour.OpenACard.Title": "Відкрити картку",
|
||||
"OnboardingTour.ShareBoard.Body": "Ви можете поділитися своєю дошкою всередині, у своїй команді або опублікувати її публічно для видимості за межами вашої організації.",
|
||||
"OnboardingTour.ShareBoard.Title": "Поділитися дошкою",
|
||||
"PersonProperty.board-members": "Члени команди",
|
||||
"PersonProperty.me": "Я",
|
||||
"PersonProperty.non-board-members": "Не учасник команди",
|
||||
"PropertyMenu.Delete": "Видалити",
|
||||
"PropertyMenu.changeType": "Змінити тип власності",
|
||||
"PropertyMenu.selectType": "Виберіть тип властивості",
|
||||
"PropertyMenu.typeTitle": "Тип",
|
||||
"PropertyType.Checkbox": "Прапорець",
|
||||
"PropertyType.CreatedBy": "Створений",
|
||||
"PropertyType.CreatedTime": "Час створення",
|
||||
"PropertyType.Date": "Дата",
|
||||
"PropertyType.Email": "Email",
|
||||
"PropertyType.MultiPerson": "Кілька осіб",
|
||||
"PropertyType.MultiSelect": "Множинний вибір",
|
||||
"PropertyType.Number": "Номер",
|
||||
"PropertyType.Person": "Особа",
|
||||
"PropertyType.Phone": "Телефон",
|
||||
"PropertyType.Select": "Обрати",
|
||||
"PropertyType.Text": "Текст",
|
||||
"PropertyType.Unknown": "Невідомий",
|
||||
"PropertyType.UpdatedBy": "Оновлено користувачем",
|
||||
"PropertyType.UpdatedTime": "Час останнього оновлення",
|
||||
"PropertyType.Url": "URL",
|
||||
"PropertyValueElement.empty": "Пусто",
|
||||
"RegistrationLink.confirmRegenerateToken": "Це призведе до скасування попередніх спільних посилань. Продовжити?",
|
||||
"RegistrationLink.copiedLink": "Скопійовано!",
|
||||
"RegistrationLink.copyLink": "Копіювати посилання",
|
||||
"RegistrationLink.description": "Поділіться цим посиланням, щоб інші могли створити облікові записи:",
|
||||
"RegistrationLink.regenerateToken": "Згенерувати новий токен",
|
||||
"RegistrationLink.tokenRegenerated": "Реєстраційне посилання відновлено",
|
||||
"ShareBoard.PublishDescription": "Опублікуйте та поділіться посиланням лише для читання з усіма в Інтернеті.",
|
||||
"ShareBoard.PublishTitle": "Опублікувати в Інтернеті",
|
||||
"ShareBoard.ShareInternal": "Поділитися всередині організації",
|
||||
"ShareBoard.ShareInternalDescription": "Користувачі, які мають дозволи, зможуть використовувати це посилання.",
|
||||
"ShareBoard.Title": "Поділиться Дошкою",
|
||||
"Sidebar.delete-board": "Видалити дошку",
|
||||
"SidebarCategories.CategoryMenu.Delete": "Видалити категорію",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Видалити дану категорію?",
|
||||
"TableHeaderMenu.delete": "Видалити",
|
||||
"View.DeleteView": "Видалити вид",
|
||||
"ViewHeader.delete-template": "Видалити",
|
||||
"generic.previous": "Попередній",
|
||||
"tutorial_tip.ok": "Гаразд",
|
||||
"shareBoard.unknown-channel-display-name": "Невідомий канал",
|
||||
"tutorial_tip.finish_tour": "Готово",
|
||||
"tutorial_tip.got_it": "Зрозуміло",
|
||||
"tutorial_tip.ok": "Далі",
|
||||
"tutorial_tip.out": "Відмовтеся від цих порад.",
|
||||
"tutorial_tip.seen": "Ви бачили це раніше?"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -3,10 +3,10 @@
|
||||
"Attachment.Attachment-title": "附件",
|
||||
"AttachmentBlock.DeleteAction": "删除",
|
||||
"AttachmentBlock.addElement": "添加 {type}",
|
||||
"AttachmentBlock.delete": "附件已删除",
|
||||
"AttachmentBlock.delete": "附件已删除。",
|
||||
"AttachmentBlock.failed": "该文件无法上传,因为已经达到了文件大小的限制。",
|
||||
"AttachmentBlock.upload": "附件正在上传。",
|
||||
"AttachmentBlock.uploadSuccess": "附件上传成功。",
|
||||
"AttachmentBlock.uploadSuccess": "附件已上传。",
|
||||
"AttachmentElement.delete-confirmation-dialog-button-text": "删除",
|
||||
"AttachmentElement.download": "下载",
|
||||
"AttachmentElement.upload-percentage": "上传中…({uploadPercent}%)",
|
||||
@ -16,7 +16,7 @@
|
||||
"BoardComponent.hide": "隐藏",
|
||||
"BoardComponent.new": "+ 新增",
|
||||
"BoardComponent.no-property": "无 {property}",
|
||||
"BoardComponent.no-property-title": "{property} 属性为空的项目将转到此处。该列无法删除。",
|
||||
"BoardComponent.no-property-title": "{property} 属性为空的项目将转到此处,该列无法删除。",
|
||||
"BoardComponent.show": "显示",
|
||||
"BoardMember.schemeAdmin": "管理",
|
||||
"BoardMember.schemeCommenter": "评论者",
|
||||
@ -27,7 +27,7 @@
|
||||
"BoardPage.newVersion": "Boards 的新版本已可用,点击这里重新加载。",
|
||||
"BoardPage.syncFailed": "板块或许已被删除或访问授权已被撤销。",
|
||||
"BoardTemplateSelector.add-template": "创建新模板",
|
||||
"BoardTemplateSelector.create-empty-board": "创建空白看板",
|
||||
"BoardTemplateSelector.create-empty-board": "创建空白板块",
|
||||
"BoardTemplateSelector.delete-template": "删除",
|
||||
"BoardTemplateSelector.description": "选择一个模板助你开始。或者创建一个空白板块,从零开始。",
|
||||
"BoardTemplateSelector.edit-template": "编辑",
|
||||
@ -35,7 +35,7 @@
|
||||
"BoardTemplateSelector.plugin.no-content-title": "创建一个看板",
|
||||
"BoardTemplateSelector.title": "创建一个看板",
|
||||
"BoardTemplateSelector.use-this-template": "使用该模板",
|
||||
"BoardsSwitcher.Title": "查找看板",
|
||||
"BoardsSwitcher.Title": "查找板块",
|
||||
"BoardsUnfurl.Limited": "由于卡片被存档,其他细节被隐藏",
|
||||
"BoardsUnfurl.Remainder": "+{remainder} 更多",
|
||||
"BoardsUnfurl.Updated": "于 {time} 更新",
|
||||
@ -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": "取消",
|
||||
@ -161,10 +161,15 @@
|
||||
"Filter.is-not-set": "未设置",
|
||||
"Filter.is-set": "被设定为",
|
||||
"Filter.not-contains": "不包含",
|
||||
"Filter.not-ends-with": "并不以",
|
||||
"Filter.not-includes": "不包含",
|
||||
"Filter.not-ends-with": "不结束于",
|
||||
"Filter.not-includes": "不含有",
|
||||
"Filter.not-starts-with": "不开始于",
|
||||
"Filter.starts-with": "开始于",
|
||||
"FilterByText.placeholder": "过滤文本",
|
||||
"FilterComponent.add-filter": "+ 增加过滤条件",
|
||||
"FilterComponent.delete": "删除",
|
||||
"FilterValue.empty": "(空)",
|
||||
"FindBoardsDialog.IntroText": "搜索板块",
|
||||
"FindBoardsDialog.NoResultsFor": "没有\"{searchQuery}\"相关的结果",
|
||||
"FindBoardsDialog.NoResultsSubtext": "请检查拼写或者查找其他内容。",
|
||||
"FindBoardsDialog.SubTitle": "输入内容来查找板块。使用<b>上/下</b>浏览。<b>ENTER</b>选择,<b>ESC</b>取消",
|
||||
@ -172,20 +177,29 @@
|
||||
"GroupBy.hideEmptyGroups": "隐藏{count}个空组",
|
||||
"GroupBy.showHiddenGroups": "显示已隐藏的{count}个组",
|
||||
"GroupBy.ungroup": "未分组",
|
||||
"HideBoard.MenuOption": "隐藏板块",
|
||||
"KanbanCard.untitled": "无标题",
|
||||
"MentionSuggestion.is-not-board-member": "(非板块成员)",
|
||||
"Mutator.new-board-from-template": "从模板创建板块",
|
||||
"Mutator.new-card-from-template": "使用模板新增卡片",
|
||||
"Mutator.new-template-from-card": "从卡片新增模板",
|
||||
"OnboardingTour.AddComments.Body": "你可以对问题进行评论,甚至可以@提及你的Mattermost同伴,以引起他们的注意。",
|
||||
"OnboardingTour.AddComments.Title": "添加评论",
|
||||
"OnboardingTour.AddDescription.Body": "在你的卡片上添加描述,以便其他人了解卡片的内容。",
|
||||
"OnboardingTour.AddDescription.Title": "添加描述",
|
||||
"OnboardingTour.AddProperties.Body": "为卡片添加各种属性,使其更加强大!",
|
||||
"OnboardingTour.AddProperties.Body": "为卡片添加各种属性,使其更加强大。",
|
||||
"OnboardingTour.AddProperties.Title": "添加属性",
|
||||
"OnboardingTour.AddView.Body": "在这里创建一个新的视图,用不同的布局来组织你的板块。",
|
||||
"OnboardingTour.AddView.Title": "添加一个新的视图",
|
||||
"OnboardingTour.CopyLink.Body": "你可以通过频道,私信和群聊分享链接来和成员们一起共享卡片。",
|
||||
"OnboardingTour.CopyLink.Title": "复制链接",
|
||||
"OnboardingTour.OpenACard.Body": "打开卡片来探索板块的高效使用方法,从而助力你的整理项目。",
|
||||
"OnboardingTour.OpenACard.Title": "打开一个卡片",
|
||||
"OnboardingTour.ShareBoard.Body": "你可以分享板块,不管是与内部成员,还是公开发布到外部的机构。",
|
||||
"OnboardingTour.ShareBoard.Title": "分享板块",
|
||||
"PersonProperty.board-members": "板块成员",
|
||||
"PersonProperty.me": "我",
|
||||
"PersonProperty.non-board-members": "非板块成员",
|
||||
"PropertyMenu.Delete": "删除",
|
||||
"PropertyMenu.changeType": "修改属性类型",
|
||||
"PropertyMenu.selectType": "选择属性类型",
|
||||
@ -195,14 +209,17 @@
|
||||
"PropertyType.CreatedTime": "创建时间",
|
||||
"PropertyType.Date": "日期",
|
||||
"PropertyType.Email": "Email",
|
||||
"PropertyType.MultiPerson": "多人",
|
||||
"PropertyType.MultiSelect": "多选",
|
||||
"PropertyType.Number": "数字",
|
||||
"PropertyType.Person": "个人",
|
||||
"PropertyType.Phone": "电话号码",
|
||||
"PropertyType.Select": "选取",
|
||||
"PropertyType.Text": "文字框",
|
||||
"PropertyType.Unknown": "未知",
|
||||
"PropertyType.UpdatedBy": "最后更新者",
|
||||
"PropertyType.UpdatedTime": "上次更新时间",
|
||||
"PropertyType.Url": "URL",
|
||||
"PropertyValueElement.empty": "空的",
|
||||
"RegistrationLink.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?",
|
||||
"RegistrationLink.copiedLink": "已复制!",
|
||||
@ -210,20 +227,22 @@
|
||||
"RegistrationLink.description": "将此链接分享给他人以建立帐号:",
|
||||
"RegistrationLink.regenerateToken": "重新生成令牌",
|
||||
"RegistrationLink.tokenRegenerated": "已重新生成注册链接",
|
||||
"ShareBoard.PublishDescription": "发布并与所有人分享 \"只读 \"链接",
|
||||
"ShareBoard.PublishDescription": "发布并与所有人分享 \"只读 \"链接。",
|
||||
"ShareBoard.PublishTitle": "发布到网上",
|
||||
"ShareBoard.ShareInternal": "内部分享",
|
||||
"ShareBoard.ShareInternalDescription": "有权限的用户将能够使用这个链接",
|
||||
"ShareBoard.ShareInternalDescription": "有权限的用户将能够使用这个链接。",
|
||||
"ShareBoard.Title": "分享板块",
|
||||
"ShareBoard.confirmRegenerateToken": "此动作将使先前分享的链接无效。确定要进行吗?",
|
||||
"ShareBoard.copiedLink": "已复制!",
|
||||
"ShareBoard.copyLink": "复制链接",
|
||||
"ShareBoard.regenerate": "重新生成令牌",
|
||||
"ShareBoard.searchPlaceholder": "搜索成员和频道",
|
||||
"ShareBoard.teamPermissionsText": "在{teamName}团队的每个人",
|
||||
"ShareBoard.tokenRegenrated": "已重新产生令牌",
|
||||
"ShareBoard.userPermissionsRemoveMemberText": "移除成员",
|
||||
"ShareBoard.userPermissionsYouText": "(你)",
|
||||
"ShareTemplate.Title": "分享模板",
|
||||
"ShareTemplate.searchPlaceholder": "搜索成员",
|
||||
"Sidebar.about": "关于 Focalboard",
|
||||
"Sidebar.add-board": "+ 新增版面",
|
||||
"Sidebar.changePassword": "变更密码",
|
||||
@ -234,18 +253,32 @@
|
||||
"Sidebar.import-archive": "导入档案",
|
||||
"Sidebar.invite-users": "邀请使用者",
|
||||
"Sidebar.logout": "登出",
|
||||
"Sidebar.new-category.badge": "新建",
|
||||
"Sidebar.new-category.drag-boards-cta": "拖动板块到这里...",
|
||||
"Sidebar.no-boards-in-category": "里面没有板块",
|
||||
"Sidebar.product-tour": "产品导览",
|
||||
"Sidebar.random-icons": "随机图标",
|
||||
"Sidebar.set-language": "设定语言",
|
||||
"Sidebar.set-theme": "设置主题",
|
||||
"Sidebar.settings": "设定",
|
||||
"Sidebar.template-from-board": "从板块新增一个模板",
|
||||
"Sidebar.untitled-board": "(无标题版面)",
|
||||
"Sidebar.untitled-view": "(未命名视图)",
|
||||
"SidebarCategories.BlocksMenu.Move": "移动到...",
|
||||
"SidebarCategories.CategoryMenu.CreateNew": "创建新类别",
|
||||
"SidebarCategories.CategoryMenu.Delete": "删除类别",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Body": "在于<b>{categoryName}</b>的板块会被移回板块类别。这并不会移除任何板块。",
|
||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "删除此类别?",
|
||||
"SidebarCategories.CategoryMenu.Update": "重命名类别",
|
||||
"SidebarTour.ManageCategories.Body": "新建并管理自定义的类别。类别是用户专属的,所以移动板块到你的类别不会影响到使用同个板块的其他成员。",
|
||||
"SidebarTour.ManageCategories.Title": "管理类别",
|
||||
"SidebarTour.SearchForBoards.Body": "打开类别切换器(Cmd/Ctrl+K)来快速查找并添加板块到你的侧边栏。",
|
||||
"SidebarTour.SearchForBoards.Title": "搜索板块",
|
||||
"SidebarTour.SidebarCategories.Body": "你所有的板块现会在侧边栏下被管理。无需在不同工作区中进行切换。基于你之前工作区的一次性自定义板块,将会作为v7.2版本更新自动创建。这个特性可以在设置里更改会或移除。",
|
||||
"SidebarTour.SidebarCategories.Link": "了解更多",
|
||||
"SidebarTour.SidebarCategories.Title": "侧边栏类别",
|
||||
"SiteStats.total_boards": "所有板块",
|
||||
"SiteStats.total_cards": "所有卡片",
|
||||
"TableComponent.add-icon": "加入图标",
|
||||
"TableComponent.name": "姓名",
|
||||
"TableComponent.plus-new": "+ 新增",
|
||||
@ -256,14 +289,23 @@
|
||||
"TableHeaderMenu.insert-right": "在右侧插入",
|
||||
"TableHeaderMenu.sort-ascending": "升序排列",
|
||||
"TableHeaderMenu.sort-descending": "降序排列",
|
||||
"TableRow.DuplicateCard": "复制卡片",
|
||||
"TableRow.MoreOption": "更多操作",
|
||||
"TableRow.open": "开启",
|
||||
"TopBar.give-feedback": "反馈问题",
|
||||
"URLProperty.copiedLink": "已复制!",
|
||||
"URLProperty.copy": "复制",
|
||||
"URLProperty.edit": "编辑",
|
||||
"UndoRedoHotKeys.canRedo": "撤回",
|
||||
"UndoRedoHotKeys.canRedo-with-description": "撤回 {description}",
|
||||
"UndoRedoHotKeys.canUndo": "撤销",
|
||||
"UndoRedoHotKeys.canUndo-with-description": "撤销 {description}",
|
||||
"UndoRedoHotKeys.cannotRedo": "已没有操作可撤回",
|
||||
"UndoRedoHotKeys.cannotUndo": "已没有操作可撤销",
|
||||
"ValueSelector.noOptions": "没有选项。现在添加一个!",
|
||||
"ValueSelector.valueSelector": "值选择器",
|
||||
"ValueSelectorLabel.openMenu": "打开菜单",
|
||||
"VersionMessage.help": "了解查看新版本有什么新特性。",
|
||||
"View.AddView": "添加视图",
|
||||
"View.Board": "板块",
|
||||
"View.DeleteView": "删除视图",
|
||||
@ -273,6 +315,8 @@
|
||||
"View.NewCalendarTitle": "日历视图",
|
||||
"View.NewGalleryTitle": "画廊视图",
|
||||
"View.NewTableTitle": "图表视图",
|
||||
"View.NewTemplateDefaultTitle": "未命名模板",
|
||||
"View.NewTemplateTitle": "未命名",
|
||||
"View.Table": "图表",
|
||||
"ViewHeader.add-template": "+ 新模板",
|
||||
"ViewHeader.delete-template": "删除",
|
||||
@ -288,40 +332,121 @@
|
||||
"ViewHeader.new": "新",
|
||||
"ViewHeader.properties": "属性",
|
||||
"ViewHeader.properties-menu": "属性菜单",
|
||||
"ViewHeader.search-text": "搜索文本",
|
||||
"ViewHeader.search-text": "搜索卡片",
|
||||
"ViewHeader.select-a-template": "选择范本",
|
||||
"ViewHeader.set-default-template": "设为默认范本",
|
||||
"ViewHeader.sort": "排序",
|
||||
"ViewHeader.untitled": "无标题",
|
||||
"ViewHeader.view-header-menu": "查看标题菜单",
|
||||
"ViewHeader.view-menu": "查看菜单",
|
||||
"ViewLimitDialog.Heading": "已达到板块观看的限制",
|
||||
"ViewLimitDialog.PrimaryButton.Title.Admin": "升级",
|
||||
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理员",
|
||||
"ViewLimitDialog.Subtext.Admin": "升级到专业版或企业版。",
|
||||
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多关于我们的付费套装。",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理员来升级到专业版和企业版。",
|
||||
"ViewLimitDialog.UpgradeImg.AltText": "升级图片",
|
||||
"ViewLimitDialog.notifyAdmin.Success": "已通知管理员",
|
||||
"ViewTitle.hide-description": "隐藏描述",
|
||||
"ViewTitle.pick-icon": "挑选图标",
|
||||
"ViewTitle.random-icon": "随机",
|
||||
"ViewTitle.remove-icon": "移除图标",
|
||||
"ViewTitle.show-description": "显示描述",
|
||||
"ViewTitle.untitled-board": "无标题版面",
|
||||
"WelcomePage.Description": "Boards 是一个项目管理工具,使用熟悉的看板视图,帮助你的团队策划、组织、跟踪和管理跨团队的工作",
|
||||
"WelcomePage.Heading": "欢迎来到看板",
|
||||
"ViewTitle.untitled-board": "无标题板块",
|
||||
"WelcomePage.Description": "板块是一个项目管理工具,使用熟悉的看板视图,帮助你的团队策划、组织、跟踪和管理跨团队的工作。",
|
||||
"WelcomePage.Explore.Button": "探索",
|
||||
"WelcomePage.Heading": "欢迎来到板块",
|
||||
"WelcomePage.NoThanks.Text": "不了,请让我自己设置",
|
||||
"WelcomePage.StartUsingIt.Text": "开始使用",
|
||||
"Workspace.editing-board-template": "您正在编辑版面模板。",
|
||||
"badge.guest": "访客",
|
||||
"boardSelector.confirm-link-board": "连接板块到频道",
|
||||
"boardSelector.confirm-link-board-button": "是的,连接板块",
|
||||
"boardSelector.confirm-link-board-subtext": "当你连接“{boardName}”到频道时,所有频道的成员(现有的或新的)都可以编辑。这并不包括访客。你随时都可以取消板块与频道的连接。",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "当你连接\"{boardName}\"到频道时,此频道的所有成员(现有的和新的)将可以进行编辑,这并不包括访客。{lineBreak} 此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。",
|
||||
"boardSelector.create-a-board": "创建板块",
|
||||
"boardSelector.link": "连接",
|
||||
"boardSelector.search-for-boards": "搜索板块",
|
||||
"boardSelector.title": "连接板块",
|
||||
"boardSelector.unlink": "取消连接",
|
||||
"calendar.month": "月",
|
||||
"calendar.today": "今天",
|
||||
"calendar.week": "周",
|
||||
"centerPanel.undefined": "不{propertyName}",
|
||||
"centerPanel.unknown-user": "陌生用户",
|
||||
"cloudMessage.learn-more": "了解更多",
|
||||
"createImageBlock.failed": "图片上传失败,超过大小限制。",
|
||||
"default-properties.badges": "评论和描述",
|
||||
"default-properties.title": "标题",
|
||||
"error.page.title": "抱歉,出错了",
|
||||
"error.back-to-home": "回到主页",
|
||||
"error.back-to-team": "回到团队",
|
||||
"error.board-not-found": "未找到板块。",
|
||||
"error.go-login": "登陆",
|
||||
"error.invalid-read-only-board": "你没有权限访问此板块,请登陆后再进行访问板块。",
|
||||
"error.not-logged-in": "尚未登陆或会话超时,请登陆后再进行访问板块。",
|
||||
"error.page.title": "抱歉,出现了一些错误",
|
||||
"error.team-undefined": "不是有效的团队。",
|
||||
"error.unknown": "发生了一些错误。",
|
||||
"generic.previous": "上一个",
|
||||
"imagePaste.upload-failed": "图片上传失败,超过大小限制",
|
||||
"guest-no-board.subtitle": "你尚未有权限访问此团队的任何一个板块,请等待某人把你添加到某个板块。",
|
||||
"guest-no-board.title": "尚未有板块",
|
||||
"imagePaste.upload-failed": "图片上传失败,超过大小限制。",
|
||||
"limitedCard.title": "卡片已隐藏",
|
||||
"login.log-in-button": "登录",
|
||||
"login.log-in-title": "登录",
|
||||
"login.register-button": "或创建一个帐户(如果您没有帐户)",
|
||||
"new_channel_modal.create_board.empty_board_description": "创建一个空白的板块",
|
||||
"new_channel_modal.create_board.empty_board_title": "空白板块",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "选择模板",
|
||||
"new_channel_modal.create_board.title": "为此频道创建一个新板块",
|
||||
"notification-box-card-limit-reached.close-tooltip": "小睡十天",
|
||||
"notification-box-card-limit-reached.contact-link": "通知你的管理员",
|
||||
"notification-box-card-limit-reached.link": "升级到付费版",
|
||||
"notification-box-card-limit-reached.title": "板块上的{cards}卡片已隐藏",
|
||||
"notification-box-cards-hidden.title": "此行动已隐藏其他卡片",
|
||||
"notification-box.card-limit-reached.not-admin.text": "要访问存档的卡片,你需要通过 {contactLink} 来升级到付费版。",
|
||||
"notification-box.card-limit-reached.text": "已达到卡片上限,如需查看旧卡片请点{link}",
|
||||
"person.add-user-to-board": "将 {username} 加入板块",
|
||||
"person.add-user-to-board-confirm-button": "添加到板块",
|
||||
"person.add-user-to-board-permissions": "权限",
|
||||
"person.add-user-to-board-question": "你想将 {username} 加入板块吗?",
|
||||
"person.add-user-to-board-warning": "{username} 不是此板块的成员,因此不会受到任何关于此板块的通知。",
|
||||
"register.login-button": "或登录(如果您已拥有帐户)",
|
||||
"register.signup-title": "注册您的帐户",
|
||||
"rhs-board-non-admin-msg": "你不是板块的管理员",
|
||||
"rhs-boards.add": "添加",
|
||||
"rhs-boards.dm": "私信",
|
||||
"rhs-boards.gm": "群聊",
|
||||
"rhs-boards.header.dm": "此私信",
|
||||
"rhs-boards.header.gm": "此群聊信息",
|
||||
"rhs-boards.last-update-at": "最后更新日为:{datetime}",
|
||||
"rhs-boards.link-boards-to-channel": "把板块连接到 {channelName}",
|
||||
"rhs-boards.linked-boards": "连接板块",
|
||||
"rhs-boards.no-boards-linked-to-channel": "尚未有板块与{channelName} 连接",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "板块是一个能帮助我们定义,组织,追踪和管理团队工作的一个专业管理工具,可通过使用熟悉的看板视图。",
|
||||
"rhs-boards.unlink-board": "取消连接板块",
|
||||
"rhs-boards.unlink-board1": "取消连接板块",
|
||||
"rhs-channel-boards-header.title": "板块",
|
||||
"share-board.publish": "发布",
|
||||
"share-board.share": "分享",
|
||||
"shareBoard.lastAdmin": "Boards 至少得有一位管理员",
|
||||
"shareBoard.channels-select-group": "频道",
|
||||
"shareBoard.confirm-change-team-role.body": "此板块低于“{role}”的所有人都将<b>于现在被提升到{role}</b>。你确认要更改此板块的最低职责?",
|
||||
"shareBoard.confirm-change-team-role.confirmBtnText": "更改板块的最低职责",
|
||||
"shareBoard.confirm-change-team-role.title": "更改板块的最低职责",
|
||||
"shareBoard.confirm-link-channel": "连接板块到频道",
|
||||
"shareBoard.confirm-link-channel-button": "连接频道",
|
||||
"shareBoard.confirm-link-channel-button-with-other-channel": "再此取消或进行连接",
|
||||
"shareBoard.confirm-link-channel-subtext": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。",
|
||||
"shareBoard.confirm-link-channel-subtext-with-other-channel": "当你把频道连接到一个板块,此频道里的所有成员(现有的或新的)都可以进行编辑,这并不包括访客。{lineBreak}此板块目前与另一个频道已有连接,如果在此进行新的连接,那么将会自动取消之前连接的频道。",
|
||||
"shareBoard.confirm-unlink.body": "当你取消频道与板块的连接,频道的所有成员(现有的或新的)将会失去板块的访问权限,除非单独给予许可。",
|
||||
"shareBoard.confirm-unlink.confirmBtnText": "取消连接频道",
|
||||
"shareBoard.confirm-unlink.title": "取消连接此板块的频道",
|
||||
"shareBoard.lastAdmin": "板块至少得有一位管理员",
|
||||
"shareBoard.members-select-group": "成员",
|
||||
"shareBoard.unknown-channel-display-name": "未知频道",
|
||||
"tutorial_tip.finish_tour": "完成",
|
||||
"tutorial_tip.got_it": "明白了",
|
||||
"tutorial_tip.got_it": "了解",
|
||||
"tutorial_tip.ok": "下一个",
|
||||
"tutorial_tip.seen": "之前见过这吗?"
|
||||
"tutorial_tip.out": "选择不使用这些提示。",
|
||||
"tutorial_tip.seen": "之前有见到过吗?"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"Attachment.Attachment-title": "附件",
|
||||
"AttachmentBlock.DeleteAction": "刪除",
|
||||
"AttachmentBlock.addElement": "添加 {type}",
|
||||
"AttachmentBlock.delete": "附件刪除成功。",
|
||||
"AttachmentBlock.delete": "已刪除附件",
|
||||
"AttachmentBlock.failed": "無法上傳文件。 附件大小已達到限制。",
|
||||
"AttachmentBlock.upload": "附件正在上傳。",
|
||||
"AttachmentBlock.uploadSuccess": "附件已上傳",
|
||||
@ -88,7 +88,7 @@
|
||||
"CardDetail.add-icon": "新增圖示",
|
||||
"CardDetail.add-property": "+ 新增屬性",
|
||||
"CardDetail.addCardText": "新增卡片文本",
|
||||
"CardDetail.limited-body": "升級到專業版或是企業計劃,以查看封存卡片,獲得無限看版,無限卡片和更多功能。",
|
||||
"CardDetail.limited-body": "升級到專業版或是企業版",
|
||||
"CardDetail.limited-button": "升級",
|
||||
"CardDetail.limited-title": "此卡片被影藏",
|
||||
"CardDetail.moveContent": "移動卡片內容",
|
||||
@ -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": "新增視圖",
|
||||
@ -193,7 +197,8 @@
|
||||
"OnboardingTour.OpenACard.Title": "瀏覽卡片",
|
||||
"OnboardingTour.ShareBoard.Body": "您可以在內部、團隊內部分享看板,或公開發布讓組織外部查看。",
|
||||
"OnboardingTour.ShareBoard.Title": "分享看板",
|
||||
"PersonProperty.board-members": "看版成員",
|
||||
"PersonProperty.board-members": "看板成員",
|
||||
"PersonProperty.me": "我",
|
||||
"PersonProperty.non-board-members": "不是看板成員",
|
||||
"PropertyMenu.Delete": "刪除",
|
||||
"PropertyMenu.changeType": "修改屬性類型",
|
||||
@ -273,7 +278,7 @@
|
||||
"SidebarTour.SidebarCategories.Link": "更多",
|
||||
"SidebarTour.SidebarCategories.Title": "邊欄類別",
|
||||
"SiteStats.total_boards": "所有看板",
|
||||
"SiteStats.total_cards": "總牌數",
|
||||
"SiteStats.total_cards": "總卡片數",
|
||||
"TableComponent.add-icon": "加入圖示",
|
||||
"TableComponent.name": "姓名",
|
||||
"TableComponent.plus-new": "+ 新增",
|
||||
@ -284,6 +289,7 @@
|
||||
"TableHeaderMenu.insert-right": "在右側插入",
|
||||
"TableHeaderMenu.sort-ascending": "升序排列",
|
||||
"TableHeaderMenu.sort-descending": "降序排列",
|
||||
"TableRow.DuplicateCard": "複製卡片",
|
||||
"TableRow.MoreOption": "更多操作",
|
||||
"TableRow.open": "開啟",
|
||||
"TopBar.give-feedback": "提供回饋",
|
||||
@ -336,9 +342,9 @@
|
||||
"ViewLimitDialog.Heading": "已達到每個看板觀看限制",
|
||||
"ViewLimitDialog.PrimaryButton.Title.Admin": "升級",
|
||||
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "通知管理者",
|
||||
"ViewLimitDialog.Subtext.Admin": "升級到專業版或企業版,獲得每個看板無限瀏覽、無限卡片,以及更多。",
|
||||
"ViewLimitDialog.Subtext.Admin": "升級到專業版或企業版",
|
||||
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "了解更多我們的計畫。",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版,獲得無限使用看板、卡片、更多。",
|
||||
"ViewLimitDialog.Subtext.RegularUser": "通知你的管理員升級到專業版或是企業版",
|
||||
"ViewLimitDialog.UpgradeImg.AltText": "升級圖片",
|
||||
"ViewLimitDialog.notifyAdmin.Success": "已經通知管理者",
|
||||
"ViewTitle.hide-description": "隱藏敘述",
|
||||
@ -355,17 +361,19 @@
|
||||
"Workspace.editing-board-template": "您正在編輯版面範本。",
|
||||
"badge.guest": "訪客",
|
||||
"boardSelector.confirm-link-board": "連結看板與頻道",
|
||||
"boardSelector.confirm-link-board-button": "是,連結看版",
|
||||
"boardSelector.confirm-link-board-button": "是,連結看板",
|
||||
"boardSelector.confirm-link-board-subtext": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。你可以在任何時候從一個頻道上取消看板的連接。",
|
||||
"boardSelector.confirm-link-board-subtext-with-other-channel": "當你將\"{boardName}\"連接到頻道時,該頻道的所有成員(現有的和新的)都可以編輯。並不包含訪客身分。{lineBreak} 看板目前正連接到另一個頻道。如果选择在這裡連接它,將取消另一個連接。",
|
||||
"boardSelector.create-a-board": "建立看版",
|
||||
"boardSelector.create-a-board": "建立看板",
|
||||
"boardSelector.link": "連結",
|
||||
"boardSelector.search-for-boards": "搜尋看板",
|
||||
"boardSelector.title": "連結看版",
|
||||
"boardSelector.title": "連結看板",
|
||||
"boardSelector.unlink": "未連結",
|
||||
"calendar.month": "月份",
|
||||
"calendar.today": "今日",
|
||||
"calendar.week": "週別",
|
||||
"centerPanel.undefined": "沒有 {propertyName}",
|
||||
"centerPanel.unknown-user": "未知使用者",
|
||||
"cloudMessage.learn-more": "學習更多",
|
||||
"createImageBlock.failed": "無法上傳檔案,檔案大小超過限制。",
|
||||
"default-properties.badges": "評論和描述",
|
||||
@ -374,19 +382,23 @@
|
||||
"error.back-to-team": "回到團隊",
|
||||
"error.board-not-found": "沒有找到看板.",
|
||||
"error.go-login": "登入",
|
||||
"error.invalid-read-only-board": "沒有權限進入此看版.登入後才能訪問.",
|
||||
"error.invalid-read-only-board": "沒有權限進入此看板.登入後才能訪問.",
|
||||
"error.not-logged-in": "已被登出,請再次登入使用看板。",
|
||||
"error.page.title": "很抱歉,發生了些錯誤",
|
||||
"error.team-undefined": "不是有效的團隊。",
|
||||
"error.unknown": "發生一個錯誤。",
|
||||
"generic.previous": "上一篇",
|
||||
"guest-no-board.subtitle": "你尚未有權限進入此看板,請等人把你加入任何看板。",
|
||||
"guest-no-board.title": "尚未有看版",
|
||||
"guest-no-board.title": "尚未有看板",
|
||||
"imagePaste.upload-failed": "有些檔案無法上傳.檔案大小達上限",
|
||||
"limitedCard.title": "影藏卡片",
|
||||
"login.log-in-button": "登錄",
|
||||
"login.log-in-title": "登錄",
|
||||
"login.register-button": "或創建一個帳戶(如果您沒有帳戶)",
|
||||
"new_channel_modal.create_board.empty_board_description": "建立新的空白看板",
|
||||
"new_channel_modal.create_board.empty_board_title": "空白看板",
|
||||
"new_channel_modal.create_board.select_template_placeholder": "選擇一個範本",
|
||||
"new_channel_modal.create_board.title": "在這個頻道新建一個看板",
|
||||
"notification-box-card-limit-reached.close-tooltip": "小睡十天",
|
||||
"notification-box-card-limit-reached.contact-link": "通知管理員",
|
||||
"notification-box-card-limit-reached.link": "升級到付費版",
|
||||
@ -412,8 +424,8 @@
|
||||
"rhs-boards.linked-boards": "連結看板",
|
||||
"rhs-boards.no-boards-linked-to-channel": "還沒有看板與{channelName}連接",
|
||||
"rhs-boards.no-boards-linked-to-channel-description": "看板是一個專案管理工具,可以使用熟悉的圖表幫助我們定義、組織、追蹤和管理跨團隊工作。",
|
||||
"rhs-boards.unlink-board": "未連結看版",
|
||||
"rhs-boards.unlink-board1": "未連結看版",
|
||||
"rhs-boards.unlink-board": "未連結看板",
|
||||
"rhs-boards.unlink-board1": "未連結看板",
|
||||
"rhs-channel-boards-header.title": "板塊",
|
||||
"share-board.publish": "發布",
|
||||
"share-board.share": "分享",
|
||||
|
4
webapp/package-lock.json
generated
4
webapp/package-lock.json
generated
@ -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",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "focalboard",
|
||||
"version": "7.9.0",
|
||||
"version": "7.10.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
|
@ -107,6 +107,7 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
|
||||
inverted={true}
|
||||
className='add-board-icon'
|
||||
icon={<AddIcon/>}
|
||||
title={'Add Board Dropdown'}
|
||||
/>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
|
@ -1,5 +1,6 @@
|
||||
.Attachment {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
.attachment-header {
|
||||
display: flex;
|
||||
@ -13,7 +14,6 @@
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
.attachment-plus-icon {
|
||||
|
@ -182,6 +182,7 @@ const CardDialog = (props: Props): JSX.Element => {
|
||||
removeUploadingAttachment(uploadingBlock)
|
||||
const block = createAttachmentBlock()
|
||||
block.fields.attachmentId = attachmentId || ''
|
||||
block.title = attachment.name
|
||||
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded successfull.'}), severity: 'normal'})
|
||||
resolve(block)
|
||||
} else {
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -1991,6 +1991,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Team Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2017,6 +2026,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
>
|
||||
@username_2
|
||||
</strong>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -728,3 +728,263 @@ exports[`src/components/shareBoard/userPermissionsRow should match snapshot in t
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/shareBoard/userPermissionsRow should match snapshot-admin 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="user-item"
|
||||
>
|
||||
<div
|
||||
class="user-item__content"
|
||||
>
|
||||
<div
|
||||
class="ml-3"
|
||||
>
|
||||
<strong />
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<strong
|
||||
class="ml-2 text-light"
|
||||
>
|
||||
(You)
|
||||
</strong>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="user-item__button"
|
||||
>
|
||||
Admin
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down CompassIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
style="top: 40px;"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Viewer"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Viewer
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Commenter"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Commenter
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Editor"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Admin"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex menu-option__check"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<svg
|
||||
class="CheckIcon Icon"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="20,60 40,80 80,40"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="MenuOption MenuSeparator menu-separator"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Remove member"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Remove member
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -548,8 +548,8 @@ describe('src/components/shareBoard/shareBoard', () => {
|
||||
}
|
||||
mockedOctoClient.getSharing.mockResolvedValue(sharing)
|
||||
const users: IUser[] = [
|
||||
{id: 'userid1', username: 'username_1'} as IUser,
|
||||
{id: 'userid2', username: 'username_2'} as IUser,
|
||||
{id: 'userid1', username: 'username_1', permissions: ['manage_team']} as IUser,
|
||||
{id: 'userid2', username: 'username_2', permissions: ['manage_system']} as IUser,
|
||||
{id: 'userid3', username: 'username_3'} as IUser,
|
||||
{id: 'userid4', username: 'username_4'} as IUser,
|
||||
]
|
||||
|
@ -32,6 +32,7 @@ import Button from '../../widgets/buttons/button'
|
||||
import {sendFlashMessage} from '../flashMessages'
|
||||
import {Permission} from '../../constants'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
import AdminBadge from '../../widgets/adminBadge/adminBadge'
|
||||
|
||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
||||
|
||||
@ -310,6 +311,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
||||
<strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong>
|
||||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||
<AdminBadge permissions={user.permissions}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -104,6 +104,38 @@ describe('src/components/shareBoard/userPermissionsRow', () => {
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot-admin', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
|
||||
const store = mockStateStore([thunk], state)
|
||||
|
||||
const newMe = Object.assign({}, me)
|
||||
newMe.permissions = ['manage_system']
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserPermissionsRow
|
||||
user={newMe}
|
||||
isMe={true}
|
||||
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||
teammateNameDisplay={'test'}
|
||||
onDeleteBoardMember={() => {}}
|
||||
onUpdateBoardMember={() => {}}
|
||||
/>
|
||||
</ReduxProvider>),
|
||||
{wrapper: MemoryRouter},
|
||||
)
|
||||
container = result.container
|
||||
})
|
||||
|
||||
const buttonElement = container?.querySelector('.user-item__button')
|
||||
expect(buttonElement).toBeDefined()
|
||||
userEvent.click(buttonElement!)
|
||||
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot in plugin mode', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||
|
@ -15,6 +15,7 @@ import {IUser} from '../../user'
|
||||
import {Utils} from '../../utils'
|
||||
import {Permission} from '../../constants'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
import AdminBadge from '../../widgets/adminBadge/adminBadge'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCurrentBoard} from '../../store/boards'
|
||||
|
||||
@ -65,6 +66,7 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
||||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||
{isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>}
|
||||
<GuestBadge show={user.is_guest}/>
|
||||
<AdminBadge permissions={user.permissions}/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -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>
|
||||
|
@ -313,6 +313,7 @@ const SidebarCategory = (props: Props) => {
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu
|
||||
position='auto'
|
||||
fixed={true}
|
||||
parentRef={menuWrapperRef}
|
||||
>
|
||||
{
|
||||
|
@ -203,6 +203,7 @@
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.MultiPerson.octo-propertyvalue,
|
||||
.Person.octo-propertyvalue,
|
||||
.DateRange.octo-propertyvalue {
|
||||
overflow: unset;
|
||||
|
@ -8,6 +8,7 @@ enum Permission {
|
||||
DeleteBoard = 'delete_board',
|
||||
ShareBoard = 'share_board',
|
||||
ManageBoardRoles = 'manage_board_roles',
|
||||
ChannelCreatePost = 'create_post',
|
||||
ManageBoardCards = 'manage_board_cards',
|
||||
ManageBoardProperties = 'manage_board_properties',
|
||||
CommentBoardCards = 'comment_board_cards',
|
||||
@ -36,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 = [
|
||||
@ -196,6 +197,7 @@ class Constants {
|
||||
}
|
||||
|
||||
static readonly globalTeamId = '0'
|
||||
static readonly noChannelID = '0'
|
||||
|
||||
static readonly myInsights = 'MY'
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {BoardView, ISortOption, createBoardView, KanbanCalculationFields} from '
|
||||
import {Card, createCard} from './blocks/card'
|
||||
import {ContentBlock} from './blocks/contentBlock'
|
||||
import {CommentBlock} from './blocks/commentBlock'
|
||||
import {AttachmentBlock} from './blocks/attachmentBlock'
|
||||
import {FilterGroup} from './blocks/filterGroup'
|
||||
import octoClient from './octoClient'
|
||||
import undoManager from './undomanager'
|
||||
@ -26,6 +27,7 @@ import store from './store'
|
||||
import {updateBoards} from './store/boards'
|
||||
import {updateViews} from './store/views'
|
||||
import {updateCards} from './store/cards'
|
||||
import {updateAttachments} from './store/attachments'
|
||||
import {updateComments} from './store/comments'
|
||||
import {updateContents} from './store/contents'
|
||||
import {addBoardUsers, removeBoardUsersById} from './store/users'
|
||||
@ -35,6 +37,7 @@ function updateAllBoardsAndBlocks(boards: Board[], blocks: Block[]) {
|
||||
store.dispatch(updateBoards(boards.filter((b: Board) => b.deleteAt !== 0) as Board[]))
|
||||
store.dispatch(updateViews(blocks.filter((b: Block) => b.type === 'view' || b.deleteAt !== 0) as BoardView[]))
|
||||
store.dispatch(updateCards(blocks.filter((b: Block) => b.type === 'card' || b.deleteAt !== 0) as Card[]))
|
||||
store.dispatch(updateAttachments(blocks.filter((b: Block) => b.type === 'attachment' || b.deleteAt !== 0) as AttachmentBlock[]))
|
||||
store.dispatch(updateComments(blocks.filter((b: Block) => b.type === 'comment' || b.deleteAt !== 0) as CommentBlock[]))
|
||||
store.dispatch(updateContents(blocks.filter((b: Block) => b.type !== 'card' && b.type !== 'view' && b.type !== 'board' && b.type !== 'comment') as ContentBlock[]))
|
||||
})
|
||||
|
@ -51,7 +51,7 @@ class OctoClient {
|
||||
localStorage.setItem('focalboardSessionId', value)
|
||||
}
|
||||
|
||||
constructor(serverUrl?: string, public teamId = Constants.globalTeamId) {
|
||||
constructor(serverUrl?: string, public teamId = Constants.globalTeamId, public channelId = Constants.noChannelID) {
|
||||
this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
@ -160,7 +160,22 @@ class OctoClient {
|
||||
}
|
||||
|
||||
async getMe(): Promise<IUser | undefined> {
|
||||
const path = '/api/v2/users/me'
|
||||
let path = '/api/v2/users/me'
|
||||
let parameters = ''
|
||||
if (this.teamId !== Constants.globalTeamId) {
|
||||
parameters = `teamID=${this.teamId}`
|
||||
}
|
||||
if (this.channelId !== Constants.noChannelID) {
|
||||
const channelClause = `channelID=${this.channelId}`
|
||||
if (parameters) {
|
||||
parameters += '&' + channelClause
|
||||
} else {
|
||||
parameters = channelClause
|
||||
}
|
||||
}
|
||||
if (parameters) {
|
||||
path += '?' + parameters
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
@ -467,12 +482,15 @@ class OctoClient {
|
||||
return this.getJson<BoardMember>(response, {} as BoardMember)
|
||||
}
|
||||
|
||||
async joinBoard(boardId: string): Promise<BoardMember|undefined> {
|
||||
async joinBoard(boardId: string, allowAdmin: boolean): Promise<BoardMember|undefined> {
|
||||
Utils.log(`joinBoard: board ${boardId}`)
|
||||
|
||||
const response = await fetch(this.getBaseURL() + `/api/v2/boards/${boardId}/join`, {
|
||||
method: 'POST',
|
||||
let path = `/api/v2/boards/${boardId}/join`
|
||||
if (allowAdmin) {
|
||||
path += '?allow_admin'
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {
|
||||
headers: this.headers(),
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
@ -680,6 +698,22 @@ class OctoClient {
|
||||
return (await this.getJson(response, [])) as IUser[]
|
||||
}
|
||||
|
||||
async getTeamUsersList(userIds: string[], teamId: string): Promise<IUser[] | []> {
|
||||
const path = this.teamPath(teamId) + '/users'
|
||||
const body = JSON.stringify(userIds)
|
||||
const response = await fetch(this.getBaseURL() + path, {
|
||||
headers: this.headers(),
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (await this.getJson(response, [])) as IUser[]
|
||||
}
|
||||
|
||||
async searchTeamUsers(searchQuery: string, excludeBots?: boolean): Promise<IUser[]> {
|
||||
let path = this.teamPath() + `/users?search=${searchQuery}`
|
||||
if (excludeBots) {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import React, {useEffect, useState, useMemo, useCallback} from 'react'
|
||||
import {batch} from 'react-redux'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useRouteMatch} from 'react-router-dom'
|
||||
import {useRouteMatch, useHistory} from 'react-router-dom'
|
||||
|
||||
import Workspace from '../../components/workspace'
|
||||
import CloudMessage from '../../components/messages/cloudMessage'
|
||||
@ -29,6 +29,7 @@ import {
|
||||
addMyBoardMemberships,
|
||||
} from '../../store/boards'
|
||||
import {getCurrentViewId, setCurrent as setCurrentView, updateViews} from '../../store/views'
|
||||
import ConfirmationDialog from '../../components/confirmationDialogBox'
|
||||
import {initialLoad, initialReadOnlyLoad, loadBoardData} from '../../store/initialLoad'
|
||||
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||
import {setTeam} from '../../store/teams'
|
||||
@ -79,6 +80,8 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const hiddenBoardIDs = useAppSelector(getHiddenBoardIDs)
|
||||
const category = useAppSelector(getCategoryOfBoard(activeBoardId))
|
||||
const [showJoinBoardDialog, setShowJoinBoardDialog] = useState<boolean>(false)
|
||||
const history = useHistory()
|
||||
|
||||
// if we're in a legacy route and not showing a shared board,
|
||||
// redirect to the new URL schema equivalent
|
||||
@ -177,18 +180,40 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
}
|
||||
}, [me?.id, activeBoardId])
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (userId: string, boardTeamId: string, boardId: string) => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0 && userId) {
|
||||
const member = await octoClient.joinBoard(boardId)
|
||||
const onConfirmJoin = async () => {
|
||||
if (me) {
|
||||
joinBoard(me, teamId, match.params.boardId, true)
|
||||
setShowJoinBoardDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => {
|
||||
const member = await octoClient.joinBoard(boardId, allowAdmin)
|
||||
if (!member) {
|
||||
if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) {
|
||||
setShowJoinBoardDialog(true)
|
||||
return
|
||||
}
|
||||
UserSettings.setLastBoardID(boardTeamId, null)
|
||||
UserSettings.setLastViewId(boardId, null)
|
||||
dispatch(setGlobalError('board-not-found'))
|
||||
return
|
||||
}
|
||||
await dispatch(loadBoardData(boardId))
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length > 0 && myUser.id) {
|
||||
// set board as most recently viewed board
|
||||
UserSettings.setLastBoardID(boardTeamId, boardId)
|
||||
}
|
||||
}
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (myUser: IUser, boardTeamId: string, boardId: string) => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0 && myUser.id) {
|
||||
joinBoard(myUser, boardTeamId, boardId, false)
|
||||
} else {
|
||||
// set board as most recently viewed board
|
||||
UserSettings.setLastBoardID(boardTeamId, boardId)
|
||||
}
|
||||
|
||||
dispatch(fetchBoardMembers({
|
||||
@ -204,9 +229,6 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
// set the active board
|
||||
dispatch(setCurrentBoard(match.params.boardId))
|
||||
|
||||
// and set it as most recently viewed board
|
||||
UserSettings.setLastBoardID(teamId, match.params.boardId)
|
||||
|
||||
if (viewId !== Constants.globalTeamId) {
|
||||
// reset current, even if empty string
|
||||
dispatch(setCurrentView(viewId))
|
||||
@ -215,13 +237,15 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
UserSettings.setLastViewId(match.params.boardId, viewId)
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.readonly && me) {
|
||||
loadOrJoinBoard(me.id, teamId, match.params.boardId)
|
||||
}
|
||||
}
|
||||
}, [teamId, match.params.boardId, viewId, me?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (match.params.boardId && !props.readonly && me) {
|
||||
loadOrJoinBoard(me, teamId, match.params.boardId)
|
||||
}
|
||||
}, [teamId, match.params.boardId, me?.id])
|
||||
|
||||
const handleUnhideBoard = async (boardID: string) => {
|
||||
if (!me || !category) {
|
||||
return
|
||||
@ -249,6 +273,27 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showJoinBoardDialog &&
|
||||
<ConfirmationDialog
|
||||
dialogBox={{
|
||||
heading: intl.formatMessage({id: 'boardPage.confirm-join-title', defaultMessage: 'Join private board'}),
|
||||
subText: intl.formatMessage({
|
||||
id: 'boardPage.confirm-join-text',
|
||||
defaultMessage: 'You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?',
|
||||
}),
|
||||
confirmButtonText: intl.formatMessage({id: 'boardPage.confirm-join-button', defaultMessage: 'Join'}),
|
||||
destructive: true, //board.channelId !== '',
|
||||
|
||||
onConfirm: onConfirmJoin,
|
||||
onClose: () => {
|
||||
setShowJoinBoardDialog(false)
|
||||
history.goBack()
|
||||
},
|
||||
}}
|
||||
/>}
|
||||
|
||||
{!showJoinBoardDialog &&
|
||||
<div className='BoardPage'>
|
||||
{!props.new && <TeamToBoardAndViewRedirect/>}
|
||||
<BackwardCompatibilityQueryParamsRedirect/>
|
||||
@ -281,7 +326,6 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
<div className='error'>
|
||||
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
|
||||
</div>}
|
||||
|
||||
{
|
||||
|
||||
// Don't display Templates page
|
||||
@ -292,6 +336,8 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,9 @@ 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>
|
||||
<div
|
||||
class="WelcomePage__content"
|
||||
>
|
||||
<img
|
||||
alt="Boards Welcome Image"
|
||||
class="WelcomePage__image WelcomePage__image--large"
|
||||
@ -28,6 +31,9 @@ exports[`pages/welcome Welcome Page shows Explore Page 1`] = `
|
||||
class="WelcomePage__image WelcomePage__image--small"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
<div
|
||||
class="WelcomePage__buttons"
|
||||
>
|
||||
<button
|
||||
class="Button filled size--large"
|
||||
type="button"
|
||||
@ -46,6 +52,8 @@ exports[`pages/welcome Welcome Page shows Explore Page 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -67,6 +75,9 @@ 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>
|
||||
<div
|
||||
class="WelcomePage__content"
|
||||
>
|
||||
<img
|
||||
alt="Boards Welcome Image"
|
||||
class="WelcomePage__image WelcomePage__image--large"
|
||||
@ -77,6 +88,9 @@ exports[`pages/welcome Welcome Page shows Explore Page with subpath 1`] = `
|
||||
class="WelcomePage__image WelcomePage__image--small"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
<div
|
||||
class="WelcomePage__buttons"
|
||||
>
|
||||
<button
|
||||
class="Button filled size--large"
|
||||
type="button"
|
||||
@ -95,5 +109,7 @@ exports[`pages/welcome Welcome Page shows Explore Page with subpath 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -127,6 +127,7 @@ const WelcomePage = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='WelcomePage__content'>
|
||||
{/* This image will be rendered on large screens over 2000px */}
|
||||
<img
|
||||
src={Utils.buildURL(BoardWelcomePNG, true)}
|
||||
@ -141,6 +142,7 @@ const WelcomePage = () => {
|
||||
alt='Boards Welcome Image'
|
||||
/>
|
||||
|
||||
<div className='WelcomePage__buttons'>
|
||||
{me?.is_guest !== true &&
|
||||
<Button
|
||||
onClick={startTour}
|
||||
@ -182,6 +184,8 @@ const WelcomePage = () => {
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -302,7 +302,7 @@ describe('properties/dateRange', () => {
|
||||
// About `Date()`
|
||||
// > "When called as a function, returns a string representation of the current date and time"
|
||||
const date = new Date()
|
||||
const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12)
|
||||
|
||||
const {getByText, getByTitle} = render(component)
|
||||
const dayDisplay = getByText('Empty')
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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)
|
||||
@ -97,6 +103,7 @@ function DateRange(props: PropertyProps): JSX.Element {
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
const range: DateProperty = {}
|
||||
day.setHours(12)
|
||||
if (isRange) {
|
||||
const newRange = DateUtils.addDayToRange(day, {from: dateFrom, to: dateTo})
|
||||
range.from = newRange.from?.getTime()
|
||||
|
@ -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)
|
||||
if (props.readOnly) {
|
||||
saveTextPropertyRef.current = () => null
|
||||
} else {
|
||||
saveTextPropertyRef.current = saveTextProperty
|
||||
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveTextPropertyRef.current && saveTextPropertyRef.current()
|
||||
|
@ -26,7 +26,9 @@ const attachmentSlice = createSlice({
|
||||
state.attachmentsByCard[attachment.parentId] = [attachment]
|
||||
return
|
||||
}
|
||||
if (state.attachmentsByCard[attachment.parentId].findIndex((a) => a.id === attachment.id) === -1) {
|
||||
state.attachmentsByCard[attachment.parentId].push(attachment)
|
||||
}
|
||||
} else {
|
||||
const parentId = state.attachments[attachment.id]?.parentId
|
||||
if (!state.attachmentsByCard[parentId]) {
|
||||
|
@ -36,7 +36,7 @@ export const fetchBoardMembers = createAsyncThunk(
|
||||
const users = [] as IUser[]
|
||||
const userIDs = members.map((member) => member.userId)
|
||||
|
||||
const usersData = await client.getUsersList(userIDs)
|
||||
const usersData = await client.getTeamUsersList(userIDs, teamId)
|
||||
users.push(...usersData)
|
||||
|
||||
thunkAPI.dispatch(setBoardUsers(users))
|
||||
@ -85,9 +85,13 @@ export const updateMembersEnsuringBoardsAndUsers = createAsyncThunk(
|
||||
if (boardUsers[m.userId]) {
|
||||
return
|
||||
}
|
||||
const user = await client.getUser(m.userId)
|
||||
|
||||
const board = await client.getBoard(m.boardId)
|
||||
if (board) {
|
||||
const user = await client.getTeamUsersList([m.userId], board.teamId)
|
||||
if (user) {
|
||||
thunkAPI.dispatch(addBoardUsers([user]))
|
||||
thunkAPI.dispatch(addBoardUsers(user))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -14,6 +14,7 @@ interface IUser {
|
||||
update_at: number
|
||||
is_bot: boolean
|
||||
is_guest: boolean
|
||||
permissions?: string[]
|
||||
roles: string
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`widgets/adminBadge should match the snapshot for Admin 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/adminBadge should match the snapshot for TeamAdmin 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Team Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
16
webapp/src/widgets/adminBadge/adminBadge.scss
Normal file
16
webapp/src/widgets/adminBadge/adminBadge.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.AdminBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 10px 0 4px;
|
||||
}
|
||||
|
||||
.AdminBadge__box {
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
32
webapp/src/widgets/adminBadge/adminBadge.test.tsx
Normal file
32
webapp/src/widgets/adminBadge/adminBadge.test.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import AdminBadge from './adminBadge'
|
||||
|
||||
describe('widgets/adminBadge', () => {
|
||||
test('should match the snapshot for TeamAdmin', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team']}/>))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match the snapshot for Admin', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team', 'manage_system']}/>))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match the snapshot for empty', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={[]}/>))
|
||||
expect(container).toMatchInlineSnapshot('<div />')
|
||||
})
|
||||
|
||||
test('should match the snapshot for invalid permission', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['invalid_permission']}/>))
|
||||
expect(container).toMatchInlineSnapshot('<div />')
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user