1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-21 13:38:56 +02:00

Merge branch 'main' into rolling-stable

This commit is contained in:
Scott Bishel 2023-02-14 08:25:24 -07:00
commit 100bd70807
96 changed files with 4136 additions and 1263 deletions

View File

@ -12,136 +12,69 @@ require (
)
require (
code.sajari.com/docconv v1.3.5 // indirect
github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/BurntSushi/toml v1.2.0 // indirect
github.com/Masterminds/squirrel v1.5.2 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/RoaringBitmap/roaring v1.2.1 // indirect
github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 // indirect
github.com/aws/aws-sdk-go v1.44.138 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.3.3 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/blevesearch/bleve/v2 v2.3.6-0.20221111171245-56dc9b25507e // indirect
github.com/blevesearch/bleve_index_api v1.0.5 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.1.4 // indirect
github.com/blevesearch/segment v0.9.0 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.1 // indirect
github.com/blevesearch/vellum v1.0.9 // indirect
github.com/blevesearch/zapx/v11 v11.3.7 // indirect
github.com/blevesearch/zapx/v12 v12.3.7 // indirect
github.com/blevesearch/zapx/v13 v13.3.7 // indirect
github.com/blevesearch/zapx/v14 v14.3.7 // indirect
github.com/blevesearch/zapx/v15 v15.3.7 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/set v0.2.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/getsentry/sentry-go v0.15.0 // indirect
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/golang-migrate/migrate/v4 v4.15.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/graph-gophers/graphql-go v1.4.0 // indirect
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect
github.com/hashicorp/go-hclog v1.3.1 // indirect
github.com/hashicorp/go-plugin v1.4.6 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 // indirect
github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
github.com/mattermost/logr/v2 v2.0.15 // indirect
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e // indirect
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 // indirect
github.com/mattermost/squirrel v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mholt/archiver/v3 v3.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.43 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/otiai10/gosseract/v2 v2.4.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/reflog/dateconstraints v0.2.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rs/cors v1.8.2 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/rudderlabs/analytics-go v3.3.3+incompatible // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
@ -150,31 +83,19 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
github.com/splitio/go-client/v6 v6.2.1 // indirect
github.com/splitio/go-split-commons/v3 v3.1.0 // indirect
github.com/splitio/go-toolkit/v4 v4.2.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/throttled/throttled v2.2.5+incompatible // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.1.6 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.4 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/yuin/goldmark v1.5.3 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/sync v0.1.0 // indirect
@ -184,9 +105,7 @@ require (
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -95,8 +95,8 @@ func (a *appAPI) GetBlockHistory(blockID string, opts model.QueryBlockHistoryOpt
return a.store.GetBlockHistory(blockID, opts)
}
func (a *appAPI) GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
return a.store.GetSubTree2(boardID, blockID, opts)
func (a *appAPI) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
return a.store.GetBlockHistoryNewestChildren(parentID, opts)
}
func (a *appAPI) GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) {

View File

@ -20,7 +20,7 @@ const manifestStr = `
"support_url": "https://github.com/mattermost/focalboard/issues",
"release_notes_url": "https://github.com/mattermost/focalboard/releases",
"icon_path": "assets/starter-template-icon.svg",
"version": "7.8.0",
"version": "7.9.0",
"min_server_version": "7.2.0",
"server": {
"executables": {

View File

@ -6,7 +6,8 @@ function blockList(line) {
line.startsWith('.GlobalHeaderComponent') ||
line.startsWith('.boards-rhs-icon') ||
line.startsWith('.focalboard-plugin-root') ||
line.startsWith('.FocalboardUnfurl');
line.startsWith('.FocalboardUnfurl') ||
line.startsWith('.CreateBoardFromTemplate');
}
module.exports = function loader(source) {

View File

@ -14,6 +14,7 @@
"react-intl": "^5.20.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"react-select": "^5.2.2",
"trim-newlines": "4.0.2"
},
"devDependencies": {
@ -2038,6 +2039,150 @@
"node": ">=10.0.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz",
"integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.17.12",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/serialize": "^1.1.1",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.1.3"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@emotion/cache": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
"integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
"dependencies": {
"@emotion/memoize": "^0.8.0",
"@emotion/sheet": "^1.2.1",
"@emotion/utils": "^1.2.0",
"@emotion/weak-memoize": "^0.3.0",
"stylis": "4.1.3"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
"node_modules/@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
},
"node_modules/@emotion/react": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz",
"integrity": "sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.10.5",
"@emotion/cache": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
"@emotion/utils": "^1.2.0",
"@emotion/weak-memoize": "^0.3.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/react/node_modules/@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
"dependencies": {
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/unitless": "^0.8.0",
"@emotion/utils": "^1.2.0",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
"integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
"integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
"integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -2103,6 +2248,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@floating-ui/core": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.0.tgz",
"integrity": "sha512-zbsLwtnHo84w1Kc8rScAo5GMk1GdecSlrflIbfnEBJwvTSj1SL6kkOYV+nHraMCPEy+RNZZUaZyL8JosDGCtGQ=="
},
"node_modules/@floating-ui/dom": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.0.tgz",
"integrity": "sha512-TSogMPVxbRe77QCj1dt8NmRiJasPvuc+eT5jnJ6YpLqgOD2zXc5UA3S1qwybN+GVCDNdKfpKy1oj8RpzLJvh6A==",
"dependencies": {
"@floating-ui/core": "^1.0.5"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.3.tgz",
@ -4615,6 +4773,11 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/prettier": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
@ -4703,7 +4866,6 @@
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
"integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
@ -5922,6 +6084,35 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz",
@ -6895,7 +7086,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -8203,6 +8393,15 @@
"integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==",
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -10199,8 +10398,7 @@
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"dev": true
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/find-up": {
"version": "4.1.0",
@ -11689,7 +11887,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@ -11705,7 +11902,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"engines": {
"node": ">=4"
}
@ -14736,8 +14932,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@ -14865,8 +15060,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/linked-list": {
"version": "0.1.0",
@ -17247,7 +17441,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@ -17259,7 +17452,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@ -17338,7 +17530,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -18067,6 +18258,31 @@
"react": ">=15"
}
},
"node_modules/react-select": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz",
"integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-select/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"node_modules/react-shallow-renderer": {
"version": "16.14.1",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz",
@ -18080,6 +18296,21 @@
"react": "^16.0.0 || ^17.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@ -18518,9 +18749,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/regenerator-transform": {
"version": "0.15.0",
@ -20116,6 +20347,11 @@
"webpack": "^5.0.0"
}
},
"node_modules/stylis": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
"integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
},
"node_modules/sudo-prompt": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz",
@ -21113,6 +21349,19 @@
"node": ">=0.10.0"
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@ -21915,6 +22164,14 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@ -23275,6 +23532,125 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true
},
"@emotion/babel-plugin": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz",
"integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==",
"requires": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/plugin-syntax-jsx": "^7.17.12",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/serialize": "^1.1.1",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.1.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
}
}
},
"@emotion/cache": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz",
"integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==",
"requires": {
"@emotion/memoize": "^0.8.0",
"@emotion/sheet": "^1.2.1",
"@emotion/utils": "^1.2.0",
"@emotion/weak-memoize": "^0.3.0",
"stylis": "4.1.3"
}
},
"@emotion/hash": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
"integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
},
"@emotion/memoize": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz",
"integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA=="
},
"@emotion/react": {
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz",
"integrity": "sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==",
"requires": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.10.5",
"@emotion/cache": "^11.10.5",
"@emotion/serialize": "^1.1.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
"@emotion/utils": "^1.2.0",
"@emotion/weak-memoize": "^0.3.0",
"hoist-non-react-statics": "^3.3.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
}
}
},
"@emotion/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==",
"requires": {
"@emotion/hash": "^0.9.0",
"@emotion/memoize": "^0.8.0",
"@emotion/unitless": "^0.8.0",
"@emotion/utils": "^1.2.0",
"csstype": "^3.0.2"
}
},
"@emotion/sheet": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz",
"integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA=="
},
"@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz",
"integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==",
"requires": {}
},
"@emotion/utils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz",
"integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw=="
},
"@emotion/weak-memoize": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz",
"integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg=="
},
"@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@ -23324,6 +23700,19 @@
}
}
},
"@floating-ui/core": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.0.tgz",
"integrity": "sha512-zbsLwtnHo84w1Kc8rScAo5GMk1GdecSlrflIbfnEBJwvTSj1SL6kkOYV+nHraMCPEy+RNZZUaZyL8JosDGCtGQ=="
},
"@floating-ui/dom": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.0.tgz",
"integrity": "sha512-TSogMPVxbRe77QCj1dt8NmRiJasPvuc+eT5jnJ6YpLqgOD2zXc5UA3S1qwybN+GVCDNdKfpKy1oj8RpzLJvh6A==",
"requires": {
"@floating-ui/core": "^1.0.5"
}
},
"@formatjs/ecma402-abstract": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.3.tgz",
@ -25370,6 +25759,11 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/prettier": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
@ -25457,7 +25851,6 @@
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
"integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
"dev": true,
"requires": {
"@types/react": "*"
}
@ -26375,6 +26768,30 @@
"@types/babel__traverse": "^7.0.6"
}
},
"babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"requires": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"dependencies": {
"cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
}
}
}
},
"babel-plugin-polyfill-corejs2": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz",
@ -27157,8 +27574,7 @@
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
},
"camelcase": {
"version": "5.3.1",
@ -28159,6 +28575,15 @@
"integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==",
"dev": true
},
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -29709,8 +30134,7 @@
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"dev": true
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "4.1.0",
@ -30796,7 +31220,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@ -30805,8 +31228,7 @@
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
}
}
},
@ -33068,8 +33490,7 @@
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"json-schema-traverse": {
"version": "0.4.1",
@ -33170,8 +33591,7 @@
"lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"linked-list": {
"version": "0.1.0",
@ -35065,7 +35485,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"requires": {
"callsites": "^3.0.0"
}
@ -35074,7 +35493,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@ -35131,8 +35549,7 @@
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
"pend": {
"version": "1.2.0",
@ -35675,6 +36092,29 @@
"tiny-warning": "^1.0.0"
}
},
"react-select": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz",
"integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==",
"requires": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
},
"dependencies": {
"memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
}
}
},
"react-shallow-renderer": {
"version": "16.14.1",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz",
@ -35685,6 +36125,17 @@
"react-is": "^16.12.0 || ^17.0.0"
}
},
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@ -36054,9 +36505,9 @@
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"regenerator-transform": {
"version": "0.15.0",
@ -37307,6 +37758,11 @@
"dev": true,
"requires": {}
},
"stylis": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
"integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
},
"sudo-prompt": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz",
@ -38049,6 +38505,12 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"peer": true
},
"use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"requires": {}
},
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@ -38632,6 +39094,11 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",

View File

@ -93,7 +93,8 @@
"react-intl": "^5.20.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"trim-newlines": "4.0.2"
"trim-newlines": "4.0.2",
"react-select": "^5.2.2"
},
"jest": {
"testEnvironment": "jsdom",

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/createBoardFromTemplate renders the Create Boards from template component and match snapshot 1`] = `
<div>
<div
class="CreateBoardFromTemplate"
>
<div
class="add-board-to-channel"
>
<label>
<input
data-testid="add-board-to-channel-check"
id="add-board-to-channel"
type="checkbox"
/>
<span>
Create a board for this channel
</span>
<i
class="icon-information-outline"
/>
</label>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,78 @@
.CreateBoardFromTemplate {
width: 100%;
.add-board-to-channel {
display: flex;
margin-top: 24px;
padding-bottom: 5px;
flex-direction: column;
label {
display: flex;
color: rgba(var(--center-channel-color-rgb), 0.8);
cursor: pointer;
font-weight: 400;
input[type=checkbox] {
margin-top: 0 !important;
-moz-appearance: none;
-webkit-appearance: none;
-o-appearance: none;
content: none;
outline: none;
}
input[type=checkbox]::before {
display: block;
width: 15px;
height: 15px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
margin-right: 7px;
background: var(--center-channel-bg);
border-radius: 2px;
color: transparent !important;
content: "\f00c";
font-family: "FontAwesome";
font-size: 12px;
}
input[type=checkbox]:checked::before {
background: var(--button-bg);
color: var(--center-channel-bg) !important;
}
span {
margin-top: -3px;
}
}
i.icon-information-outline {
color: rgba(var(--center-channel-color-rgb), 0.7);
margin-top: -3px;
margin-left: 3px;
font-size: 18px;
}
}
.templates-selector {
margin-top: 15px;
}
}
.CreateBoardFromTemplate--templates-selector__menu-portal {
&__option {
&__icon {
display: inline-block;
width: 19px;
}
&__title {
margin-left: 10px;
}
&__description {
display: block;
font-size: 12px;
margin-left: 29px;
color: rgba(var(--center-channel-color-rgb), 0.5);
}
}
}

View File

@ -0,0 +1,81 @@
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {render, screen, act} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {mockStateStore} from '../../../../webapp/src/testUtils'
import {wrapIntl} from '../../../../webapp/src/testUtils'
import CreateBoardFromTemplate from './createBoardFromTemplate'
jest.mock('../../../../webapp/src/hooks/useGetAllTemplates', () => ({
useGetAllTemplates: () => [{id: 'id', title: 'title', description: 'description', icon: '🍔'}]
}))
describe('components/createBoardFromTemplate', () => {
const state = {
language: {
value: 'en',
},
}
it('renders the Create Boards from template component and match snapshot', async () => {
const store = mockStateStore([], state)
let container: Element | DocumentFragment | null = null
const setCanCreate = jest.fn
const setAction = jest.fn
const newBoardInfoIcon = (<i className="icon-information-outline" />)
await act(async () => {
const result = render(wrapIntl(
<ReduxProvider store={store}>
<CreateBoardFromTemplate
setAction={setAction}
setCanCreate={setCanCreate}
newBoardInfoIcon={newBoardInfoIcon}
/>
</ReduxProvider>
))
container = result.container
})
expect(container).toMatchSnapshot()
})
it('clicking checkbox toggles the templates selector', async () => {
const store = mockStateStore([], state)
const setCanCreate = jest.fn
const setAction = jest.fn
const newBoardInfoIcon = (<i className="icon-information-outline" />)
await act(async () => {
render(wrapIntl(
<ReduxProvider store={store}>
<CreateBoardFromTemplate
setAction={setAction}
setCanCreate={setCanCreate}
newBoardInfoIcon={newBoardInfoIcon}
/>
</ReduxProvider>
))
})
// click to show the template selector
let checkbox = screen.getByRole('checkbox', {checked: false})
await act(async () => {
await userEvent.click(checkbox)
const templatesSelector = screen.queryByText('Select a template')
expect(templatesSelector).toBeTruthy()
})
// click to hide the template selector
checkbox = screen.getByRole('checkbox', {checked: true})
await act(async () => {
await userEvent.click(checkbox)
const templatesSelector = screen.queryByText('Select a template')
expect(templatesSelector).toBeNull()
})
})
})

View File

@ -0,0 +1,261 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {createIntl, createIntlCache, IntlProvider} from 'react-intl'
import Select from 'react-select/async'
import {components, FormatOptionLabelMeta, GroupBase, PlaceholderProps} from 'react-select'
import {SingleValue} from 'react-select'
import {CSSObject} from '@emotion/serialize'
import {getCurrentLanguage, getMessages} from '../../../../webapp/src/i18n'
import {getLanguage} from '../../../../webapp/src/store/language'
import CompassIcon from '../../../../webapp/src/widgets/icons/compassIcon'
import {useAppSelector} from '../../../../webapp/src/store/hooks'
import {mutator} from '../../../../webapp/src/mutator'
import {useGetAllTemplates} from '../../../../webapp/src/hooks/useGetAllTemplates'
import './createBoardFromTemplate.scss'
import {Board} from '../../../../webapp/src/blocks/board'
type Props = {
setCanCreate: (canCreate: boolean) => void;
setAction: (fn: () => (channelId: string, teamId: string) => Promise<Board | undefined>) => void;
newBoardInfoIcon: React.ReactNode;
}
type ReactSelectItem = {
id: string;
title: string;
icon?: string;
description: string;
}
const EMPTY_BOARD = 'empty_board'
const TEMPLATE_DESCRIPTION_LENGTH = 70
const cache = createIntlCache()
const intl = createIntl({
locale: getCurrentLanguage(),
messages: getMessages(getCurrentLanguage())
}, cache)
const {ValueContainer, Placeholder} = components
const CreateBoardFromTemplate = (props: Props) => {
const {formatMessage} = intl
const [addBoard, setAddBoard] = useState(false)
const allTemplates = useGetAllTemplates()
const [selectedBoardTemplateId, setSelectedBoardTemplateId] = useState<string>('')
const addBoardRef = useRef(false)
addBoardRef.current = addBoard
const templateIdRef = useRef('')
templateIdRef.current = selectedBoardTemplateId
const showNewBoardTemplateSelector = async () => {
setAddBoard((prev: boolean) => !prev)
}
// CreateBoardFromTemplate
const addBoardToChannel = async (channelId: string, teamId: string) => {
if (!addBoardRef.current || !templateIdRef.current) {
return
}
const ACTION_DESCRIPTION = 'board created from channel'
const LINKED_CHANNEL = 'linked channel'
const asTemplate = false
let boardsAndBlocks = undefined
if (selectedBoardTemplateId === EMPTY_BOARD) {
boardsAndBlocks = await mutator.addEmptyBoard(teamId, intl)
} else {
boardsAndBlocks = await mutator.duplicateBoard(templateIdRef.current as string, ACTION_DESCRIPTION, asTemplate, undefined, undefined, teamId)
}
const board = boardsAndBlocks.boards[0]
await mutator.updateBoard({...board, channelId: channelId}, board, LINKED_CHANNEL)
return board
}
useEffect(() => {
props.setAction(() => addBoardToChannel)
}, [])
useEffect(() => {
props.setCanCreate(!addBoard || (addBoard && selectedBoardTemplateId !== ''))
}, [addBoard, selectedBoardTemplateId])
const getSubstringWithCompleteWords = (str: string, len: number) => {
if (str?.length <= len) {
return str
}
// get the final part of the string in order to find the next whitespace if any
const finalStringPart = str.substring(len)
const wordBreakingIndex = finalStringPart.indexOf(' ')
// if there is no whitespace is because the lenght in this case falls into an entire word and doesn't affect the display, so just return it
if (wordBreakingIndex === -1) {
return str
}
return `${str.substring(0, (len + wordBreakingIndex))}`
}
const formatOptionLabel = ({ id, title, icon, description }: ReactSelectItem, optionLabel: FormatOptionLabelMeta<ReactSelectItem>) => {
const cssPrefix = 'CreateBoardFromTemplate--templates-selector__menu-portal__option'
const descriptionLabel = description ? getSubstringWithCompleteWords(description, TEMPLATE_DESCRIPTION_LENGTH) : 'ㅤ'
const templateDescription = (
<span className={`${cssPrefix}__description`}>
{descriptionLabel}
</span>
)
// do not show the description for the selected option so the input only show the icon and title of the template
const selectedOption = id === optionLabel.selectValue[0]?.id
return (
<div key={id}>
<span className={`${cssPrefix}__icon`}>
{icon || <CompassIcon icon='product-boards'/>}
</span>
<span className={`${cssPrefix}__title`}>
{title}
</span>
{!selectedOption && templateDescription}
</div>
)
}
const CustomValueContainer = ({ children, ...props }: any) => {
return (
<ValueContainer {...props}>
<Placeholder {...props}>
{props.selectProps.placeholder}
</Placeholder>
{React.Children.map(children, (child) =>
child && child.type !== Placeholder ? child : null
)}
</ValueContainer>
)
}
const loadOptions = useCallback(async (value = '') => {
let templates = allTemplates.map((template) => {
return {
id: template.id,
title: template.title,
icon: template.icon,
description: template.description,
}
})
const emptyBoard = {
id: EMPTY_BOARD,
title: formatMessage({id: 'new_channel_modal.create_board.empty_board_title', defaultMessage: 'Empty board'}),
icon: '',
description: formatMessage({id: 'new_channel_modal.create_board.empty_board_description', defaultMessage: 'Create a new empty board'}),
}
templates.push(emptyBoard)
if (value !== '') {
templates = templates.filter(template => template.title.toLowerCase().includes(value.toLowerCase()))
}
return templates
}, [allTemplates])
const onChange = useCallback((item: SingleValue<ReactSelectItem>) => {
if (item) {
setSelectedBoardTemplateId(item.id)
}
}, [setSelectedBoardTemplateId])
const selectorStyles = {
menu: (baseStyles: CSSObject): CSSObject => ({
...baseStyles,
height: '164px',
}),
menuList: (baseStyles: CSSObject): CSSObject => ({
...baseStyles,
height: '160px',
}),
menuPortal: (baseStyles: CSSObject): CSSObject => ({
...baseStyles,
zIndex: 9999,
}),
valueContainer: (baseStyles: CSSObject): CSSObject => ({
...baseStyles,
overflow: 'visible'
}),
placeholder: (baseStyles: CSSObject, state: PlaceholderProps<ReactSelectItem, false, GroupBase<ReactSelectItem>>): CSSObject => {
const modifyPlaceholder = state.selectProps.menuIsOpen || (!state.selectProps.menuIsOpen && state.hasValue)
return {
...baseStyles,
position: 'absolute',
backgroundColor: 'var(--sys-center-channel-bg)',
padding: '0 3px',
top: modifyPlaceholder ? -15 : '18%',
transition: 'top 0.5s, font-size 0.5s, color 0.5s',
fontSize: modifyPlaceholder ? 10 : 16,
color: modifyPlaceholder ? 'var(--sidebar-text-active-border)' : 'rgba(var(--center-channel-color-rgb), 0.42)',
}
},
}
return (
<div className='CreateBoardFromTemplate'>
<div className='add-board-to-channel'>
<label>
<input
type='checkbox'
onChange={showNewBoardTemplateSelector}
checked={addBoard}
id={'add-board-to-channel'}
data-testid='add-board-to-channel-check'
/>
<span>
{formatMessage({id: 'new_channel_modal.create_board.title', defaultMessage: 'Create a board for this channel'})}
</span>
{props.newBoardInfoIcon}
</label>
{addBoard && <div className='templates-selector'>
<Select
classNamePrefix={'CreateBoardFromTemplate--templates-selector'}
placeholder={formatMessage({id: 'new_channel_modal.create_board.select_template_placeholder', defaultMessage: 'Select a template'})}
onChange={onChange}
components={{IndicatorSeparator: () => null, ValueContainer: CustomValueContainer}}
loadOptions={loadOptions}
getOptionValue={(v) => v.id}
getOptionLabel={(v) => v.title}
formatOptionLabel={formatOptionLabel}
styles={selectorStyles}
menuPortalTarget={document.body}
defaultOptions={true}
/>
</div>}
</div>
</div>
)
}
const IntlCreateBoardFromTemplate = (props: Props) => {
const language = useAppSelector<string>(getLanguage)
return (
<IntlProvider
locale={language.split(/[_]/)[0]}
messages={getMessages(language)}
>
<CreateBoardFromTemplate {...props}/>
</IntlProvider>
)
}
export default IntlCreateBoardFromTemplate

View File

@ -39,6 +39,7 @@ import '../../../webapp/src/styles/main.scss'
import '../../../webapp/src/styles/labels.scss'
import octoClient from '../../../webapp/src/octoClient'
import {Constants} from '../../../webapp/src/constants'
import {Board} from '../../../webapp/src/blocks/board'
import appBarIcon from '../../../webapp/static/app-bar-icon.png'
@ -66,6 +67,7 @@ import {PluginRegistry} from './types/mattermost-webapp'
import './plugin.scss'
import CloudUpgradeNudge from "./components/cloudUpgradeNudge/cloudUpgradeNudge"
import CreateBoardFromTemplate from './components/createBoardFromTemplate'
function getSubpath(siteURL: string): string {
const url = new URL(siteURL)
@ -336,6 +338,22 @@ export default class Plugin {
this.registry.registerAppBarComponent(Utils.buildURL(appBarIcon, true), () => mmStore.dispatch(toggleRHSPlugin), intl.formatMessage({id: 'AppBar.Tooltip', defaultMessage: 'Toggle Linked Boards'}))
}
if (this.registry.registerActionAfterChannelCreation) {
this.registry.registerActionAfterChannelCreation((props: {
setCanCreate: (canCreate: boolean) => void,
setAction: (fn: () => (channelId: string, teamId: string) => Promise<Board | undefined>) => void,
newBoardInfoIcon: React.ReactNode,
}) => (
<ReduxProvider store={store}>
<CreateBoardFromTemplate
setCanCreate={props.setCanCreate}
setAction={props.setAction}
newBoardInfoIcon={props.newBoardInfoIcon}
/>
</ReduxProvider>
))
}
this.registry.registerPostWillRenderEmbedComponent(
(embed) => embed.type === 'boards',
(props: {embed: {data: string}, webSocketClient: MMWebSocketClient}) => (

View File

@ -2,6 +2,10 @@
font-size: 20px;
}
.focalboard-body .RightControlsContainer-eacbOh {
flex-basis: auto;
}
.focalboard-body .feature-global-header>header {
z-index: 1000;

View File

@ -20,5 +20,7 @@ export interface PluginRegistry {
registerInsightsHandler(handler: (timeRange: string, page: number, perPage: number, teamId: string, insightType: string) => void)
registerSiteStatisticsHandler(handler: () => void)
registerActionAfterChannelCreation(component: React.Element)
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
}

View File

@ -97,6 +97,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
a.registerBlocksRoutes(apiv2)
a.registerContentBlocksRoutes(apiv2)
a.registerStatisticsRoutes(apiv2)
a.registerComplianceRoutes(apiv2)
// V3 routes
a.registerCardsRoutes(apiv2)

View File

@ -9,6 +9,7 @@ import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
@ -55,9 +56,15 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
boardID := vars["boardID"]
userID := getUserID(r)
// check user has permission to board
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
// if this user has `manage_system` permission and there is a license with the compliance
// feature enabled, then we will allow the export.
license := a.app.GetLicense()
if !a.permissions.HasPermissionTo(userID, mmModel.PermissionManageSystem) || license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)

447
server/api/compliance.go Normal file
View File

@ -0,0 +1,447 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const (
complianceDefaultPage = "0"
complianceDefaultPerPage = "60"
)
func (a *API) registerComplianceRoutes(r *mux.Router) {
// Compliance APIs
r.HandleFunc("/admin/boards", a.sessionRequired(a.handleGetBoardsForCompliance)).Methods("GET")
r.HandleFunc("/admin/boards_history", a.sessionRequired(a.handleGetBoardsComplianceHistory)).Methods("GET")
r.HandleFunc("/admin/blocks_history", a.sessionRequired(a.handleGetBlocksComplianceHistory)).Methods("GET")
}
func (a *API) handleGetBoardsForCompliance(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards getBoardsForCompliance
//
// Returns boards for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: team_id
// in: query
// description: Team ID. If empty then boards across all teams are included.
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of boards to return per page(default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
teamID := query.Get("team_id")
strPage := query.Get("page")
strPerPage := query.Get("per_page")
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getAllBoards"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getAllBoards"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsForComplianceOptions{
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsForCompliance(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsForCompliance",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBoardsComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards_history getBoardsComplianceHistory
//
// Returns boards histories for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then board histories across all teams are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of board histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBoardsHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBoardsHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsComplianceHistory",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceHistoryResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBlocksComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/blocks_history getBlocksComplianceHistory
//
// Returns block histories for a specific team, specific board, or all teams and boards.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then block histories across all teams are included
// required: false
// type: string
// - name: board_id
// in: query
// description: Board ID. If empty then block histories for all boards are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of block histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BlocksComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
boardID := query.Get("board_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBlocksHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBlocksHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
// check for valid board if specified
if boardID != "" {
_, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid board id: "+boardID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBlocksComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
BoardID: boardID,
Page: page,
PerPage: perPage,
}
blocks, more, err := a.app.GetBlocksComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBlocksComplianceHistory",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.Int("blocksCount", len(blocks)),
mlog.Bool("hasNext", more),
)
response := model.BlocksComplianceHistoryResponse{
HasNext: more,
Results: blocks,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}

View File

@ -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
@ -166,7 +170,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()
}

View File

@ -150,6 +150,11 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
return
}
if !a.permissions.HasPermissionToTeam(reqBoardMember.UserID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
newBoardMember := &model.BoardMember{
UserID: reqBoardMember.UserID,
BoardID: boardID,

View File

@ -110,3 +110,7 @@ func (a *App) SetCardLimit(cardLimit int) {
defer a.cardLimitMux.Unlock()
a.cardLimit = cardLimit
}
func (a *App) GetLicense() *mm_model.License {
return a.store.GetLicense()
}

View File

@ -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,7 +358,24 @@ func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) e
mlog.Err(err),
)
}
block.Fields["fileId"] = destFilename
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

View File

@ -202,13 +202,21 @@ 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 {
blockIDs = append(blockIDs, block.ID)
blockPatches = append(blockPatches, model.BlockPatch{
UpdatedFields: map[string]interface{}{
"fileId": fileID,
},
})
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{}{
fieldName: fieldID,
},
})
}
}
}
a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs)))

15
server/app/compliance.go Normal file
View File

@ -0,0 +1,15 @@
package app
import "github.com/mattermost/focalboard/server/model"
func (a *App) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
return a.store.GetBoardsForCompliance(opts)
}
func (a *App) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
return a.store.GetBoardsComplianceHistory(opts)
}
func (a *App) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
return a.store.GetBlocksComplianceHistory(opts)
}

View File

@ -241,6 +241,15 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
// add users to all the new boards (if not the fake system user).
for _, board := range boardsAndBlocks.Boards {
// make sure an admin user gets added
adminMember := &model.BoardMember{
BoardID: board.ID,
UserID: opt.ModifiedBy,
SchemeAdmin: true,
}
if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil {
return "", fmt.Errorf("cannot add adminMember to board: %w", err2)
}
for _, boardMember := range boardMembers {
bm := &model.BoardMember{
BoardID: board.ID,

View File

@ -47,9 +47,8 @@ func TestApp_ImportArchive(t *testing.T) {
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil)
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{boardMember}, nil)
// th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
// th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil)
// th.Store.EXPECT().GetUserCategoryBoards("user", "test-team").Return([]model.CategoryBoards{}, nil)
th.Store.EXPECT().GetBoard(board.ID).Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(board.ID, "user").Return(boardMember, nil)
th.Store.EXPECT().GetUserCategoryBoards("user", "test-team").Return([]model.CategoryBoards{
{
Category: model.Category{

View File

@ -11,6 +11,7 @@ import (
"github.com/mattermost/focalboard/server/api"
"github.com/mattermost/focalboard/server/model"
mmModel "github.com/mattermost/mattermost-server/v6/model"
)
@ -987,6 +988,61 @@ func (c *Client) GetStatistics() (*model.BoardsStatistics, *Response) {
return stats, BuildResponse(r)
}
func (c *Client) GetBoardsForCompliance(teamID string, page, perPage int) (*model.BoardsComplianceResponse, *Response) {
query := fmt.Sprintf("?team_id=%s&page=%d&per_page=%d", teamID, page, perPage)
r, err := c.DoAPIGet("/admin/boards"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var res *model.BoardsComplianceResponse
err = json.NewDecoder(r.Body).Decode(&res)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
return res, BuildResponse(r)
}
func (c *Client) GetBoardsComplianceHistory(
modifiedSince int64, includeDeleted bool, teamID string, page, perPage int) (*model.BoardsComplianceHistoryResponse, *Response) {
query := fmt.Sprintf("?modified_since=%d&include_deleted=%t&team_id=%s&page=%d&per_page=%d",
modifiedSince, includeDeleted, teamID, page, perPage)
r, err := c.DoAPIGet("/admin/boards_history"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var res *model.BoardsComplianceHistoryResponse
err = json.NewDecoder(r.Body).Decode(&res)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
return res, BuildResponse(r)
}
func (c *Client) GetBlocksComplianceHistory(
modifiedSince int64, includeDeleted bool, teamID, boardID string, page, perPage int) (*model.BlocksComplianceHistoryResponse, *Response) {
query := fmt.Sprintf("?modified_since=%d&include_deleted=%t&team_id=%s&board_id=%s&page=%d&per_page=%d",
modifiedSince, includeDeleted, teamID, boardID, page, perPage)
r, err := c.DoAPIGet("/admin/blocks_history"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var res *model.BlocksComplianceHistoryResponse
err = json.NewDecoder(r.Body).Decode(&res)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
return res, BuildResponse(r)
}
func (c *Client) HideBoard(teamID, categoryID, boardID string) *Response {
r, err := c.DoAPIPut(c.GetTeamRoute(teamID)+"/categories/"+categoryID+"/boards/"+boardID+"/hide", "")
if err != nil {

View File

@ -65,7 +65,6 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.45 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

View File

@ -85,8 +85,6 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE=
github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc=
github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
@ -407,7 +405,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@ -428,8 +425,7 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
@ -675,8 +671,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@ -685,8 +680,7 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
@ -703,8 +697,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
@ -811,14 +804,12 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.13 h1:1XxvOiqXZ8SULZUKim/wncr3wZ38H4yCuVDvKdK9OGs=
github.com/klauspost/cpuid/v2 v2.0.13/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -850,8 +841,7 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
@ -899,8 +889,8 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -910,8 +900,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@ -920,7 +910,6 @@ github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
@ -936,8 +925,7 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.28 h1:VMr3K5qGIEt+/KW3poopRh8mzi5RwuCjmrmstK196Fg=
github.com/minio/minio-go/v7 v7.0.28/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg=
github.com/minio/minio-go/v7 v7.0.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
@ -945,7 +933,6 @@ github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
@ -1139,8 +1126,7 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rudderlabs/analytics-go v3.3.2+incompatible h1:bDajEJTYhfHjNYxbQFMA/2dHlOjyeSgxS7GPIdMZ52Q=
github.com/rudderlabs/analytics-go v3.3.2+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30=
github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY=
github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -1154,8 +1140,7 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
@ -1194,8 +1179,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -1238,8 +1223,8 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@ -1250,8 +1235,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@ -1260,15 +1245,14 @@ github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
@ -1297,8 +1281,7 @@ github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg=
github.com/wiggin77/merror v1.0.3 h1:8+ZHV+aSnJoYghE3EUThl15C6rvF2TYRSvOSBjdmNR8=
github.com/wiggin77/merror v1.0.3/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
github.com/wiggin77/merror v1.0.4 h1:XxFLEevmQQfgJW2AxhapuMG7C1fQqfbim/XyUmYv/ZM=
github.com/wiggin77/merror v1.0.4/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
@ -1379,7 +1362,7 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@ -1419,8 +1402,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1471,8 +1453,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1702,7 +1683,6 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1716,7 +1696,6 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 h1:PgOr27OhUx2IRqGJ2RxAWI4dJQ7bi9cSrB82uzFzfUA=
@ -1726,6 +1705,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1743,8 +1723,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@ -1850,8 +1830,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
@ -1980,8 +1960,7 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220614165028-45ed7f3ff16e h1:ubR4JUtqN3ffdFjpKylv8scWk/mZstGmzXbgYSkuMl0=
google.golang.org/genproto v0.0.0-20220614165028-45ed7f3ff16e/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ=
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@ -2020,8 +1999,7 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
@ -2038,8 +2016,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@ -2057,8 +2034,7 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
@ -2147,24 +2123,6 @@ lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.24 h1:vlCqjhVwX15t1uwlMPpOpNRC7JTjMZ9lT9DYHKQTFuA=
modernc.org/cc/v3 v3.35.24/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@ -2172,54 +2130,6 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM=
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8=
modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4=
modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8=
modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I=
modernc.org/ccgo/v3 v3.15.14/go.mod h1:144Sz2iBCKogb9OKwsu7hQEub3EVgOlyI8wMUPGKUXQ=
modernc.org/ccgo/v3 v3.15.15/go.mod h1:z5qltXjU4PJl0pE5nhYQCvA9DhPHiWsl5GWl89+NSYE=
modernc.org/ccgo/v3 v3.15.16/go.mod h1:XbKRMeMWMdq712Tr5ECgATYMrzJ+g9zAZEj2ktzBe24=
modernc.org/ccgo/v3 v3.15.17 h1:svaDk4rfh7XQPBwkqzjKK8bta/vK4VVL3JP6ZLbcr0w=
modernc.org/ccgo/v3 v3.15.17/go.mod h1:bofnFkpRFf5gLY+mBZIyTW6FEcp26xi2lgOFk2Rlvs0=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
@ -2238,57 +2148,6 @@ modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVS
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34=
modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ=
modernc.org/libc v1.14.6/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak=
modernc.org/libc v1.14.7/go.mod h1:f8xfWXW8LW41qb4X5+huVQo5dcfPlq7Cbny2TDheMv0=
modernc.org/libc v1.14.8/go.mod h1:9+JCLb1MWSY23smyOpIPbd5ED+rSS/ieiDWUpdyO3mo=
modernc.org/libc v1.14.10/go.mod h1:y1MtIWhwpJFpLYm6grAThtuXJKEsY6xkdZmXbRngIdo=
modernc.org/libc v1.14.11/go.mod h1:l5/Mz/GrZwOqzwRHA3abgSCnSeJzzTl+Ify0bAwKbAw=
modernc.org/libc v1.14.12 h1:pUBZTYoISfbb4pCf4PECENpbvwDBxeKc+/dS9LyOWFM=
modernc.org/libc v1.14.12/go.mod h1:fJdoe23MHu2ruPQkFPPqCpToDi5cckzsbmkI6Ez0LqQ=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA=
@ -2298,15 +2157,10 @@ modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/memory v1.0.6/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.0.7 h1:UE3cxTRFa5tfUibAV7Jqq8P7zRY0OlJg+yWVIIaluEE=
modernc.org/memory v1.0.7/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
@ -2317,9 +2171,6 @@ modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y=
modernc.org/sqlite v1.15.3 h1:3C4AWicF7S5vUUFJuBi7Ws8eWlPjqyo/c4Z1UGYBbyg=
modernc.org/sqlite v1.15.3/go.mod h1:J7GAPbk8Txp0DJnT8TGwpUqJW0Z1cK2YpzjoXaZRU8k=
modernc.org/sqlite v1.18.0 h1:ef66qJSgKeyLyrF4kQ2RHw/Ue3V89fyFNbGL073aDjI=
modernc.org/sqlite v1.18.0/go.mod h1:B9fRWZacNxJBHoCJZQr1R54zhVn3fjfl0aszflrTSxY=
modernc.org/sqlite v1.20.1/go.mod h1:fODt+bFmc/j8LcoCbMSkAuKuGmhxjG45KGc25N2705M=
@ -2329,9 +2180,7 @@ modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg=
modernc.org/tcl v1.11.2 h1:mXpsx3AZqJt83uDiFu9UYQVBjNjaWKGCF1YDSlpCL6Y=
modernc.org/tcl v1.11.2/go.mod h1:BRzgpajcGdS2qTxniOx9c/dcxjlbA7p12eJNmiriQYo=
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
@ -2339,9 +2188,7 @@ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo=
modernc.org/z v1.3.2 h1:4GWBVMa48UDC7KQ9tnaggN/yTlXg+CdCX9bhgHPQ9AM=
modernc.org/z v1.3.2/go.mod h1:PEU2oK2OEA1CfzDTd+8E908qEXhC9s0MfyKp5LZsd+k=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@ -13,10 +13,6 @@ import (
"github.com/stretchr/testify/require"
)
const (
testTeamID = "team-id"
)
func TestGetBoards(t *testing.T) {
t.Run("a non authenticated client should be rejected", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()

View File

@ -29,6 +29,7 @@ const (
user1Username = "user1"
user2Username = "user2"
password = "Pa$$word"
testTeamID = "team-id"
)
const (
@ -457,6 +458,16 @@ func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *mod
return board
}
func (th *TestHelper) CreateBoards(teamID string, boardType model.BoardType, count int) []*model.Board {
boards := make([]*model.Board, 0, count)
for i := 0; i < count; i++ {
board := th.CreateBoard(teamID, boardType)
boards = append(boards, board)
}
return boards
}
func (th *TestHelper) CreateCategory(category model.Category) *model.Category {
cat, resp := th.Client.CreateCategory(category)
th.CheckOK(resp)

View File

@ -0,0 +1,360 @@
package integrationtests
import (
"math"
"os"
"strconv"
"testing"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/stretchr/testify/require"
)
var (
OneHour int64 = 360000
OneDay int64 = OneHour * 24
OneYear int64 = OneDay * 365
)
func setupTestHelperForCompliance(t *testing.T, complianceLicense bool) (*TestHelper, Clients) {
os.Setenv("FOCALBOARD_UNIT_TESTING_COMPLIANCE", strconv.FormatBool(complianceLicense))
th := SetupTestHelperPluginMode(t)
clients := setupClients(th)
th.Client = clients.TeamMember
th.Client2 = clients.TeamMember
return th, clients
}
func TestGetBoardsForCompliance(t *testing.T) {
t.Run("missing Features.Compliance license should fail", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, false)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, 0, 0)
th.CheckNotImplemented(resp)
require.Nil(t, bcr)
})
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
th.Logout(th.Client)
bcr, resp := clients.Anon.GetBoardsForCompliance(testTeamID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bcr)
})
t.Run("a user without manage_system permission should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bcr, resp := clients.TeamMember.GetBoardsForCompliance(testTeamID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bcr)
})
t.Run("good call", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 10
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count)
bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, 0, 0)
th.CheckOK(resp)
require.False(t, bcr.HasNext)
require.Len(t, bcr.Results, count)
})
t.Run("pagination", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 20
const perPage = 3
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count)
boards := make([]*model.Board, 0, count)
page := 0
for {
bcr, resp := clients.Admin.GetBoardsForCompliance(testTeamID, page, perPage)
page++
th.CheckOK(resp)
boards = append(boards, bcr.Results...)
if !bcr.HasNext {
break
}
}
require.Len(t, boards, count)
require.Equal(t, int(math.Floor((count/perPage)+1)), page)
})
t.Run("invalid teamID", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bcr, resp := clients.Admin.GetBoardsForCompliance(utils.NewID(utils.IDTypeTeam), 0, 0)
th.CheckBadRequest(resp)
require.Nil(t, bcr)
})
}
func TestGetBoardsComplianceHistory(t *testing.T) {
t.Run("missing Features.Compliance license should fail", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, false)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0)
th.CheckNotImplemented(resp)
require.Nil(t, bchr)
})
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
th.Logout(th.Client)
bchr, resp := clients.Anon.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bchr)
})
t.Run("a user without manage_system permission should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.TeamMember.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bchr)
})
t.Run("good call, exclude deleted", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 10
boards := th.CreateBoards(testTeamID, model.BoardTypeOpen, count)
deleted, resp := th.Client.DeleteBoard(boards[0].ID)
th.CheckOK(resp)
require.True(t, deleted)
deleted, resp = th.Client.DeleteBoard(boards[1].ID)
th.CheckOK(resp)
require.True(t, deleted)
bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, false, testTeamID, 0, 0)
th.CheckOK(resp)
require.False(t, bchr.HasNext)
require.Len(t, bchr.Results, count-2) // two boards deleted
})
t.Run("good call, include deleted", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 10
boards := th.CreateBoards(testTeamID, model.BoardTypeOpen, count)
deleted, resp := th.Client.DeleteBoard(boards[0].ID)
th.CheckOK(resp)
require.True(t, deleted)
deleted, resp = th.Client.DeleteBoard(boards[1].ID)
th.CheckOK(resp)
require.True(t, deleted)
bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, 0, 0)
th.CheckOK(resp)
require.False(t, bchr.HasNext)
require.Len(t, bchr.Results, count+2) // both deleted boards have 2 history records each
})
t.Run("pagination", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 20
const perPage = 3
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, count)
boardHistory := make([]*model.BoardHistory, 0, count)
page := 0
for {
bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, page, perPage)
page++
th.CheckOK(resp)
boardHistory = append(boardHistory, bchr.Results...)
if !bchr.HasNext {
break
}
}
require.Len(t, boardHistory, count)
require.Equal(t, int(math.Floor((count/perPage)+1)), page)
})
t.Run("invalid teamID", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_ = th.CreateBoards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Admin.GetBoardsComplianceHistory(utils.GetMillis()-OneDay, true, utils.NewID(utils.IDTypeTeam), 0, 0)
th.CheckBadRequest(resp)
require.Nil(t, bchr)
})
}
func TestGetBlocksComplianceHistory(t *testing.T) {
t.Run("missing Features.Compliance license should fail", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, false)
defer th.TearDown()
board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0)
th.CheckNotImplemented(resp)
require.Nil(t, bchr)
})
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Anon.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bchr)
})
t.Run("a user without manage_system permission should be rejected", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.TeamMember.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0)
th.CheckUnauthorized(resp)
require.Nil(t, bchr)
})
t.Run("good call, exclude deleted", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 10
board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count)
deleted, resp := th.Client.DeleteBlock(board.ID, cards[0].ID, true)
th.CheckOK(resp)
require.True(t, deleted)
deleted, resp = th.Client.DeleteBlock(board.ID, cards[1].ID, true)
th.CheckOK(resp)
require.True(t, deleted)
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, false, testTeamID, board.ID, 0, 0)
th.CheckOK(resp)
require.False(t, bchr.HasNext)
require.Len(t, bchr.Results, count-2) // 2 blocks deleted
})
t.Run("good call, include deleted", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 10
board, cards := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count)
deleted, resp := th.Client.DeleteBlock(board.ID, cards[0].ID, true)
th.CheckOK(resp)
require.True(t, deleted)
deleted, resp = th.Client.DeleteBlock(board.ID, cards[1].ID, true)
th.CheckOK(resp)
require.True(t, deleted)
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, 0, 0)
th.CheckOK(resp)
require.False(t, bchr.HasNext)
require.Len(t, bchr.Results, count+2) // both deleted boards have 2 history records each
})
t.Run("pagination", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
const count = 20
const perPage = 3
board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, count)
blockHistory := make([]*model.BlockHistory, 0, count)
page := 0
for {
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, board.ID, page, perPage)
page++
th.CheckOK(resp)
blockHistory = append(blockHistory, bchr.Results...)
if !bchr.HasNext {
break
}
}
require.Len(t, blockHistory, count)
require.Equal(t, int(math.Floor((count/perPage)+1)), page)
})
t.Run("invalid teamID", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
board, _ := th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, utils.NewID(utils.IDTypeTeam), board.ID, 0, 0)
th.CheckBadRequest(resp)
require.Nil(t, bchr)
})
t.Run("invalid boardID", func(t *testing.T) {
th, clients := setupTestHelperForCompliance(t, true)
defer th.TearDown()
_, _ = th.CreateBoardAndCards(testTeamID, model.BoardTypeOpen, 2)
bchr, resp := clients.Admin.GetBlocksComplianceHistory(utils.GetMillis()-OneDay, true, testTeamID, utils.NewID(utils.IDTypeBoard), 0, 0)
th.CheckBadRequest(resp)
require.Nil(t, bchr)
})
}

View File

@ -2,6 +2,8 @@ package integrationtests
import (
"errors"
"os"
"strconv"
"strings"
"github.com/mattermost/focalboard/server/model"
@ -89,7 +91,7 @@ func (s *PluginTestStore) GetTeam(id string) (*model.Team, error) {
return s.baseTeam, nil
case "other-team":
return s.otherTeam, nil
case "test-team":
case "test-team", testTeamID:
return s.testTeam, nil
case "empty-team":
return s.emptyTeam, nil
@ -293,3 +295,27 @@ func (s *PluginTestStore) SearchBoardsForUser(term string, field model.BoardSear
}
return resultBoards, nil
}
func (s *PluginTestStore) GetLicense() *mmModel.License {
license := s.Store.GetLicense()
if license == nil {
license = &mmModel.License{
Id: mmModel.NewId(),
StartsAt: mmModel.GetMillis() - 2629746000, // 1 month
ExpiresAt: mmModel.GetMillis() + 2629746000, //
IssuedAt: mmModel.GetMillis() - 2629746000,
Features: &mmModel.Features{},
}
license.Features.SetDefaults()
}
complianceLicense := os.Getenv("FOCALBOARD_UNIT_TESTING_COMPLIANCE")
if complianceLicense != "" {
if val, err := strconv.ParseBool(complianceLicense); err == nil {
license.Features.Compliance = mmModel.NewBool(val)
}
}
return license
}

View File

@ -0,0 +1,57 @@
package integrationtests
import (
"testing"
"github.com/stretchr/testify/require"
)
// This test is there to guarantee that the board templates needed for
// the work template are present in the default templates.
// If this fails, you might need to sync with the channels team.
func TestGetTemplatesForWorkTemplate(t *testing.T) {
// map[name]trackingTemplateId
knownInWorkTemplates := map[string]string{
"Company Goals & OKRs": "7ba22ccfdfac391d63dea5c4b8cde0de",
"Competitive Analysis": "06f4bff367a7c2126fab2380c9dec23c",
"Content Calendar": "c75fbd659d2258b5183af2236d176ab4",
"Meeting Agenda ": "54fcf9c610f0ac5e4c522c0657c90602",
"Personal Goals ": "7f32dc8d2ae008cfe56554e9363505cc",
"Personal Tasls ": "dfb70c146a4584b8a21837477c7b5431",
"Project Tasks ": "a4ec399ab4f2088b1051c3cdf1dde4c3",
"Roadmap ": "b728c6ca730e2cfc229741c5a4712b65",
"Sales Pipeline CRM": "ecc250bb7dff0fe02247f1110f097544",
"Sprint Planner ": "99b74e26d2f5d0a9b346d43c0a7bfb09",
"Team Retrospective": "e4f03181c4ced8edd4d53d33d569a086",
"User Research Sessions": "6c345c7f50f6833f78b7d0f08ce450a3",
}
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
err := th.Server.App().InitTemplates()
require.NoError(t, err, "InitTemplates should not fail")
rBoards, resp := th.Client.GetTemplatesForTeam("0")
th.CheckOK(resp)
require.NotNil(t, rBoards)
trackingTemplateIDs := []string{}
for _, board := range rBoards {
property, _ := board.GetPropertyString("trackingTemplateId")
if property != "" {
trackingTemplateIDs = append(trackingTemplateIDs, property)
}
}
// make sure all known templates are in trackingTemplateIds
for name, ttID := range knownInWorkTemplates {
found := false
for _, trackingTemplateID := range trackingTemplateIDs {
if trackingTemplateID == ttID {
found = true
break
}
}
require.True(t, found, "trackingTemplateId %s for %s not found", ttID, name)
}
}

View File

@ -199,6 +199,14 @@ type QueryBoardHistoryOptions struct {
Descending bool // if true then the records are sorted by insert_at in descending order
}
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
type QueryBlockHistoryChildOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited)
}
func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) {
if userID == SingleUser {
userID = ""

View File

@ -14,13 +14,16 @@ import (
type BlockType string
const (
TypeUnknown = "unknown"
TypeBoard = "board"
TypeCard = "card"
TypeView = "view"
TypeText = "text"
TypeComment = "comment"
TypeImage = "image"
TypeUnknown = "unknown"
TypeBoard = "board"
TypeCard = "card"
TypeView = "view"
TypeText = "text"
TypeCheckbox = "checkbox"
TypeComment = "comment"
TypeImage = "image"
TypeAttachment = "attachment"
TypeDivider = "divider"
)
func (bt BlockType) String() string {
@ -38,10 +41,16 @@ func BlockTypeFromString(s string) (BlockType, error) {
return TypeView, nil
case "text":
return TypeText, nil
case "checkbox":
return TypeCheckbox, nil
case "comment":
return TypeComment, nil
case "image":
return TypeImage, nil
case "attachment":
return TypeAttachment, nil
case "divider":
return TypeDivider, nil
}
return TypeUnknown, ErrInvalidBlockType{s}
}
@ -55,8 +64,10 @@ func BlockType2IDType(blockType BlockType) utils.IDType {
return utils.IDTypeCard
case TypeView:
return utils.IDTypeView
case TypeText, TypeComment:
case TypeText, TypeCheckbox, TypeComment, TypeDivider:
return utils.IDTypeBlock
case TypeImage, TypeAttachment:
return utils.IDTypeAttachment
}
return utils.IDTypeNone
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// BaordsComplianceResponse is the response body to a request for boards.
// swagger:model
type BoardsComplianceResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of board records.
// required: true
Results []*Board `json:"results"`
}
// BoardsComplianceHistoryResponse is the response body to a request for boards history.
// swagger:model
type BoardsComplianceHistoryResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of BoardHistory records.
// required: true
Results []*BoardHistory `json:"results"`
}
// BlocksComplianceHistoryResponse is the response body to a request for blocks history.
// swagger:model
type BlocksComplianceHistoryResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of BlockHistory records.
// required: true
Results []*BlockHistory `json:"results"`
}
// BoardHistory provides information about the history of a board.
// swagger:model
type BoardHistory struct {
ID string `json:"id"`
TeamID string `json:"teamId"`
IsDeleted bool `json:"isDeleted"`
DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"`
DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"`
CreatedBy string `json:"createdBy"`
LastModifiedBy string `json:"lastModifiedBy"`
}
// BlockHistory provides information about the history of a block.
// swagger:model
type BlockHistory struct {
ID string `json:"id"`
TeamID string `json:"teamId"`
BoardID string `json:"boardId"`
Type string `json:"type"`
IsDeleted bool `json:"isDeleted"`
LastUpdateAt int64 `json:"lastUpdateAt"`
FirstUpdateAt int64 `json:"firstUpdateAt"`
CreatedBy string `json:"createdBy"`
LastModifiedBy string `json:"lastModifiedBy"`
}
type QueryBoardsForComplianceOptions struct {
TeamID string // if not empty then filter for specific team, otherwise all teams are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}
type QueryBoardsComplianceHistoryOptions struct {
ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince
IncludeDeleted bool // if true then deleted blocks are included
TeamID string // if not empty then filter for specific team, otherwise all teams are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}
type QueryBlocksComplianceHistoryOptions struct {
ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince
IncludeDeleted bool // if true then deleted blocks are included
TeamID string // if not empty then filter for specific team, otherwise all teams are included
BoardID string // if not empty then filter for specific board, otherwise all boards are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}

View File

@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/focalboard/server/model (interfaces: PropValueResolver)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/focalboard/server/model"
)
// MockPropValueResolver is a mock of PropValueResolver interface.
type MockPropValueResolver struct {
ctrl *gomock.Controller
recorder *MockPropValueResolverMockRecorder
}
// MockPropValueResolverMockRecorder is the mock recorder for MockPropValueResolver.
type MockPropValueResolverMockRecorder struct {
mock *MockPropValueResolver
}
// NewMockPropValueResolver creates a new mock instance.
func NewMockPropValueResolver(ctrl *gomock.Controller) *MockPropValueResolver {
mock := &MockPropValueResolver{ctrl: ctrl}
mock.recorder = &MockPropValueResolverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPropValueResolver) EXPECT() *MockPropValueResolverMockRecorder {
return m.recorder
}
// GetUserByID mocks base method.
func (m *MockPropValueResolver) GetUserByID(arg0 string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByID", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByID indicates an expected call of GetUserByID.
func (mr *MockPropValueResolverMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockPropValueResolver)(nil).GetUserByID), arg0)
}

View File

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

View File

@ -11,7 +11,7 @@ import (
type AppAPI interface {
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error)
GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error)
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
GetUserByID(userID string) (*model.User, error)

View File

@ -149,10 +149,10 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr
}
// fetch all card content blocks that were updated after last notify
opts := model.QuerySubtreeOptions{
opts := model.QueryBlockHistoryChildOptions{
AfterUpdateAt: dg.lastNotifyAt,
}
blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts)
blocks, _, err := dg.store.GetBlockHistoryNewestChildren(card.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err)
}

View File

@ -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)
@ -246,7 +249,7 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
msg = child.NewBlock.Title
}
if child.NewBlock == nil && child.OldBlock != nil {
if (child.NewBlock == nil || child.NewBlock.DeleteAt != 0) && child.OldBlock != nil {
// deleted comment
format = "~~`%s`~~"
msg = stripNewlines(child.OldBlock.Title)
@ -264,38 +267,100 @@ func appendCommentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Dif
return fields
}
func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField {
func appendAttachmentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff) []*mm_model.SlackAttachmentField {
for _, child := range cardDiff.Diffs {
if child.BlockType != model.TypeComment {
var newTitle, oldTitle string
if child.OldBlock != nil {
oldTitle = child.OldBlock.Title
}
if child.NewBlock != nil {
newTitle = child.NewBlock.Title
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)
}
// only strip newlines when modifying or deleting
if child.OldBlock != nil && child.NewBlock == nil {
newTitle = stripNewlines(newTitle)
oldTitle = stripNewlines(oldTitle)
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),
})
}
if newTitle == oldTitle {
continue
}
markdown := generateMarkdownDiff(oldTitle, newTitle, logger)
if markdown == "" {
continue
}
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Description",
Value: markdown,
})
}
}
return fields
}
func appendContentChanges(fields []*mm_model.SlackAttachmentField, cardDiff *Diff, logger mlog.LoggerIFace) []*mm_model.SlackAttachmentField {
for _, child := range cardDiff.Diffs {
var opAdd, opDelete bool
var opString string
switch {
case child.OldBlock == nil && child.NewBlock != nil:
opAdd = true
opString = "added" // TODO: localize when i18n added to server
case child.NewBlock == nil || child.NewBlock.DeleteAt != 0:
opDelete = true
opString = "deleted"
default:
opString = "modified"
}
var newTitle, oldTitle string
if child.OldBlock != nil {
oldTitle = child.OldBlock.Title
}
if child.NewBlock != nil {
newTitle = child.NewBlock.Title
}
switch child.BlockType {
case model.TypeDivider, model.TypeComment:
// do nothing
continue
case model.TypeImage:
if newTitle == "" {
newTitle = "An image was " + opString + "." // TODO: localize when i18n added to server
}
oldTitle = ""
case model.TypeAttachment:
if newTitle == "" {
newTitle = "A file attachment was " + opString + "." // TODO: localize when i18n added to server
}
oldTitle = ""
default:
if !opAdd {
if opDelete {
newTitle = ""
}
// only strip newlines when modifying or deleting
oldTitle = stripNewlines(oldTitle)
newTitle = stripNewlines(newTitle)
}
if newTitle == oldTitle {
continue
}
}
logger.Debug("appendContentChanges",
mlog.String("type", string(child.BlockType)),
mlog.String("opString", opString),
mlog.String("oldTitle", oldTitle),
mlog.String("newTitle", newTitle),
)
markdown := generateMarkdownDiff(oldTitle, newTitle, logger)
if markdown == "" {
continue
}
fields = append(fields, &mm_model.SlackAttachmentField{
Short: false,
Title: "Description",
Value: markdown,
})
}
return fields
}

View File

@ -200,7 +200,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
}
opts := DiffConvOpts{
Language: "en", // TODO: use correct language with i18n available on server.
Language: "en", // TODO: use correct language when i18n is available on server.
MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string {
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID))
},

View File

@ -5,6 +5,8 @@ package notifysubscriptions
import (
"fmt"
"os"
"strconv"
"time"
"github.com/mattermost/focalboard/server/model"
@ -73,6 +75,16 @@ func (b *Backend) Name() string {
}
func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration {
// check for env variable override
sFreq := os.Getenv("MM_BOARDS_NOTIFY_FREQ_SECONDS")
if sFreq != "" && sFreq != "0" {
if freq, err := strconv.ParseInt(sFreq, 10, 64); err != nil {
b.logger.Error("Environment variable MM_BOARDS_NOTIFY_FREQ_SECONDS invalid (ignoring)", mlog.Err(err))
} else {
return time.Second * time.Duration(freq)
}
}
switch blockType {
case model.TypeCard:
return time.Second * time.Duration(b.notifyFreqCardSeconds)

View File

@ -80,8 +80,9 @@ type storeMetadata struct {
}
var blacklistedStoreMethodNames = map[string]bool{
"Shutdown": true,
"DBType": true,
"Shutdown": true,
"DBType": true,
"DBVersion": true,
}
func extractMethodMetadata(method *ast.Field, src []byte) methodData {

View File

@ -457,6 +457,22 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
}
// GetBlockHistoryNewestChildren mocks base method.
func (m *MockStore) GetBlockHistoryNewestChildren(arg0 string, arg1 model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlockHistoryNewestChildren", arg0, arg1)
ret0, _ := ret[0].([]*model.Block)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetBlockHistoryNewestChildren indicates an expected call of GetBlockHistoryNewestChildren.
func (mr *MockStoreMockRecorder) GetBlockHistoryNewestChildren(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryNewestChildren", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryNewestChildren), arg0, arg1)
}
// GetBlocks mocks base method.
func (m *MockStore) GetBlocks(arg0 model.QueryBlocksOptions) ([]*model.Block, error) {
m.ctrl.T.Helper()
@ -487,6 +503,22 @@ func (mr *MockStoreMockRecorder) GetBlocksByIDs(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByIDs", reflect.TypeOf((*MockStore)(nil).GetBlocksByIDs), arg0)
}
// GetBlocksComplianceHistory mocks base method.
func (m *MockStore) GetBlocksComplianceHistory(arg0 model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksComplianceHistory", arg0)
ret0, _ := ret[0].([]*model.BlockHistory)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetBlocksComplianceHistory indicates an expected call of GetBlocksComplianceHistory.
func (mr *MockStoreMockRecorder) GetBlocksComplianceHistory(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBlocksComplianceHistory), arg0)
}
// GetBlocksForBoard mocks base method.
func (m *MockStore) GetBlocksForBoard(arg0 string) ([]*model.Block, error) {
m.ctrl.T.Helper()
@ -639,6 +671,38 @@ func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardMemberHistory", reflect.TypeOf((*MockStore)(nil).GetBoardMemberHistory), arg0, arg1, arg2)
}
// GetBoardsComplianceHistory mocks base method.
func (m *MockStore) GetBoardsComplianceHistory(arg0 model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBoardsComplianceHistory", arg0)
ret0, _ := ret[0].([]*model.BoardHistory)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetBoardsComplianceHistory indicates an expected call of GetBoardsComplianceHistory.
func (mr *MockStoreMockRecorder) GetBoardsComplianceHistory(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBoardsComplianceHistory), arg0)
}
// GetBoardsForCompliance mocks base method.
func (m *MockStore) GetBoardsForCompliance(arg0 model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBoardsForCompliance", arg0)
ret0, _ := ret[0].([]*model.Board)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetBoardsForCompliance indicates an expected call of GetBoardsForCompliance.
func (mr *MockStoreMockRecorder) GetBoardsForCompliance(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForCompliance", reflect.TypeOf((*MockStore)(nil).GetBoardsForCompliance), arg0)
}
// GetBoardsForUserAndTeam mocks base method.
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
m.ctrl.T.Helper()

View File

@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"github.com/mattermost/focalboard/server/utils"
@ -42,27 +43,31 @@ func (s *SQLStore) timestampToCharField(name string, as string) string {
}
}
func (s *SQLStore) blockFields() []string {
func (s *SQLStore) blockFields(tableAlias string) []string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
return []string{
"id",
"parent_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"COALESCE(fields, '{}')",
s.timestampToCharField("insert_at", "insertAt"),
"create_at",
"update_at",
"delete_at",
"COALESCE(board_id, '0')",
tableAlias + "id",
tableAlias + "parent_id",
tableAlias + "created_by",
tableAlias + "modified_by",
tableAlias + s.escapeField("schema"),
tableAlias + "type",
tableAlias + "title",
"COALESCE(" + tableAlias + "fields, '{}')",
s.timestampToCharField(tableAlias+"insert_at", "insertAt"),
tableAlias + "create_at",
tableAlias + "update_at",
tableAlias + "delete_at",
"COALESCE(" + tableAlias + "board_id, '0')",
}
}
func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks")
if opts.BoardID != "" {
@ -115,7 +120,7 @@ func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID strin
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": ids})
@ -150,7 +155,7 @@ func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string
// getSubTree2 returns blocks within 2 levels of the given blockID.
func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Or{sq.Eq{"id": blockID}, sq.Eq{"parent_id": blockID}}).
Where(sq.Eq{"board_id": boardID}).
@ -550,7 +555,7 @@ func (s *SQLStore) getBoardCount(db sq.BaseRunner) (int64, error) {
func (s *SQLStore) getBlock(db sq.BaseRunner, blockID string) (*model.Block, error) {
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": blockID})
@ -580,7 +585,7 @@ func (s *SQLStore) getBlockHistory(db sq.BaseRunner, blockID string, opts model.
}
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks_history").
Where(sq.Eq{"id": blockID}).
OrderBy("insert_at " + order + ", update_at" + order)
@ -614,7 +619,7 @@ func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string,
}
query := s.getQueryBuilder(db).
Select(s.blockFields()...).
Select(s.blockFields("")...).
From(s.tablePrefix + "blocks_history").
Where(sq.Eq{"board_id": boardID}).
OrderBy("insert_at " + order + ", update_at" + order)
@ -641,6 +646,83 @@ func (s *SQLStore) getBlockHistoryDescendants(db sq.BaseRunner, boardID string,
return s.blocksFromRows(rows)
}
// getBlockHistoryNewestChildren returns the newest (latest) version child blocks for the
// specified parent from the blocks_history table. This includes any deleted children.
func (s *SQLStore) getBlockHistoryNewestChildren(db sq.BaseRunner, parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
// as we're joining 2 queries, we need to avoid numbered
// placeholders until the join is done, so we use the default
// question mark placeholder here
builder := s.getQueryBuilder(db).PlaceholderFormat(sq.Question)
sub := builder.
Select("bh2.id", "MAX(bh2.insert_at) AS max_insert_at").
From(s.tablePrefix + "blocks_history AS bh2").
Where(sq.Eq{"bh2.parent_id": parentID}).
GroupBy("bh2.id")
if opts.AfterUpdateAt != 0 {
sub = sub.Where(sq.Gt{"bh2.update_at": opts.AfterUpdateAt})
}
if opts.BeforeUpdateAt != 0 {
sub = sub.Where(sq.Lt{"bh2.update_at": opts.BeforeUpdateAt})
}
subQuery, subArgs, err := sub.ToSql()
if err != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate subquery: %w", err)
}
query := s.getQueryBuilder(db).
Select(s.blockFields("bh")...).
From(s.tablePrefix+"blocks_history AS bh").
InnerJoin("("+subQuery+") AS sub ON bh.id=sub.id AND bh.insert_at=sub.max_insert_at", subArgs...)
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// limit+1 to detect if more records available
query = query.Limit(uint64(opts.PerPage + 1))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to generate sql: %w", err)
}
// if we're using postgres or sqlite, we need to replace the
// question mark placeholder with the numbered dollar one, now
// that the full query is built
if s.dbType == model.PostgresDBType || s.dbType == model.SqliteDBType {
var rErr error
sql, rErr = sq.Dollar.ReplacePlaceholders(sql)
if rErr != nil {
return nil, false, fmt.Errorf("getBlockHistoryNewestChildren unable to replace sql placeholders: %w", rErr)
}
}
rows, err := db.Query(sql, args...)
if err != nil {
s.logger.Error(`getBlockHistoryNewestChildren ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
blocks, err := s.blocksFromRows(rows)
if err != nil {
return nil, false, err
}
hasMore := false
if opts.PerPage > 0 && len(blocks) > opts.PerPage {
blocks = blocks[:opts.PerPage]
hasMore = true
}
return blocks, hasMore, nil
}
// getBoardAndCardByID returns the first parent of type `card` and first parent of type `board` for the block specified by ID.
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
func (s *SQLStore) getBoardAndCardByID(db sq.BaseRunner, blockID string) (board *model.Board, card *model.Block, err error) {

View File

@ -17,41 +17,31 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func boardFields(prefix string) []string {
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"modified_by",
"type",
"minimum_role",
"title",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"create_at",
"update_at",
"delete_at",
func boardFields(tableAlias string) []string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
if prefix == "" {
return fields
return []string{
tableAlias + "id",
tableAlias + "team_id",
"COALESCE(" + tableAlias + "channel_id, '')",
"COALESCE(" + tableAlias + "created_by, '')",
tableAlias + "modified_by",
tableAlias + "type",
tableAlias + "minimum_role",
tableAlias + "title",
tableAlias + "description",
tableAlias + "icon",
tableAlias + "show_description",
tableAlias + "is_template",
tableAlias + "template_version",
"COALESCE(" + tableAlias + "properties, '{}')",
"COALESCE(" + tableAlias + "card_properties, '[]')",
tableAlias + "create_at",
tableAlias + "update_at",
tableAlias + "delete_at",
}
prefixedFields := make([]string, len(fields))
for i, field := range fields {
if strings.HasPrefix(field, "COALESCE(") {
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
} else {
prefixedFields[i] = prefix + field
}
}
return prefixedFields
}
func boardHistoryFields() []string {

View File

@ -0,0 +1,241 @@
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (s *SQLStore) getBoardsForCompliance(db sq.BaseRunner, opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"b.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsForCompliance ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(boards) > opts.PerPage {
boards = boards[0:opts.PerPage]
hasMore = true
}
return boards, hasMore, nil
}
func (s *SQLStore) getBoardsComplianceHistory(db sq.BaseRunner, opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
queryDescendentLastUpdate := s.getQueryBuilder(db).
Select("MAX(blk1.update_at)").
From(s.tablePrefix + "blocks_history as blk1").
Where("blk1.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentLastUpdate.Where(sq.Eq{"blk1.delete_at": 0})
}
sqlDescendentLastUpdate, _, _ := queryDescendentLastUpdate.ToSql()
queryDescendentFirstUpdate := s.getQueryBuilder(db).
Select("MIN(blk2.update_at)").
From(s.tablePrefix + "blocks_history as blk2").
Where("blk2.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentFirstUpdate.Where(sq.Eq{"blk2.delete_at": 0})
}
sqlDescendentFirstUpdate, _, _ := queryDescendentFirstUpdate.ToSql()
query := s.getQueryBuilder(db).
Select(
"bh.id",
"bh.team_id",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"COALESCE(("+sqlDescendentLastUpdate+"),0) as decendentLastUpdateAt",
"COALESCE(("+sqlDescendentFirstUpdate+"),0) as decendentFirstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "boards_history as bh")
if !opts.IncludeDeleted {
// filtering out deleted boards; join with boards table to ensure no history
// for deleted boards are returned. Deleted boards won't exist in boards table.
query = query.Join(s.tablePrefix + "boards as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "bh.team_id", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("decendentLastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"bh.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.boardsHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) getBlocksComplianceHistory(db sq.BaseRunner, opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
query := s.getQueryBuilder(db).
Select(
"bh.id",
"brd.team_id",
"bh.board_id",
"bh.type",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"max(bh.update_at) as lastUpdateAt",
"min(bh.update_at) as firstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "blocks_history as bh").
Join(s.tablePrefix + "boards_history as brd on brd.id=bh.board_id")
if !opts.IncludeDeleted {
// filtering out deleted blocks; join with blocks table to ensure no history
// for deleted blocks are returned. Deleted blocks won't exist in blocks table.
query = query.Join(s.tablePrefix + "blocks as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "brd.team_id", "bh.board_id", "bh.type", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("lastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"brd.team_id": opts.TeamID})
}
if opts.BoardID != "" {
query = query.Where(sq.Eq{"bh.board_id": opts.BoardID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlocksComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.blocksHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) boardsHistoryFromRows(rows *sql.Rows) ([]*model.BoardHistory, error) {
history := []*model.BoardHistory{}
for rows.Next() {
boardHistory := &model.BoardHistory{}
err := rows.Scan(
&boardHistory.ID,
&boardHistory.TeamID,
&boardHistory.IsDeleted,
&boardHistory.DescendantLastUpdateAt,
&boardHistory.DescendantFirstUpdateAt,
&boardHistory.CreatedBy,
&boardHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("boardsHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, boardHistory)
}
return history, nil
}
func (s *SQLStore) blocksHistoryFromRows(rows *sql.Rows) ([]*model.BlockHistory, error) {
history := []*model.BlockHistory{}
for rows.Next() {
blockHistory := &model.BlockHistory{}
err := rows.Scan(
&blockHistory.ID,
&blockHistory.TeamID,
&blockHistory.BoardID,
&blockHistory.Type,
&blockHistory.IsDeleted,
&blockHistory.LastUpdateAt,
&blockHistory.FirstUpdateAt,
&blockHistory.CreatedBy,
&blockHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("blocksHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, blockHistory)
}
return history, nil
}

View File

@ -143,11 +143,6 @@ func (s *SQLStore) CreateUser(user *model.User) (*model.User, error) {
}
func (s *SQLStore) DBVersion() string {
return s.dBVersion()
}
func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error {
if s.dbType == model.SqliteDBType {
return s.deleteBlock(s.db, blockID, modifiedBy)
@ -333,6 +328,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
}
func (s *SQLStore) GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error) {
return s.getBlockHistoryNewestChildren(s.db, parentID, opts)
}
func (s *SQLStore) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) {
return s.getBlocks(s.db, opts)
@ -343,6 +343,11 @@ func (s *SQLStore) GetBlocksByIDs(ids []string) ([]*model.Block, error) {
}
func (s *SQLStore) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
return s.getBlocksComplianceHistory(s.db, opts)
}
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]*model.Block, error) {
return s.getBlocksForBoard(s.db, boardID)
@ -393,6 +398,16 @@ func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit ui
}
func (s *SQLStore) GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
return s.getBoardsComplianceHistory(s.db, opts)
}
func (s *SQLStore) GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
return s.getBoardsForCompliance(s.db, opts)
}
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) {
return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards)

View File

@ -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",
"TABLE_NAME": s.tablePrefix + "schema_migrations",
})
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) {

View File

@ -185,7 +185,7 @@ func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mmMode
return nil, store.NewNotSupportedError("get channel not supported on standalone mode")
}
func (s *SQLStore) dBVersion() string {
func (s *SQLStore) DBVersion() string {
var version string
var row *sql.Row

View File

@ -29,6 +29,7 @@ func TestSQLStore(t *testing.T) {
t.Run("StoreTestCategoryStore", func(t *testing.T) { storetests.StoreTestCategoryStore(t, SetupTests) })
t.Run("StoreTestCategoryBoardsStore", func(t *testing.T) { storetests.StoreTestCategoryBoardsStore(t, SetupTests) })
t.Run("BoardsInsightsStore", func(t *testing.T) { storetests.StoreTestBoardsInsightsStore(t, SetupTests) })
t.Run("ComplianceHistoryStore", func(t *testing.T) { storetests.StoreTestComplianceHistoryStore(t, SetupTests) })
}
// tests for utility functions inside sqlstore.go

View File

@ -38,6 +38,7 @@ type Store interface {
PatchBlock(blockID string, blockPatch *model.BlockPatch, userID string) error
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
GetBlockHistoryDescendants(boardID string, opts model.QueryBlockHistoryOptions) ([]*model.Block, error)
GetBlockHistoryNewestChildren(parentID string, opts model.QueryBlockHistoryChildOptions) ([]*model.Block, bool, error)
GetBoardHistory(boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error)
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
GetBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error)
@ -173,6 +174,11 @@ type Store interface {
GetUserBoardsInsights(teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error)
GetUserTimezone(userID string) (string, error)
// Compliance
GetBoardsForCompliance(opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error)
GetBoardsComplianceHistory(opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error)
GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error)
// For unit testing only
DeleteBoardRecord(boardID, modifiedBy string) error
DeleteBlockRecord(blockID, modifiedBy string) error

View File

@ -1,6 +1,8 @@
package storetests
import (
"math"
"strconv"
"testing"
"time"
@ -79,6 +81,11 @@ func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, f
defer tearDown()
testUndeleteBlockChildren(t, store)
})
t.Run("GetBlockHistoryNewestChildren", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetBlockHistoryNewestChildren(t, store)
})
}
func testInsertBlock(t *testing.T, store store.Store) {
@ -1066,17 +1073,18 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
}
func testUndeleteBlockChildren(t *testing.T, store store.Store) {
boards := createTestBoards(t, store, testUserID, 2)
boards := createTestBoards(t, store, testTeamID, testUserID, 2)
boardDelete := boards[0]
boardKeep := boards[1]
userID := testUserID
// create some blocks to be deleted
cardsDelete := createTestCards(t, store, testUserID, boardDelete.ID, 3)
cardsDelete := createTestCards(t, store, userID, boardDelete.ID, 3)
blocksDelete := createTestBlocksForCard(t, store, cardsDelete[0].ID, 5)
require.Len(t, blocksDelete, 5)
// create some blocks to keep
cardsKeep := createTestCards(t, store, testUserID, boardKeep.ID, 3)
cardsKeep := createTestCards(t, store, userID, boardKeep.ID, 3)
blocksKeep := createTestBlocksForCard(t, store, cardsKeep[0].ID, 4)
require.Len(t, blocksKeep, 4)
@ -1153,3 +1161,94 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete))
})
}
func testGetBlockHistoryNewestChildren(t *testing.T, store store.Store) {
boards := createTestBoards(t, store, testTeamID, testUserID, 2)
board := boards[0]
const cardCount = 10
const patchCount = 5
// create a card and some content blocks
cards := createTestCards(t, store, testUserID, board.ID, 1)
card := cards[0]
content := createTestBlocksForCard(t, store, card.ID, cardCount)
// patch the content blocks to create some history records
for i := 1; i <= patchCount; i++ {
for _, block := range content {
title := strconv.FormatInt(int64(i), 10)
patch := &model.BlockPatch{
Title: &title,
}
err := store.PatchBlock(block.ID, patch, testUserID)
require.NoError(t, err, "error patching content blocks")
}
}
// delete some of the content blocks
err := store.DeleteBlock(content[0].ID, testUserID)
require.NoError(t, err, "error deleting content block")
err = store.DeleteBlock(content[3].ID, testUserID)
require.NoError(t, err, "error deleting content block")
err = store.DeleteBlock(content[7].ID, testUserID)
require.NoError(t, err, "error deleting content block")
t.Run("invalid card", func(t *testing.T) {
opts := model.QueryBlockHistoryChildOptions{}
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(utils.NewID(utils.IDTypeCard), opts)
require.NoError(t, err)
require.False(t, hasMore)
require.Empty(t, blocks)
})
t.Run("valid card with no children", func(t *testing.T) {
opts := model.QueryBlockHistoryChildOptions{}
emptyCard := createTestCards(t, store, testUserID, board.ID, 1)[0]
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(emptyCard.ID, opts)
require.NoError(t, err)
require.False(t, hasMore)
require.Empty(t, blocks)
})
t.Run("valid card with children", func(t *testing.T) {
opts := model.QueryBlockHistoryChildOptions{}
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts)
require.NoError(t, err)
require.False(t, hasMore)
require.Len(t, blocks, cardCount)
require.ElementsMatch(t, extractIDs(t, blocks), extractIDs(t, content))
expected := strconv.FormatInt(patchCount, 10)
for _, b := range blocks {
require.Equal(t, expected, b.Title)
}
})
t.Run("pagination", func(t *testing.T) {
opts := model.QueryBlockHistoryChildOptions{
PerPage: 3,
}
collected := make([]*model.Block, 0)
reps := 0
for {
reps++
blocks, hasMore, err := store.GetBlockHistoryNewestChildren(card.ID, opts)
require.NoError(t, err)
collected = append(collected, blocks...)
if !hasMore {
break
}
opts.Page++
}
assert.Len(t, collected, cardCount)
assert.Equal(t, math.Floor(float64(cardCount/opts.PerPage)+1), float64(reps))
expected := strconv.FormatInt(patchCount, 10)
for _, b := range collected {
require.Equal(t, expected, b.Title)
}
})
}

View File

@ -0,0 +1,294 @@
package storetests
import (
"math"
"testing"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func StoreTestComplianceHistoryStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
t.Run("GetBoardsForCompliance", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetBoardsForCompliance(t, store)
})
t.Run("GetBoardsComplianceHistory", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetBoardsComplianceHistory(t, store)
})
t.Run("GetBlocksComplianceHistory", func(t *testing.T) {
store, tearDown := setup(t)
defer tearDown()
testGetBlocksComplianceHistory(t, store)
})
}
func testGetBoardsForCompliance(t *testing.T, store store.Store) {
team1 := testTeamID
team2 := utils.NewID(utils.IDTypeTeam)
boardsAdded1 := createTestBoards(t, store, team1, testUserID, 10)
boardsAdded2 := createTestBoards(t, store, team2, testUserID, 7)
deleteTestBoard(t, store, boardsAdded1[0].ID, testUserID)
deleteTestBoard(t, store, boardsAdded1[1].ID, testUserID)
boardsAdded1 = boardsAdded1[2:]
t.Run("Invalid teamID", func(t *testing.T) {
opts := model.QueryBoardsForComplianceOptions{
TeamID: utils.NewID(utils.IDTypeTeam),
}
boards, hasMore, err := store.GetBoardsForCompliance(opts)
assert.Empty(t, boards)
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("All teams", func(t *testing.T) {
opts := model.QueryBoardsForComplianceOptions{}
boards, hasMore, err := store.GetBoardsForCompliance(opts)
assert.ElementsMatch(t, extractIDs(t, boards), extractIDs(t, boardsAdded1, boardsAdded2))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Specific team", func(t *testing.T) {
opts := model.QueryBoardsForComplianceOptions{
TeamID: team1,
}
boards, hasMore, err := store.GetBoardsForCompliance(opts)
assert.ElementsMatch(t, extractIDs(t, boards), extractIDs(t, boardsAdded1))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Pagination", func(t *testing.T) {
opts := model.QueryBoardsForComplianceOptions{
Page: 0,
PerPage: 3,
}
reps := 0
allBoards := make([]*model.Board, 0, 20)
for {
boards, hasMore, err := store.GetBoardsForCompliance(opts)
require.NoError(t, err)
require.NotEmpty(t, boards)
allBoards = append(allBoards, boards...)
if !hasMore {
break
}
opts.Page++
reps++
}
assert.ElementsMatch(t, extractIDs(t, allBoards), extractIDs(t, boardsAdded1, boardsAdded2))
})
}
func testGetBoardsComplianceHistory(t *testing.T, store store.Store) {
team1 := testTeamID
team2 := utils.NewID(utils.IDTypeTeam)
boardsTeam1 := createTestBoards(t, store, team1, testUserID, 11)
boardsTeam2 := createTestBoards(t, store, team2, testUserID, 7)
boardsAdded := make([]*model.Board, 0)
boardsAdded = append(boardsAdded, boardsTeam1...)
boardsAdded = append(boardsAdded, boardsTeam2...)
deleteTestBoard(t, store, boardsTeam1[0].ID, testUserID)
deleteTestBoard(t, store, boardsTeam1[1].ID, testUserID)
boardsDeleted := boardsTeam1[0:2]
boardsTeam1 = boardsTeam1[2:]
t.Log("boardsTeam1: ", extractIDs(t, boardsTeam1))
t.Log("boardsTeam2: ", extractIDs(t, boardsTeam2))
t.Log("boardsAdded: ", extractIDs(t, boardsAdded))
t.Log("boardsDeleted: ", extractIDs(t, boardsDeleted))
t.Run("Invalid teamID", func(t *testing.T) {
opts := model.QueryBoardsComplianceHistoryOptions{
TeamID: utils.NewID(utils.IDTypeTeam),
}
boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts)
assert.Empty(t, boardHistories)
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("All teams, include deleted", func(t *testing.T) {
opts := model.QueryBoardsComplianceHistoryOptions{
IncludeDeleted: true,
}
boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts)
// boardHistories should contain a record for each board added, plus a record for the 2 deleted.
assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsAdded, boardsDeleted))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("All teams, exclude deleted", func(t *testing.T) {
opts := model.QueryBoardsComplianceHistoryOptions{
IncludeDeleted: false,
}
boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts)
// boardHistories should contain a record for each board added, minus the two deleted.
assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsTeam1, boardsTeam2))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Specific team", func(t *testing.T) {
opts := model.QueryBoardsComplianceHistoryOptions{
TeamID: team1,
}
boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts)
assert.ElementsMatch(t, extractIDs(t, boardHistories), extractIDs(t, boardsTeam1))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Pagination", func(t *testing.T) {
opts := model.QueryBoardsComplianceHistoryOptions{
Page: 0,
PerPage: 3,
}
reps := 0
allHistories := make([]*model.BoardHistory, 0)
for {
reps++
boardHistories, hasMore, err := store.GetBoardsComplianceHistory(opts)
require.NoError(t, err)
require.NotEmpty(t, boardHistories)
allHistories = append(allHistories, boardHistories...)
if !hasMore {
break
}
opts.Page++
}
assert.ElementsMatch(t, extractIDs(t, allHistories), extractIDs(t, boardsTeam1, boardsTeam2))
expectedCount := len(boardsTeam1) + len(boardsTeam2)
assert.Equal(t, math.Floor(float64(expectedCount/opts.PerPage)+1), float64(reps))
})
}
func testGetBlocksComplianceHistory(t *testing.T, store store.Store) {
team1 := testTeamID
team2 := utils.NewID(utils.IDTypeTeam)
boardsTeam1 := createTestBoards(t, store, team1, testUserID, 3)
boardsTeam2 := createTestBoards(t, store, team2, testUserID, 1)
// add cards (13 in total)
cards1Team1 := createTestCards(t, store, testUserID, boardsTeam1[0].ID, 3)
cards2Team1 := createTestCards(t, store, testUserID, boardsTeam1[1].ID, 5)
cards3Team1 := createTestCards(t, store, testUserID, boardsTeam1[2].ID, 2)
cards1Team2 := createTestCards(t, store, testUserID, boardsTeam2[0].ID, 3)
deleteTestBoard(t, store, boardsTeam1[0].ID, testUserID)
cardsDeleted := cards1Team1
t.Run("Invalid teamID", func(t *testing.T) {
opts := model.QueryBlocksComplianceHistoryOptions{
TeamID: utils.NewID(utils.IDTypeTeam),
}
boards, hasMore, err := store.GetBlocksComplianceHistory(opts)
assert.Empty(t, boards)
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("All teams, include deleted", func(t *testing.T) {
opts := model.QueryBlocksComplianceHistoryOptions{
IncludeDeleted: true,
}
blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts)
// blockHistories should have records for all cards added, plus all cards deleted
assert.ElementsMatch(t, extractIDs(t, blockHistories, nil),
extractIDs(t, cards1Team1, cards2Team1, cards3Team1, cards1Team2, cardsDeleted))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("All teams, exclude deleted", func(t *testing.T) {
opts := model.QueryBlocksComplianceHistoryOptions{}
blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts)
// blockHistories should have records for all cards added that have not been deleted
assert.ElementsMatch(t, extractIDs(t, blockHistories, nil),
extractIDs(t, cards2Team1, cards3Team1, cards1Team2))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Specific team", func(t *testing.T) {
opts := model.QueryBlocksComplianceHistoryOptions{
TeamID: team1,
}
blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts)
assert.ElementsMatch(t, extractIDs(t, blockHistories), extractIDs(t, cards2Team1, cards3Team1))
assert.False(t, hasMore)
assert.NoError(t, err)
})
t.Run("Pagination", func(t *testing.T) {
opts := model.QueryBlocksComplianceHistoryOptions{
Page: 0,
PerPage: 3,
}
reps := 0
allHistories := make([]*model.BlockHistory, 0)
for {
reps++
blockHistories, hasMore, err := store.GetBlocksComplianceHistory(opts)
require.NoError(t, err)
require.NotEmpty(t, blockHistories)
allHistories = append(allHistories, blockHistories...)
if !hasMore {
break
}
opts.Page++
}
assert.ElementsMatch(t, extractIDs(t, allHistories), extractIDs(t, cards2Team1, cards3Team1, cards1Team2))
expectedCount := len(cards2Team1) + len(cards3Team1) + len(cards1Team2)
assert.Equal(t, math.Floor(float64(expectedCount/opts.PerPage)+1), float64(reps))
})
}

View File

@ -5,6 +5,7 @@ package storetests
import (
"fmt"
"sort"
"testing"
"github.com/mattermost/focalboard/server/model"
@ -71,6 +72,7 @@ func createTestBlocksForCard(t *testing.T, store store.Store, cardID string, num
return blocks
}
//nolint:unparam
func createTestCards(t *testing.T, store store.Store, userID string, boardID string, num int) []*model.Block {
var blocks []*model.Block
for i := 0; i < num; i++ {
@ -90,12 +92,13 @@ func createTestCards(t *testing.T, store store.Store, userID string, boardID str
return blocks
}
func createTestBoards(t *testing.T, store store.Store, userID string, num int) []*model.Board {
//nolint:unparam
func createTestBoards(t *testing.T, store store.Store, teamID string, userID string, num int) []*model.Board {
var boards []*model.Board
for i := 0; i < num; i++ {
board := &model.Board{
ID: utils.NewID(utils.IDTypeBoard),
TeamID: testTeamID,
TeamID: teamID,
Type: "O",
CreatedBy: userID,
Title: fmt.Sprintf("board %d", i),
@ -107,3 +110,50 @@ func createTestBoards(t *testing.T, store store.Store, userID string, num int) [
}
return boards
}
//nolint:unparam
func deleteTestBoard(t *testing.T, store store.Store, boardID string, userID string) {
err := store.DeleteBoard(boardID, userID)
require.NoError(t, err)
}
// extractIDs is a test helper that extracts a sorted slice of IDs from slices of various struct types.
// Might have used generics here except that would require implementing a `GetID` method on each type.
func extractIDs(t *testing.T, arr ...any) []string {
ids := make([]string, 0)
for _, item := range arr {
if item == nil {
continue
}
switch tarr := item.(type) {
case []*model.Board:
for _, b := range tarr {
if b != nil {
ids = append(ids, b.ID)
}
}
case []*model.BoardHistory:
for _, bh := range tarr {
ids = append(ids, bh.ID)
}
case []*model.Block:
for _, b := range tarr {
if b != nil {
ids = append(ids, b.ID)
}
}
case []*model.BlockHistory:
for _, bh := range tarr {
ids = append(ids, bh.ID)
}
default:
t.Errorf("unsupported type %T extracting board ID", item)
}
}
// sort the ids to make it easier to compare lists of ids visually.
sort.Strings(ids)
return ids
}

View File

@ -11,15 +11,16 @@ import (
type IDType byte
const (
IDTypeNone IDType = '7'
IDTypeTeam IDType = 't'
IDTypeBoard IDType = 'b'
IDTypeCard IDType = 'c'
IDTypeView IDType = 'v'
IDTypeSession IDType = 's'
IDTypeUser IDType = 'u'
IDTypeToken IDType = 'k'
IDTypeBlock IDType = 'a'
IDTypeNone IDType = '7'
IDTypeTeam IDType = 't'
IDTypeBoard IDType = 'b'
IDTypeCard IDType = 'c'
IDTypeView IDType = 'v'
IDTypeSession IDType = 's'
IDTypeUser IDType = 'u'
IDTypeToken IDType = 'k'
IDTypeBlock IDType = 'a'
IDTypeAttachment IDType = 'i'
)
// NewId is a globally unique identifier. It is a [A-Z0-9] string 27

View File

@ -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')
}

View File

@ -3,10 +3,10 @@
"Attachment.Attachment-title": "Anhang",
"AttachmentBlock.DeleteAction": "Löschen",
"AttachmentBlock.addElement": "{type} hinzufügen",
"AttachmentBlock.delete": "Anhang erfolgreich gelöscht.",
"AttachmentBlock.failed": "Kann Datei nicht hochladen. Limit für Dateigröße erreicht.",
"AttachmentBlock.delete": "Anhang gelöscht.",
"AttachmentBlock.failed": "Kann Datei nicht hochladen, da das Limit für Dateigröße erreicht ist.",
"AttachmentBlock.upload": "Anhang wird hochgeladen.",
"AttachmentBlock.uploadSuccess": "Anhang erfolgreich hochgeladen.",
"AttachmentBlock.uploadSuccess": "Anhang hochgeladen.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Löschen",
"AttachmentElement.download": "Herunterladen",
"AttachmentElement.upload-percentage": "Hochladen...({uploadPercent}%)",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Symbol hinzufügen",
"CardDetail.add-property": "+ Eigenschaft hinzufügen",
"CardDetail.addCardText": "Kartentext hinzufügen",
"CardDetail.limited-body": "Aktualisiere auf unseren Professional oder Enterprise Plan um archivierte Karten zu betrachten, unbeschränkte Sichten pro Board, unbeschränkte Karten und mehr zu haben.",
"CardDetail.limited-body": "Aktualisiere auf unseren Professional oder Enterprise Plan.",
"CardDetail.limited-button": "Aktualisiere",
"CardDetail.limited-title": "Diese Karte ist versteckt",
"CardDetail.moveContent": "Karteninhalt verschieben",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "{propertyName} erfolgreich gelöscht!",
"CardDetailProperty.property-name-change-subtext": "Typ von \"{oldPropType}\" zu \"{newPropType}\"",
"CardDetial.limited-link": "Erfahre mehr über unsere Pläne.",
"CardDialog.delete-confirmation-dialog-attachment": "Bestätige das Löschen des Anhangs!",
"CardDialog.delete-confirmation-dialog-attachment": "Löschen des Anhangs bestätigen",
"CardDialog.delete-confirmation-dialog-button-text": "Löschen",
"CardDialog.delete-confirmation-dialog-heading": "Karte wirklich löschen!",
"CardDialog.delete-confirmation-dialog-heading": "Karte löschen bestätigen",
"CardDialog.editing-template": "Du bearbeitest eine Vorlage.",
"CardDialog.nocard": "Diese Karte existiert nicht oder ist nicht verfügbar.",
"Categories.CreateCategoryDialog.CancelText": "Abbrechen",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Aktualisieren",
"CenterPanel.Login": "Anmeldung",
"CenterPanel.Share": "Teilen",
"ChannelIntro.CreateBoard": "Erstelle ein Board",
"CloudMessage.cloud-server": "Hole deine eigenen freien Cloud Server.",
"ColorOption.selectColor": "Wähle Farbe {color}",
"Comment.delete": "Löschen",
"CommentsList.send": "Abschicken",
"ConfirmPerson.empty": "Leer",
"ConfirmPerson.search": "Suche...",
"ConfirmationDialog.cancel-action": "Abbrechen",
"ConfirmationDialog.confirm-action": "Bestätigen",
"ContentBlock.Delete": "Löschen",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "Filtertext",
"FilterComponent.add-filter": "+ Filter hinzufügen",
"FilterComponent.delete": "Löschen",
"FilterValue.empty": "(leer)",
"FindBoardsDialog.IntroText": "Suche nach Boards",
"FindBoardsDialog.NoResultsFor": "Keine Ergebnisse für \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Prüfe die Schreibweise oder versuche eine weitere Suche.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Kommentare hinzufügen",
"OnboardingTour.AddDescription.Body": "Füge deiner Karte eine Beschreibung hinzu, damit deine Teamkollegen wissen, worum es in der Karte geht.",
"OnboardingTour.AddDescription.Title": "Beschreibung hinzufügen",
"OnboardingTour.AddProperties.Body": "Füge den Karten verschiedene Eigenschaften hinzu, um sie noch leistungsfähiger zu machen!",
"OnboardingTour.AddProperties.Body": "Füge den Karten verschiedene Eigenschaften hinzu, um sie noch leistungsfähiger zu machen.",
"OnboardingTour.AddProperties.Title": "Eigenschaften hinzufügen",
"OnboardingTour.AddView.Body": "Hier kannst Du eine neue Ansicht erstellen, um dein Board mit verschiedenen Layouts zu organisieren.",
"OnboardingTour.AddView.Title": "Eine neue Ansicht hinzufügen",
@ -194,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Du kannst dein Board intern, innerhalb deines Teams, freigeben oder es öffentlich veröffentlichen, damit es auch außerhalb deines Unternehmens sichtbar ist.",
"OnboardingTour.ShareBoard.Title": "Board teilen",
"PersonProperty.board-members": "Board Mitglieder",
"PersonProperty.me": "Ich",
"PersonProperty.non-board-members": "Keine Board Mitglieder",
"PropertyMenu.Delete": "Löschen",
"PropertyMenu.changeType": "Eigenschaftstyp ändern",
@ -284,8 +289,8 @@
"TableHeaderMenu.insert-right": "Rechts einfügen",
"TableHeaderMenu.sort-ascending": "Aufsteigend sortieren",
"TableHeaderMenu.sort-descending": "Absteigend sortieren",
"TableRow.DuplicateCard": "Kopiere Karte",
"TableRow.MoreOption": "Weitere Aktionen",
"TableRow.delete": "Entfernen",
"TableRow.open": "Öffnen",
"TopBar.give-feedback": "Feedback geben",
"URLProperty.copiedLink": "Kopiert!",
@ -337,9 +342,9 @@
"ViewLimitDialog.Heading": "Ansichten pro Board Limit erreicht",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Aktualisieren",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Admin benachrichtigen",
"ViewLimitDialog.Subtext.Admin": "Aktualisiere auf unseren Professional oder Enterprise Plan um unbeschränkte Ansichten pro Board, unbeschränkte Karten und mehr zu bekommen.",
"ViewLimitDialog.Subtext.Admin": "Aktualisiere auf unseren Professional oder Enterprise Plan.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Erfahre mehr über unsere Pläne.",
"ViewLimitDialog.Subtext.RegularUser": "Benachrichtige deinen Admin um auf unseren Professional oder Enterprise Plan zu aktualisieren um unbeschränkte Ansichten pro Board, unbeschränkte Karten und mehr zu bekommen.",
"ViewLimitDialog.Subtext.RegularUser": "Benachrichtige deinen Admin um auf unseren Professional oder Enterprise Plan zu aktualisieren.",
"ViewLimitDialog.UpgradeImg.AltText": "Bild aktualisieren",
"ViewLimitDialog.notifyAdmin.Success": "Dein Admin wurde benachrichtigt",
"ViewTitle.hide-description": "Beschreibung ausblenden",
@ -367,8 +372,10 @@
"calendar.month": "Monat",
"calendar.today": "HEUTE",
"calendar.week": "Woche",
"centerPanel.undefined": "Kein(e) {propertyName}",
"centerPanel.unknown-user": "Unbekannter Benutzer",
"cloudMessage.learn-more": "Erfahre mehr",
"createImageBlock.failed": "Kann Datei nicht hochladen. Limit für Dateigröße erreicht.",
"createImageBlock.failed": "Kann Datei nicht hochladen, da das Limit für Dateigröße erreicht ist.",
"default-properties.badges": "Kommentare und Beschreibung",
"default-properties.title": "Titel",
"error.back-to-home": "Zurück zur Startseite",
@ -383,11 +390,15 @@
"generic.previous": "Zurück",
"guest-no-board.subtitle": "Du hast noch keinen Zugang zu einem Board in diesem Team. Bitte warte, bis dich jemand zu einem Board hinzufügt.",
"guest-no-board.title": "Noch keine Boards",
"imagePaste.upload-failed": "Einige Dateien nicht hochgeladen. Limit für Dateigröße erreicht",
"imagePaste.upload-failed": "Einige Dateien nicht hochgeladen, da das Limit für Dateigröße erreicht ist.",
"limitedCard.title": "Versteckte Karten",
"login.log-in-button": "Anmelden",
"login.log-in-title": "Anmelden",
"login.register-button": "oder erstelle einen Account wenn du noch keines hast",
"new_channel_modal.create_board.empty_board_description": "Neues leeres Board erstellen",
"new_channel_modal.create_board.empty_board_title": "Leeres Board",
"new_channel_modal.create_board.select_template_placeholder": "Vorlage auswählen",
"new_channel_modal.create_board.title": "Erstelle ein Board für diesen Kanal",
"notification-box-card-limit-reached.close-tooltip": "Für 10 Tage schlummern",
"notification-box-card-limit-reached.contact-link": "Benachrichtige deinen Admin",
"notification-box-card-limit-reached.link": "Wechsel auf einen kostenpflichtigen Plan",

View File

@ -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": "notify your admin",
"notification-box-card-limit-reached.link": "Upgrade to a paid plan",

View File

@ -1,11 +1,12 @@
{
"AppBar.Tooltip": "Toggle Linked Boards",
"AppBar.Tooltip": "Toggle linked boards",
"Attachment.Attachment-title": "Attachment",
"AttachmentBlock.DeleteAction": "delete",
"AttachmentBlock.addElement": "add {type}",
"AttachmentBlock.delete": "Attachment deleted successfully.",
"AttachmentBlock.failed": "Unable to upload the file. Attachment size limit reached.",
"AttachmentBlock.delete": "Attachment deleted.",
"AttachmentBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.",
"AttachmentBlock.upload": "Attachment uploading.",
"AttachmentBlock.uploadSuccess": "Attachment uploaded successfully.",
"AttachmentBlock.uploadSuccess": "Attachment uploaded.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Delete",
"AttachmentElement.download": "Download",
"AttachmentElement.upload-percentage": "Uploading...({uploadPercent}%)",
@ -15,7 +16,7 @@
"BoardComponent.hide": "Hide",
"BoardComponent.new": "+ New",
"BoardComponent.no-property": "No {property}",
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.",
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column can't be removed.",
"BoardComponent.show": "Show",
"BoardMember.schemeAdmin": "Admin",
"BoardMember.schemeCommenter": "Commenter",
@ -26,7 +27,7 @@
"BoardPage.newVersion": "A new version of Boards is available, click here to reload.",
"BoardPage.syncFailed": "Board may be deleted or access revoked.",
"BoardTemplateSelector.add-template": "Create new template",
"BoardTemplateSelector.create-empty-board": "Create empty board",
"BoardTemplateSelector.create-empty-board": "Create an empty board",
"BoardTemplateSelector.delete-template": "Delete",
"BoardTemplateSelector.description": "Add a board to the sidebar using any of the templates defined below or start from scratch.",
"BoardTemplateSelector.edit-template": "Edit",
@ -34,7 +35,7 @@
"BoardTemplateSelector.plugin.no-content-title": "Create a board",
"BoardTemplateSelector.title": "Create a board",
"BoardTemplateSelector.use-this-template": "Use this template",
"BoardsSwitcher.Title": "Find Boards",
"BoardsSwitcher.Title": "Find boards",
"BoardsUnfurl.Limited": "Additional details are hidden due to the card being archived",
"BoardsUnfurl.Remainder": "+{remainder} more",
"BoardsUnfurl.Updated": "Updated {time}",
@ -87,7 +88,7 @@
"CardDetail.add-icon": "Add icon",
"CardDetail.add-property": "+ Add a property",
"CardDetail.addCardText": "add card text",
"CardDetail.limited-body": "Upgrade to the Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.",
"CardDetail.limited-body": "Upgrade to the Professional or Enterprise plan.",
"CardDetail.limited-button": "Upgrade",
"CardDetail.limited-title": "This card is hidden",
"CardDetail.moveContent": "Move card content",
@ -104,7 +105,7 @@
"CardDetial.limited-link": "Learn more about our plans.",
"CardDialog.delete-confirmation-dialog-attachment": "Confirm attachment deletion",
"CardDialog.delete-confirmation-dialog-button-text": "Delete",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card deletion?",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card deletion",
"CardDialog.editing-template": "You're editing a template.",
"CardDialog.nocard": "This card doesn't exist or is inaccessible.",
"Categories.CreateCategoryDialog.CancelText": "Cancel",
@ -113,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Update",
"CenterPanel.Login": "Login",
"CenterPanel.Share": "Share",
"ChannelIntro.CreateBoard": "Create a board",
"CloudMessage.cloud-server": "Get your own free cloud server.",
"ColorOption.selectColor": "Select {color} Colour",
"Comment.delete": "Delete",
"CommentsList.send": "Send",
"ConfirmPerson.empty": "Empty",
"ConfirmPerson.search": "Search...",
"ConfirmationDialog.cancel-action": "Cancel",
"ConfirmationDialog.confirm-action": "Confirm",
"ContentBlock.Delete": "Delete",
@ -130,6 +134,7 @@
"ContentBlock.editText": "Edit text",
"ContentBlock.image": "image",
"ContentBlock.insertAbove": "Insert above",
"ContentBlock.moveBlock": "move card content",
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
@ -163,6 +168,7 @@
"FilterByText.placeholder": "filter text",
"FilterComponent.add-filter": "+ Add filter",
"FilterComponent.delete": "Delete",
"FilterValue.empty": "(empty)",
"FindBoardsDialog.IntroText": "Search for boards",
"FindBoardsDialog.NoResultsFor": "No results for '\\{searchQuery}'\\",
"FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.",
@ -181,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Add comments",
"OnboardingTour.AddDescription.Body": "Add a description to your card so your teammates know what the card is about.",
"OnboardingTour.AddDescription.Title": "Add description",
"OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful!",
"OnboardingTour.AddProperties.Body": "Add various properties to cards to make them more powerful.",
"OnboardingTour.AddProperties.Title": "Add properties",
"OnboardingTour.AddView.Body": "Go here to create a new view to organise your board using different layouts.",
"OnboardingTour.AddView.Title": "Add a new view",
@ -192,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "You can share your board internally, within your team or publish it publicly for visibility outside of your organisation.",
"OnboardingTour.ShareBoard.Title": "Share board",
"PersonProperty.board-members": "Board members",
"PersonProperty.me": "Me",
"PersonProperty.non-board-members": "Not board members",
"PropertyMenu.Delete": "Delete",
"PropertyMenu.changeType": "Change property type",
@ -230,11 +237,11 @@
"ShareBoard.copyLink": "Copy link",
"ShareBoard.regenerate": "Regenerate token",
"ShareBoard.searchPlaceholder": "Search for people and channels",
"ShareBoard.teamPermissionsText": "Everyone at {teamName} Team",
"ShareBoard.teamPermissionsText": "Everyone at {teamName} team",
"ShareBoard.tokenRegenrated": "Token regenerated",
"ShareBoard.userPermissionsRemoveMemberText": "Remove member",
"ShareBoard.userPermissionsYouText": "(You)",
"ShareTemplate.Title": "Share Template",
"ShareTemplate.Title": "Share template",
"ShareTemplate.searchPlaceholder": "Search for people",
"Sidebar.about": "About Focalboard",
"Sidebar.add-board": "+ Add board",
@ -270,8 +277,8 @@
"SidebarTour.SidebarCategories.Body": "All your boards are now organised under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.",
"SidebarTour.SidebarCategories.Link": "Learn more",
"SidebarTour.SidebarCategories.Title": "Sidebar categories",
"SiteStats.total_boards": "Total Boards",
"SiteStats.total_cards": "Total Cards",
"SiteStats.total_boards": "Total boards",
"SiteStats.total_cards": "Total cards",
"TableComponent.add-icon": "Add icon",
"TableComponent.name": "Name",
"TableComponent.plus-new": "+ New",
@ -282,8 +289,8 @@
"TableHeaderMenu.insert-right": "Insert right",
"TableHeaderMenu.sort-ascending": "Sort ascending",
"TableHeaderMenu.sort-descending": "Sort descending",
"TableRow.DuplicateCard": "duplicate card",
"TableRow.MoreOption": "More actions",
"TableRow.delete": "Delete",
"TableRow.open": "Open",
"TopBar.give-feedback": "Give feedback",
"URLProperty.copiedLink": "Copied",
@ -335,9 +342,9 @@
"ViewLimitDialog.Heading": "Views per board limit reached",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Upgrade",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Notify Admin",
"ViewLimitDialog.Subtext.Admin": "Upgrade to the Professional or Enterprise plan to have unlimited views per boards, unlimited cards and more.",
"ViewLimitDialog.Subtext.Admin": "Upgrade to the Professional or Enterprise plan.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Learn more about our plans.",
"ViewLimitDialog.Subtext.RegularUser": "Ask your Admin to upgrade to the Professional or Enterprise plan to have unlimited views per boards, unlimited cards and more.",
"ViewLimitDialog.Subtext.RegularUser": "Ask your Admin to upgrade to the Professional or Enterprise plan.",
"ViewLimitDialog.UpgradeImg.AltText": "upgrade image",
"ViewLimitDialog.notifyAdmin.Success": "Your admin has been contacted",
"ViewTitle.hide-description": "hide description",
@ -365,11 +372,13 @@
"calendar.month": "Month",
"calendar.today": "TODAY",
"calendar.week": "Week",
"centerPanel.undefined": "No {propertyName}",
"centerPanel.unknown-user": "Unknown user",
"cloudMessage.learn-more": "Learn more",
"createImageBlock.failed": "Unable to upload the file. Individual file size limit exceeded.",
"createImageBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.",
"default-properties.badges": "Comments and description",
"default-properties.title": "Title",
"error.back-to-home": "Back to Home",
"error.back-to-home": "Back to home",
"error.back-to-team": "Back to team",
"error.board-not-found": "Board not found.",
"error.go-login": "Log in",
@ -381,11 +390,15 @@
"generic.previous": "Previous",
"guest-no-board.subtitle": "You don't have access to any board in this team yet, please wait until somebody adds you to any board.",
"guest-no-board.title": "No boards yet",
"imagePaste.upload-failed": "Some files were not uploaded because they exceeded the individual file size limit.",
"imagePaste.upload-failed": "Some files weren't uploaded because the file size limit has been reached.",
"limitedCard.title": "Cards hidden",
"login.log-in-button": "Log in",
"login.log-in-title": "Log in",
"login.register-button": "or create an account if you don't have one",
"new_channel_modal.create_board.empty_board_description": "Create a new empty board",
"new_channel_modal.create_board.empty_board_title": "Empty board",
"new_channel_modal.create_board.select_template_placeholder": "Select a template",
"new_channel_modal.create_board.title": "Create a board for this channel",
"notification-box-card-limit-reached.close-tooltip": "Snooze for 10 days",
"notification-box-card-limit-reached.contact-link": "Contact your adminstrator",
"notification-box-card-limit-reached.link": "Upgrade to a paid plan",
@ -397,15 +410,15 @@
"person.add-user-to-board-confirm-button": "Add to board",
"person.add-user-to-board-permissions": "Permissions",
"person.add-user-to-board-question": "Do you want to add {username} to the board?",
"person.add-user-to-board-warning": "{username} is not a member of the board, and will not receive any notifications about it.",
"person.add-user-to-board-warning": "{username} isn't a member of the board and won't receive any notifications for it.",
"register.login-button": "or log in if you already have an account",
"register.signup-title": "Sign up for your account",
"rhs-board-non-admin-msg": "You are not an admin of the board",
"rhs-board-non-admin-msg": "You're not an admin of the board",
"rhs-boards.add": "Add",
"rhs-boards.dm": "DM",
"rhs-boards.gm": "GM",
"rhs-boards.header.dm": "this Direct Message",
"rhs-boards.header.gm": "this Group Message",
"rhs-boards.header.dm": "this direct message",
"rhs-boards.header.gm": "this group message",
"rhs-boards.last-update-at": "Last update at: {datetime}",
"rhs-boards.link-boards-to-channel": "Link boards to {channelName}",
"rhs-boards.linked-boards": "Linked boards",

View File

@ -1,5 +1,16 @@
{
"BoardComponent.add-a-group": "+ اضافه کردن گروه",
"AppBar.Tooltip": "تغییر وضعیت تخته‌های مرتبط",
"Attachment.Attachment-title": "ضمیمه",
"AttachmentBlock.DeleteAction": "حذف",
"AttachmentBlock.addElement": "افزودن {type}",
"AttachmentBlock.delete": "ضمیمه حذف شد.",
"AttachmentBlock.failed": "به دلیل محدودیت حجم، این پرونده نمی‌تواند بارگذاری شود.",
"AttachmentBlock.upload": "بارگذاری ضمیمه.",
"AttachmentBlock.uploadSuccess": "ضمیمه بارگذاری شد.",
"AttachmentElement.delete-confirmation-dialog-button-text": "حذف",
"AttachmentElement.download": "بارگیری",
"AttachmentElement.upload-percentage": "بارگذاری...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ افزودن گروه",
"BoardComponent.delete": "حذف",
"BoardComponent.hidden-columns": "ستون های مخفی",
"BoardComponent.hide": "مخفی",
@ -13,30 +24,30 @@
"BoardMember.schemeViewer": "بیننده",
"BoardPage.newVersion": "نسخه جدیدی از برنامه Boards موجود است، برای بارگیری مجدد اینجا را کلیک کنید.",
"BoardPage.syncFailed": "تابلو ممکن است حذف شود یا دسترسی آن لغو شود.",
"BoardTemplateSelector.add-template": "قالب جدید",
"BoardTemplateSelector.add-template": "ایجاد قالب جدید",
"BoardTemplateSelector.create-empty-board": "ایجاد تابلو خالی",
"BoardTemplateSelector.delete-template": "حذف",
"BoardTemplateSelector.description": "یک الگو را انتخاب کنید تا به شما در شروع کار کمک کند. به راحتی قالب را مطابق با نیازهای خود سفارشی کنید، یا یک تابلوی خالی ایجاد کنید تا از ابتدا شروع کنید.",
"BoardTemplateSelector.description": "با استفاده از الگوهای زیر، یک تابلو به نوار کناری اضافه کنید یا از اول شروع کنید.",
"BoardTemplateSelector.edit-template": "ویرایش",
"BoardTemplateSelector.plugin.no-content-description": "با استفاده از هر یک از الگوهای تعریف شده در زیر، تابلویی را به نوار کناری اضافه کنید یا از ابتدا شروع کنید.{lineBreak} اعضای \"{teamName}\" به تابلوهای ایجاد شده در اینجا دسترسی خواهند داشت.",
"BoardTemplateSelector.plugin.no-content-title": "ایجاد یک تابلو در {teamName}",
"BoardTemplateSelector.title": "یک تابلو ایجاد کنید",
"BoardTemplateSelector.plugin.no-content-description": "با استفاده از الگوهای زیر، یک تابلو به نوار کناری اضافه کنید یا از اول شروع کنید.",
"BoardTemplateSelector.plugin.no-content-title": "ایجاد یک تابلو",
"BoardTemplateSelector.title": "ایجاد یک تابلو",
"BoardTemplateSelector.use-this-template": "از این قالب استفاده کنید",
"BoardsSwitcher.Title": "تخته ها را پیدا کنید",
"BoardsSwitcher.Title": "جستجوی تابلوها",
"BoardsUnfurl.Remainder": "+{remainder} بیشتر",
"BoardsUnfurl.Updated": "به روز شد {time}",
"Calculations.Options.average.displayName": "میانگین",
"Calculations.Options.average.label": "میانگین",
"Calculations.Options.count.displayName": "شمردن",
"Calculations.Options.count.label": "شمردن",
"Calculations.Options.countChecked.displayName": "بررسی شد",
"Calculations.Options.countChecked.label": "تعداد بررسی شد",
"Calculations.Options.countUnchecked.displayName": دون علامت",
"Calculations.Options.countUnchecked.label": "شمارش بدون علامت",
"Calculations.Options.countUniqueValue.displayName": "منحصر بفرد",
"Calculations.Options.countUniqueValue.label": "شمارش ارزش های منحصر به فرد",
"Calculations.Options.countValue.displayName": "ارزش های",
"Calculations.Options.countValue.label": "شمارش ارزش",
"Calculations.Options.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": "اولین",
@ -52,9 +63,9 @@
"Calculations.Options.none.displayName": "محاسبه",
"Calculations.Options.none.label": "هیچ یک",
"Calculations.Options.percentChecked.displayName": "بررسی شد",
"Calculations.Options.percentChecked.label": "درصد بررسی شد",
"Calculations.Options.percentChecked.label": "درصد انتخاب‌شده‌ها",
"Calculations.Options.percentUnchecked.displayName": "بدون علامت",
"Calculations.Options.percentUnchecked.label": "درصد بدون علامت",
"Calculations.Options.percentUnchecked.label": "درصد بی‌نشان",
"Calculations.Options.range.displayName": "بازه",
"Calculations.Options.range.label": "بازه",
"Calculations.Options.sum.displayName": "جمع",
@ -70,17 +81,17 @@
"CardDetail.addCardText": "متن کارت را اضافه کنید",
"CardDetail.moveContent": "انتقال محتوای کارت",
"CardDetail.new-comment-placeholder": "افزودن نظر...",
"CardDetailProperty.confirm-delete-heading": "حذف ویژگی را تایید کنید",
"CardDetailProperty.confirm-delete-heading": "تایید حذف ویژگی",
"CardDetailProperty.confirm-delete-subtext": "آیا مطمئن هستید که می خواهید ویژگی \"{propertyName}\" را حذف کنید؟ با حذف آن، اموال از تمام کارت های موجود در این تابلو حذف می شود.",
"CardDetailProperty.confirm-property-name-change-subtext": "آیا مطمئن هستید که می خواهید ویژگی \"{propertyName}\" {customText} را تغییر دهید؟ این روی مقدار(های) کارت(های) {numOfCards} در این برد تأثیر می گذارد و می تواند منجر به از دست رفتن داده شود.",
"CardDetailProperty.confirm-property-type-change": غییر نوع ویژگی را تأیید کنید!",
"CardDetailProperty.confirm-property-type-change": ایید تغییر نوع ویژگی",
"CardDetailProperty.delete-action-button": "حذف",
"CardDetailProperty.property-change-action-button": "تغییر ویژگی",
"CardDetailProperty.property-changed": "تغییر ویژگی با موفقیت!",
"CardDetailProperty.property-deleted": "{propertyName} با موفقیت حذف شد!",
"CardDetailProperty.property-name-change-subtext": "از \"{oldPropType}\" به \"{newPropType}\" تایپ کنید",
"CardDialog.delete-confirmation-dialog-button-text": "حذف",
"CardDialog.delete-confirmation-dialog-heading": "حذف کارت را تایید کنید!",
"CardDialog.delete-confirmation-dialog-heading": "تایید حذف کارت",
"CardDialog.editing-template": "شما در حال ویرایش یک الگو هستید.",
"CardDialog.nocard": "این کارت وجود ندارد یا غیرقابل دسترسی است.",
"Categories.CreateCategoryDialog.CancelText": "لغو کنید",
@ -95,5 +106,19 @@
"ConfirmationDialog.cancel-action": "لغو کنید",
"ConfirmationDialog.confirm-action": "تایید",
"ContentBlock.Delete": "حذف",
"ContentBlock.DeleteAction": "حذف"
"ContentBlock.DeleteAction": "حذف",
"GroupBy.hideEmptyGroups": "پنهان‌کردن {count} گروه خالی",
"GroupBy.showHiddenGroups": "نمایش {count} گروه پنهان‌شده",
"ViewHeader.view-menu": "نمایش فهرست",
"ViewLimitDialog.Heading": "محدودیت تعداد نما برای تخته رسید",
"ViewLimitDialog.PrimaryButton.Title.Admin": "ارتقا",
"ViewLimitDialog.UpgradeImg.AltText": "ارتقا عکس",
"ViewLimitDialog.notifyAdmin.Success": "مدیر شما مطلع شد",
"ViewTitle.hide-description": "پنهان کردن توضیحات",
"ViewTitle.pick-icon": "انتخاب تصویرک",
"ViewTitle.random-icon": "تصادفی",
"ViewTitle.remove-icon": "پاک‌کردن تصویرک",
"ViewTitle.show-description": "نمایش توضیحات",
"ViewTitle.untitled-board": "تخته بدون عنوان",
"badge.guest": "مهمان"
}

View File

@ -263,7 +263,6 @@
"TableHeaderMenu.insert-right": "Insérer à droite",
"TableHeaderMenu.sort-ascending": "Tri ascendant",
"TableHeaderMenu.sort-descending": "Tri descendant",
"TableRow.delete": "Supprimer",
"TableRow.open": "Ouvrir",
"TopBar.give-feedback": "Donner un avis",
"URLProperty.copiedLink": "Copié !",

View File

@ -3,10 +3,10 @@
"Attachment.Attachment-title": "Prilog",
"AttachmentBlock.DeleteAction": "izbriši",
"AttachmentBlock.addElement": "dodaj {type}",
"AttachmentBlock.delete": "Prilog je uspješno izbrisan.",
"AttachmentBlock.failed": "Nije moguće prenijeti datoteku. Dosegnuta je granica veličine datoteke.",
"AttachmentBlock.delete": "Prilog je izbrisan.",
"AttachmentBlock.failed": "Nije moguće prenijeti datoteku jer je dosegnuta granica veličine datoteke.",
"AttachmentBlock.upload": "Prijenos priloga.",
"AttachmentBlock.uploadSuccess": "Prilog je uspješno prenesen.",
"AttachmentBlock.uploadSuccess": "Prilog je prenesen.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Izbriši",
"AttachmentElement.download": "Preuzmi",
"AttachmentElement.upload-percentage": "Prijenos … ({uploadPercent} %)",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Dodaj ikonu",
"CardDetail.add-property": "+ Dodaj svojstvo",
"CardDetail.addCardText": "dodaj tekst kartice",
"CardDetail.limited-body": "Nadogradi na našu profesionalnu tarifu ili na tarifu za prikaz arhiviranih kartica. Dobij neograničeni broj prikaza po ploči, neograničen broj kartica i još mnogo toga.",
"CardDetail.limited-body": "Nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.",
"CardDetail.limited-button": "Nadogradi",
"CardDetail.limited-title": "Ova je kartica skrivena",
"CardDetail.moveContent": "Pomakni sadržaj kartice",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "Svojstvo {propertyName} uspješno izbrisano!",
"CardDetailProperty.property-name-change-subtext": "vrste „{oldPropType}” u „{newPropType}”",
"CardDetial.limited-link": "Saznaj više o našim tarifma.",
"CardDialog.delete-confirmation-dialog-attachment": "Potvrdi brisanje priloga!",
"CardDialog.delete-confirmation-dialog-attachment": "Potvrdi brisanje priloga",
"CardDialog.delete-confirmation-dialog-button-text": "Izbriši",
"CardDialog.delete-confirmation-dialog-heading": "Potvrdi brisanje kartice!",
"CardDialog.delete-confirmation-dialog-heading": "Potvrdi brisanje kartice",
"CardDialog.editing-template": "Uređuješ predložak.",
"CardDialog.nocard": "Ova kartica ne postoji ili je nedostupna.",
"Categories.CreateCategoryDialog.CancelText": "Odustani",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Aktualiziraj",
"CenterPanel.Login": "Prijava",
"CenterPanel.Share": "Dijeli",
"ChannelIntro.CreateBoard": "Stvori ploču",
"CloudMessage.cloud-server": "Nabavi vlastiti besplatni poslužitelj u oblaku.",
"ColorOption.selectColor": "Odaberi boju {color}",
"Comment.delete": "Izbriši",
"CommentsList.send": "Pošalji",
"ConfirmPerson.empty": "Prazno",
"ConfirmPerson.search": "Traži …",
"ConfirmationDialog.cancel-action": "Odustani",
"ConfirmationDialog.confirm-action": "Potvrdi",
"ContentBlock.Delete": "Izbriši",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "filtriraj tekst",
"FilterComponent.add-filter": "+ Dodaj filtar",
"FilterComponent.delete": "Izbriši",
"FilterValue.empty": "(prazno)",
"FindBoardsDialog.IntroText": "Traži ploče",
"FindBoardsDialog.NoResultsFor": "Nema rezultata za „{searchQuery}”",
"FindBoardsDialog.NoResultsSubtext": "Provjeri pravopis ili pretraži s jednim drugim pojmom.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Dodaj komentare",
"OnboardingTour.AddDescription.Body": "Dodaj opis za tvoju karticu kako bi tvoji članovi tima znali o čemu se radi.",
"OnboardingTour.AddDescription.Title": "Dodaj opis",
"OnboardingTour.AddProperties.Body": "Dodaj razna svojstva karticama kako bi bile snažnije!",
"OnboardingTour.AddProperties.Body": "Dodaj razna svojstva karticama kako bi bile još snažnije.",
"OnboardingTour.AddProperties.Title": "Dodaj svojstva",
"OnboardingTour.AddView.Body": "Prijeđi ovamo za stvaranje novog prikaza za organiziranje tvoje ploče koristeći različite rasporede.",
"OnboardingTour.AddView.Title": "Dodaj novi prikaz",
@ -194,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Tvoju ploču možeš dijeliti interno, unutar tvog tima ili je javno objaviti radi vidljivosti izvan tvoje organizacije.",
"OnboardingTour.ShareBoard.Title": "Dijeli ploču",
"PersonProperty.board-members": "Članovi ploče",
"PersonProperty.me": "Ja",
"PersonProperty.non-board-members": "Ne članovi ploče",
"PropertyMenu.Delete": "Izbriši",
"PropertyMenu.changeType": "Promijei vrstu svojstva",
@ -284,8 +289,8 @@
"TableHeaderMenu.insert-right": "Umetni desno",
"TableHeaderMenu.sort-ascending": "Razvrstaj uzlazno",
"TableHeaderMenu.sort-descending": "Razvrstaj silazno",
"TableRow.DuplicateCard": "dupliciraj karticu",
"TableRow.MoreOption": "Daljnje radnje",
"TableRow.delete": "Izbriši",
"TableRow.open": "Otvori",
"TopBar.give-feedback": "Pošalji povratne informacije",
"URLProperty.copiedLink": "Kopirano!",
@ -337,9 +342,9 @@
"ViewLimitDialog.Heading": "Ograničenje prikaza po ploči dosegnuta",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Nadogradi",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Obavijesti aministratora",
"ViewLimitDialog.Subtext.Admin": "Nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća za neograničen broj prikaza po pločama, neograničen broj kartica i još više.",
"ViewLimitDialog.Subtext.Admin": "Nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Saznaj više o našim tarifama.",
"ViewLimitDialog.Subtext.RegularUser": "Obavijesti svog administratora da nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća za neograničen broj prikaza po pločama, neograničen broj kartica i još više.",
"ViewLimitDialog.Subtext.RegularUser": "Obavijesti svog administratora da nadogradi na našu profesionalnu tarifu ili na tarifu za poduzeća.",
"ViewLimitDialog.UpgradeImg.AltText": "nadogradi sliku",
"ViewLimitDialog.notifyAdmin.Success": "Tvoj je administrator obaviješten",
"ViewTitle.hide-description": "sakrij opis",
@ -367,8 +372,10 @@
"calendar.month": "Mjesec",
"calendar.today": "DANAS",
"calendar.week": "Tjedan",
"centerPanel.undefined": "Bez {propertyName}",
"centerPanel.unknown-user": "Nepoznat korisnik",
"cloudMessage.learn-more": "Saznaj više",
"createImageBlock.failed": "Nije moguće prenijeti datoteku. Dosegnuto je ograničenje veličine datoteke.",
"createImageBlock.failed": "Nije moguće prenijeti ovu datoteku jer je dosegnuta granica veličine datoteke.",
"default-properties.badges": "Komentari i opis",
"default-properties.title": "Naslov",
"error.back-to-home": "Natrag na početnu stranicu",
@ -383,11 +390,15 @@
"generic.previous": "Prethodno",
"guest-no-board.subtitle": "Još nemaš pristup nijednoj ploči u ovom timu, pričekaj dok te netko ne doda u bilo koju ploču.",
"guest-no-board.title": "Još nema ploča",
"imagePaste.upload-failed": "Neke datoteke nisu prenesene. Dosegnuto je ograničenje veličine datoteke",
"imagePaste.upload-failed": "Neke datoteke nisu prenesene jer je dosegnuta granica veličine datoteke.",
"limitedCard.title": "Skrivene kartice",
"login.log-in-button": "Prijavi se",
"login.log-in-title": "Prijavi se",
"login.register-button": "ili stvori račun, ako ga još nemaš",
"new_channel_modal.create_board.empty_board_description": "Stvori novu praznu ploču",
"new_channel_modal.create_board.empty_board_title": "Prazna ploča",
"new_channel_modal.create_board.select_template_placeholder": "Odaberi predložak",
"new_channel_modal.create_board.title": "Stvori ploču za ovaj kanal",
"notification-box-card-limit-reached.close-tooltip": "Postavi pripravno stanje na 10 dana",
"notification-box-card-limit-reached.contact-link": "obavijesti svog administratora",
"notification-box-card-limit-reached.link": "Nadogradi na plaćenu tarifu",

View File

@ -285,7 +285,6 @@
"TableHeaderMenu.sort-ascending": "Rendezés növekvő sorrendben",
"TableHeaderMenu.sort-descending": "Rendezés csökkenő sorrendben",
"TableRow.MoreOption": "További műveletek",
"TableRow.delete": "Törlés",
"TableRow.open": "Megnyitás",
"TopBar.give-feedback": "Visszajelzés",
"URLProperty.copiedLink": "Másolva!",

View File

@ -1,5 +1,15 @@
{
"AppBar.Tooltip": "リンク先ボードの表示切り替え",
"AppBar.Tooltip": "リンク先Boardの切替え",
"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": "非表示",
@ -14,18 +24,18 @@
"BoardMember.schemeNone": "なし",
"BoardMember.schemeViewer": "閲覧者",
"BoardMember.unlinkChannel": "リンク解除",
"BoardPage.newVersion": "ボードの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。",
"BoardPage.syncFailed": "ボードが削除されたか、アクセスが取り消されました。",
"BoardTemplateSelector.add-template": "新しいテンプレート",
"BoardTemplateSelector.create-empty-board": "空のボードを作成する",
"BoardPage.newVersion": "Boardsの新しいバージョンが利用可能です。ここをクリックして再読み込みしてください。",
"BoardPage.syncFailed": "Boardが削除されたか、アクセスが取り消されました。",
"BoardTemplateSelector.add-template": "テンプレート新規作成",
"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}",
@ -71,20 +81,21 @@
"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-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-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": "プロパティの変更",
@ -92,6 +103,7 @@
"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": "テンプレートを編集しています。",
@ -102,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "更新",
"CenterPanel.Login": "ログイン",
"CenterPanel.Share": "共有",
"ChannelIntro.CreateBoard": "Boardを作成する",
"CloudMessage.cloud-server": "専用の無料クラウドサーバーを入手する。",
"ColorOption.selectColor": "{color} 色を選択",
"Comment.delete": "削除",
"CommentsList.send": "送信",
"ConfirmPerson.empty": "空",
"ConfirmPerson.search": "検索中...",
"ConfirmationDialog.cancel-action": "キャンセル",
"ConfirmationDialog.confirm-action": "確認",
"ContentBlock.Delete": "削除",
@ -119,6 +134,7 @@
"ContentBlock.editText": "テキストを編集する...",
"ContentBlock.image": "画像",
"ContentBlock.insertAbove": "上に挿入する",
"ContentBlock.moveBlock": "カード内容の移動",
"ContentBlock.moveDown": "下へ移動する",
"ContentBlock.moveUp": "上へ移動する",
"ContentBlock.text": "テキスト",
@ -128,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": "モバイルウェブのサポートは現在、初期ベータ版です。一部の機能が利用できない場合があります。",
@ -152,35 +168,37 @@
"FilterByText.placeholder": "フィルター文字列",
"FilterComponent.add-filter": "+ フィルターを追加する",
"FilterComponent.delete": "削除",
"FindBoardsDialog.IntroText": "ボードを検索",
"FilterValue.empty": "(空)",
"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ユーザーの注意を引くために@メンションすることもできます。",
"OnboardingTour.AddComments.Title": "コメントを追加する",
"OnboardingTour.AddDescription.Body": "カードに説明を追加し、チームメイトに何のカードかわかるようにしましょう。",
"OnboardingTour.AddDescription.Title": "説明を追加する",
"OnboardingTour.AddProperties.Body": "カードに様々なプロパティを追加し、より便利にしましよう!",
"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",
"PropertyMenu.Delete": "削除",
"PropertyMenu.changeType": "プロパティのタイプを変更する",
@ -213,7 +231,7 @@
"ShareBoard.PublishTitle": "Web上へ公開する",
"ShareBoard.ShareInternal": "内部で共有する",
"ShareBoard.ShareInternalDescription": "権限のあるユーザーは、このリンクを使用することができます。",
"ShareBoard.Title": "ボードを共有する",
"ShareBoard.Title": "Boardを共有",
"ShareBoard.confirmRegenerateToken": "実行すると以前に共有されたリンクは無効になります。続行しますか?",
"ShareBoard.copiedLink": "コピーしました!",
"ShareBoard.copyLink": "リンクをコピー",
@ -226,38 +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.no-boards-in-category": "カテゴリ内にボードがありません",
"Sidebar.new-category.badge": "新規",
"Sidebar.new-category.drag-boards-cta": "ここにBoardをドラッグ...",
"Sidebar.no-boards-in-category": "カテゴリ内にBoardがありません",
"Sidebar.product-tour": "プロダクトツアー",
"Sidebar.random-icons": "ランダムアイコン",
"Sidebar.set-language": "言語設定",
"Sidebar.set-theme": "テーマ設定",
"Sidebar.settings": "設定",
"Sidebar.template-from-board": "ボードからの新しいテンプレート",
"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": "名前",
@ -269,7 +289,8 @@
"TableHeaderMenu.insert-right": "右に挿入",
"TableHeaderMenu.sort-ascending": "昇順でソート",
"TableHeaderMenu.sort-descending": "降順でソート",
"TableRow.delete": "削除",
"TableRow.DuplicateCard": "カードを複製する",
"TableRow.MoreOption": "その他のアクション",
"TableRow.open": "開く",
"TopBar.give-feedback": "フィードバックを送る",
"URLProperty.copiedLink": "コピーしました!",
@ -286,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": "テーブル表示",
@ -302,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": "エクスポートが失敗しました!",
@ -318,12 +339,12 @@
"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プランにアップグレードしてください。",
"ViewLimitDialog.Subtext.Admin": "ProfessionalプランまたはEnterpriseプランにアップグレードしてください。",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "各プランの詳細についてはこちらをご覧ください。",
"ViewLimitDialog.Subtext.RegularUser": "無制限のビュー数や無制限のカード数などの機能を利用するために、ProfessionalプランまたはEnterpriseプランへアップグレードするよう管理者に連絡してください。",
"ViewLimitDialog.Subtext.RegularUser": "ProfessionalプランまたはEnterpriseプランへアップグレードするよう管理者に連絡してください。",
"ViewLimitDialog.UpgradeImg.AltText": "アップグレードイメージ",
"ViewLimitDialog.notifyAdmin.Success": "管理者に通知されました",
"ViewTitle.hide-description": "説明を非表示",
@ -331,90 +352,96 @@
"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": "今日",
"calendar.week": "週",
"centerPanel.undefined": "{propertyName} 無し",
"centerPanel.unknown-user": "不明なユーザー",
"cloudMessage.learn-more": "さらに詳しく",
"createImageBlock.failed": "ファイルをアップロードできません。ファイルサイズの制限に達しています。",
"createImageBlock.failed": "ファイルサイズの上限に達しているため、ファイルをアップロードできませんでした。",
"default-properties.badges": "コメントと説明",
"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": "まだボードはありません",
"imagePaste.upload-failed": "一部のファイルをアップロードできませんでした。ファイルサイズの制限に達しています",
"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": "完了",

View File

@ -285,7 +285,6 @@
"TableHeaderMenu.sort-ascending": "오름차순 정렬",
"TableHeaderMenu.sort-descending": "내림차순 정렬",
"TableRow.MoreOption": "더 많은 행동",
"TableRow.delete": "삭제",
"TableRow.open": "열기",
"TopBar.give-feedback": "피드백 하기",
"URLProperty.copiedLink": "복사되었습니다!",

View File

@ -226,7 +226,6 @@
"TableHeaderMenu.insert-right": "വലത്തേക്ക് തിരുകുക",
"TableHeaderMenu.sort-ascending": "ആരോഹണക്രമത്തിൽ അടുക്കുക",
"TableHeaderMenu.sort-descending": "അവരോഹണക്രമം അടുക്കുക",
"TableRow.delete": "ഇല്ലാതാക്കുക",
"TableRow.open": "തുറക്കുക",
"TopBar.give-feedback": "അഭിപ്രായം അറിയിക്കുക",
"URLProperty.copiedLink": "പകർത്തി!",

View File

@ -3,10 +3,10 @@
"Attachment.Attachment-title": "Bijlage",
"AttachmentBlock.DeleteAction": "verwijderen",
"AttachmentBlock.addElement": "voeg {type} toe",
"AttachmentBlock.delete": "Bijlage succesvol verwijderd.",
"AttachmentBlock.failed": "Kan het bestand niet uploaden. Limietgrootte bijlage bereikt.",
"AttachmentBlock.delete": "Bijlage verwijderd.",
"AttachmentBlock.failed": "Dit bestand kon niet worden geüpload omdat de bestandslimiet wordt overschreden.",
"AttachmentBlock.upload": "Bijlage aan het uploaden.",
"AttachmentBlock.uploadSuccess": "Bijlage succesvol geüpload.",
"AttachmentBlock.uploadSuccess": "Bijlage geüpload.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Verwijderen",
"AttachmentElement.download": "Downloaden",
"AttachmentElement.upload-percentage": "Uploaden...({uploadPercent}%)",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Pictogram toevoegen",
"CardDetail.add-property": "+ Een eigenschap toevoegen",
"CardDetail.addCardText": "kaarttekst toevoegen",
"CardDetail.limited-body": "Upgrade naar ons Professional- of Enterprise-plan om gearchiveerde kaarten te bekijken, ongelimiteerde weergaven per bord, ongelimiteerde kaarten en meer.",
"CardDetail.limited-body": "Upgrade naar ons Professional- of Enterprise-plan.",
"CardDetail.limited-button": "Upgraden",
"CardDetail.limited-title": "Deze kaart is verborgen",
"CardDetail.moveContent": "Inhoud van de kaart verplaatsen",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "{propertyName} werd succesvol verwijderd!",
"CardDetailProperty.property-name-change-subtext": "type van \"{oldPropType}\" naar \"{newPropType}\"",
"CardDetial.limited-link": "Meer informatie over onze plannen.",
"CardDialog.delete-confirmation-dialog-attachment": "Bevestig het verwijderen van de bijlage!",
"CardDialog.delete-confirmation-dialog-attachment": "Bevestig het verwijderen van de bijlage",
"CardDialog.delete-confirmation-dialog-button-text": "Verwijderen",
"CardDialog.delete-confirmation-dialog-heading": "Bevestig kaart verwijderen!",
"CardDialog.delete-confirmation-dialog-heading": "Bevestig verwijderen kaart",
"CardDialog.editing-template": "Je bent een sjabloon aan het bewerken.",
"CardDialog.nocard": "Deze kaart bestaat niet of is ontoegankelijk.",
"Categories.CreateCategoryDialog.CancelText": "Annuleren",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Bijwerken",
"CenterPanel.Login": "Aanmelden",
"CenterPanel.Share": "Delen",
"ChannelIntro.CreateBoard": "Een bord aanmaken",
"CloudMessage.cloud-server": "Krijg jouw eigen gratis cloud server.",
"ColorOption.selectColor": "Selecteer {color} Kleur",
"Comment.delete": "Verwijderen",
"CommentsList.send": "Verzenden",
"ConfirmPerson.empty": "Leeg",
"ConfirmPerson.search": "Zoeken...",
"ConfirmationDialog.cancel-action": "Annuleren",
"ConfirmationDialog.confirm-action": "Bevestigen",
"ContentBlock.Delete": "Verwijderen",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "filtertekst",
"FilterComponent.add-filter": "+ Filter toevoegen",
"FilterComponent.delete": "Verwijderen",
"FilterValue.empty": "(leeg)",
"FindBoardsDialog.IntroText": "Zoeken naar borden",
"FindBoardsDialog.NoResultsFor": "Geen resultaten voor \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Controleer de spelling of probeer een andere zoekopdracht.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Opmerkingen toevoegen",
"OnboardingTour.AddDescription.Body": "Voeg een beschrijving toe aan je kaart, zodat je teamgenoten weten waar de kaart over gaat.",
"OnboardingTour.AddDescription.Title": "Beschrijving toevoegen",
"OnboardingTour.AddProperties.Body": "Voeg verschillende eigenschappen toe aan kaarten om ze effectiever te maken!",
"OnboardingTour.AddProperties.Body": "Voeg verschillende eigenschappen toe aan kaarten om ze effectiever te maken.",
"OnboardingTour.AddProperties.Title": "Eigenschappen toevoegen",
"OnboardingTour.AddView.Body": "Kom hier naartoe om een nieuwe weergave te maken om uw bord te organiseren met verschillende lay-outs.",
"OnboardingTour.AddView.Title": "Een nieuwe weergave toevoegen",
@ -194,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Je kan jouw bord intern delen, binnen jouw team, of het publiek publiceren voor zichtbaarheid buiten jouw organisatie.",
"OnboardingTour.ShareBoard.Title": "Bord delen",
"PersonProperty.board-members": "Deelnemers aan het bord",
"PersonProperty.me": "Ik",
"PersonProperty.non-board-members": "Niet-deelnemers aan het bord",
"PropertyMenu.Delete": "Verwijderen",
"PropertyMenu.changeType": "Type eigenschap wijzigen",
@ -284,8 +289,8 @@
"TableHeaderMenu.insert-right": "Rechts invoegen",
"TableHeaderMenu.sort-ascending": "Sorteer oplopend",
"TableHeaderMenu.sort-descending": "Aflopend sorteren",
"TableRow.DuplicateCard": "dupliceren kaart",
"TableRow.MoreOption": "Meer acties",
"TableRow.delete": "Verwijderen",
"TableRow.open": "Openen",
"TopBar.give-feedback": "Geef feedback",
"URLProperty.copiedLink": "Gekopieerd!",
@ -337,9 +342,9 @@
"ViewLimitDialog.Heading": "Limiet aantal views per board bereikt",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Upgraden",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Verwittig Admin",
"ViewLimitDialog.Subtext.Admin": "Upgrade naar ons Professional- of Enterprise-plan om gearchiveerde kaarten te bekijken, ongelimiteerde weergaven per bord, ongelimiteerde kaarten en meer.",
"ViewLimitDialog.Subtext.Admin": "Upgrade naar ons Professional- of Enterprise-plan.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Meer informatie over onze plannen.",
"ViewLimitDialog.Subtext.RegularUser": "Verwittig jouw Admin om te upgraden naar ons Professioneel of Enterprise plan om onbeperkte weergaven per bord, onbeperkte kaarten, en meer te hebben.",
"ViewLimitDialog.Subtext.RegularUser": "Verwittig jouw Admin om te upgraden naar ons Professioneel of Enterprise plan.",
"ViewLimitDialog.UpgradeImg.AltText": "upgrade afbeelding",
"ViewLimitDialog.notifyAdmin.Success": "Jouw beheerder is op de hoogte gebracht",
"ViewTitle.hide-description": "beschrijving verbergen",
@ -367,8 +372,10 @@
"calendar.month": "Maand",
"calendar.today": "VANDAAG",
"calendar.week": "Week",
"centerPanel.undefined": "Geen {propertyName}",
"centerPanel.unknown-user": "Onbekende gebruiker",
"cloudMessage.learn-more": "Meer info",
"createImageBlock.failed": "Kan het bestand niet uploaden. Limiet bestandsgrootte bereikt.",
"createImageBlock.failed": "Dit bestand kon niet worden geüpload omdat de bestandslimiet wordt overschreden.",
"default-properties.badges": "Opmerkingen en beschrijving",
"default-properties.title": "Titel",
"error.back-to-home": "Terug naar startpagina",
@ -383,11 +390,15 @@
"generic.previous": "Vorige",
"guest-no-board.subtitle": "Je hebt nog geen toegang tot een board in dit team, wacht tot iemand je toevoegt aan een board.",
"guest-no-board.title": "Nog geen borden",
"imagePaste.upload-failed": "Sommige bestanden niet geupload. Limiet bestandsgrootte bereikt",
"imagePaste.upload-failed": "Sommige bestanden zijn niet geüpload omdat de limiet voor de bestandsgrootte is bereikt.",
"limitedCard.title": "Verborgen kaarten",
"login.log-in-button": "Aanmelden",
"login.log-in-title": "Aanmelden",
"login.register-button": "of maak een account aan als je er nog geen hebt",
"new_channel_modal.create_board.empty_board_description": "Maak een nieuw leeg bord",
"new_channel_modal.create_board.empty_board_title": "Leeg bord",
"new_channel_modal.create_board.select_template_placeholder": "Kies een sjabloon",
"new_channel_modal.create_board.title": "Maak een bord voor dit kanaal",
"notification-box-card-limit-reached.close-tooltip": "Snooze voor 10 dagen",
"notification-box-card-limit-reached.contact-link": "breng je beheerder op de hoogte",
"notification-box-card-limit-reached.link": "Upgrade naar een betaald plan",

View File

@ -1,12 +1,12 @@
{
"AppBar.Tooltip": "Przełączanie Podlinkowanych Tablic",
"AppBar.Tooltip": "Przełączanie podlinkowanych tablic",
"Attachment.Attachment-title": "Załącznik",
"AttachmentBlock.DeleteAction": "usuń",
"AttachmentBlock.addElement": "dodaj {type}",
"AttachmentBlock.delete": "Załącznik pomyślnie usunięty.",
"AttachmentBlock.failed": "Nie można przesłać pliku. Osiągnięto limit wielkości załącznika.",
"AttachmentBlock.delete": "Załącznik usunięty.",
"AttachmentBlock.failed": "Ten plik nie mógł zostać przesłany, ponieważ został osiągnięty limit rozmiaru pliku.",
"AttachmentBlock.upload": "Przesyłanie załączników.",
"AttachmentBlock.uploadSuccess": "Załącznik pomyślnie przesłany.",
"AttachmentBlock.uploadSuccess": "Załącznik przesłany.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Usuń",
"AttachmentElement.download": "Pobierz",
"AttachmentElement.upload-percentage": "Przesyłanie...({uploadPercent}%)",
@ -16,7 +16,7 @@
"BoardComponent.hide": "Ukryj",
"BoardComponent.new": "+ Nowy",
"BoardComponent.no-property": "Brak {property}",
"BoardComponent.no-property-title": "Tutaj trafią elementy z pustą właściwością {property}. Tej kolumny nie można usunąć.",
"BoardComponent.no-property-title": "Elementy z pustą właściwością {property} trafią tutaj. Tej kolumny nie można usunąć.",
"BoardComponent.show": "Pokaż",
"BoardMember.schemeAdmin": "Administrator",
"BoardMember.schemeCommenter": "Komentujący",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Dodaj ikonę",
"CardDetail.add-property": "+ Dodaj właściwość",
"CardDetail.addCardText": "dodaj tekst karty",
"CardDetail.limited-body": "Uaktualnij plan do wersji Professional lub Enterprise, aby przeglądać zarchiwizowane karty, mieć nieograniczoną liczbę odsłon na tablice, kart i wiele więcej.",
"CardDetail.limited-body": "Uaktualnij do naszego planu Professional lub Enterprise.",
"CardDetail.limited-button": "Zmień plan",
"CardDetail.limited-title": "Ta karta jest ukryta",
"CardDetail.moveContent": "Przenieś zawartość karty",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "Usunięto pomyślnie {propertyName}!",
"CardDetailProperty.property-name-change-subtext": "typ z \"{oldPropType}\" do \"{newPropType}\"",
"CardDetial.limited-link": "Dowiedz się więcej o naszych planach.",
"CardDialog.delete-confirmation-dialog-attachment": "Potwierdź usunięcie załącznika!",
"CardDialog.delete-confirmation-dialog-attachment": "Potwierdź usunięcie załącznika",
"CardDialog.delete-confirmation-dialog-button-text": "Usuń",
"CardDialog.delete-confirmation-dialog-heading": "Potwierdzenie usunięcia karty!",
"CardDialog.delete-confirmation-dialog-heading": "Potwierdź usunięcie karty",
"CardDialog.editing-template": "Edytujesz szablon.",
"CardDialog.nocard": "Ta karta nie istnieje lub jest niedostępna.",
"Categories.CreateCategoryDialog.CancelText": "Anuluj",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Zmień",
"CenterPanel.Login": "Logowanie",
"CenterPanel.Share": "Udostępnij",
"ChannelIntro.CreateBoard": "Utwórz tablicę",
"CloudMessage.cloud-server": "Uzyskaj własny, bezpłatny serwer w chmurze.",
"ColorOption.selectColor": "Wybierz Kolor {color}",
"Comment.delete": "Usuń",
"CommentsList.send": "Wyślij",
"ConfirmPerson.empty": "Puste",
"ConfirmPerson.search": "Szukaj...",
"ConfirmationDialog.cancel-action": "Anuluj",
"ConfirmationDialog.confirm-action": "Potwierdź",
"ContentBlock.Delete": "Usuń",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "tekst filtra",
"FilterComponent.add-filter": "+ Dodaj filtr",
"FilterComponent.delete": "Usuń",
"FilterValue.empty": "(pusty)",
"FindBoardsDialog.IntroText": "Wyszukiwanie tablic",
"FindBoardsDialog.NoResultsFor": "Brak wyników dla „{searchQuery}”",
"FindBoardsDialog.NoResultsSubtext": "Sprawdź pisownię lub spróbuj innego wyszukiwania.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Dodawanie komentarzy",
"OnboardingTour.AddDescription.Body": "Dodaj opis do karty, aby członkowie zespołu wiedzieli, czego ona dotyczy .",
"OnboardingTour.AddDescription.Title": "Dodaj opis",
"OnboardingTour.AddProperties.Body": "Dodawaj różne właściwości do kart, aby zwiększyć ich moc!",
"OnboardingTour.AddProperties.Body": "Dodawaj różne właściwości do kart, aby zwiększyć ich moc.",
"OnboardingTour.AddProperties.Title": "Dodawanie właściwości",
"OnboardingTour.AddView.Body": "Tutaj utworzysz nowy widok, którym uporządkujesz tablicę za pomocą różnych układów.",
"OnboardingTour.AddView.Title": "Dodawanie nowego widoku",
@ -194,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Możesz udostępniać swoją tablicę wewnętrznie, w ramach zespołu albo opublikować ją, aby była widoczna poza organizacją.",
"OnboardingTour.ShareBoard.Title": "Udostępnianie tablicy",
"PersonProperty.board-members": "Członkowie tablicy",
"PersonProperty.me": "Ja",
"PersonProperty.non-board-members": "Nie-członkowie tablicy",
"PropertyMenu.Delete": "Usuń",
"PropertyMenu.changeType": "Zmień typ właściwości",
@ -232,11 +237,11 @@
"ShareBoard.copyLink": "Kopiuj odnośnik",
"ShareBoard.regenerate": "Wygeneruj ponownie token",
"ShareBoard.searchPlaceholder": "Wyszukiwanie osób",
"ShareBoard.teamPermissionsText": "Wszyscy w Zespole {teamName}",
"ShareBoard.teamPermissionsText": "Wszyscy w zespole {teamName}",
"ShareBoard.tokenRegenrated": "Token wygenerowany",
"ShareBoard.userPermissionsRemoveMemberText": "Usuń użytkownika",
"ShareBoard.userPermissionsYouText": "(Ty)",
"ShareTemplate.Title": "Udostępnij Szablon",
"ShareTemplate.Title": "Udostępnij szablon",
"ShareTemplate.searchPlaceholder": "Wyszukiwanie osób",
"Sidebar.about": "O Focalboard",
"Sidebar.add-board": "+ Dodaj tablicę",
@ -284,8 +289,8 @@
"TableHeaderMenu.insert-right": "Wstaw z prawej",
"TableHeaderMenu.sort-ascending": "Sortuj rosnąco",
"TableHeaderMenu.sort-descending": "Sortuj malejąco",
"TableRow.DuplicateCard": "duplikuj kartę",
"TableRow.MoreOption": "Więcej działań",
"TableRow.delete": "Usuń",
"TableRow.open": "Otwórz",
"TopBar.give-feedback": "Przekaż informację zwrotną",
"URLProperty.copiedLink": "Skopiowane!",
@ -337,9 +342,9 @@
"ViewLimitDialog.Heading": "Osiągnięty limit odsłon na tablicę",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Aktualizuj",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Powiadom Administratora",
"ViewLimitDialog.Subtext.Admin": "Uaktualnij do naszego planu Professional lub Enterprise, aby mieć nieograniczone widoki na tablice, nieograniczone karty i więcej.",
"ViewLimitDialog.Subtext.Admin": "Uaktualnij do naszego planu Professional lub Enterprise.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Dowiedz się więcej o naszych planach.",
"ViewLimitDialog.Subtext.RegularUser": "Powiadom swojego Administratora, aby uaktualnić do naszego planu Professional lub Enterprise, aby mieć nieograniczone widoki na tablicach, nieograniczone karty i więcej.",
"ViewLimitDialog.Subtext.RegularUser": "Powiadom swojego Administratora, aby uaktualnić do naszego planu Professional lub Enterprise.",
"ViewLimitDialog.UpgradeImg.AltText": "aktualizuj obraz",
"ViewLimitDialog.notifyAdmin.Success": "Twój administrator został powiadomiony",
"ViewTitle.hide-description": "ukryj opis",
@ -367,8 +372,10 @@
"calendar.month": "Miesiąc",
"calendar.today": "DZIŚ",
"calendar.week": "Tydzień",
"centerPanel.undefined": "Brak {propertyName}",
"centerPanel.unknown-user": "Nieznany użytkownik",
"cloudMessage.learn-more": "Dowiedź się więcej",
"createImageBlock.failed": "Nie można przesłać pliku. Osiągnięto limit rozmiaru pliku.",
"createImageBlock.failed": "Ten plik nie mógł zostać przesłany, ponieważ został osiągnięty limit rozmiaru pliku.",
"default-properties.badges": "Uwagi i opis",
"default-properties.title": "Tytuł",
"error.back-to-home": "Powrót na stronę główną",
@ -383,11 +390,15 @@
"generic.previous": "Wstecz",
"guest-no-board.subtitle": "Nie masz jeszcze dostępu do żadnej tablicy w tym zespole, poczekaj aż ktoś doda Cię do jakiejkolwiek tablicy.",
"guest-no-board.title": "Nie ma jeszcze tablic",
"imagePaste.upload-failed": "Niektóre pliki nie zostały przesłane. Osiągnięto limit rozmiaru pliku",
"imagePaste.upload-failed": "Niektóre pliki nie zostały przesłane, ponieważ został osiągnięty limit rozmiaru pliku.",
"limitedCard.title": "Ukryte karty",
"login.log-in-button": "Zaloguj się",
"login.log-in-title": "Zaloguj się",
"login.register-button": "lub załóż konto, jeśli jeszcze go nie masz",
"new_channel_modal.create_board.empty_board_description": "Utwórz nową pustą tablicę",
"new_channel_modal.create_board.empty_board_title": "Wyczyść tablicę",
"new_channel_modal.create_board.select_template_placeholder": "Wybierz szablon",
"new_channel_modal.create_board.title": "Utwórz tablicę dla tego kanału",
"notification-box-card-limit-reached.close-tooltip": "Uśpij na 10 dni",
"notification-box-card-limit-reached.contact-link": "powiadom swojego administratora",
"notification-box-card-limit-reached.link": "Uaktualnienie do planu płatnego",
@ -406,8 +417,8 @@
"rhs-boards.add": "Dodaj",
"rhs-boards.dm": "DM",
"rhs-boards.gm": "GM",
"rhs-boards.header.dm": "ta Bezpośrednia Wiadomość",
"rhs-boards.header.gm": "ta Wiadomość Grupowa",
"rhs-boards.header.dm": "ta bezpośrednia wiadomość",
"rhs-boards.header.gm": "ta wiadomość grupowa",
"rhs-boards.last-update-at": "Ostatnia aktualizacja o: {datetime}",
"rhs-boards.link-boards-to-channel": "Połączenie tablic z {channelName}",
"rhs-boards.linked-boards": "Połączone tablice",

View File

@ -267,7 +267,6 @@
"TableHeaderMenu.insert-right": "Inserir à direita",
"TableHeaderMenu.sort-ascending": "Ordem ascendente",
"TableHeaderMenu.sort-descending": "Ordem descendente",
"TableRow.delete": "Excluir",
"TableRow.open": "Abrir",
"TopBar.give-feedback": "Dar feedback",
"URLProperty.copiedLink": "Copiado!",

View File

@ -285,7 +285,6 @@
"TableHeaderMenu.sort-ascending": "Сортировать по возрастанию",
"TableHeaderMenu.sort-descending": "Сортировать по убыванию",
"TableRow.MoreOption": "Больше действий",
"TableRow.delete": "Удалить",
"TableRow.open": "Открыть",
"TopBar.give-feedback": "Дать обратную связь",
"URLProperty.copiedLink": "Скопировано!",

View File

@ -1,10 +1,10 @@
{
"AppBar.Tooltip": "Växla länkade Boards",
"AppBar.Tooltip": "Växla länkade boards",
"Attachment.Attachment-title": "Bilaga",
"AttachmentBlock.DeleteAction": "radera",
"AttachmentBlock.addElement": "lägg till {type}",
"AttachmentBlock.delete": "Bilagan har tagits bort.",
"AttachmentBlock.failed": "Det går inte att ladda upp filen. Gränsen för bilagans filstorlek har nåtts.",
"AttachmentBlock.failed": "Flen kunde inte laddas upp eftersom gränsen för filstorlek har nåtts.",
"AttachmentBlock.upload": "Bilagor laddas upp.",
"AttachmentBlock.uploadSuccess": "Bilagan är uppladdad.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Radera",
@ -35,7 +35,7 @@
"BoardTemplateSelector.plugin.no-content-title": "Skapa en Board",
"BoardTemplateSelector.title": "Skapa en board",
"BoardTemplateSelector.use-this-template": "Använd den här mallen",
"BoardsSwitcher.Title": "Hitta Board",
"BoardsSwitcher.Title": "Hitta board",
"BoardsUnfurl.Limited": "Ytterligare uppgifter är dolda eftersom kortet är arkiverat",
"BoardsUnfurl.Remainder": "+{remainder} mer",
"BoardsUnfurl.Updated": "Uppdaterad {time}",
@ -88,7 +88,7 @@
"CardDetail.add-icon": "Lägg till ikon",
"CardDetail.add-property": "+ Lägg till egenskap",
"CardDetail.addCardText": "lägg till korttext",
"CardDetail.limited-body": "Uppgradera till Professional- eller Enterprise-abonnemang för att visa arkiverade kort, få obegränsat antal visningar per board, obegränsat antal kort och mycket mer.",
"CardDetail.limited-body": "Uppgradera till Professional- eller Enterprise-abonnemang.",
"CardDetail.limited-button": "Uppgradera",
"CardDetail.limited-title": "Detta kort är dolt",
"CardDetail.moveContent": "Flytta kortinnehåll",
@ -103,9 +103,9 @@
"CardDetailProperty.property-deleted": "{propertyName} har raderats!",
"CardDetailProperty.property-name-change-subtext": "typ från \"{oldPropType}\" till \"{newPropType}\"",
"CardDetial.limited-link": "Läs mer om våra abonnemang.",
"CardDialog.delete-confirmation-dialog-attachment": "Bekräfta att bilagor ska raderas!",
"CardDialog.delete-confirmation-dialog-attachment": "Bekräfta att bilagor ska raderas",
"CardDialog.delete-confirmation-dialog-button-text": "Radera",
"CardDialog.delete-confirmation-dialog-heading": "Bekräfta att kortet ska raderas!",
"CardDialog.delete-confirmation-dialog-heading": "Bekräfta att kortet ska raderas",
"CardDialog.editing-template": "Du redigerar en mall.",
"CardDialog.nocard": "Detta kort existerar inte eller är oåtkomligt.",
"Categories.CreateCategoryDialog.CancelText": "Avbryt",
@ -114,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Uppdatera",
"CenterPanel.Login": "Logga in",
"CenterPanel.Share": "Dela",
"ChannelIntro.CreateBoard": "Skapa en board",
"CloudMessage.cloud-server": "Skaffa din egen molnserver gratis.",
"ColorOption.selectColor": "Välj {color} färg",
"Comment.delete": "Radera",
"CommentsList.send": "Skicka",
"ConfirmPerson.empty": "Tom",
"ConfirmPerson.search": "Sök...",
"ConfirmationDialog.cancel-action": "Avbryt",
"ConfirmationDialog.confirm-action": "Godkänn",
"ContentBlock.Delete": "Radera",
@ -165,6 +168,7 @@
"FilterByText.placeholder": "filtrera text",
"FilterComponent.add-filter": "+ Lägg till filter",
"FilterComponent.delete": "Radera",
"FilterValue.empty": "(tom)",
"FindBoardsDialog.IntroText": "Sök efter boards",
"FindBoardsDialog.NoResultsFor": "Inga sökresultat för \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Kontrollera stavningen eller sök igen.",
@ -183,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Lägg till kommentarer",
"OnboardingTour.AddDescription.Body": "Lägg till en beskrivning till kortet så ditt team vet vad kortet handlar om.",
"OnboardingTour.AddDescription.Title": "Lägg till beskrivning",
"OnboardingTour.AddProperties.Body": "Lägg till egenskaper till korten för att göra dem mer informativa!",
"OnboardingTour.AddProperties.Body": "Lägg till egenskaper till korten för att göra dem mer informativa.",
"OnboardingTour.AddProperties.Title": "Lägg till egenskaper",
"OnboardingTour.AddView.Body": "Gå hit för att skapa en ny vy för att organisera ditt Board med hjälp av olika layouter.",
"OnboardingTour.AddView.Title": "Lägg till en ny vy",
@ -194,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Du kan dela ditt Board internt, inom ditt team, eller publicera den publikt för att visa det utanför din organisation.",
"OnboardingTour.ShareBoard.Title": "Dela Board",
"PersonProperty.board-members": "Board-medlemmar",
"PersonProperty.me": "Jag",
"PersonProperty.non-board-members": "Inte board-medlemmar",
"PropertyMenu.Delete": "Radera",
"PropertyMenu.changeType": "Ändra egenskapstyp",
@ -232,7 +237,7 @@
"ShareBoard.copyLink": "Kopiera länk",
"ShareBoard.regenerate": "Generera nytt Token",
"ShareBoard.searchPlaceholder": "Sök efter personer och kanaler",
"ShareBoard.teamPermissionsText": "Alla i Teamet {teamName}",
"ShareBoard.teamPermissionsText": "Alla i teamet {teamName}",
"ShareBoard.tokenRegenrated": "Åtkomstnyckel återskapad",
"ShareBoard.userPermissionsRemoveMemberText": "Ta bort användare",
"ShareBoard.userPermissionsYouText": "(du)",
@ -272,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 boards",
"SiteStats.total_cards": "Totalt antal kort",
"TableComponent.add-icon": "Lägg till ikon",
"TableComponent.name": "Namn",
@ -284,8 +289,8 @@
"TableHeaderMenu.insert-right": "Infoga till höger",
"TableHeaderMenu.sort-ascending": "Sortera stigande",
"TableHeaderMenu.sort-descending": "Sortera fallande",
"TableRow.DuplicateCard": "duplicera kort",
"TableRow.MoreOption": "Fler åtgärder",
"TableRow.delete": "Ta bort",
"TableRow.open": "Öppna",
"TopBar.give-feedback": "Ge återkoppling",
"URLProperty.copiedLink": "Kopierad!",
@ -337,9 +342,9 @@
"ViewLimitDialog.Heading": "Gränsen för antalet visningar per board har uppnåtts",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Uppgradera",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Meddela administratör",
"ViewLimitDialog.Subtext.Admin": "Uppgradera till Professional- eller Enterprise-abonnemang för att få obegränsat antal visningar per board, obegränsat antal kort och mycket mer.",
"ViewLimitDialog.Subtext.Admin": "Uppgradera till Professional- eller Enterprise-abonnemang.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Läs mer om våra abonnemang.",
"ViewLimitDialog.Subtext.RegularUser": "Meddela din administratör att uppgradera till Professional- eller Enterprise-abonnemang för att få obegränsat antal visningar per board, obegränsat antal kort och mycket mer.",
"ViewLimitDialog.Subtext.RegularUser": "Meddela din administratör att uppgradera till Professional- eller Enterprise-abonnemang.",
"ViewLimitDialog.UpgradeImg.AltText": "bild som föreställer en uppgradering",
"ViewLimitDialog.notifyAdmin.Success": "Din systemadministratör har blivit notifierad",
"ViewTitle.hide-description": "dölj beskrivning",
@ -367,8 +372,10 @@
"calendar.month": "Månad",
"calendar.today": "IDAG",
"calendar.week": "Vecka",
"centerPanel.undefined": "Ingen {propertyName}",
"centerPanel.unknown-user": "Okänd användare",
"cloudMessage.learn-more": "Läs mer",
"createImageBlock.failed": "Det går inte att ladda upp filen. Gränsen för filstorlek har uppnåtts.",
"createImageBlock.failed": "Filen kunde inte laddas upp eftersom gränsen för filstorlek har uppnåtts.",
"default-properties.badges": "Kommentarer och beskrivning",
"default-properties.title": "Titel",
"error.back-to-home": "Tillbaka till förstasidan",
@ -383,11 +390,15 @@
"generic.previous": "Föregående",
"guest-no-board.subtitle": "Du har inte tillgång till något board i teamet ännu, vänta tills någon lägger till dig i ett board.",
"guest-no-board.title": "Inga board ännu",
"imagePaste.upload-failed": "Vissa filer har inte laddats upp. Gränsen för filstorlek har nåtts",
"imagePaste.upload-failed": "Några filer kunde inte laddas upp eftersom gränsen för filstorlek har nåtts.",
"limitedCard.title": "Dolda kort",
"login.log-in-button": "Logga in",
"login.log-in-title": "Logga in",
"login.register-button": "eller skapa ett konto om du inte redan har ett",
"new_channel_modal.create_board.empty_board_description": "Skapa en ny tom tavla",
"new_channel_modal.create_board.empty_board_title": "Tom tavla",
"new_channel_modal.create_board.select_template_placeholder": "Välj en mall",
"new_channel_modal.create_board.title": "Skapa en tavla för den här kanalen",
"notification-box-card-limit-reached.close-tooltip": "Sov i 10 dagar",
"notification-box-card-limit-reached.contact-link": "notifiera din administratör",
"notification-box-card-limit-reached.link": "Uppgradera till ett betal-abonnemang",

View File

@ -1,5 +1,15 @@
{
"AppBar.Tooltip": "Bağlantılı panoları aç/kapat",
"Attachment.Attachment-title": "Ek dosya",
"AttachmentBlock.DeleteAction": "sil",
"AttachmentBlock.addElement": "{type} ekle",
"AttachmentBlock.delete": "Ek dosya silindi.",
"AttachmentBlock.failed": "Dosya boyutu sınırı aşıldığından bu dosya yüklenemedi.",
"AttachmentBlock.upload": "Ek dosya yükleniyor.",
"AttachmentBlock.uploadSuccess": "Ek dosya yüklendi.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Sil",
"AttachmentElement.download": "İndir",
"AttachmentElement.upload-percentage": "Yükleniyor...(%{uploadPercent})",
"BoardComponent.add-a-group": "+ Grup ekle",
"BoardComponent.delete": "Sil",
"BoardComponent.hidden-columns": "Gizli sütunlar",
@ -16,8 +26,8 @@
"BoardMember.unlinkChannel": "Bağlantıyı kaldır",
"BoardPage.newVersion": "Yeni bir pano sürümü yayınlanmış. Yeniden yüklemek için buraya tıklayın.",
"BoardPage.syncFailed": "Pano silinmiş ya da erişim izni geri alınmış olabilir.",
"BoardTemplateSelector.add-template": "Yeni kalıp",
"BoardTemplateSelector.create-empty-board": "Boş pano ekle",
"BoardTemplateSelector.add-template": "Yeni kalıp ekle",
"BoardTemplateSelector.create-empty-board": "Boş bir pano ekle",
"BoardTemplateSelector.delete-template": "Sil",
"BoardTemplateSelector.description": "Kalıplardan birini kullanarak ya da sıfırdan başlayarak yan çubuğa bir pano ekleyin.",
"BoardTemplateSelector.edit-template": "Düzenle",
@ -71,13 +81,14 @@
"CardBadges.title-checkboxes": "İşaret kutuları",
"CardBadges.title-comments": "Yorumlar",
"CardBadges.title-description": "Bu kartın bir açıklaması var",
"CardDetail.Attach": "Dosya ekle",
"CardDetail.Follow": "İzle",
"CardDetail.Following": "İzleniyor",
"CardDetail.add-content": "İçerik ekle",
"CardDetail.add-icon": "Simge ekle",
"CardDetail.add-property": "+ Bir özellik ekle",
"CardDetail.addCardText": "kart metni ekle",
"CardDetail.limited-body": "Arşivlenmiş kartları görüntülemek, her pano için sınırsız görüntüleme, sınırsız sayıda kart gibi daha fazla özelliğe sahip olmak için Professional ya da Enterprise tarifesine geçin.",
"CardDetail.limited-body": "Professional ya da Enterprise tarifesine geçin.",
"CardDetail.limited-button": "Üst tarifeye geç",
"CardDetail.limited-title": "Bu kart gizli",
"CardDetail.moveContent": "Kart içeriğini taşı",
@ -92,8 +103,9 @@
"CardDetailProperty.property-deleted": "{propertyName} silindi!",
"CardDetailProperty.property-name-change-subtext": "\"{oldPropType}\" türünden \"{newPropType}\" türüne",
"CardDetial.limited-link": "Tarifelerimiz hakkında ayrıntılı bilgi alın.",
"CardDialog.delete-confirmation-dialog-attachment": "Ek dosyanın silinmesini onaylayın",
"CardDialog.delete-confirmation-dialog-button-text": "Sil",
"CardDialog.delete-confirmation-dialog-heading": "Kartı silmeyi onaylayın!",
"CardDialog.delete-confirmation-dialog-heading": "Kartı silmeyi onaylayın",
"CardDialog.editing-template": "Bir kalıbı düzenliyorsunuz.",
"CardDialog.nocard": "Bu kart bulunamadı ya da erişilebilir değil.",
"Categories.CreateCategoryDialog.CancelText": "İptal",
@ -102,10 +114,13 @@
"Categories.CreateCategoryDialog.UpdateText": "Güncelle",
"CenterPanel.Login": "Oturum aç",
"CenterPanel.Share": "Paylaş",
"ChannelIntro.CreateBoard": "Bir pano ekle",
"CloudMessage.cloud-server": "Kendi ücretsiz bulut sunucunuzu edinin.",
"ColorOption.selectColor": "{color} rengi seçin",
"Comment.delete": "Sil",
"CommentsList.send": "Gönder",
"ConfirmPerson.empty": "Boş",
"ConfirmPerson.search": "Arama...",
"ConfirmationDialog.cancel-action": "İptal",
"ConfirmationDialog.confirm-action": "Onayla",
"ContentBlock.Delete": "Sil",
@ -119,6 +134,7 @@
"ContentBlock.editText": "Metni düzenle...",
"ContentBlock.image": "görsel",
"ContentBlock.insertAbove": "Üste ekle",
"ContentBlock.moveBlock": "kart içeriğini taşı",
"ContentBlock.moveDown": "Alta taşı",
"ContentBlock.moveUp": "Üste taşı",
"ContentBlock.text": "metin",
@ -152,6 +168,7 @@
"FilterByText.placeholder": "metni süz",
"FilterComponent.add-filter": "+ Süzgeç ekle",
"FilterComponent.delete": "Sil",
"FilterValue.empty": "(boş)",
"FindBoardsDialog.IntroText": "Pano arama",
"FindBoardsDialog.NoResultsFor": "\"{searchQuery}\" için bir sonuç bulunamadı",
"FindBoardsDialog.NoResultsSubtext": "Yazımı denetleyin ya da başka bir arama yapmayı deneyin.",
@ -170,7 +187,7 @@
"OnboardingTour.AddComments.Title": "Yorum yap",
"OnboardingTour.AddDescription.Body": "Takım arkadaşlarınızın kartın ne ile ilgili olduğunu anlaması için kartınıza bir açıklama ekleyin.",
"OnboardingTour.AddDescription.Title": "Açıklama ekle",
"OnboardingTour.AddProperties.Body": "Daha güçlü kılmak için kartlara çeşitli özellikler ekleyin!",
"OnboardingTour.AddProperties.Body": "Daha güçlü kılmak için kartlara çeşitli özellikler ekleyin.",
"OnboardingTour.AddProperties.Title": "Özellikler ekle",
"OnboardingTour.AddView.Body": "Farklı görünümler kullanarak panonuzu düzenleyecek yeni bir görünüm oluşturmak için buraya gidin.",
"OnboardingTour.AddView.Title": "Yeni bir görünüm ekle",
@ -181,6 +198,7 @@
"OnboardingTour.ShareBoard.Body": "Panonuzu içeride, ekibiniz ile paylaşabilir ya da kuruluşunuzun dışında herkese açık olarak yayınlayabilirsiniz.",
"OnboardingTour.ShareBoard.Title": "Panoyu paylaş",
"PersonProperty.board-members": "Pano üyeleri",
"PersonProperty.me": "Benim",
"PersonProperty.non-board-members": "Pano üyesi olmayanlar",
"PropertyMenu.Delete": "Sil",
"PropertyMenu.changeType": "Özellik türünü değiştir",
@ -235,6 +253,8 @@
"Sidebar.import-archive": "Arşivi içe aktar",
"Sidebar.invite-users": "Kullanıcıları çağır",
"Sidebar.logout": "Oturumu kapat",
"Sidebar.new-category.badge": "Yeni",
"Sidebar.new-category.drag-boards-cta": "Panoları sürükleyip buraya bırakın...",
"Sidebar.no-boards-in-category": "İçeride bir pano yok",
"Sidebar.product-tour": "Tanıtım turu",
"Sidebar.random-icons": "Rastgele simgeler",
@ -269,7 +289,8 @@
"TableHeaderMenu.insert-right": "Sağa ekle",
"TableHeaderMenu.sort-ascending": "Artan sıralama",
"TableHeaderMenu.sort-descending": "Azalan sıralama",
"TableRow.delete": "Sil",
"TableRow.DuplicateCard": "kartı kopyala",
"TableRow.MoreOption": "Diğer işlemler",
"TableRow.open": "Aç",
"TopBar.give-feedback": "Geri bildirimde bulunun",
"URLProperty.copiedLink": "Kopyalandı!",
@ -321,9 +342,9 @@
"ViewLimitDialog.Heading": "Bir panoyu görüntüleme sınırına ulaşıldı",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Üst tarifeye geç",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Yöneticiyi bilgilendir",
"ViewLimitDialog.Subtext.Admin": "Sınırsız sayıda pano görüntüleyebilmek, sınırsız sayıda kart kullanmak ve diğer özellikler için Professional ya da Enterprise tarifemize geçin.",
"ViewLimitDialog.Subtext.Admin": "Professional ya da Enterprise tarifemize geçin.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Tarifelerimiz hakkında ayrıntılı bilgi alın.",
"ViewLimitDialog.Subtext.RegularUser": "Sınırsız sayıda pano görüntüleyebilmek, sınırsız sayıda kart kullanabilmek ve diğer özellikler için Professional ya da Enterprise tarifesine geçmesi hakkında yöneticinizi bilgilendirin.",
"ViewLimitDialog.Subtext.RegularUser": "Yöneticinizi Professional ya da Enterprise tarifesine geçmesi hakkında bilgilendirin.",
"ViewLimitDialog.UpgradeImg.AltText": "üst tarifeye geçiş görseli",
"ViewLimitDialog.notifyAdmin.Success": "Yöneticiniz bilgilendirildi",
"ViewTitle.hide-description": "açıklamayı gizle",
@ -351,8 +372,10 @@
"calendar.month": "Ay",
"calendar.today": "Bugün",
"calendar.week": "Hafta",
"centerPanel.undefined": "{propertyName} yok",
"centerPanel.unknown-user": "Kullanıcı bilinmiyor",
"cloudMessage.learn-more": "Ayrıntılı bilgi alın",
"createImageBlock.failed": "Dosya yüklenemedi. Dosya boyutu sınırı aşıldı.",
"createImageBlock.failed": "Dosya boyutu sınırı aşıldığından bu dosya yüklenemedi.",
"default-properties.badges": "Yorumlar ve açıklama",
"default-properties.title": "Başlık",
"error.back-to-home": "Girişe dön",
@ -367,7 +390,7 @@
"generic.previous": "Önceki",
"guest-no-board.subtitle": "Henüz bu takımdaki herhangi bir panoya erişme izniniz yok. Lütfen biri sizi bir panoya ekleyene kadar bekleyin.",
"guest-no-board.title": "Henüz bir pano yok",
"imagePaste.upload-failed": "Bazı dosyalar yüklenemedi. Dosya boyutu sınırı aşıldı",
"imagePaste.upload-failed": "Dosya boyutu sınırı aşıldığından bazı dosyalar yüklenemedi.",
"limitedCard.title": "Kartlar gizli",
"login.log-in-button": "Oturum aç",
"login.log-in-title": "Oturum açın",
@ -383,7 +406,7 @@
"person.add-user-to-board-confirm-button": "Panoya ekle",
"person.add-user-to-board-permissions": "İzinler",
"person.add-user-to-board-question": "{username} kullanıcısını panoya eklemek ister misiniz?",
"person.add-user-to-board-warning": "{username} panonun üyesi değil ve pano ile ilgili herhangi bir bildirim almayacak.",
"person.add-user-to-board-warning": "{username} panonun bir üyesi değil ve pano ile ilgili herhangi bir bildirim almayacak.",
"register.login-button": "ya da bir hesabınız varsa oturum açın",
"register.signup-title": "Hesap açın",
"rhs-board-non-admin-msg": "Panonun yöneticilerinden değilsiniz",

View File

@ -1,27 +1,144 @@
{
"AppBar.Tooltip": "Перемкнути пов’язані дошки",
"Attachment.Attachment-title": "Прикріплення",
"AttachmentBlock.DeleteAction": "видалити",
"AttachmentBlock.addElement": "додати {type}",
"AttachmentBlock.delete": "Прикріплення успішно видалено.",
"AttachmentBlock.failed": "Не вдалося завантажити цей файл, оскільки досягнуто обмеження розміру файлу.",
"AttachmentBlock.upload": "Прикріплення завантажуються.",
"AttachmentBlock.uploadSuccess": "Вкладення завантажено.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Видалити",
"AttachmentElement.download": "Завантажити",
"AttachmentElement.upload-percentage": "Завантаження...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Додати групу",
"BoardComponent.delete": "Видалити",
"BoardComponent.hidden-columns": "Приховані стовпці",
"BoardComponent.hide": "Приховати",
"BoardComponent.new": "+ Створити",
"BoardComponent.no-property": "Немає {property}",
"BoardComponent.no-property-title": "Елементи з порожнім полем {property} потраплять сюди. Цей стовпець неможливо видалити.",
"BoardComponent.show": "Показати",
"BoardMember.schemeAdmin": "Адміністратор",
"BoardMember.schemeCommenter": "Коментатор",
"BoardMember.schemeEditor": "Редактор",
"BoardMember.schemeNone": "Жоден",
"BoardMember.schemeViewer": "Спостерігач",
"BoardMember.unlinkChannel": "Від’єднати",
"BoardPage.newVersion": "Доступна оновлена версія Панелі, тицьни тут щоб оновити.",
"BoardPage.syncFailed": "Можливо Панель видалено або права анульовано.",
"BoardTemplateSelector.add-template": "Новий шаблон",
"BoardTemplateSelector.add-template": "Створити новий шаблон",
"BoardTemplateSelector.create-empty-board": "Створити порожню доску",
"BoardTemplateSelector.delete-template": "Видалити",
"BoardTemplateSelector.description": "Виберіть шаблон який допоможе розпочати. Ви можете налаштувати шаблон відповідно до ваших потреб, чи створити новий та налаштувати з нуля",
"BoardTemplateSelector.description": "Додайте дошку на бічній панелі, використовуючи будь-який із наведених нижче шаблонів, або почніть з нуля.",
"BoardTemplateSelector.edit-template": "Редагувати",
"BoardTemplateSelector.plugin.no-content-title": "Створити Доску у {teamName}",
"BoardTemplateSelector.plugin.no-content-description": "Додайте дошку на бічній панелі, використовуючи будь-який із наведених нижче шаблонів, або почніть з нуля.",
"BoardTemplateSelector.plugin.no-content-title": "Створити дошку",
"BoardTemplateSelector.title": "Створити доску",
"BoardTemplateSelector.use-this-template": "Використати цей шаблон",
"BoardsSwitcher.Title": "Знайти дошки",
"BoardsUnfurl.Limited": "Додаткові деталі приховані бо картку архівовано",
"BoardsUnfurl.Remainder": "+{remainder} більше",
"BoardsUnfurl.Updated": "Оновлено {time}",
"Calculations.Options.average.displayName": "Середній",
"Calculations.Options.average.label": "Середній",
"Calculations.Options.count.displayName": "Кількість",
"Calculations.Options.count.label": "Кількість",
"Calculations.Options.countChecked.displayName": "Перевірено",
"Calculations.Options.countChecked.label": "Кількість перевірено",
"Calculations.Options.countUnchecked.displayName": "Не перевірено",
"Calculations.Options.countUnchecked.label": "Підрахунок не перевірено",
"Calculations.Options.countUniqueValue.displayName": "Унікальний",
"Calculations.Options.countUniqueValue.label": "Підрахувати унікальні значення",
"Calculations.Options.countValue.displayName": "Значення",
"Calculations.Options.countValue.label": "Розрахунок значення",
"Calculations.Options.dateRange.displayName": "Діапазон",
"Calculations.Options.dateRange.label": "Діапазон",
"Calculations.Options.earliest.displayName": "Найраніший",
"Calculations.Options.earliest.label": "Найраніший",
"Calculations.Options.latest.displayName": "Останній",
"Calculations.Options.latest.label": "Останній",
"Calculations.Options.max.displayName": "Макс",
"Calculations.Options.max.label": "Макс",
"Calculations.Options.median.displayName": "Медіана",
"Calculations.Options.median.label": "Медіана",
"Calculations.Options.min.displayName": "Мін",
"Calculations.Options.min.label": "Мін",
"Calculations.Options.none.displayName": "Обчислити",
"Calculations.Options.none.label": "Жодного",
"Calculations.Options.percentChecked.displayName": "Перевірено",
"Calculations.Options.percentChecked.label": "Відсоток перевірено",
"Calculations.Options.percentUnchecked.displayName": "Не перевірено",
"Calculations.Options.percentUnchecked.label": "Відсоток не перевірено",
"Calculations.Options.range.displayName": "Діапазон",
"Calculations.Options.range.label": "Діапазон",
"Calculations.Options.sum.displayName": "Сума",
"Calculations.Options.sum.label": "Сума",
"CalendarCard.untitled": "Без назви",
"CardActionsMenu.copiedLink": "Скопійовано!",
"CardActionsMenu.copyLink": "Копіювати посилання",
"CardActionsMenu.delete": "Видалити",
"CardActionsMenu.duplicate": "Дублювати",
"CardBadges.title-checkboxes": "Прапорці",
"CardBadges.title-comments": "Коментарі",
"CardBadges.title-description": "Ця картка має опис",
"CardDetail.Attach": "Прикріпити",
"CardDetail.Follow": "Слідкувати",
"CardDetail.Following": "Відслідковувати",
"CardDetail.add-content": "Додайте вміст",
"CardDetail.add-icon": "Додати значок",
"CardDetail.add-property": "+ Додати властивість",
"CardDetail.addCardText": "додати текст картки",
"CardDetail.limited-body": "Перейдіть на наш план Professional або Enterprise.",
"CardDetail.limited-button": "Оновлення",
"CardDetail.limited-title": "Ця прихована картка",
"CardDetail.moveContent": "Перемістити вміст картки",
"CardDetail.new-comment-placeholder": "Додати коментар...",
"CardDetailProperty.confirm-delete-heading": "Підтвердьте видалення властивості",
"CardDetailProperty.confirm-delete-subtext": "Ви впевнені, що хочете видалити властивість \"{propertyName}\"? При видаленні властивість буде видалено з усіх карток на цій дошці.",
"CardDetailProperty.confirm-property-name-change-subtext": "Ви дійсно хочете змінити властивість \"{propertyName}\" {customText}? Це вплине на значення(-я) на {numOfCards} картці(-ах) на цій дошці і може призвести до втрати даних.",
"CardDetailProperty.confirm-property-type-change": "Підтвердити зміну типу власності",
"CardDetailProperty.delete-action-button": "Видалити",
"CardDetailProperty.property-change-action-button": "Змінити властивість",
"CardDetailProperty.property-changed": "Властивість змінена успішно!",
"CardDetailProperty.property-deleted": "{propertyName} успішно видалено!",
"CardDetailProperty.property-name-change-subtext": "тип з \"{oldPropType}\" в \"{newPropType}\"",
"CardDetial.limited-link": "Дізнайтеся більше про наші плани.",
"CardDialog.delete-confirmation-dialog-attachment": "Підтвердити видалення вкладення",
"CardDialog.delete-confirmation-dialog-button-text": "Видалити",
"CardDialog.delete-confirmation-dialog-heading": "Підтвердити видалення картки",
"CardDialog.editing-template": "Ви редагуєте шаблон.",
"CardDialog.nocard": "Ця картка не існує або недоступна.",
"Categories.CreateCategoryDialog.CancelText": "Скасувати",
"Categories.CreateCategoryDialog.CreateText": "Створити",
"Categories.CreateCategoryDialog.Placeholder": "Назвіть свою категорію",
"Categories.CreateCategoryDialog.UpdateText": "Оновити",
"CenterPanel.Login": "Логін",
"CenterPanel.Share": "Поділитися",
"ChannelIntro.CreateBoard": "Створити дошку",
"CloudMessage.cloud-server": "Отримайте власний безкоштовний хмарний сервер.",
"ColorOption.selectColor": "Виберіть колір {color}",
"Comment.delete": "Видалити",
"CommentsList.send": "Надіслати",
"ConfirmPerson.empty": "Порожній",
"ConfirmPerson.search": "Пошук...",
"ConfirmationDialog.cancel-action": "Скасувати",
"ConfirmationDialog.confirm-action": "Підтвердити",
"ContentBlock.Delete": "Видалити",
"ContentBlock.DeleteAction": "видалити",
"DeleteBoardDialog.confirm-delete": "Видалити",
"FilterComponent.delete": "Видалити",
"PropertyMenu.Delete": "Видалити",
"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": "Ви бачили це раніше?"
}

View File

@ -1,11 +1,12 @@
{
"AppBar.Tooltip": "切换链接的板块",
"Attachment.Attachment-title": "附件",
"AttachmentBlock.DeleteAction": "删除",
"AttachmentBlock.addElement": "添加 {type}",
"AttachmentBlock.delete": "附件删除成功。",
"AttachmentBlock.failed": "无法上传文件。附件大小达到限制。",
"AttachmentBlock.delete": "附件删除。",
"AttachmentBlock.failed": "该文件无法上传,因为已经达到了文件大小的限制。",
"AttachmentBlock.upload": "附件正在上传。",
"AttachmentBlock.uploadSuccess": "附件上传成功。",
"AttachmentBlock.uploadSuccess": "附件上传。",
"AttachmentElement.delete-confirmation-dialog-button-text": "删除",
"AttachmentElement.download": "下载",
"AttachmentElement.upload-percentage": "上传中…({uploadPercent}%)",
@ -15,9 +16,10 @@
"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": "视图",
@ -25,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": "编辑",
@ -33,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} 更新",
@ -86,6 +88,7 @@
"CardDetail.add-icon": "新增图标",
"CardDetail.add-property": "+ 新增属性",
"CardDetail.addCardText": "新增卡片文本",
"CardDetail.limited-body": "升级到我们的专业或企业计划。",
"CardDetail.limited-button": "升级",
"CardDetail.limited-title": "这张卡片是隐藏的",
"CardDetail.moveContent": "移动卡片内容",
@ -100,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": "取消",
@ -111,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": "删除",
@ -128,6 +134,7 @@
"ContentBlock.editText": "编辑文字...",
"ContentBlock.image": "图片",
"ContentBlock.insertAbove": "在上方插入",
"ContentBlock.moveBlock": "移动卡片内容",
"ContentBlock.moveDown": "下移",
"ContentBlock.moveUp": "上移",
"ContentBlock.text": "文字",
@ -138,18 +145,31 @@
"DeleteBoardDialog.confirm-cancel": "取消",
"DeleteBoardDialog.confirm-delete": "删除",
"DeleteBoardDialog.confirm-info": "确定要删除版块\"{boardTitle}\"?删除后,也将删除此版块中的所有卡片。",
"DeleteBoardDialog.confirm-info-template": "你确定要删除板块模板\"{boardTitle}\"吗?",
"DeleteBoardDialog.confirm-tite": "确认删除板块",
"DeleteBoardDialog.confirm-tite-template": "确认删除板块模板",
"Dialog.closeDialog": "关闭对话框",
"EditableDayPicker.today": "今天",
"Error.mobileweb": "移动端页面的支持目前处于早期测试阶段。不是所有的功能都已实现。",
"Error.websocket-closed": "Websocket 连接关闭,连接中断。如果这种情况仍然存在,请检查您的服务器或网页代理配置。",
"Filter.contains": "包含",
"Filter.ends-with": "结束于",
"Filter.includes": "含有",
"Filter.is": "是",
"Filter.is-empty": "为空",
"Filter.is-not-empty": "不为空",
"Filter.not-includes": "不包含",
"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>取消",
@ -157,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": "选择属性类型",
@ -180,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": "已复制!",
@ -195,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": "变更密码",
@ -219,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": "+ 新增",
@ -241,15 +289,23 @@
"TableHeaderMenu.insert-right": "在右侧插入",
"TableHeaderMenu.sort-ascending": "升序排列",
"TableHeaderMenu.sort-descending": "降序排列",
"TableRow.delete": "删除",
"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": "删除视图",
@ -259,6 +315,8 @@
"View.NewCalendarTitle": "日历视图",
"View.NewGalleryTitle": "画廊视图",
"View.NewTableTitle": "图表视图",
"View.NewTemplateDefaultTitle": "未命名模板",
"View.NewTemplateTitle": "未命名",
"View.Table": "图表",
"ViewHeader.add-template": "+ 新模板",
"ViewHeader.delete-template": "删除",
@ -274,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": "之前有见到过吗?"
}

View File

@ -6,7 +6,7 @@
"AttachmentBlock.delete": "附件刪除成功。",
"AttachmentBlock.failed": "無法上傳文件。 附件大小已達到限制。",
"AttachmentBlock.upload": "附件正在上傳。",
"AttachmentBlock.uploadSuccess": "附件上傳成功。",
"AttachmentBlock.uploadSuccess": "附件上傳",
"AttachmentElement.delete-confirmation-dialog-button-text": "刪除",
"AttachmentElement.download": "下載",
"AttachmentElement.upload-percentage": "正在上傳...({uploadPercent}%)",
@ -285,7 +285,6 @@
"TableHeaderMenu.sort-ascending": "升序排列",
"TableHeaderMenu.sort-descending": "降序排列",
"TableRow.MoreOption": "更多操作",
"TableRow.delete": "刪除",
"TableRow.open": "開啟",
"TopBar.give-feedback": "提供回饋",
"URLProperty.copiedLink": "已複製!",

View File

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

View File

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

View File

@ -240,6 +240,20 @@ exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] =
<div
class="toolbar--right"
>
<div
class="d-flex"
>
<button
type="button"
>
<i
class="CompassIcon icon-paperclip"
/>
<span>
Attach
</span>
</button>
</div>
<button
aria-label="Close dialog"
title="Close dialog"

View File

@ -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 {

View File

@ -159,7 +159,7 @@ const CardDialog = (props: Props): JSX.Element => {
const attachmentBlock = createAttachmentBlock(uploadingBlock)
attachmentBlock.isUploading = true
dispatch(updateAttachments([attachmentBlock]))
if (attachment.size > clientConfig.maxFileSize) {
if (attachment.size > clientConfig.maxFileSize && Utils.isFocalboardPlugin()) {
removeUploadingAttachment(uploadingBlock)
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.failed', defaultMessage: 'Unable to upload the file. Attachment size limit reached.'}), severity: 'normal'})
} else {
@ -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 {
@ -262,7 +263,10 @@ const CardDialog = (props: Props): JSX.Element => {
</>
)
return (<>{attachBtn()}{following ? unfollowBtn : followBtn}</>)
if (!isTemplate && Utils.isFocalboardPlugin() && !card?.limited) {
return (<>{attachBtn()}{following ? unfollowBtn : followBtn}</>)
}
return (<>{attachBtn()}</>)
}
const followingCards = useAppSelector(getUserBlockSubscriptionList)
@ -276,7 +280,7 @@ const CardDialog = (props: Props): JSX.Element => {
className='cardDialog'
onClose={props.onClose}
toolsMenu={!props.readonly && !card?.limited && menu}
toolbar={!isTemplate && Utils.isFocalboardPlugin() && !card?.limited && toolbar}
toolbar={toolbar}
>
{isTemplate &&
<div className='banner'>

View File

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

View File

@ -30,7 +30,7 @@ const Entry = (props: EntryComponentProps): ReactElement => {
/>
<div className={theme?.mentionSuggestionsEntryText}>
{mention.name}
{BotBadge && <BotBadge show={mention.is_bot}/>}
{BotBadge && mention.is_bot && <BotBadge/>}
<GuestBadge show={mention.is_guest}/>
</div>
<div className={theme?.mentionSuggestionsEntryText}>

View File

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

View File

@ -313,6 +313,7 @@ const SidebarCategory = (props: Props) => {
<IconButton icon={<OptionsIcon/>}/>
<Menu
position='auto'
fixed={true}
parentRef={menuWrapperRef}
>
{

View File

@ -103,6 +103,8 @@ const TableRow = (props: Props) => {
} else if (template && template.type === 'updatedBy') {
groupValue = card.modifiedBy
}
} else if (Array.isArray(groupValue)) {
groupValue = groupValue[0]
}
if (collapsedOptionIds.indexOf(groupValue) > -1) {
className += ' hidden'

View File

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

View File

@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useEffect, useMemo} from 'react'
import {Board} from '../blocks/board'
import octoClient from '../octoClient'
import {useAppDispatch, useAppSelector} from '../store/hooks'
import {fetchGlobalTemplates, getGlobalTemplates} from '../store/globalTemplates'
import {getTemplates} from '../store/boards'
import {Constants} from '../constants'
export const useGetAllTemplates = () => {
const dispatch = useAppDispatch()
const globalTemplates = useAppSelector<Board[]>(getGlobalTemplates) || []
useEffect(() => {
if (octoClient.teamId !== Constants.globalTeamId && globalTemplates.length === 0) {
dispatch(fetchGlobalTemplates())
}
}, [octoClient.teamId])
const unsortedTemplates = useAppSelector(getTemplates)
const templates = useMemo(() => Object.values(unsortedTemplates).sort((a: Board, b: Board) => a.createAt - b.createAt), [unsortedTemplates])
return useMemo(() => globalTemplates.concat(templates), [globalTemplates])
}

View File

@ -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[]))
})
@ -665,19 +668,39 @@ class Mutator {
const newBlockIDs: string[] = []
if (propertyTemplate.type !== newType) {
if (propertyTemplate.type === 'select' || propertyTemplate.type === 'multiSelect') { // If the old type was either select or multiselect
const isNewTypeSelectOrMulti = newType === 'select' || newType === 'multiSelect'
const isNewTypeSelectOrMulti = newType === 'select' || newType === 'multiSelect'
const isNewTypePersonOrMulti = newType === 'person' || newType === 'multiPerson'
const isOldTypeSelectOrMulti = propertyTemplate.type === 'select' || propertyTemplate.type === 'multiSelect'
const isOldTypePersonOrMulti = propertyTemplate.type === 'person' || propertyTemplate.type === 'multiPerson'
// If the old type was either select/multiselect or person/multiperson
if (isOldTypeSelectOrMulti || isOldTypePersonOrMulti) {
for (const card of cards) {
const oldValue = Array.isArray(card.fields.properties[propertyTemplate.id]) ? (card.fields.properties[propertyTemplate.id].length > 0 && card.fields.properties[propertyTemplate.id][0]) : card.fields.properties[propertyTemplate.id]
// if array get first value, if exists
const oldValue = Array.isArray(card.fields.properties[propertyTemplate.id]) ? (card.fields.properties[propertyTemplate.id].length > 0 && card.fields.properties[propertyTemplate.id][0] as string) : card.fields.properties[propertyTemplate.id] as string
if (oldValue) {
const newValue = isNewTypeSelectOrMulti ? propertyTemplate.options.find((o) => o.id === oldValue)?.id : propertyTemplate.options.find((o) => o.id === oldValue)?.value
let newValue: string | undefined
if (isOldTypePersonOrMulti) {
if (isNewTypePersonOrMulti) {
newValue = oldValue
}
} else if (isNewTypeSelectOrMulti) {
if (isOldTypeSelectOrMulti) {
newValue = propertyTemplate.options.find((o) => o.id === oldValue)?.id
} else {
newValue = propertyTemplate.options.find((o) => o.id === oldValue)?.value
}
}
const newCard = createCard(card)
if (newValue) {
newCard.fields.properties[propertyTemplate.id] = newType === 'multiSelect' ? [newValue] : newValue
if (newType === 'multiSelect' || newType === 'multiPerson') {
newCard.fields.properties[propertyTemplate.id] = [newValue]
} else {
newCard.fields.properties[propertyTemplate.id] = newValue
}
} else {
// This was an invalid select option, so delete it
// This was an invalid select option or old person id, so delete it
delete newCard.fields.properties[propertyTemplate.id]
}
@ -690,7 +713,7 @@ class Mutator {
newTemplate.options = propertyTemplate.options
}
}
} else if (newType === 'select' || newType === 'multiSelect') { // if the new type is either select or multiselect
} else if (isNewTypeSelectOrMulti) { // if the new type is either select or multiselect - old type is other
// Map values to new template option IDs
for (const card of cards) {
const oldValue = card.fields.properties[propertyTemplate.id] as string
@ -708,6 +731,18 @@ class Mutator {
const newCard = createCard(card)
newCard.fields.properties[propertyTemplate.id] = newType === 'multiSelect' ? [option.id] : option.id
newBlocks.push(newCard)
newBlockIDs.push(newCard.id)
oldBlocks.push(card)
}
}
} else if (isNewTypePersonOrMulti) { // if the new type is either person or multiperson - old type is other
// Clear old values
for (const card of cards) {
const oldValue = card.fields.properties[propertyTemplate.id] as string
if (oldValue) {
const newCard = createCard(card)
delete newCard.fields.properties[propertyTemplate.id]
newBlocks.push(newCard)
newBlockIDs.push(newCard.id)
oldBlocks.push(card)
@ -1088,8 +1123,8 @@ class Mutator {
async addEmptyBoard(
teamId: string,
intl: IntlShape,
afterRedo: (id: string) => Promise<void>,
beforeUndo: () => Promise<void>,
afterRedo?: (id: string) => Promise<void>,
beforeUndo?: () => Promise<void>,
): Promise<BoardsAndBlocks> {
const board = createBoard()
board.teamId = teamId
@ -1106,7 +1141,7 @@ class Mutator {
async (bab: BoardsAndBlocks) => {
const newBoard = bab.boards[0]
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CreateBoard, {board: newBoard?.id})
await afterRedo(newBoard?.id || '')
afterRedo && await afterRedo(newBoard?.id || '')
},
beforeUndo,
)

View File

@ -215,13 +215,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.id, teamId, match.params.boardId)
}
}, [teamId, match.params.boardId, me?.id])
const handleUnhideBoard = async (boardID: string) => {
if (!me || !category) {
return

View File

@ -61,7 +61,6 @@ function DateRange(props: PropertyProps): JSX.Element {
const onChange = useCallback((newValue) => {
if (value !== newValue) {
setValue(newValue)
mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue)
}
}, [value, board.id, card, propertyTemplate.id])
@ -150,7 +149,9 @@ function DateRange(props: PropertyProps): JSX.Element {
}
const onClose = () => {
onChange(datePropertyToString(dateProperty))
const newDate = datePropertyToString(dateProperty)
onChange(newDate)
mutator.changePropertyValue(board.id, card, propertyTemplate.id, newDate)
setShowDialog(false)
}

View File

@ -85,7 +85,7 @@ const ConfirmPerson = (props: PropertyProps): JSX.Element => {
setConfirmAddUser(null)
await mutator.createBoardMember(newMember)
if (userIDs) {
if (propertyTemplate.type === 'multiPerson') {
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, [...userIDs, newMember.userId])
} else {
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, newMember.userId)

View File

@ -26,7 +26,9 @@ const attachmentSlice = createSlice({
state.attachmentsByCard[attachment.parentId] = [attachment]
return
}
state.attachmentsByCard[attachment.parentId].push(attachment)
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]) {

View File

@ -1,12 +1,6 @@
@import '../../styles/z-index';
.status-dropdown-menu {
> .Menu {
position: inherit;
}
}
.Menu {
.Menu.noselect {
@include z-index(menu);
display: flex;
flex-direction: column;
@ -179,7 +173,7 @@
}
}
.Menu,
.Menu.noselect,
.SubMenuOption .SubMenu {
@media screen and (max-width: 430px) {
position: fixed;