Merge branch 'master' into MM-50966-in-product-expansion-backend

Этот коммит содержится в:
Conor Macpherson
2023-04-17 15:39:37 -04:00
родитель de3ada24dc 3f022e728f
Коммит a47c3cf859
229 изменённых файлов: 5793 добавлений и 4904 удалений

Просмотреть файл

@@ -145,4 +145,42 @@ describe('Edit Message', () => {
cy.get(postText).should('have.text', `${secondMessage} Another new message Edited`); cy.get(postText).should('have.text', `${secondMessage} Another new message Edited`);
}); });
}); });
it('MM-T5416 should discard any changes made after cancelling the edit and opening the edit textbox again should display the original message', () => {
const message = 'World!';
cy.postMessage(message);
// * Verify message is sent and not pending
cy.getLastPostId().then((postId) => {
const postText = `#postMessageText_${postId}`;
cy.get(postText).should('have.text', message);
// # Open edit textbox
cy.uiGetPostTextBox().type('{uparrow}');
// * Edit Post Input should appear, and edit the post
cy.get('#edit_textbox').should('be.visible');
// * Press the escape key to cancel
cy.get('#edit_textbox').should('have.text', message).type(' Another new message{esc}');
cy.get('#edit_textbox').should('not.exist');
// * Check that the message wasn't edited
cy.get(postText).should('have.text', message);
});
cy.getLastPostId().then((postId) => {
const postText = `#postMessageText_${postId}`;
cy.get(postText).should('have.text', message);
// # Open edit textbox again
cy.uiGetPostTextBox().type('{uparrow}');
// * Edit Post Input should appear, and edit the post
cy.get('#edit_textbox').should('be.visible');
// * Opening the edit textbox again after previously cancelling the edit should contain the original message.
cy.get('#edit_textbox').should('have.text', message);
});
});
}); });

122
go.mod
Просмотреть файл

@@ -4,12 +4,12 @@ go 1.19
require ( require (
code.sajari.com/docconv v1.3.5 code.sajari.com/docconv v1.3.5
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.4
github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1 github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1
github.com/aws/aws-sdk-go v1.44.173 github.com/aws/aws-sdk-go v1.44.240
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/blevesearch/bleve/v2 v2.3.6 github.com/blevesearch/bleve/v2 v2.3.7
github.com/cespare/xxhash/v2 v2.2.0 github.com/cespare/xxhash/v2 v2.2.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3
@@ -17,7 +17,7 @@ require (
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a
github.com/francoispqt/gojay v1.2.13 github.com/francoispqt/gojay v1.2.13
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
github.com/getsentry/sentry-go v0.16.0 github.com/getsentry/sentry-go v0.20.0
github.com/go-sql-driver/mysql v1.7.0 github.com/go-sql-driver/mysql v1.7.0
github.com/golang-migrate/migrate/v4 v4.15.2 github.com/golang-migrate/migrate/v4 v4.15.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
@@ -31,9 +31,9 @@ require (
github.com/graph-gophers/dataloader/v7 v7.1.0 github.com/graph-gophers/dataloader/v7 v7.1.0
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
github.com/hashicorp/go-hclog v1.4.0 github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-plugin v1.4.8 github.com/hashicorp/go-plugin v1.4.9
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80
@@ -42,32 +42,31 @@ require (
github.com/mattermost/gziphandler v0.0.1 github.com/mattermost/gziphandler v0.0.1
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d
github.com/mattermost/logr/v2 v2.0.16 github.com/mattermost/logr/v2 v2.0.16
github.com/mattermost/mattermost-plugin-playbooks/client v0.7.0
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0
github.com/mattermost/squirrel v0.2.0 github.com/mattermost/squirrel v0.2.0
github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538 github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.21 github.com/microcosm-cc/bluemonday v1.0.23
github.com/minio/minio-go/v7 v7.0.45 github.com/minio/minio-go/v7 v7.0.51
github.com/mitchellh/mapstructure v1.4.3 github.com/mitchellh/mapstructure v1.5.0
github.com/oklog/run v1.1.0 github.com/oklog/run v1.1.0
github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb
github.com/opentracing/opentracing-go v1.2.0 github.com/opentracing/opentracing-go v1.2.0
github.com/pborman/uuid v1.2.1 github.com/pborman/uuid v1.2.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_golang v1.14.0
github.com/reflog/dateconstraints v0.2.1 github.com/reflog/dateconstraints v0.2.1
github.com/rivo/uniseg v0.4.3 github.com/rivo/uniseg v0.4.4
github.com/rs/cors v1.8.3 github.com/rs/cors v1.8.3
github.com/rudderlabs/analytics-go v3.3.3+incompatible github.com/rudderlabs/analytics-go v3.3.3+incompatible
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sergi/go-diff v1.2.0 github.com/sergi/go-diff v1.3.1
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.10.1 github.com/spf13/viper v1.15.0
github.com/splitio/go-client/v6 v6.2.1 github.com/splitio/go-client/v6 v6.3.1
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.2
github.com/throttled/throttled v2.2.5+incompatible github.com/throttled/throttled v2.2.5+incompatible
github.com/tinylib/msgp v1.1.8 github.com/tinylib/msgp v1.1.8
github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/uber/jaeger-client-go v2.30.0+incompatible
@@ -76,14 +75,14 @@ require (
github.com/wiggin77/merror v1.0.4 github.com/wiggin77/merror v1.0.4
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c
github.com/yuin/goldmark v1.5.3 github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.5.0 golang.org/x/crypto v0.8.0
golang.org/x/image v0.3.0 golang.org/x/image v0.7.0
golang.org/x/net v0.8.0 golang.org/x/net v0.9.0
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
golang.org/x/text v0.8.0 golang.org/x/text v0.9.0
golang.org/x/tools v0.6.0 golang.org/x/tools v0.8.0
gopkg.in/guregu/null.v4 v4.0.0 gopkg.in/guregu/null.v4 v4.0.0
gopkg.in/mail.v2 v2.3.1 gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
@@ -91,16 +90,17 @@ require (
) )
require ( require (
github.com/HdrHistogram/hdrhistogram-go v0.9.0 // indirect
github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 // indirect github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/RoaringBitmap/roaring v1.2.1 // indirect github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625 // indirect github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.4.0 // indirect github.com/bits-and-blooms/bitset v1.5.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect
github.com/blevesearch/bleve_index_api v1.0.5 // indirect github.com/blevesearch/bleve_index_api v1.0.5 // indirect
github.com/blevesearch/geo v0.1.17 // indirect github.com/blevesearch/geo v0.1.17 // indirect
@@ -116,21 +116,21 @@ require (
github.com/blevesearch/zapx/v12 v12.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/v13 v13.3.7 // indirect
github.com/blevesearch/zapx/v14 v14.3.7 // indirect github.com/blevesearch/zapx/v14 v14.3.7 // indirect
github.com/blevesearch/zapx/v15 v15.3.8 // indirect github.com/blevesearch/zapx/v15 v15.3.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.15.0 // indirect
github.com/fatih/set v0.2.1 // indirect github.com/fatih/set v0.2.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // 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-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-resty/resty/v2 v2.7.0 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect github.com/golang/geo v0.0.0-20230404232722-c4acd7a044dc // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
@@ -141,17 +141,17 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.14 // indirect github.com/klauspost/compress v1.16.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
@@ -162,26 +162,28 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/otiai10/gosseract/v2 v2.4.0 // indirect github.com/otiai10/gosseract/v2 v2.4.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/philhofer/fwd v1.1.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.33.0 // indirect github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.9.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect github.com/redis/go-redis/v9 v9.0.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/rs/xid v1.4.0 // indirect github.com/rs/xid v1.4.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect github.com/segmentio/backo-go v1.0.1 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/splitio/go-split-commons/v4 v4.2.3 // indirect github.com/splitio/go-split-commons/v4 v4.3.1 // indirect
github.com/splitio/go-toolkit/v5 v5.2.2 // indirect github.com/splitio/go-toolkit/v5 v5.3.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
@@ -189,25 +191,25 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect github.com/wiggin77/srslog v1.0.1 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/bbolt v1.3.7 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.6.0 // indirect golang.org/x/sys v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.51.0 // indirect google.golang.org/grpc v1.54.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.20.1 // indirect modernc.org/sqlite v1.21.1 // indirect
modernc.org/strutil v1.1.3 // indirect modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.1.0 // indirect
) )

633
go.sum

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -239,10 +239,10 @@ const (
Office365SettingsDefaultTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token" Office365SettingsDefaultTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
Office365SettingsDefaultUserAPIEndpoint = "https://graph.microsoft.com/v1.0/me" Office365SettingsDefaultUserAPIEndpoint = "https://graph.microsoft.com/v1.0/me"
CloudSettingsDefaultCwsURL = "https://customers.cloud.mattermost.com" CloudSettingsDefaultCwsURL = "https://customers.mattermost.com"
CloudSettingsDefaultCwsAPIURL = "https://portal.internal.prod.cloud.mattermost.com" CloudSettingsDefaultCwsAPIURL = "https://portal.internal.prod.cloud.mattermost.com"
// TODO: update to "https://portal.test.cloud.mattermost.com" when ready to use test license key // TODO: update to "https://portal.test.cloud.mattermost.com" when ready to use test license key
CloudSettingsDefaultCwsURLTest = "https://customers.cloud.mattermost.com" CloudSettingsDefaultCwsURLTest = "https://customers.mattermost.com"
// TODO: update to // "https://api.internal.test.cloud.mattermost.com" when ready to use test license key // TODO: update to // "https://api.internal.test.cloud.mattermost.com" when ready to use test license key
CloudSettingsDefaultCwsAPIURLTest = "https://portal.internal.prod.cloud.mattermost.com" CloudSettingsDefaultCwsAPIURLTest = "https://portal.internal.prod.cloud.mattermost.com"

Просмотреть файл

@@ -72,7 +72,6 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query() query := r.URL.Query()
parentID := query.Get("parent_id") parentID := query.Get("parent_id")
blockType := query.Get("type") blockType := query.Get("type")
all := query.Get("all")
blockID := query.Get("block_id") blockID := query.Get("block_id")
boardID := mux.Vars(r)["boardID"] boardID := mux.Vars(r)["boardID"]
@@ -122,18 +121,11 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("boardID", boardID) auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("parentID", parentID) auditRec.AddMeta("parentID", parentID)
auditRec.AddMeta("blockType", blockType) auditRec.AddMeta("blockType", blockType)
auditRec.AddMeta("all", all)
auditRec.AddMeta("blockID", blockID) auditRec.AddMeta("blockID", blockID)
var blocks []*model.Block var blocks []*model.Block
var block *model.Block var block *model.Block
switch { switch {
case all != "":
blocks, err = a.app.GetBlocksForBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
case blockID != "": case blockID != "":
block, err = a.app.GetBlockByID(blockID) block, err = a.app.GetBlockByID(blockID)
if err != nil { if err != nil {
@@ -148,7 +140,12 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
blocks = append(blocks, block) blocks = append(blocks, block)
default: default:
blocks, err = a.app.GetBlocks(boardID, parentID, blockType) opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
BlockType: model.BlockType(blockType),
}
blocks, err = a.app.GetBlocks(opts)
if err != nil { if err != nil {
a.errorResponse(w, r, err) a.errorResponse(w, r, err)
return return

Просмотреть файл

@@ -18,20 +18,11 @@ import (
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards") var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]*model.Block, error) { func (a *App) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) {
if boardID == "" { if opts.BoardID == "" {
return []*model.Block{}, nil return []*model.Block{}, nil
} }
return a.store.GetBlocks(opts)
if blockType != "" && parentID != "" {
return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType)
}
if blockType != "" {
return a.store.GetBlocksWithType(boardID, blockType)
}
return a.store.GetBlocksWithParent(boardID, parentID)
} }
func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) { func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]*model.Block, error) {
@@ -514,10 +505,6 @@ func (a *App) GetBlockCountsByType() (map[string]int64, error) {
return a.store.GetBlockCountsByType() return a.store.GetBlockCountsByType()
} }
func (a *App) GetBlocksForBoard(boardID string) ([]*model.Block, error) {
return a.store.GetBlocksForBoard(boardID)
}
func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) { func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) {
// don't notify if notifications service disabled, or block change is generated via system user. // don't notify if notifications service disabled, or block change is generated via system user.
if a.notifications == nil || modifiedByID == model.SystemUserID { if a.notifications == nil || modifiedByID == model.SystemUserID {

Просмотреть файл

@@ -207,10 +207,17 @@ func TestIsWithinViewsLimit(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "board_id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("board_id", "parent_id", "view").Return([]*model.Block{{}}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil)
withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"})
assert.NoError(t, err) assert.NoError(t, err)
@@ -225,10 +232,17 @@ func TestIsWithinViewsLimit(t *testing.T) {
Views: mm_model.NewInt(1), Views: mm_model.NewInt(1),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "board_id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("board_id", "parent_id", "view").Return([]*model.Block{{}}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil)
withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"})
assert.NoError(t, err) assert.NoError(t, err)
@@ -243,10 +257,17 @@ func TestIsWithinViewsLimit(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "board_id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("board_id", "parent_id", "view").Return([]*model.Block{{}, {}, {}}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}, {}, {}}, nil)
withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"})
assert.NoError(t, err) assert.NoError(t, err)
@@ -261,10 +282,17 @@ func TestIsWithinViewsLimit(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "board_id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("board_id", "parent_id", "view").Return([]*model.Block{}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{}, nil)
withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"}) withinLimits, err := th.App.isWithinViewsLimit("board_id", &model.Block{ParentID: "parent_id"})
assert.NoError(t, err) assert.NoError(t, err)
@@ -333,10 +361,17 @@ func TestInsertBlocks(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "test-board-id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]*model.Block{{}}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil)
_, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1")
require.NoError(t, err) require.NoError(t, err)
@@ -365,10 +400,17 @@ func TestInsertBlocks(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "test-board-id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]*model.Block{{}, {}}, nil) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}, {}}, nil)
_, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1") _, err := th.App.InsertBlocks([]*model.Block{block}, "user-id-1")
require.Error(t, err) require.Error(t, err)
@@ -406,10 +448,17 @@ func TestInsertBlocks(t *testing.T) {
Views: mm_model.NewInt(2), Views: mm_model.NewInt(2),
}, },
} }
opts := model.QueryBlocksOptions{
BoardID: "test-board-id",
ParentID: "parent_id",
BlockType: model.BlockType("view"),
}
th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil).Times(2) th.Store.EXPECT().GetCloudLimits().Return(cloudLimit, nil).Times(2)
th.Store.EXPECT().GetUsedCardsCount().Return(1, nil).Times(2) th.Store.EXPECT().GetUsedCardsCount().Return(1, nil).Times(2)
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil).Times(2) th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil).Times(2)
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]*model.Block{{}}, nil).Times(2) th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{{}}, nil).Times(2)
_, err := th.App.InsertBlocks([]*model.Block{view1, view2}, "user-id-1") _, err := th.App.InsertBlocks([]*model.Block{view1, view2}, "user-id-1")
require.Error(t, err) require.Error(t, err)

Просмотреть файл

@@ -86,7 +86,7 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp
var files []string var files []string
// write the board's blocks // write the board's blocks
// TODO: paginate this // TODO: paginate this
blocks, err := a.GetBlocksForBoard(board.ID) blocks, err := a.GetBlocks(model.QueryBlocksOptions{BoardID: board.ID})
if err != nil { if err != nil {
return err return err
} }

Просмотреть файл

@@ -58,6 +58,10 @@ func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error)
return false, err return false, err
} }
if !a.config.EnablePublicSharedBoards {
return false, errors.New("public shared boards disabled")
}
if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) { if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) {
return true, nil return true, nil
} }

Просмотреть файл

@@ -133,7 +133,7 @@ func TestCreateBoardsAndBlocks(t *testing.T) {
require.Equal(t, "public board", board1.Title) require.Equal(t, "public board", board1.Title)
require.Equal(t, model.BoardTypeOpen, board1.Type) require.Equal(t, model.BoardTypeOpen, board1.Type)
require.NotEqual(t, "board-id-1", board1.ID) require.NotEqual(t, "board-id-1", board1.ID)
blocks1, err := th.Server.App().GetBlocksForBoard(board1.ID) blocks1, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: board1.ID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks1, 1) require.Len(t, blocks1, 1)
require.Equal(t, "block 1", blocks1[0].Title) require.Equal(t, "block 1", blocks1[0].Title)
@@ -147,7 +147,7 @@ func TestCreateBoardsAndBlocks(t *testing.T) {
require.Equal(t, "private board", board2.Title) require.Equal(t, "private board", board2.Title)
require.Equal(t, model.BoardTypePrivate, board2.Type) require.Equal(t, model.BoardTypePrivate, board2.Type)
require.NotEqual(t, "board-id-2", board2.ID) require.NotEqual(t, "board-id-2", board2.ID)
blocks2, err := th.Server.App().GetBlocksForBoard(board2.ID) blocks2, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: board2.ID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks2, 1) require.Len(t, blocks2, 1)
require.Equal(t, "block 2", blocks2[0].Title) require.Equal(t, "block 2", blocks2[0].Title)

Просмотреть файл

@@ -62,7 +62,7 @@ func TestExportBoard(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, boardsImported, 1) require.Len(t, boardsImported, 1)
boardImported := boardsImported[0] boardImported := boardsImported[0]
blocksImported, err := th.Server.App().GetBlocksForBoard(boardImported.ID) blocksImported, err := th.Server.App().GetBlocks(model.QueryBlocksOptions{BoardID: boardImported.ID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocksImported, 1) require.Len(t, blocksImported, 1)
require.Equal(t, block.Title, blocksImported[0].Title) require.Equal(t, block.Title, blocksImported[0].Title)

Просмотреть файл

@@ -585,6 +585,35 @@ func TestPermissionsGetBoard(t *testing.T) {
}) })
} }
func TestPermissionsGetBoardPublic(t *testing.T) {
ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userAnon, http.StatusUnauthorized, 1},
{"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userTeamMember, http.StatusForbidden, 1},
}
t.Run("plugin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
cfg := th.Server.Config()
cfg.EnablePublicSharedBoards = false
th.Server.UpdateAppConfig()
clients := setupClients(th)
testData := setupData(t, th)
runTestCases(t, ttCases, testData, clients)
})
t.Run("local", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
cfg := th.Server.Config()
cfg.EnablePublicSharedBoards = false
th.Server.UpdateAppConfig()
clients := setupLocalClients(th)
testData := setupData(t, th)
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsPatchBoard(t *testing.T) { func TestPermissionsPatchBoard(t *testing.T) {
ttCases := []TestCase{ ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0},

Просмотреть файл

@@ -176,7 +176,7 @@ type QueryBlocksOptions struct {
ParentID string // if not empty then filter for blocks belonging to specified parent ParentID string // if not empty then filter for blocks belonging to specified parent
BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type
Page int // page number to select when paginating Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited) PerPage int // number of blocks per page (default=0, meaning unlimited)
} }
// QuerySubtreeOptions are query options that can be passed to GetSubTree methods. // QuerySubtreeOptions are query options that can be passed to GetSubTree methods.

Просмотреть файл

@@ -536,66 +536,6 @@ func (mr *MockStoreMockRecorder) GetBlocksComplianceHistory(arg0 interface{}) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBlocksComplianceHistory), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksComplianceHistory", reflect.TypeOf((*MockStore)(nil).GetBlocksComplianceHistory), arg0)
} }
// GetBlocksForBoard mocks base method.
func (m *MockStore) GetBlocksForBoard(arg0 string) ([]*model0.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksForBoard", arg0)
ret0, _ := ret[0].([]*model0.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksForBoard indicates an expected call of GetBlocksForBoard.
func (mr *MockStoreMockRecorder) GetBlocksForBoard(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0)
}
// GetBlocksWithParent mocks base method.
func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]*model0.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithParent", arg0, arg1)
ret0, _ := ret[0].([]*model0.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithParent indicates an expected call of GetBlocksWithParent.
func (mr *MockStoreMockRecorder) GetBlocksWithParent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParent", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParent), arg0, arg1)
}
// GetBlocksWithParentAndType mocks base method.
func (m *MockStore) GetBlocksWithParentAndType(arg0, arg1, arg2 string) ([]*model0.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithParentAndType", arg0, arg1, arg2)
ret0, _ := ret[0].([]*model0.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithParentAndType indicates an expected call of GetBlocksWithParentAndType.
func (mr *MockStoreMockRecorder) GetBlocksWithParentAndType(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithParentAndType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithParentAndType), arg0, arg1, arg2)
}
// GetBlocksWithType mocks base method.
func (m *MockStore) GetBlocksWithType(arg0, arg1 string) ([]*model0.Block, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBlocksWithType", arg0, arg1)
ret0, _ := ret[0].([]*model0.Block)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBlocksWithType indicates an expected call of GetBlocksWithType.
func (mr *MockStoreMockRecorder) GetBlocksWithType(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithType", reflect.TypeOf((*MockStore)(nil).GetBlocksWithType), arg0, arg1)
}
// GetBoard mocks base method. // GetBoard mocks base method.
func (m *MockStore) GetBoard(arg0 string) (*model0.Board, error) { func (m *MockStore) GetBoard(arg0 string) (*model0.Board, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

Просмотреть файл

@@ -105,23 +105,6 @@ func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([
return s.blocksFromRows(rows) return s.blocksFromRows(rows)
} }
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
BlockType: model.BlockType(blockType),
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
ParentID: parentID,
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) { func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Block, error) {
query := s.getQueryBuilder(db). query := s.getQueryBuilder(db).
Select(s.blockFields("")...). Select(s.blockFields("")...).
@@ -148,14 +131,6 @@ func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]*model.Bloc
return blocks, nil return blocks, nil
} }
func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
BlockType: model.BlockType(blockType),
}
return s.getBlocks(db, opts)
}
// getSubTree2 returns blocks within 2 levels of the given blockID. // 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) { func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) {
query := s.getQueryBuilder(db). query := s.getQueryBuilder(db).
@@ -188,13 +163,6 @@ func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string,
return s.blocksFromRows(rows) return s.blocksFromRows(rows)
} }
func (s *SQLStore) getBlocksForBoard(db sq.BaseRunner, boardID string) ([]*model.Block, error) {
opts := model.QueryBlocksOptions{
BoardID: boardID,
}
return s.getBlocks(db, opts)
}
func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]*model.Block, error) { func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
results := []*model.Block{} results := []*model.Block{}

Просмотреть файл

@@ -166,7 +166,7 @@ func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID strin
} }
bab.Boards = []*model.Board{board} bab.Boards = []*model.Board{board}
blocks, err := s.getBlocksForBoard(db, boardID) blocks, err := s.getBlocks(db, model.QueryBlocksOptions{BoardID: boardID})
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

Просмотреть файл

@@ -326,26 +326,6 @@ func (s *SQLStore) GetBlocksComplianceHistory(opts model.QueryBlocksComplianceHi
} }
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]*model.Block, error) {
return s.getBlocksForBoard(s.db, boardID)
}
func (s *SQLStore) GetBlocksWithParent(boardID string, parentID string) ([]*model.Block, error) {
return s.getBlocksWithParent(s.db, boardID, parentID)
}
func (s *SQLStore) GetBlocksWithParentAndType(boardID string, parentID string, blockType string) ([]*model.Block, error) {
return s.getBlocksWithParentAndType(s.db, boardID, parentID, blockType)
}
func (s *SQLStore) GetBlocksWithType(boardID string, blockType string) ([]*model.Block, error) {
return s.getBlocksWithType(s.db, boardID, blockType)
}
func (s *SQLStore) GetBoard(id string) (*model.Board, error) { func (s *SQLStore) GetBoard(id string) (*model.Board, error) {
return s.getBoard(s.db, id) return s.getBoard(s.db, id)

Просмотреть файл

@@ -18,12 +18,8 @@ const CardLimitTimestampSystemKey = "card_limit_timestamp"
// Store represents the abstraction of the data storage. // Store represents the abstraction of the data storage.
type Store interface { type Store interface {
GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error) GetBlocks(opts model.QueryBlocksOptions) ([]*model.Block, error)
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]*model.Block, error)
GetBlocksWithParent(boardID, parentID string) ([]*model.Block, error)
GetBlocksByIDs(ids []string) ([]*model.Block, error) GetBlocksByIDs(ids []string) ([]*model.Block, error)
GetBlocksWithType(boardID, blockType string) ([]*model.Block, error)
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error) GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]*model.Block, error)
GetBlocksForBoard(boardID string) ([]*model.Block, error)
// @withTransaction // @withTransaction
InsertBlock(block *model.Block, userID string) error InsertBlock(block *model.Block, userID string) error
// @withTransaction // @withTransaction

Просмотреть файл

@@ -69,7 +69,7 @@ func testInsertBlock(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
boardID := testBoardID boardID := testBoardID
blocks, errBlocks := store.GetBlocksForBoard(boardID) blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, errBlocks) require.NoError(t, errBlocks)
initialCount := len(blocks) initialCount := len(blocks)
@@ -85,7 +85,7 @@ func testInsertBlock(t *testing.T, store store.Store) {
err := store.InsertBlock(block, "user-id-1") err := store.InsertBlock(block, "user-id-1")
require.NoError(t, err) require.NoError(t, err)
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+1) require.Len(t, blocks, initialCount+1)
@@ -105,7 +105,7 @@ func testInsertBlock(t *testing.T, store store.Store) {
err := store.InsertBlock(block, "user-id-1") err := store.InsertBlock(block, "user-id-1")
require.Error(t, err) require.Error(t, err)
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+1) require.Len(t, blocks, initialCount+1)
}) })
@@ -121,7 +121,7 @@ func testInsertBlock(t *testing.T, store store.Store) {
err := store.InsertBlock(block, "user-id-1") err := store.InsertBlock(block, "user-id-1")
require.Error(t, err) require.Error(t, err)
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+1) require.Len(t, blocks, initialCount+1)
}) })
@@ -204,7 +204,7 @@ func testInsertBlock(t *testing.T, store store.Store) {
func testInsertBlocks(t *testing.T, store store.Store) { func testInsertBlocks(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
blocks, errBlocks := store.GetBlocksForBoard("id-test") blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: "id-test"})
require.NoError(t, errBlocks) require.NoError(t, errBlocks)
initialCount := len(blocks) initialCount := len(blocks)
@@ -227,7 +227,7 @@ func testInsertBlocks(t *testing.T, store store.Store) {
err := store.InsertBlocks(newBlocks, "user-id-1") err := store.InsertBlocks(newBlocks, "user-id-1")
require.Error(t, err) require.Error(t, err)
blocks, err := store.GetBlocksForBoard("id-test") blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: "id-test"})
require.NoError(t, err) require.NoError(t, err)
// no blocks should have been inserted // no blocks should have been inserted
require.Len(t, blocks, initialCount) require.Len(t, blocks, initialCount)
@@ -249,7 +249,7 @@ func testPatchBlock(t *testing.T, store store.Store) {
err := store.InsertBlock(block, "user-id-1") err := store.InsertBlock(block, "user-id-1")
require.NoError(t, err) require.NoError(t, err)
blocks, errBlocks := store.GetBlocksForBoard(boardID) blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, errBlocks) require.NoError(t, errBlocks)
initialCount := len(blocks) initialCount := len(blocks)
@@ -259,7 +259,7 @@ func testPatchBlock(t *testing.T, store store.Store) {
require.ErrorAs(t, err, &nf) require.ErrorAs(t, err, &nf)
require.True(t, model.IsErrNotFound(err)) require.True(t, model.IsErrNotFound(err))
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount) require.Len(t, blocks, initialCount)
}) })
@@ -272,7 +272,7 @@ func testPatchBlock(t *testing.T, store store.Store) {
err := store.PatchBlock("id-test", blockPatch, "user-id-1") err := store.PatchBlock("id-test", blockPatch, "user-id-1")
require.Error(t, err) require.Error(t, err)
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount) require.Len(t, blocks, initialCount)
}) })
@@ -452,7 +452,7 @@ var (
func testGetSubTree2(t *testing.T, store store.Store) { func testGetSubTree2(t *testing.T, store store.Store) {
boardID := testBoardID boardID := testBoardID
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
initialCount := len(blocks) initialCount := len(blocks)
@@ -460,7 +460,7 @@ func testGetSubTree2(t *testing.T, store store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
defer DeleteBlocks(t, store, subtreeSampleBlocks, "test") defer DeleteBlocks(t, store, subtreeSampleBlocks, "test")
blocks, err = store.GetBlocksForBoard(boardID) blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+6) require.Len(t, blocks, initialCount+6)
@@ -492,7 +492,7 @@ func testDeleteBlock(t *testing.T, store store.Store) {
userID := testUserID userID := testUserID
boardID := testBoardID boardID := testBoardID
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
initialCount := len(blocks) initialCount := len(blocks)
@@ -516,7 +516,7 @@ func testDeleteBlock(t *testing.T, store store.Store) {
InsertBlocks(t, store, blocksToInsert, "user-id-1") InsertBlocks(t, store, blocksToInsert, "user-id-1")
defer DeleteBlocks(t, store, blocksToInsert, "test") defer DeleteBlocks(t, store, blocksToInsert, "test")
blocks, err = store.GetBlocksForBoard(boardID) blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+3) require.Len(t, blocks, initialCount+3)
@@ -550,7 +550,7 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
boardID := testBoardID boardID := testBoardID
userID := testUserID userID := testUserID
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
initialCount := len(blocks) initialCount := len(blocks)
@@ -574,7 +574,7 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
InsertBlocks(t, store, blocksToInsert, "user-id-1") InsertBlocks(t, store, blocksToInsert, "user-id-1")
defer DeleteBlocks(t, store, blocksToInsert, "test") defer DeleteBlocks(t, store, blocksToInsert, "test")
blocks, err = store.GetBlocksForBoard(boardID) blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, initialCount+3) require.Len(t, blocks, initialCount+3)
@@ -643,7 +643,7 @@ func testUndeleteBlock(t *testing.T, store store.Store) {
func testGetBlocks(t *testing.T, store store.Store) { func testGetBlocks(t *testing.T, store store.Store) {
boardID := testBoardID boardID := testBoardID
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
blocksToInsert := []*model.Block{ blocksToInsert := []*model.Block{
@@ -686,65 +686,74 @@ func testGetBlocks(t *testing.T, store store.Store) {
InsertBlocks(t, store, blocksToInsert, "user-id-1") InsertBlocks(t, store, blocksToInsert, "user-id-1")
defer DeleteBlocks(t, store, blocksToInsert, "test") defer DeleteBlocks(t, store, blocksToInsert, "test")
t.Run("not existing parent", func(t *testing.T) { t.Run("not existing parent with type", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParentAndType(boardID, "not-exists", "test") opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "not-exists", BlockType: model.BlockType("test")}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, blocks) require.Empty(t, blocks)
}) })
t.Run("not existing type", func(t *testing.T) { t.Run("not existing type with parent", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "not-existing") opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1", BlockType: model.BlockType("not-existing")}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, blocks) require.Empty(t, blocks)
}) })
t.Run("valid parent and type", func(t *testing.T) { t.Run("valid parent and type", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParentAndType(boardID, "block1", "test") opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1", BlockType: model.BlockType("test")}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 2) require.Len(t, blocks, 2)
}) })
t.Run("not existing parent", func(t *testing.T) { t.Run("not existing parent", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParent(boardID, "not-exists") opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "not-exists"}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, blocks) require.Empty(t, blocks)
}) })
t.Run("valid parent", func(t *testing.T) { t.Run("valid parent", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithParent(boardID, "block1") opts := model.QueryBlocksOptions{BoardID: boardID, ParentID: "block1"}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 3) require.Len(t, blocks, 3)
}) })
t.Run("not existing type", func(t *testing.T) { t.Run("not existing type", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithType(boardID, "not-exists") opts := model.QueryBlocksOptions{BoardID: boardID, BlockType: model.BlockType("not-exists")}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, blocks) require.Empty(t, blocks)
}) })
t.Run("valid type", func(t *testing.T) { t.Run("valid type", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksWithType(boardID, "test") opts := model.QueryBlocksOptions{BoardID: boardID, BlockType: model.BlockType("test")}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 4) require.Len(t, blocks, 4)
}) })
t.Run("not existing board", func(t *testing.T) { t.Run("not existing board", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksForBoard("not-exists") opts := model.QueryBlocksOptions{BoardID: "not-exists"}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, blocks) require.Empty(t, blocks)
}) })
t.Run("all blocks of the a board", func(t *testing.T) { t.Run("all blocks of the a board", func(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
blocks, err = store.GetBlocksForBoard(boardID) opts := model.QueryBlocksOptions{BoardID: boardID}
blocks, err = store.GetBlocks(opts)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 5) require.Len(t, blocks, 5)
}) })
@@ -863,7 +872,7 @@ func testDuplicateBlock(t *testing.T, store store.Store) {
func testGetBlockMetadata(t *testing.T, store store.Store) { func testGetBlockMetadata(t *testing.T, store store.Store) {
boardID := testBoardID boardID := testBoardID
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
blocksToInsert := []*model.Block{ blocksToInsert := []*model.Block{
@@ -1082,12 +1091,20 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
require.Nil(t, block) require.Nil(t, block)
// ensure the card children were deleted // ensure the card children were deleted
blocks, err := store.GetBlocksWithParentAndType(cardDelete.BoardID, cardDelete.ID, model.TypeText) blocks, err := store.GetBlocks(model.QueryBlocksOptions{
BoardID: cardDelete.BoardID,
ParentID: cardDelete.ID,
BlockType: model.TypeText},
)
require.NoError(t, err) require.NoError(t, err)
assert.Empty(t, blocks) assert.Empty(t, blocks)
// ensure the other card children remain. // ensure the other card children remain.
blocks, err = store.GetBlocksWithParentAndType(cardKeep.BoardID, cardKeep.ID, model.TypeText) blocks, err = store.GetBlocks(model.QueryBlocksOptions{
BoardID: cardKeep.BoardID,
ParentID: cardKeep.ID,
BlockType: model.TypeText},
)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, blocks, len(blocksKeep)) assert.Len(t, blocks, len(blocksKeep))
@@ -1101,7 +1118,11 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
require.NotNil(t, block) require.NotNil(t, block)
// ensure the card children were restored // ensure the card children were restored
blocks, err = store.GetBlocksWithParentAndType(cardDelete.BoardID, cardDelete.ID, model.TypeText) blocks, err = store.GetBlocks(model.QueryBlocksOptions{
BoardID: cardDelete.BoardID,
ParentID: cardDelete.ID,
BlockType: model.TypeText},
)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, blocks, len(blocksDelete)) assert.Len(t, blocks, len(blocksDelete))
}) })
@@ -1117,12 +1138,12 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
require.Nil(t, board) require.Nil(t, board)
// ensure all cards and blocks for the board were deleted // ensure all cards and blocks for the board were deleted
blocks, err := store.GetBlocksForBoard(boardDelete.ID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardDelete.ID})
require.NoError(t, err) require.NoError(t, err)
assert.Empty(t, blocks) assert.Empty(t, blocks)
// ensure the other board's cards and blocks remain. // ensure the other board's cards and blocks remain.
blocks, err = store.GetBlocksForBoard(boardKeep.ID) blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardKeep.ID})
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, blocks, len(blocksKeep)+len(cardsKeep)) assert.Len(t, blocks, len(blocksKeep)+len(cardsKeep))
@@ -1136,7 +1157,7 @@ func testUndeleteBlockChildren(t *testing.T, store store.Store) {
require.NotNil(t, board) require.NotNil(t, board)
// ensure the board's cards and blocks were restored. // ensure the board's cards and blocks were restored.
blocks, err = store.GetBlocksForBoard(boardDelete.ID) blocks, err = store.GetBlocks(model.QueryBlocksOptions{BoardID: boardDelete.ID})
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete)) assert.Len(t, blocks, len(blocksDelete)+len(cardsDelete))
}) })

Просмотреть файл

@@ -98,7 +98,7 @@ func LoadData(t *testing.T, store store.Store) {
func testRunDataRetention(t *testing.T, store store.Store, batchSize int) { func testRunDataRetention(t *testing.T, store store.Store, batchSize int) {
LoadData(t, store) LoadData(t, store)
blocks, err := store.GetBlocksForBoard(boardID) blocks, err := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, blocks, 4) require.Len(t, blocks, 4)
initialCount := len(blocks) initialCount := len(blocks)
@@ -115,7 +115,7 @@ func testRunDataRetention(t *testing.T, store store.Store, batchSize int) {
require.True(t, deletions > int64(initialCount)) require.True(t, deletions > int64(initialCount))
// expect all blocks to be deleted. // expect all blocks to be deleted.
blocks, errBlocks := store.GetBlocksForBoard(boardID) blocks, errBlocks := store.GetBlocks(model.QueryBlocksOptions{BoardID: boardID})
require.NoError(t, errBlocks) require.NoError(t, errBlocks)
require.Equal(t, 0, len(blocks)) require.Equal(t, 0, len(blocks))

Просмотреть файл

@@ -190,6 +190,8 @@ func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
s["CanReceiveNotifications"] = c.App.SendTestPushNotification(deviceID) s["CanReceiveNotifications"] = c.App.SendTestPushNotification(deviceID)
} }
s["ActiveSearchBackend"] = c.App.ActiveSearchBackend()
if s[model.STATUS] != model.StatusOk { if s[model.STATUS] != model.StatusOk {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
@@ -295,7 +297,7 @@ func databaseRecycle(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
c.App.RecycleDatabaseConnection() c.App.RecycleDatabaseConnection(c.AppContext)
auditRec.Success() auditRec.Success()
ReturnStatusOK(w) ReturnStatusOK(w)
@@ -348,7 +350,7 @@ func queryLogs(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
logs, logerr := c.App.QueryLogs(c.Params.Page, c.Params.LogsPerPage, logFilter) logs, logerr := c.App.QueryLogs(c.AppContext, c.Params.Page, c.Params.LogsPerPage, logFilter)
if logerr != nil { if logerr != nil {
c.Err = logerr c.Err = logerr
return return
@@ -387,7 +389,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
lines, appErr := c.App.GetLogs(c.Params.Page, c.Params.LogsPerPage) lines, appErr := c.App.GetLogs(c.AppContext, c.Params.Page, c.Params.LogsPerPage)
if appErr != nil { if appErr != nil {
c.Err = appErr c.Err = appErr
return return

Просмотреть файл

@@ -117,6 +117,11 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
if team.SchemeId != nil && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementPermissions) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementPermissions)
return
}
rteam, err := c.App.CreateTeamWithUser(c.AppContext, &team, c.AppContext.Session().UserId) rteam, err := c.App.CreateTeamWithUser(c.AppContext, &team, c.AppContext.Session().UserId)
if err != nil { if err != nil {
c.Err = err c.Err = err

Просмотреть файл

@@ -95,6 +95,39 @@ func TestCreateTeam(t *testing.T) {
CheckForbiddenStatus(t, resp) CheckForbiddenStatus(t, resp)
}) })
t.Run("should verify user permissions during team creation", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("custom_permissions_schemes"))
th.App.SetPhase2PermissionsMigrationStatus(true)
sc := th.SystemAdminClient
scheme, _, err := sc.CreateScheme(&model.Scheme{
DisplayName: "dn_" + model.NewId(),
Name: model.NewId(),
Scope: model.SchemeScopeTeam,
})
require.NoError(t, err)
team, _, err := sc.CreateTeam(&model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
SchemeId: &scheme.Id,
})
require.NoError(t, err)
require.Equal(t, scheme.Id, *team.SchemeId)
_, r, err := th.Client.CreateTeam(&model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
SchemeId: &scheme.Id,
})
require.Error(t, err)
CheckForbiddenStatus(t, r)
})
t.Run("should take under consideration the server language when creating a new team", func(t *testing.T) { t.Run("should take under consideration the server language when creating a new team", func(t *testing.T) {
c := th.SystemAdminClient c := th.SystemAdminClient
cfg, _, err := c.GetConfig() cfg, _, err := c.GetConfig()

Просмотреть файл

@@ -11,17 +11,17 @@ import (
"time" "time"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache" "github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n" "github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail" "github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
) )
var latestVersionCache = cache.NewLRU(cache.LRUOptions{ var latestVersionCache = cache.NewLRU(cache.LRUOptions{
Size: 1, Size: 1,
}) })
func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) { func (s *Server) GetLogs(c request.CTX, page, perPage int) ([]string, *model.AppError) {
var lines []string var lines []string
license := s.License() license := s.License()
@@ -33,7 +33,7 @@ func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) {
lines = append(lines, "-----------------------------------------------------------------------------------------------------------") lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
lines = append(lines, "-----------------------------------------------------------------------------------------------------------") lines = append(lines, "-----------------------------------------------------------------------------------------------------------")
} else { } else {
mlog.Error("Could not get cluster info") c.Logger().Error("Could not get cluster info")
} }
} }
@@ -56,7 +56,7 @@ func (s *Server) GetLogs(page, perPage int) ([]string, *model.AppError) {
return lines, nil return lines, nil
} }
func (s *Server) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) { func (s *Server) QueryLogs(c request.CTX, page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
logData := make(map[string][]string) logData := make(map[string][]string)
serverName := "default" serverName := "default"
@@ -66,7 +66,7 @@ func (s *Server) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[s
if info := s.platform.Cluster().GetMyClusterInfo(); info != nil { if info := s.platform.Cluster().GetMyClusterInfo(); info != nil {
serverName = info.Hostname serverName = info.Hostname
} else { } else {
mlog.Error("Could not get cluster info") c.Logger().Error("Could not get cluster info")
} }
} }
@@ -111,12 +111,12 @@ func AddLocalLogs(logData map[string][]string, s *Server, page, perPage int, ser
return nil return nil
} }
func (a *App) QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) { func (a *App) QueryLogs(c request.CTX, page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
return a.Srv().QueryLogs(page, perPage, logFilter) return a.Srv().QueryLogs(c, page, perPage, logFilter)
} }
func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) { func (a *App) GetLogs(c request.CTX, page, perPage int) ([]string, *model.AppError) {
return a.Srv().GetLogs(page, perPage) return a.Srv().GetLogs(c, page, perPage)
} }
func (s *Server) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) { func (s *Server) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) {
@@ -146,15 +146,15 @@ func (s *Server) InvalidateAllCachesSkipSend() {
} }
func (a *App) RecycleDatabaseConnection() { func (a *App) RecycleDatabaseConnection(c request.CTX) {
mlog.Info("Attempting to recycle database connections.") c.Logger().Info("Attempting to recycle database connections.")
// This works by setting 10 seconds as the max conn lifetime for all DB connections. // This works by setting 10 seconds as the max conn lifetime for all DB connections.
// This allows in gradually closing connections as they expire. In future, we can think // This allows in gradually closing connections as they expire. In future, we can think
// of exposing this as a param from the REST api. // of exposing this as a param from the REST api.
a.Srv().Store().RecycleDBConnections(10 * time.Second) a.Srv().Store().RecycleDBConnections(10 * time.Second)
mlog.Info("Finished recycling database connections.") c.Logger().Info("Finished recycling database connections.")
} }
func (a *App) TestSiteURL(siteURL string) *model.AppError { func (a *App) TestSiteURL(siteURL string) *model.AppError {

Просмотреть файл

@@ -264,8 +264,6 @@ type AppIface interface {
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this // MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is relatively small. // function is only exposed to sysadmins and the possibility of this edge case is relatively small.
MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError MoveChannel(c request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError
// NewWebConn returns a new WebConn instance.
NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn
// NotifySessionsExpired is called periodically from the job server to notify any mobile sessions that have expired. // NotifySessionsExpired is called periodically from the job server to notify any mobile sessions that have expired.
NotifySessionsExpired() error NotifySessionsExpired() error
// OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon, // OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon,
@@ -402,6 +400,7 @@ type AppIface interface {
VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError
AccountMigration() einterfaces.AccountMigrationInterface AccountMigration() einterfaces.AccountMigrationInterface
ActivateMfa(userID, token string) *model.AppError ActivateMfa(userID, token string) *model.AppError
ActiveSearchBackend() string
AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError AddChannelsToRetentionPolicy(policyID string, channelIDs []string) *model.AppError
AddConfigListener(listener func(*model.Config, *model.Config)) string AddConfigListener(listener func(*model.Config, *model.Config)) string
AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError AddDirectChannels(c request.CTX, teamID string, user *model.User) *model.AppError
@@ -682,7 +681,7 @@ type AppIface interface {
GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError) GetJobsPage(page int, perPage int) ([]*model.Job, *model.AppError)
GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError)
GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError) GetLatestVersion(latestVersionUrl string) (*model.GithubReleaseInfo, *model.AppError)
GetLogs(page, perPage int) ([]string, *model.AppError) GetLogs(c request.CTX, page, perPage int) ([]string, *model.AppError)
GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError) GetLogsSkipSend(page, perPage int, logFilter *model.LogFilter) ([]string, *model.AppError)
GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError)
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
@@ -961,9 +960,9 @@ type AppIface interface {
PublishUserTyping(userID, channelID, parentId string) *model.AppError PublishUserTyping(userID, channelID, parentId string) *model.AppError
PurgeBleveIndexes() *model.AppError PurgeBleveIndexes() *model.AppError
PurgeElasticsearchIndexes() *model.AppError PurgeElasticsearchIndexes() *model.AppError
QueryLogs(page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) QueryLogs(c request.CTX, page, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError)
ReadFile(path string) ([]byte, *model.AppError) ReadFile(path string) ([]byte, *model.AppError)
RecycleDatabaseConnection() RecycleDatabaseConnection(c request.CTX)
RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError)
RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError)
RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError)

Просмотреть файл

@@ -89,6 +89,23 @@ func (a *OpenTracingAppLayer) ActivateMfa(userID string, token string) *model.Ap
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) ActiveSearchBackend() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ActiveSearchBackend")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ActiveSearchBackend()
return resultVar0
}
func (a *OpenTracingAppLayer) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts app.ChannelMemberOpts) (*model.ChannelMember, *model.AppError) { func (a *OpenTracingAppLayer) AddChannelMember(c request.CTX, userID string, channel *model.Channel, opts app.ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelMember") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AddChannelMember")
@@ -7111,7 +7128,7 @@ func (a *OpenTracingAppLayer) GetLdapGroup(ldapGroupID string) (*model.Group, *m
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) GetLogs(page int, perPage int) ([]string, *model.AppError) { func (a *OpenTracingAppLayer) GetLogs(c request.CTX, page int, perPage int) ([]string, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogs") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLogs")
@@ -7123,7 +7140,7 @@ func (a *OpenTracingAppLayer) GetLogs(page int, perPage int) ([]string, *model.A
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.GetLogs(page, perPage) resultVar0, resultVar1 := a.app.GetLogs(c, page, perPage)
if resultVar1 != nil { if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1)) span.LogFields(spanlog.Error(resultVar1))
@@ -12761,23 +12778,6 @@ func (a *OpenTracingAppLayer) NewPluginAPI(c *request.Context, manifest *model.M
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NewWebConn")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NewWebConn(cfg)
return resultVar0
}
func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError { func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck")
@@ -13624,7 +13624,7 @@ func (a *OpenTracingAppLayer) PurgeElasticsearchIndexes() *model.AppError {
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) QueryLogs(page int, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) { func (a *OpenTracingAppLayer) QueryLogs(c request.CTX, page int, perPage int, logFilter *model.LogFilter) (map[string][]string, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.QueryLogs") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.QueryLogs")
@@ -13636,7 +13636,7 @@ func (a *OpenTracingAppLayer) QueryLogs(page int, perPage int, logFilter *model.
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.QueryLogs(page, perPage, logFilter) resultVar0, resultVar1 := a.app.QueryLogs(c, page, perPage, logFilter)
if resultVar1 != nil { if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1)) span.LogFields(spanlog.Error(resultVar1))
@@ -13668,7 +13668,7 @@ func (a *OpenTracingAppLayer) ReadFile(path string) ([]byte, *model.AppError) {
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) RecycleDatabaseConnection() { func (a *OpenTracingAppLayer) RecycleDatabaseConnection(c request.CTX) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RecycleDatabaseConnection") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RecycleDatabaseConnection")
@@ -13680,7 +13680,7 @@ func (a *OpenTracingAppLayer) RecycleDatabaseConnection() {
}() }()
defer span.Finish() defer span.Finish()
a.app.RecycleDatabaseConnection() a.app.RecycleDatabaseConnection(c)
} }
func (a *OpenTracingAppLayer) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) { func (a *OpenTracingAppLayer) RegenCommandToken(cmd *model.Command) (*model.Command, *model.AppError) {

Просмотреть файл

@@ -352,7 +352,7 @@ func (ch *Channels) syncPlugins() *model.AppError {
} }
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path)) mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path))
if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil { if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.blocked.app_error" && err.Id != "app.plugin.skip_installation.app_error" {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err)) mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err))
} }
}(plugin) }(plugin)
@@ -952,6 +952,11 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa
defer wg.Done() defer wg.Done()
p, err := ch.processPrepackagedPlugin(psPath) p, err := ch.processPrepackagedPlugin(psPath)
if err != nil { if err != nil {
var appErr *model.AppError
// A log line already appears if the plugin is on the blocklist
if errors.As(err, &appErr) && (appErr.Id == "app.plugin.blocked.app_error" || appErr.Id == "app.plugin.skip_installation.app_error") {
return
}
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err)) mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err))
return return
} }

Просмотреть файл

@@ -92,7 +92,10 @@ func (ch *Channels) installPluginFromData(data model.PluginEventData) {
manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways) manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways)
if appErr != nil { if appErr != nil {
// A log line already appears if the plugin is on the blocklist or skipped
if appErr.Id != "app.plugin.blocked.app_error" && appErr.Id != "app.plugin.skip_installation.app_error" {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr)) mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
}
return return
} }
@@ -330,8 +333,8 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD
// Check plugin id is not blocked // Check plugin id is not blocked
if plugin.PluginIDIsBlocked(manifest.Id) { if plugin.PluginIDIsBlocked(manifest.Id) {
mlog.Debug("Skipping installation of plugin since plugin is on blocklist", mlog.String("plugin_id", manifest.Id)) mlog.Debug("Skipping installation of plugin since plugin is on blocklist. Some plugins are blocked because they are built into this version of Mattermost.", mlog.String("plugin_id", manifest.Id))
return nil, nil return nil, model.NewAppError("installExtractedPlugin", "app.plugin.blocked.app_error", map[string]any{"Id": manifest.Id}, "", http.StatusInternalServerError)
} }
// Check for plugins installed with the same ID. // Check for plugins installed with the same ID.
@@ -365,7 +368,7 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD
if version.LTE(existingVersion) { if version.LTE(existingVersion) {
mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id)) mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id))
return nil, nil return nil, model.NewAppError("installExtractedPlugin", "app.plugin.skip_installation.app_error", map[string]any{"Id": manifest.Id}, "", http.StatusInternalServerError)
} }
} }

Просмотреть файл

@@ -172,10 +172,9 @@ func TestInstallPluginLocally(t *testing.T) {
defer th.TearDown() defer th.TearDown()
cleanExistingBundles(t, th) cleanExistingBundles(t, th)
manifest, appErr := installPlugin(t, th, "playbooks", "0.0.1", installPluginLocallyAlways) _, appErr := installPlugin(t, th, "playbooks", "0.0.1", installPluginLocallyAlways)
require.Nil(t, appErr) require.NotNil(t, appErr)
require.Nil(t, manifest) require.Equal(t, "app.plugin.blocked.app_error", appErr.Id)
assertBundleInfoManifests(t, th, []*model.Manifest{}) assertBundleInfoManifests(t, th, []*model.Manifest{})
}) })
@@ -222,9 +221,9 @@ func TestInstallPluginLocally(t *testing.T) {
require.Nil(t, appErr) require.Nil(t, appErr)
require.NotNil(t, existingManifest) require.NotNil(t, existingManifest)
manifest, appErr := installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNewOrUpgrade) _, appErr = installPlugin(t, th, "valid", "0.0.1", installPluginLocallyOnlyIfNewOrUpgrade)
require.Nil(t, appErr) require.NotNil(t, appErr)
require.Nil(t, manifest) require.Equal(t, "app.plugin.skip_installation.app_error", appErr.Id)
assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest}) assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest})
}) })
@@ -238,9 +237,9 @@ func TestInstallPluginLocally(t *testing.T) {
require.Nil(t, appErr) require.Nil(t, appErr)
require.NotNil(t, existingManifest) require.NotNil(t, existingManifest)
manifest, appErr := installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade) _, appErr = installPlugin(t, th, "valid", "0.0.2", installPluginLocallyOnlyIfNewOrUpgrade)
require.Nil(t, appErr) require.NotNil(t, appErr)
require.Nil(t, manifest) require.Equal(t, "app.plugin.skip_installation.app_error", appErr.Id)
assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest}) assertBundleInfoManifests(t, th, []*model.Manifest{existingManifest})
}) })

Просмотреть файл

@@ -60,3 +60,7 @@ func (a *App) PurgeBleveIndexes() *model.AppError {
} }
return nil return nil
} }
func (a *App) ActiveSearchBackend() string {
return a.ch.srv.platform.SearchEngine.ActiveEngine()
}

Просмотреть файл

@@ -260,8 +260,17 @@ func NewServer(options ...Option) (*Server, error) {
product.CommandKey: app, product.CommandKey: app,
} }
// Step 4: Initialize products. // It is important to initialize the hub only after the global logger is set
// Depends on s.httpService. // to avoid race conditions while logging from inside the hub.
// Step 4: Start platform
s.platform.Start()
// NOTE: There should be no call to App.Srv().Channels() before step 5 is done
// otherwise it will throw a panic.
// Step 5: Initialize products.
// Depends on s.httpService, and depends on the hub to be initialized.
// Otherwise we run into race conditions.
err = s.initializeProducts(product.GetProducts(), serviceMap) err = s.initializeProducts(product.GetProducts(), serviceMap)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to initialize products") return nil, errors.Wrap(err, "failed to initialize products")
@@ -275,11 +284,6 @@ func NewServer(options ...Option) (*Server, error) {
} }
app.ch = channelsWrapper.app.ch app.ch = channelsWrapper.app.ch
// It is important to initialize the hub only after the global logger is set
// to avoid race conditions while logging from inside the hub.
// Step 5: Start hub in platform which the hub depends on s.Channels() (step 4)
s.platform.Start()
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Everything below this is not order sensitive and safe to be moved around. // Everything below this is not order sensitive and safe to be moved around.
// If you are adding a new field that is non-channels specific, please add // If you are adding a new field that is non-channels specific, please add

Просмотреть файл

@@ -13,8 +13,3 @@ import (
func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) { func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error) {
return a.Srv().Platform().PopulateWebConnConfig(s, cfg, seqVal) return a.Srv().Platform().PopulateWebConnConfig(s, cfg, seqVal)
} }
// NewWebConn returns a new WebConn instance.
func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
return a.Srv().Platform().NewWebConn(cfg, a, a.ch)
}

Просмотреть файл

@@ -10,7 +10,7 @@ import (
"regexp" "regexp"
"strings" "strings"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client" pbclient "github.com/mattermost/mattermost-server/v6/server/playbooks/client"
fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model" fb_model "github.com/mattermost/mattermost-server/v6/server/boards/model"

Просмотреть файл

@@ -17,7 +17,7 @@ import (
"github.com/mattermost/mattermost-server/v6/server/channels/app/request" "github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates" "github.com/mattermost/mattermost-server/v6/server/channels/app/worktemplates"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client" pbclient "github.com/mattermost/mattermost-server/v6/server/playbooks/client"
) )
func TestGetWorkTemplateCategories(t *testing.T) { func TestGetWorkTemplateCategories(t *testing.T) {

Просмотреть файл

@@ -6,7 +6,7 @@ import (
"errors" "errors"
"net/http" "net/http"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client" pbclient "github.com/mattermost/mattermost-server/v6/server/playbooks/client"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
) )

Просмотреть файл

@@ -10,7 +10,7 @@ import (
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client" pbclient "github.com/mattermost/mattermost-server/v6/server/playbooks/client"
) )
func TestCanBeExecuted(t *testing.T) { func TestCanBeExecuted(t *testing.T) {

Просмотреть файл

@@ -5931,6 +5931,10 @@
"id": "app.oauth.update_app.updating.app_error", "id": "app.oauth.update_app.updating.app_error",
"translation": "We encountered an error updating the app." "translation": "We encountered an error updating the app."
}, },
{
"id": "app.plugin.blocked.app_error",
"translation": "Plugin {{.Id}} is on the block list. Some plugins are blocked because they are built into this version of Mattermost."
},
{ {
"id": "app.plugin.cluster.save_config.app_error", "id": "app.plugin.cluster.save_config.app_error",
"translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled."
@@ -6063,6 +6067,10 @@
"id": "app.plugin.signature_decode.app_error", "id": "app.plugin.signature_decode.app_error",
"translation": "Unable to decode base64 signature." "translation": "Unable to decode base64 signature."
}, },
{
"id": "app.plugin.skip_installation.app_error",
"translation": "Skipping installation of plugin {{.Id}} since existing version is equal or newer."
},
{ {
"id": "app.plugin.store_bundle.app_error", "id": "app.plugin.store_bundle.app_error",
"translation": "Unable to store the plugin to the configured file store." "translation": "Unable to store the plugin to the configured file store."
@@ -7787,6 +7795,10 @@
"id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error", "id": "ent.elasticsearch.indexer.index_batch.nothing_left_to_index.error",
"translation": "Trying to index a new batch when all the entities are completed" "translation": "Trying to index a new batch when all the entities are completed"
}, },
{
"id": "ent.elasticsearch.max_version.app_error",
"translation": "Elasticsearch version {{.Version}} is higher than max supported version of {{.MaxVersion}}"
},
{ {
"id": "ent.elasticsearch.not_started.error", "id": "ent.elasticsearch.not_started.error",
"translation": "Elasticsearch is not started" "translation": "Elasticsearch is not started"
@@ -7855,10 +7867,6 @@
"id": "ent.elasticsearch.search_users.unmarshall_user_failed", "id": "ent.elasticsearch.search_users.unmarshall_user_failed",
"translation": "Failed to decode search results" "translation": "Failed to decode search results"
}, },
{
"id": "ent.elasticsearch.start.already_started.app_error",
"translation": "Elasticsearch is already started."
},
{ {
"id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error", "id": "ent.elasticsearch.start.create_bulk_processor_failed.app_error",
"translation": "Failed to create Elasticsearch bulk processor." "translation": "Failed to create Elasticsearch bulk processor."

Просмотреть файл

@@ -9837,5 +9837,337 @@
{ {
"id": "api.templates.license_up_for_renewal_contact_sales", "id": "api.templates.license_up_for_renewal_contact_sales",
"translation": "Contact Sales" "translation": "Contact Sales"
},
{
"id": "worktemplate.product_teams.sprint_planning.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.sprint_planning.channel",
"translation": "Chat with your team in a channel that connects easily with your boards and integrations."
},
{
"id": "worktemplate.product_teams.sprint_planning.board",
"translation": "Track your team's progress toward weekly goals with sprint breakdowns, prioritisation, owner assignment and comments."
},
{
"id": "worktemplate.product_teams.product_roadmap.channel",
"translation": "Chat with your team about your customers' feedback, prioritisation and get aligned on progress together."
},
{
"id": "worktemplate.product_teams.product_roadmap.board",
"translation": "Use the Product Roadmap board to manage user feedback, assign resources, view deliverables in a calendar view and prioritise issues."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.board",
"translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.product_teams.feature_release.description.board",
"translation": "Keep meetings on track with the Meeting Agenda board. Manage your workload with the Project Tasks board."
},
{
"id": "worktemplate.product_teams.bug_bash.playbook",
"translation": "Use checklists to assign testing areas and automated tasks to run a comprehensive bug bash process. Use a retrospective to review your process and improve it for next time."
},
{
"id": "worktemplate.product_teams.bug_bash.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools such as Jira to track your bug bash progress. These will be downloaded for you."
},
{
"id": "worktemplate.product_teams.bug_bash.channel",
"translation": "Plan and manage bug reports and resolutions in a single channel thats easily accessible to your team and organisation."
},
{
"id": "worktemplate.leadership.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.leadership.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.leadership.goals_and_okrs.board",
"translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.devops.product_release.playbook",
"translation": "Create repeatable workflows that are easy to follow and implement so product releases are reliable and on time."
},
{
"id": "worktemplate.devops.product_release.channel",
"translation": "Chat with your team about daily milestones, any blockers and changes to deliverables, easily and quickly."
},
{
"id": "worktemplate.devops.product_release.board",
"translation": "Use the Product Release board to support your release timeframe and process, ensuring everyone knows which tasks are due."
},
{
"id": "worktemplate.devops.incident_resolution.description.playbook",
"translation": "Use checklists and automation to bring in key team members and share how your incident is tracking toward resolution."
},
{
"id": "worktemplate.devops.incident_resolution.description.channel",
"translation": "Chat with your team about priorities, add stakeholders, provide updates and work toward a resolution in a single channel."
},
{
"id": "worktemplate.devops.incident_resolution.description.board",
"translation": "Use the Incident Resolution board to support repeatable processes and assign defined tasks across the team."
},
{
"id": "worktemplate.companywide.goals_and_okrs.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools such as Zoom to facilitate easy collaboration. These will be downloaded for you."
},
{
"id": "worktemplate.companywide.goals_and_okrs.channel",
"translation": "Chat about your goals and progress with your team, async or real-time and stay up to date with changes in a single channel."
},
{
"id": "worktemplate.companywide.goals_and_okrs.board",
"translation": "Track your team's progress toward organisational goals with the Goals and OKR board. Keep meetings on track with the Meeting Agenda board."
},
{
"id": "worktemplate.companywide.create_project.integration",
"translation": "Increase productivity in your channel by integrating your most commonly used tools. These will be downloaded for you."
},
{
"id": "worktemplate.companywide.create_project.channel",
"translation": "Chat with your team about your new project and decide how youre going to structure it in a collaborative channel."
},
{
"id": "worktemplate.companywide.create_project.board",
"translation": "Use a Kanban board to define and track your project tasks and progress."
},
{
"id": "model.license_record.is_valid.bytes.app_error",
"translation": "Invalid value for bytes when uploading a licence."
},
{
"id": "app.user.run.update_status.title",
"translation": "Status update"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Update status"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Reminder for next update"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.",
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Also mark the run as finished"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Finish run"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Change since last update"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Confirm finish run"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Finish run"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?",
"other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Short summary shown in the timeline"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 characters"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Summary"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Add new task"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Add task"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Description"
},
{
"id": "app.user.new_run.title",
"translation": "Run playbook"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Start run"
},
{
"id": "app.user.new_run.run_name",
"translation": "Run name"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.intro",
"translation": "**Owner** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "You have 0 assigned tasks."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "You have {{.Count}} assigned task that is now due:",
"other": "You have {{.Count}} assigned tasks that are now due:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "You have {{.Count}} assigned task:",
"other": "You have {{.Count}} total assigned tasks:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Your assigned tasks"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Due yesterday"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Due {{.Count}} days ago"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Due today"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Due in {{.Count}} day",
"other": "Due in {{.Count}} days"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "You have **{{.Count}} assigned task due after today**.",
"other": "You have **{{.Count}} assigned tasks due after today**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Please use `/playbook todo` to see all your tasks."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "You have 0 runs currently in progress."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "You have {{.Count}} run currently in progress:",
"other": "You have {{.Count}} runs currently in progress:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Runs in Progress"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "You have 0 runs overdue."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "You have {{.Count}} run overdue for a status update:",
"other": "You have {{.Count}} runs overdue for a status update:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Overdue Status Updates"
},
{
"id": "app.oauth.remove_auth_data_by_client_id.app_error",
"translation": "Unable to remove OAuth data."
},
{
"id": "app.command.execute.error",
"translation": "Unable to execute command."
},
{
"id": "api.server.cws.subscribe_to_newsletter.app_error",
"translation": "CWS Server failed to subscribe to newsletter."
},
{
"id": "api.license.request-trial.bad-request.business-email",
"translation": "Invalid business email for trial"
} }
] ]

Просмотреть файл

@@ -9829,5 +9829,45 @@
{ {
"id": "api.templates.license_up_for_renewal_contact_sales", "id": "api.templates.license_up_for_renewal_contact_sales",
"translation": "Contacteer de verkoopsafdeling" "translation": "Contacteer de verkoopsafdeling"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Vandaag te voldoen"
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Gebruik `/playbook todo` om al je taken te zien."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Je hebt momenteel 0 runs lopen."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Runs in uitvoering"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Je hebt 0 runs achterstand."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Achterstallige statusupdates"
},
{
"id": "app.command.execute.error",
"translation": "Kan commando niet uitvoeren."
},
{
"id": "api.server.cws.subscribe_to_newsletter.app_error",
"translation": "CWS-server kan zich niet abonneren op nieuwsbrief."
},
{
"id": "api.server.cws.needs_enterprise_edition",
"translation": "Dienst alleen beschikbaar in Mattermost Enterprise editie"
},
{
"id": "api.license.request-trial.bad-request.business-email",
"translation": "Ongeldig zakelijk e-mailadres voor proefperiode"
} }
] ]

Просмотреть файл

@@ -1505,7 +1505,7 @@
}, },
{ {
"id": "api.slackimport.slack_add_channels.added", "id": "api.slackimport.slack_add_channels.added",
"translation": "\nKanallar eklendi:\n" "translation": "\nEklenen kanallar:\n"
}, },
{ {
"id": "api.slackimport.slack_add_channels.failed_to_add_user", "id": "api.slackimport.slack_add_channels.failed_to_add_user",
@@ -1521,7 +1521,7 @@
}, },
{ {
"id": "api.slackimport.slack_add_users.created", "id": "api.slackimport.slack_add_users.created",
"translation": "\nKullanıcılar eklendi:\n" "translation": "\nEklenen kullanıcılar:\n"
}, },
{ {
"id": "api.slackimport.slack_add_users.email_pwd", "id": "api.slackimport.slack_add_users.email_pwd",
@@ -9508,7 +9508,7 @@
}, },
{ {
"id": "worktemplate.category.product_teams", "id": "worktemplate.category.product_teams",
"translation": "Ürün takımları" "translation": "Ürün"
}, },
{ {
"id": "model.draft.is_valid.user_id.app_error", "id": "model.draft.is_valid.user_id.app_error",
@@ -9833,5 +9833,345 @@
{ {
"id": "api.command_templates.desc", "id": "api.command_templates.desc",
"translation": "Kalıptan oluştur penceresini aç" "translation": "Kalıptan oluştur penceresini aç"
},
{
"id": "worktemplate.product_teams.sprint_planning.board",
"translation": "Acil sorunlar, önceliklendirme, sahip atama ve yorumlarla ekibinizin haftalık hedeflere doğru ilerlemesini izleyin."
},
{
"id": "worktemplate.product_teams.sprint_planning.integration",
"translation": "Zoom gibi sık kullandığınız araçlar ile bütünleştirerek kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.product_teams.sprint_planning.channel",
"translation": "Panolarınız ve bütünleştirmelerinizle kolayca bağlantı kuran bir kanalda ekibinizle sohbet edin."
},
{
"id": "worktemplate.product_teams.product_roadmap.channel",
"translation": "Müşterilerinizin geri bildirimleri ve önceliklendirme hakkında ekibinizle sohbet ederek birlikte ilerleme kaydedin."
},
{
"id": "worktemplate.product_teams.product_roadmap.board",
"translation": "Kullanıcı geri bildirimlerini yönetmek, kaynak atamak, çıktıları takvimde görüntülemek ve sorunları önceliklendirmek için ürün yol haritası panosunu kullanın."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.integration",
"translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.channel",
"translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın."
},
{
"id": "worktemplate.product_teams.goals_and_okrs.board",
"translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin."
},
{
"id": "worktemplate.product_teams.feature_release.description.playbook",
"translation": "Özellik geliştirme sürecinizi destekleyen görev kontrol listeleri ve otomasyon ile işlevsel ekipler arasında işbirliğini artırın. İşiniz bittiğinde bir geçmiş değerlendirmesi yaparak ve süreci bir sonraki sürümünüz için iyileştirin."
},
{
"id": "worktemplate.product_teams.feature_release.description.integration",
"translation": "Özellikleri yayınlamak için GitHub gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.product_teams.feature_release.description.channel",
"translation": "Panolarınıza, senaryolarınıza ve diğer bütünleştirmelere kolayca bağlanan bir kanalda sürüm engelleyicileri ve değişiklikler hakkında ekibinizle sohbet edin."
},
{
"id": "worktemplate.product_teams.feature_release.description.board",
"translation": "Toplantı gündemi panosu ile toplantıları izleyin. Proje görevleri panosu ile iş yükünüzü yönetin."
},
{
"id": "worktemplate.product_teams.bug_bash.playbook",
"translation": "Kapsamlı bir hata ayıklama süreci yürütmek için deneme alanları ve otomatik görevler atamak üzere kontrol listeleri kullanın. Sürecinizi gözden geçirmek ve gelecek sefer daha iyi olmasını sağlamak için bir geçmiş değerlendirmesi yapın."
},
{
"id": "worktemplate.product_teams.bug_bash.integration",
"translation": "Hata ayıklama işlerinizi kolaylaştırmak için Jira gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.product_teams.bug_bash.channel",
"translation": "Hata raporlarını ve çözümlerini ekibinizin ve kuruluşunuzun kolayca erişebileceği tek bir kanalda planlayın ve yönetin."
},
{
"id": "worktemplate.leadership.goals_and_okrs.integration",
"translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.leadership.goals_and_okrs.channel",
"translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın."
},
{
"id": "worktemplate.leadership.goals_and_okrs.board",
"translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin."
},
{
"id": "worktemplate.devops.product_release.playbook",
"translation": "Ürün çıkışlarının güvenilir ve zamanında olması için izlenmesi ve uygulanması kolay, yinelenebilir iş akışları oluşturun."
},
{
"id": "worktemplate.devops.product_release.channel",
"translation": "Ekibinizle günlük kilometre taşları, engeller ve çıktılardaki değişiklikler hakkında kolay ve hızlı bir şekilde sohbet edin."
},
{
"id": "worktemplate.devops.product_release.board",
"translation": "Sürüm zaman çerçevenizi ve sürecinizi desteklemek için ürün çıkarma panosunu kullanın ve herkesin hangi görevlerin bitiş zamanının geldiğini bilmesini sağlayın."
},
{
"id": "worktemplate.devops.incident_resolution.description.playbook",
"translation": "Kilit ekip üyelerini bir araya getirmek için kontrol listeleri ve otomasyon kullanarak olayın çözüme doğru ilerlemesini paylaşın."
},
{
"id": "worktemplate.devops.incident_resolution.description.channel",
"translation": "Ekibinizle tek bir kanal kullanarak öncelikler hakkında sohbet edin, paydaşlar ekleyin, güncellemeler yayınlayın ve çözüm için çalışın."
},
{
"id": "worktemplate.devops.incident_resolution.description.board",
"translation": "Yinelenebilen süreçleri desteklemek ve ekip genelinde tanımlanmış görevler atamak için olay çözümleme panosunu kullanın."
},
{
"id": "worktemplate.companywide.goals_and_okrs.integration",
"translation": "İşbirliğini kolaylaştırmak için Zoom gibi sık kullandığınız araçları bütünleştirin ve kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.companywide.goals_and_okrs.channel",
"translation": "Hedefleriniz ve ilerlemeniz hakkında ekibinizle farklı zamanlarda ya da gerçek zamanlı olarak sohbet edin ve değişiklikleri tek bir kanaldan izleyerek güncel kalın."
},
{
"id": "worktemplate.companywide.goals_and_okrs.board",
"translation": "Amaçlar ve anahtar sonuçlar (OKR) panosu ile ekibinizin kurumsal hedeflere doğru ilerlemesini izleyin. Toplantı gündemi panosu ile toplantıları izleyin."
},
{
"id": "model.license_record.is_valid.bytes.app_error",
"translation": "Bir lisans yüklenirken bayt değeri geçersiz."
},
{
"id": "worktemplate.companywide.create_project.integration",
"translation": "Sık kullandığınız araçlar ile bütünleştirerek kanalınızdaki üretkenliği artırın. Bunlar sizin için indirilir."
},
{
"id": "worktemplate.companywide.create_project.channel",
"translation": "Ekibinizle yeni projeniz üzerine sohbet edin ve işbirlikli bir kanalda projeyi nasıl yapılandıracağınıza karar verin."
},
{
"id": "worktemplate.companywide.create_project.board",
"translation": "Proje görevlerinizi ve ilerlemenizi tanımlamak ve izlemek için bir Kanban panosu kullanın."
},
{
"id": "app.user.run.update_status.title",
"translation": "Durum güncellemesi"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Durumu güncelle"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Sonraki güncelleme anımsatıcısı"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak.",
"other": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Ayrıca oyunu da tamamlanmış olarak işaretle"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Oyunu tamamla"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Son güncellemeden sonraki değişiklik"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini etkinleştirdi"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini devre dışı bıraktı"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}}, [{{.RunName}}]({{.RunURL}}) için bir durum güncellemesi istedi. \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} bir oyun katılımcısı ve bu kanala katılmak istiyor. Kanalın herhangi bir üyesi onu çağırabilir.\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Oyunu tamamlamayı onayla"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Oyunu tamamla"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?",
"other": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Oyun zaman akışına ekle"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Zaman akışında görüntülenecek kısa açıklama"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "En fazla 64 karakter"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Özet"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Oyun zaman akışına ekle"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Senaryo oyunu"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Yeni görev ekle"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Görev ekle"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Ad"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Açıklama"
},
{
"id": "app.user.new_run.title",
"translation": "Senaryoyu oyna"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Oyunu başlat"
},
{
"id": "app.user.new_run.run_name",
"translation": "Oyun adı"
},
{
"id": "app.user.new_run.playbook",
"translation": "Senaryo"
},
{
"id": "app.user.new_run.intro",
"translation": "**Sahibi** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Size atanmış bir görev yok."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Süresi dolmuş {{.Count}} atanmış göreviniz var:",
"other": "Süresi dolmuş {{.Count}} atanmış göreviniz var:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "{{.Count}} atanmış göreviniz var:",
"other": "{{.Count}} atanmış göreviniz var:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Atanmış görevleriniz"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Süresi dün doldu"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Süresi {{.Count}} gün önce doldu"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Süresi bugün dolacak"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "{{.Count}} gün içinde süresi dolacak",
"other": "{{.Count}} gün içinde süresi dolacak"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**.",
"other": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Tüm görevlerinizi görüntülemek için `/playbook todo` kullanın."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Süren bir oyununuz yok."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Süren {{.Count}} oyununuz var:",
"other": "Süren {{.Count}} oyununuz var:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Süren oyunlar"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Gecikmiş bir oyununuz yok."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Bir durum güncellemesi için {{.Count}} oyun gecikmeniz var:",
"other": "Bir durum güncellemesi için {{.Count}} oyun gecikmeniz var:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Gecikmiş durum güncellemeleri"
},
{
"id": "app.command.execute.error",
"translation": "Komut yürütülemedi."
},
{
"id": "api.server.cws.subscribe_to_newsletter.app_error",
"translation": "CWS sunucusu duyurulara abone olamadı."
},
{
"id": "api.license.request-trial.bad-request.business-email",
"translation": "Deneme için iş e-postası geçersiz"
} }
] ]

Просмотреть файл

@@ -45,8 +45,19 @@ func (seb *Broker) GetActiveEngines() []SearchEngineInterface {
if seb.ElasticsearchEngine != nil && seb.ElasticsearchEngine.IsActive() { if seb.ElasticsearchEngine != nil && seb.ElasticsearchEngine.IsActive() {
engines = append(engines, seb.ElasticsearchEngine) engines = append(engines, seb.ElasticsearchEngine)
} }
if seb.BleveEngine != nil && seb.BleveEngine.IsActive() { if seb.BleveEngine != nil && seb.BleveEngine.IsActive() && seb.BleveEngine.IsIndexingEnabled() {
engines = append(engines, seb.BleveEngine) engines = append(engines, seb.BleveEngine)
} }
return engines return engines
} }
func (seb *Broker) ActiveEngine() string {
activeEngines := seb.GetActiveEngines()
if len(activeEngines) > 0 {
return activeEngines[0].GetName()
}
if *seb.cfg.SqlSettings.DisableDatabaseSearch {
return "none"
}
return "database"
}

Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package searchengine
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine/mocks"
"github.com/stretchr/testify/assert"
)
func TestActiveEngine(t *testing.T) {
cfg := &model.Config{}
cfg.SetDefaults()
b := NewBroker(cfg)
esMock := &mocks.SearchEngineInterface{}
esMock.On("IsActive").Return(true)
esMock.On("GetName").Return("elasticsearch")
bleveMock := &mocks.SearchEngineInterface{}
bleveMock.On("IsActive").Return(true)
bleveMock.On("IsIndexingEnabled").Return(true)
bleveMock.On("GetName").Return("bleve")
assert.Equal(t, "database", b.ActiveEngine())
b.ElasticsearchEngine = esMock
assert.Equal(t, "elasticsearch", b.ActiveEngine())
b.ElasticsearchEngine = nil
b.BleveEngine = bleveMock
assert.Equal(t, "bleve", b.ActiveEngine())
b.BleveEngine = nil
*b.cfg.SqlSettings.DisableDatabaseSearch = true
assert.Equal(t, "none", b.ActiveEngine())
}

Просмотреть файл

@@ -1405,6 +1405,10 @@ paths:
type: boolean type: boolean
description: A boolean indicating whether the playbook runs created from this playbook should be public or private. description: A boolean indicating whether the playbook runs created from this playbook should be public or private.
example: true example: true
public:
type: boolean
description: A boolean indicating whether the playbook is licensed as public or private. Required 'true' for free tier.
example: true
checklists: checklists:
type: array type: array
description: The stages defined by this playbook. description: The stages defined by this playbook.

Просмотреть файл

@@ -1,3 +1,2 @@
save-exact=true save-exact=true
legacy-peer-deps=true legacy-peer-deps=true
global-style=true

28
webapp/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,28 @@
# Mattermost Web App
This folder contains the client code for the Mattermost web app. It's broken up into multiple packages each of which either contains an area of the app (such as `playbooks` or `boards`) or shared logic used across other packages (such as the packages located in the `platform` directory). For anyone who's used to working in [the mattermost/mattermost-webapp repo](https://github.com/mattermost/mattermost-webapp), most of that is now located in `channels`.
## npm Workspaces
To interact with a workspace using npm, such as to add a dependency or run a script, use the `--workspace` (or `--workspaces`) flag. This can be done when using built-in npm commands such as `npm add` or when running scripts. Those commands should be run from this directory.
```sh
# Add a dependency to a single package
npm add react --workspace=boards
# Build multiple packages
npm run build --workspace=packages/client --workspace=packages/components
# Test all workspaces
npm test --workspaces
# Clean all workspaces that have a clean script defined
npm run clean --workspaces --if-present
```
To install dependencies for a workspace, simply run `npm install` from this folder as you would do normally. Most packages' dependencies will be included in the root `node_modules`, and all packages' dependencies will appear in the `package-lock.json`. A `node_modules` will only be created inside a package if one of its dependencies conflicts with that of another package.
## Useful Links
- [Developer setup](https://developers.mattermost.com/contribute/developer-setup/), now included with the Mattermost server developer setup
- [Web app developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/)

Просмотреть файл

@@ -91,9 +91,6 @@
"error", "error",
{ "allowSameFolder": true, "rootDir": "webapp/boards"} { "allowSameFolder": true, "rootDir": "webapp/boards"}
], ],
/* "no-restricted-imports": ["error", {
"patterns": ["..*"]
}], */
"import-newlines/enforce": [ "import-newlines/enforce": [
2, 2,
3 3
@@ -101,7 +98,8 @@
"object-curly-spacing": [ "object-curly-spacing": [
2, 2,
"never" "never"
] ],
"formatjs/no-multiple-whitespaces": 2
}, },
"overrides": [ "overrides": [
{ {

Просмотреть файл

@@ -1,5 +1,5 @@
{ {
"AppBar.Tooltip": "تغییر وضعیت تخته‌های مرتبط", "AppBar.Tooltip": "تغییر وضعیت تابلوهای مرتبط",
"Attachment.Attachment-title": "ضمیمه", "Attachment.Attachment-title": "ضمیمه",
"AttachmentBlock.DeleteAction": "حذف", "AttachmentBlock.DeleteAction": "حذف",
"AttachmentBlock.addElement": "افزودن {type}", "AttachmentBlock.addElement": "افزودن {type}",

Просмотреть файл

@@ -5,7 +5,7 @@
"BoardComponent.hidden-columns": "დამალული სვეტები", "BoardComponent.hidden-columns": "დამალული სვეტები",
"BoardComponent.hide": "დამალვა", "BoardComponent.hide": "დამალვა",
"BoardComponent.new": "+ ახალი", "BoardComponent.new": "+ ახალი",
"BoardComponent.no-property": "არ არის {Property}", "BoardComponent.no-property": "არ არის {property}",
"BoardComponent.no-property-title": "ცარიელი {property} საკუთრების მქონე ელემენტები აქ წავა. ამ სვეტის წაშლა შეუძლებელია.", "BoardComponent.no-property-title": "ცარიელი {property} საკუთრების მქონე ელემენტები აქ წავა. ამ სვეტის წაშლა შეუძლებელია.",
"BoardComponent.show": "ჩვენება", "BoardComponent.show": "ჩვენება",
"BoardMember.schemeAdmin": "ადმინისტრატორი", "BoardMember.schemeAdmin": "ადმინისტრატორი",

Просмотреть файл

@@ -1 +1,456 @@
{} {
"AdminBadge.SystemAdmin": "Administratorius",
"AdminBadge.TeamAdmin": "Komandos administratorius",
"AppBar.Tooltip": "Perjungti susietas lentas",
"Attachment.Attachment-title": "Priedas",
"AttachmentBlock.DeleteAction": "Ištrinti",
"AttachmentBlock.addElement": "pridėti {type}",
"AttachmentBlock.delete": "Priedas ištrintas.",
"AttachmentBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.",
"AttachmentBlock.upload": "Priedo įkėlimas.",
"AttachmentBlock.uploadSuccess": "Priedas įkeltas.",
"AttachmentElement.delete-confirmation-dialog-button-text": "Ištrinti",
"AttachmentElement.download": "Parsisiųsti",
"AttachmentElement.upload-percentage": "Įkeliama...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Pridėti grupę",
"BoardComponent.delete": "Ištrinti",
"BoardComponent.hidden-columns": "Paslėpti stulpeliai",
"BoardComponent.hide": "Slėpti",
"BoardComponent.new": "+ Naujas",
"BoardComponent.no-property": "Nėra {property}",
"BoardComponent.no-property-title": "Elementai su tuščia nuosavybe {property} pateks čia. Šio stulpelio pašalinti negalima.",
"BoardComponent.show": "Rodyti",
"BoardMember.schemeAdmin": "Administratorius",
"BoardMember.schemeCommenter": "Komentuotojas",
"BoardMember.schemeEditor": "Redaktorius",
"BoardMember.schemeNone": "Joks",
"BoardMember.schemeViewer": "Žiūriklis",
"BoardMember.unlinkChannel": "Atsieti",
"BoardPage.newVersion": "Yra nauja Boards versija. Spustelėkite čia, kad įkeltumėte iš naujo.",
"BoardPage.syncFailed": "Lenta gali būti ištrinta arba jos prieiga gali būti atšaukta.",
"BoardTemplateSelector.add-template": "Sukurti naują šabloną",
"BoardTemplateSelector.create-empty-board": "Sukurti tuščią lentą",
"BoardTemplateSelector.delete-template": "Ištrinti",
"BoardTemplateSelector.description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.",
"BoardTemplateSelector.edit-template": "Redaguoti",
"BoardTemplateSelector.plugin.no-content-description": "Pridėkite lentą prie šoninės juostos naudodami bet kurį iš toliau nurodytų šablonų arba pradėkite nuo nulio.",
"BoardTemplateSelector.plugin.no-content-title": "Sukurti lentą",
"BoardTemplateSelector.title": "Sukurti lentą",
"BoardTemplateSelector.use-this-template": "Naudoti šį šabloną",
"BoardsSwitcher.Title": "Rasti lentas",
"BoardsUnfurl.Limited": "Papildoma informacija yra paslėpta, nes kortelė yra archyvuojama",
"BoardsUnfurl.Remainder": "+{remainder} dar",
"BoardsUnfurl.Updated": "Atnaujinta {time}",
"Calculations.Options.average.displayName": "Vidutinis",
"Calculations.Options.average.label": "Vidutinis",
"Calculations.Options.count.displayName": "Skaičius",
"Calculations.Options.count.label": "Skaičius",
"Calculations.Options.countChecked.displayName": "Pažymėta",
"Calculations.Options.countChecked.label": "Skaičius patikrintas",
"Calculations.Options.countUnchecked.displayName": "Nepažymėta",
"Calculations.Options.countUnchecked.label": "Skaičiaus žymėjimas panaikintas",
"Calculations.Options.countUniqueValue.displayName": "Unikalus",
"Calculations.Options.countUniqueValue.label": "Suskaičiuoti unikalias reikšmes",
"Calculations.Options.countValue.displayName": "Reikšmės",
"Calculations.Options.countValue.label": "Skaičius",
"Calculations.Options.dateRange.displayName": "Diapazonas",
"Calculations.Options.dateRange.label": "Diapazonas",
"Calculations.Options.earliest.displayName": "Anksčiausiai",
"Calculations.Options.earliest.label": "Anksčiausiai",
"Calculations.Options.latest.displayName": "Naujausias",
"Calculations.Options.latest.label": "Naujausias",
"Calculations.Options.max.displayName": "Maks.",
"Calculations.Options.max.label": "Maks.",
"Calculations.Options.median.displayName": "Mediana",
"Calculations.Options.median.label": "Mediana",
"Calculations.Options.min.displayName": "Min.",
"Calculations.Options.min.label": "Min.",
"Calculations.Options.none.displayName": "Apskaičiuoti",
"Calculations.Options.none.label": "Joks",
"Calculations.Options.percentChecked.displayName": "Pažymėta",
"Calculations.Options.percentChecked.label": "Patikrinta procentais",
"Calculations.Options.percentUnchecked.displayName": "Nepažymėta",
"Calculations.Options.percentUnchecked.label": "Nepažymėtas procentas",
"Calculations.Options.range.displayName": "Diapazonas",
"Calculations.Options.range.label": "Diapazonas",
"Calculations.Options.sum.displayName": "Iš viso",
"Calculations.Options.sum.label": "Iš viso",
"CalendarCard.untitled": "Be pavadinimo",
"CardActionsMenu.copiedLink": "Nukopijuota!",
"CardActionsMenu.copyLink": "Kopijuoti nuorodą",
"CardActionsMenu.delete": "Ištrinti",
"CardActionsMenu.duplicate": "Pasikartojantis",
"CardBadges.title-checkboxes": "Žymimieji langeliai",
"CardBadges.title-comments": "Komentarai",
"CardBadges.title-description": "Ši kortelė turi aprašymą",
"CardDetail.Attach": "Prisegti",
"CardDetail.Follow": "Sekti",
"CardDetail.Following": "Sekama",
"CardDetail.add-content": "Pridėti turinį",
"CardDetail.add-icon": "Pridėti piktogramą",
"CardDetail.add-property": "+ Pridėti savybę",
"CardDetail.addCardText": "pridėti kortelės tekstą",
"CardDetail.limited-body": "Naujovinkite į mūsų Professional arba Enterprise planą.",
"CardDetail.limited-button": "Patobulinti",
"CardDetail.limited-title": "Ši kortelė paslėpta",
"CardDetail.moveContent": "Perkelti kortelės turinį",
"CardDetail.new-comment-placeholder": "Pridėti komentarą...",
"CardDetailProperty.confirm-delete-heading": "Patvirtinkite nuosavybės ištrynimą",
"CardDetailProperty.confirm-delete-subtext": "Ar tikrai norite ištrinti savybę „{propertyName}“? Ją ištrynus, savybė bus ištrinta iš visų šios lentos kortelių.",
"CardDetailProperty.confirm-property-name-change-subtext": "Ar tikrai norite pakeisti savybę „{propertyName}“ {customText}? Tai turės įtakos {numOfCards} kortelės reikšmei (-ėms) šioje lentoje ir duomenys gali būti prarasti.",
"CardDetailProperty.confirm-property-type-change": "Patvirtinkite nuosavybės tipo pakeitimą",
"CardDetailProperty.delete-action-button": "Ištrinti",
"CardDetailProperty.property-change-action-button": "Keisti savybę",
"CardDetailProperty.property-changed": "Savybė sėkmingai pakeista!",
"CardDetailProperty.property-deleted": "{propertyName} sėkmingai ištrinta!",
"CardDetailProperty.property-name-change-subtext": "įveskite iš „{oldPropType}“ į „{newPropType}“",
"CardDetial.limited-link": "Sužinokite daugiau apie mūsų planus.",
"CardDialog.delete-confirmation-dialog-attachment": "Patvirtinkite priedo ištrynimą",
"CardDialog.delete-confirmation-dialog-button-text": "Ištrinti",
"CardDialog.delete-confirmation-dialog-heading": "Patvirtinkite kortelės ištrynimą",
"CardDialog.editing-template": "Jūs redaguojate šabloną.",
"CardDialog.nocard": "Šios kortelės nėra arba ji nepasiekiama.",
"Categories.CreateCategoryDialog.CancelText": "Atšaukti",
"Categories.CreateCategoryDialog.CreateText": "Sukurti",
"Categories.CreateCategoryDialog.Placeholder": "Pavadinkite savo kategoriją",
"Categories.CreateCategoryDialog.UpdateText": "Atnaujinti",
"CenterPanel.Login": "Prisijungti",
"CenterPanel.Share": "Bendrinti",
"ChannelIntro.CreateBoard": "Sukurti lentą",
"ColorOption.selectColor": "Pasirinkti {color} spalvą",
"Comment.delete": "Ištrinti",
"CommentsList.send": "Siųsti",
"ConfirmPerson.empty": "Tuščias",
"ConfirmPerson.search": "Ieškoti...",
"ConfirmationDialog.cancel-action": "Atšaukti",
"ConfirmationDialog.confirm-action": "Patvirtinti",
"ContentBlock.Delete": "Ištrinti",
"ContentBlock.DeleteAction": "Ištrinti",
"ContentBlock.addElement": "pridėti {type}",
"ContentBlock.checkbox": "žymimasis langelis",
"ContentBlock.divider": "skirtukas",
"ContentBlock.editCardCheckbox": "perjungtas žymimasis laukelis",
"ContentBlock.editCardCheckboxText": "redaguoti kortelės tekstą",
"ContentBlock.editCardText": "redaguoti kortelės tekstą",
"ContentBlock.editText": "Keisti tekstą...",
"ContentBlock.image": "Paveikslėlis",
"ContentBlock.insertAbove": "Įdėti aukščiau",
"ContentBlock.moveBlock": "perkelti kortelės turinį",
"ContentBlock.moveDown": "Perkelti žemyn",
"ContentBlock.moveUp": "Perkelti aukštyn",
"ContentBlock.text": "tekstas",
"DateFilter.empty": "Tuščias",
"DateRange.clear": "Išvalyti",
"DateRange.empty": "Tuščias",
"DateRange.endDate": "Pabaigos data",
"DateRange.today": "Šiandien",
"DeleteBoardDialog.confirm-cancel": "Atšaukti",
"DeleteBoardDialog.confirm-delete": "Ištrinti",
"DeleteBoardDialog.confirm-info": "Ar tikrai norite ištrinti lentą „{boardTitle}“? Ją ištrynus, bus ištrintos visos lentos kortelės.",
"DeleteBoardDialog.confirm-info-template": "Ar tikrai norite ištrinti lentos šabloną „{boardTitle}“?",
"DeleteBoardDialog.confirm-tite": "Patvirtinkite lentos ištrynimą",
"DeleteBoardDialog.confirm-tite-template": "Patvirtinkite lentos šablono ištrynimą",
"Dialog.closeDialog": "Uždaryti dialogo langą",
"EditableDayPicker.today": "Šiandien",
"Error.mobileweb": "Mobiliojo žiniatinklio palaikymas šiuo metu yra ankstyvoje beta versijoje. Gali būti ne visos funkcijos.",
"Error.websocket-closed": "Interneto lizdo ryšys uždarytas, ryšys nutrauktas. Jei tai išlieka, patikrinkite savo serverio arba žiniatinklio tarpinio serverio konfigūraciją.",
"Filter.contains": "turi",
"Filter.ends-with": "baigiasi",
"Filter.includes": "apima",
"Filter.is": "yra",
"Filter.is-after": "yra po",
"Filter.is-before": "yra prieš",
"Filter.is-empty": "yra tuščias",
"Filter.is-not-empty": "nėra tuščias",
"Filter.is-not-set": "nėra nustatytas",
"Filter.is-set": "yra nustatytas",
"Filter.isafter": "yra po",
"Filter.isbefore": "yra prieš",
"Filter.not-contains": "nėra",
"Filter.not-ends-with": "nesibaigia",
"Filter.not-includes": "neapima",
"Filter.not-starts-with": "neprasideda",
"Filter.starts-with": "prasideda",
"FilterByText.placeholder": "filtruoti tekstą",
"FilterComponent.add-filter": "+ Pridėti filtrą",
"FilterComponent.delete": "Ištrinti",
"FilterValue.empty": "(tuščia)",
"FindBoardsDialog.IntroText": "Ieškoti lentų",
"FindBoardsDialog.NoResultsFor": "Nėra rezultatų pagal „{searchQuery}“",
"FindBoardsDialog.NoResultsSubtext": "Patikrinkite rašybą arba pabandykite atlikti kitą paiešką.",
"FindBoardsDialog.SubTitle": "Įveskite, kad rastumėte lentą. Norėdami naršyti, naudokite <b>AUKŠTYN / ŽEMYN</b>. <b>ENTER</b> , kad pasirinktumėte, <b>ESC</b> , kad atsisakytumėte",
"FindBoardsDialog.Title": "Raskite lentas",
"GroupBy.hideEmptyGroups": "Slėpti {count} tuščias grupes",
"GroupBy.showHiddenGroups": "Rodyti paslėptas grupes: {count}",
"GroupBy.ungroup": "Išgrupuoti",
"HideBoard.MenuOption": "Paslėpti lentą",
"KanbanCard.untitled": "Be pavadinimo",
"MentionSuggestion.is-not-board-member": "(ne lentos narys)",
"Mutator.new-board-from-template": "nauja lenta pagal šabloną",
"Mutator.new-card-from-template": "nauja kortelė pagal šabloną",
"Mutator.new-template-from-card": "naujas šablonas pagal kortelę",
"OnboardingTour.AddComments.Body": "Galite komentuoti problemas ir net @paminėti kitus Mattermost naudotojus, kad atkreiptumėte jų dėmesį.",
"OnboardingTour.AddComments.Title": "Pridėti komentarų",
"OnboardingTour.AddDescription.Body": "Pridėkite aprašymą prie savo kortelės, kad komandos draugai žinotų, kam ji skirta.",
"OnboardingTour.AddDescription.Title": "Pridėti aprašymą",
"OnboardingTour.AddProperties.Body": "Pridėkite prie kortelių įvairių savybių, kad jos būtų dar galingesnės.",
"OnboardingTour.AddProperties.Title": "Pridėti savybių",
"OnboardingTour.AddView.Body": "Eikite čia, kad sukurtumėte naują rodinį ir galėtumėte tvarkyti lentą naudodami skirtingus išdėstymus.",
"OnboardingTour.AddView.Title": "Pridėti naują rodinį",
"OnboardingTour.CopyLink.Body": "Galite bendrinti savo korteles su komandos draugais nukopijuodami nuorodą ir įklijuodami ją į kanalą, tiesioginį pranešimą ar grupės pranešimą.",
"OnboardingTour.CopyLink.Title": "Kopijuoti nuorodą",
"OnboardingTour.OpenACard.Body": "Atidarykite kortelę, kad sužinotumėte, kokiais galingais būdais Boards gali padėti organizuoti darbą.",
"OnboardingTour.OpenACard.Title": "Atidaryti kortelę",
"OnboardingTour.ShareBoard.Body": "Galite bendrinti lentą viduje, savo komandoje arba paskelbti ją viešai, kad būtų matoma už organizacijos ribų.",
"OnboardingTour.ShareBoard.Title": "Bendrinti lentą",
"PersonProperty.board-members": "Lentos nariai",
"PersonProperty.me": "aš",
"PersonProperty.non-board-members": "Ne lentos nariai",
"PropertyMenu.Delete": "Ištrinti",
"PropertyMenu.changeType": "Pakeisti savybės tipą",
"PropertyMenu.selectType": "Pasirinkti savybės tipą",
"PropertyMenu.typeTitle": "Tipas",
"PropertyType.Checkbox": "Žymimasis langelis",
"PropertyType.CreatedBy": "Sukurta",
"PropertyType.CreatedTime": "Sukūrimo laikas",
"PropertyType.Date": "Data",
"PropertyType.Email": "El. paštas",
"PropertyType.MultiPerson": "Daugelio žmonių",
"PropertyType.MultiSelect": "Keletas pasirinkimų",
"PropertyType.Number": "Skaičius",
"PropertyType.Person": "Asmuo",
"PropertyType.Phone": "Telefonas",
"PropertyType.Select": "Pasirinkite",
"PropertyType.Text": "Tekstas",
"PropertyType.Unknown": "Nežinoma",
"PropertyType.UpdatedBy": "Paskutinį kartą atnaujino",
"PropertyType.UpdatedTime": "Paskutinio atnaujinimo laikas",
"PropertyType.Url": "URL",
"PropertyValueElement.empty": "Tuščias",
"RegistrationLink.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?",
"RegistrationLink.copiedLink": "Nukopijuota!",
"RegistrationLink.copyLink": "Kopijuoti nuorodą",
"RegistrationLink.description": "Pasidalykite šia nuoroda, kad kiti galėtų susikurti paskyras:",
"RegistrationLink.regenerateToken": "Atkurti prieigos raktą",
"RegistrationLink.tokenRegenerated": "Registracijos nuoroda sugeneruota iš naujo",
"ShareBoard.PublishDescription": "Paskelbti ir bendrinti tik skaitomą nuorodą su visais žiniatinklio naudotojais.",
"ShareBoard.PublishTitle": "Paskelbti žiniatinklyje",
"ShareBoard.ShareInternal": "Bendrinti viduje",
"ShareBoard.ShareInternalDescription": "Leidimus turintys naudotojai galės naudotis šia nuoroda.",
"ShareBoard.Title": "Bendrinti lentą",
"ShareBoard.confirmRegenerateToken": "Tai panaikins anksčiau bendrintas nuorodas. Tęsti?",
"ShareBoard.copiedLink": "Nukopijuota!",
"ShareBoard.copyLink": "Kopijuoti nuorodą",
"ShareBoard.regenerate": "Atkurti prieigos raktą",
"ShareBoard.searchPlaceholder": "Ieškoti žmonių ir kanalų",
"ShareBoard.teamPermissionsText": "Visi {teamName} komandos nariai",
"ShareBoard.tokenRegenrated": "Prieigos raktas atkurtas",
"ShareBoard.userPermissionsRemoveMemberText": "Pašalinti narį",
"ShareBoard.userPermissionsYouText": "(Jūs)",
"ShareTemplate.Title": "Bendrinti šabloną",
"ShareTemplate.searchPlaceholder": "Ieškoti žmonių",
"Sidebar.delete-board": "Ištrinti lentą",
"Sidebar.duplicate-board": "Pasikartojanti lenta",
"Sidebar.export-archive": "Eksportuoti archyvą",
"Sidebar.import": "Importuoti",
"Sidebar.import-archive": "Importuoti archyvą",
"Sidebar.new-category.badge": "Naujas",
"Sidebar.new-category.drag-boards-cta": "Vilkite lentas čia...",
"Sidebar.no-boards-in-category": "Viduje nėra lentų",
"Sidebar.product-tour": "Produkto apžvalga",
"Sidebar.random-icons": "Atsitiktinės piktogramos",
"Sidebar.set-language": "Nustatyti kalbą",
"Sidebar.set-theme": "Nustatyti temą",
"Sidebar.settings": "Nustatymai",
"Sidebar.template-from-board": "Naujas šablonas pagal lentą",
"Sidebar.untitled-board": "(Lenta be pavadinimo)",
"Sidebar.untitled-view": "(Rodinys be pavadinimo)",
"SidebarCategories.BlocksMenu.Move": "Perkelti į...",
"SidebarCategories.CategoryMenu.CreateNew": "Sukurti naują kategoriją",
"SidebarCategories.CategoryMenu.Delete": "Ištrinti kategoriją",
"SidebarCategories.CategoryMenu.DeleteModal.Body": "<b>{categoryName}</b> lentos bus perkeltos atgal į lentų kategorijas. Jūs nesate pašalintas iš jokių lentų.",
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Ištrinti šią kategoriją?",
"SidebarCategories.CategoryMenu.Update": "Pervadinti kategoriją",
"SidebarTour.ManageCategories.Body": "Kurkite ir tvarkykite pasirinktines kategorijas. Kategorijos priklauso nuo naudotojo, todėl lentos perkėlimas į kategoriją neturės įtakos kitiems nariams, naudojantiems tą pačią lentą.",
"SidebarTour.ManageCategories.Title": "Tvarkyti kategorijas",
"SidebarTour.SearchForBoards.Body": "Atidarykite lentos perjungiklį (Cmd / Ctrl + K), kad galėtumėte greitai ieškoti ir pridėti lentų šoninėje juostoje.",
"SidebarTour.SearchForBoards.Title": "Ieškoti lentų",
"SidebarTour.SidebarCategories.Body": "Visos Jūsų lentos dabar sutvarkytos naujoje šoninėje juostoje. Nebereikia perjungti darbo erdvių. Vienkartinės pasirinktinės kategorijos, pagrįstos ankstesnėmis darbo sritimis, gali būti automatiškai sukurtos Jums atnaujinant į 7.2 versiją. Jas galima pašalinti arba redaguoti pagal savo pageidavimus.",
"SidebarTour.SidebarCategories.Link": "Sužinoti daugiau",
"SidebarTour.SidebarCategories.Title": "Šoninės juostos kategorijos",
"SiteStats.total_boards": "Iš viso lentų",
"SiteStats.total_cards": "Iš viso kortelių",
"TableComponent.add-icon": "Pridėti piktogramą",
"TableComponent.name": "Pavadinimas",
"TableComponent.plus-new": "+ Naujas",
"TableHeaderMenu.delete": "Ištrinti",
"TableHeaderMenu.duplicate": "Pasikartojantis",
"TableHeaderMenu.hide": "Slėpti",
"TableHeaderMenu.insert-left": "Įdėkite kairėje",
"TableHeaderMenu.insert-right": "Įdėkite dešinėje",
"TableHeaderMenu.sort-ascending": "Rūšiuoti didėjančia tvarka",
"TableHeaderMenu.sort-descending": "Rūšiuoti mažėjančia tvarka",
"TableRow.DuplicateCard": "kortelės dublikatas",
"TableRow.MoreOption": "Daugiau veiksmų",
"TableRow.open": "Atverti",
"TopBar.give-feedback": "Palikti atsiliepimą",
"URLProperty.copiedLink": "Nukopijuota!",
"URLProperty.copy": "Kopijuoti",
"URLProperty.edit": "Redaguoti",
"UndoRedoHotKeys.canRedo": "Grąžinti",
"UndoRedoHotKeys.canRedo-with-description": "Perdaryti {description}",
"UndoRedoHotKeys.canUndo": "Anuliuoti",
"UndoRedoHotKeys.canUndo-with-description": "Anuliuoti {description}",
"UndoRedoHotKeys.cannotRedo": "Nėra ką perdaryti",
"UndoRedoHotKeys.cannotUndo": "Nėra ką anuliuoti",
"ValueSelector.noOptions": "Jokių parinkčių. Pradėkite rašyti, kad pridėtumėte pirmąją!",
"ValueSelector.valueSelector": "Reikšmės parinkiklis",
"ValueSelectorLabel.openMenu": "Atidaryti meniu",
"VersionMessage.help": "Patikrinkite, kas naujo šioje versijoje.",
"VersionMessage.learn-more": "Sužinoti daugiau",
"View.AddView": "Pridėti rodinį",
"View.Board": "Lenta",
"View.DeleteView": "Ištrinti rodinį",
"View.DuplicateView": "Pasikartojantis rodinys",
"View.Gallery": "Galerija",
"View.NewBoardTitle": "Lentos rodinys",
"View.NewCalendarTitle": "Kalendoriaus rodinys",
"View.NewGalleryTitle": "Galerijos vaizdas",
"View.NewTableTitle": "Lentelės rodinys",
"View.NewTemplateDefaultTitle": "Šablonas be pavadinimo",
"View.NewTemplateTitle": "Be pavadinimo",
"View.Table": "Lentelė",
"ViewHeader.add-template": "Naujas šablonas",
"ViewHeader.delete-template": "Ištrinti",
"ViewHeader.display-by": "Pateikė: {property}",
"ViewHeader.edit-template": "Redaguoti",
"ViewHeader.empty-card": "Tuščia kortelė",
"ViewHeader.export-board-archive": "Eksportuoti lentos archyvą",
"ViewHeader.export-complete": "Eksportas baigtas!",
"ViewHeader.export-csv": "Eksportuoti į CSV",
"ViewHeader.export-failed": "Eksportuoti nepavyko!",
"ViewHeader.filter": "Filtras",
"ViewHeader.group-by": "Grupuoti pagal: {property}",
"ViewHeader.new": "Naujas",
"ViewHeader.properties": "Savybės",
"ViewHeader.properties-menu": "Savybių meniu",
"ViewHeader.search-text": "Ieškoti kortelių",
"ViewHeader.select-a-template": "Pasirinkti šabloną",
"ViewHeader.set-default-template": "Nustatyti kaip numatytąjį",
"ViewHeader.sort": "Rūšiuoti",
"ViewHeader.untitled": "Be pavadinimo",
"ViewHeader.view-header-menu": "Žiūrėti antraštės meniu",
"ViewHeader.view-menu": "Žiūrėti meniu",
"ViewLimitDialog.Heading": "Pasiektas peržiūrų skaičius vienoje lentoje",
"ViewLimitDialog.PrimaryButton.Title.Admin": "Patobulinti",
"ViewLimitDialog.PrimaryButton.Title.RegularUser": "Pranešti administratoriui",
"ViewLimitDialog.Subtext.Admin": "Naujovinkite į mūsų Professional arba Enterprise planą.",
"ViewLimitDialog.Subtext.Admin.PricingPageLink": "Sužinokite daugiau apie mūsų planus.",
"ViewLimitDialog.Subtext.RegularUser": "Praneškite savo administratoriui, kad jis naujovintų į mūsų Professional arba Enterprise planą.",
"ViewLimitDialog.UpgradeImg.AltText": "atnaujinti vaizdą",
"ViewLimitDialog.notifyAdmin.Success": "Jūsų administratoriui buvo pranešta",
"ViewTitle.hide-description": "slėpti aprašymą",
"ViewTitle.pick-icon": "Pasirinkti piktogramą",
"ViewTitle.random-icon": "Atsitiktinis",
"ViewTitle.remove-icon": "Pašalinti piktogramą",
"ViewTitle.show-description": "rodyti aprašymą",
"ViewTitle.untitled-board": "Lenta be pavadinimo",
"WelcomePage.Description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.",
"WelcomePage.Explore.Button": "Apžiūrėti",
"WelcomePage.Heading": "Sveiki atvykę į Boards",
"WelcomePage.NoThanks.Text": "Ne, ačiū, išsiaiškinsiu pats",
"WelcomePage.StartUsingIt.Text": "Pradėkite jį naudoti",
"Workspace.editing-board-template": "Redaguojate lentos šabloną.",
"badge.guest": "Svečias",
"boardPage.confirm-join-button": "Prisijungti",
"boardPage.confirm-join-text": "Jūs ketinate prisijungti prie privačios lentos, tačiau lentos administratorius Jūsų nepridėjo. Ar tikrai norite prisijungti prie šios privačios lentos?",
"boardPage.confirm-join-title": "Prisijunti prie privačios lentos",
"boardSelector.confirm-link-board": "Susieti lentą su kanalu",
"boardSelector.confirm-link-board-button": "Taip, susieti lentą",
"boardSelector.confirm-link-board-subtext": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai. Galite bet kada atsieti lentą nuo kanalo.",
"boardSelector.confirm-link-board-subtext-with-other-channel": "Kai susiesite „{boardName}“ su kanalu, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak} Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.",
"boardSelector.create-a-board": "Sukurti lentą",
"boardSelector.link": "nuoroda",
"boardSelector.search-for-boards": "Ieškoti lentų",
"boardSelector.title": "Susieti lentas",
"boardSelector.unlink": "Atsieti",
"calendar.month": "Mėnuo",
"calendar.today": "Šiandien",
"calendar.week": "Savaitė",
"centerPanel.undefined": "Nėra {propertyName}",
"centerPanel.unknown-user": "Nežinomas naudotojas",
"createImageBlock.failed": "Šio failo nepavyko įkelti, nes pasiektas failo dydžio limitas.",
"default-properties.badges": "Komentarai ir aprašymas",
"default-properties.title": "Pavadinimas",
"error.back-to-home": "Grįžti namo",
"error.back-to-team": "Atgal į komandą",
"error.board-not-found": "Lenta nerasta.",
"error.go-login": "Prisijungti",
"error.invalid-read-only-board": "Neturite prieigos prie šios lentos. Prisijunkite, kad pasiektumėte lentas.",
"error.not-logged-in": "Jūsų sesija gali būti pasibaigusi arba nesate prisijungę. Prisijunkite dar kartą, kad pasiektumėte lentas.",
"error.page.title": "Atsiprašome, kažkas nutiko",
"error.team-undefined": "Netinkama komanda.",
"error.unknown": "Įvyko klaida.",
"generic.previous": "Ankstesnis",
"guest-no-board.subtitle": "Dar neturite prieigos prie jokios šios komandos lentos, palaukite, kol kas nors jus įtrauks į bet kurią lentą.",
"guest-no-board.title": "Dar nėra jokių lentų",
"imagePaste.upload-failed": "Kai kurie failai nebuvo įkelti, nes pasiektas failo dydžio limitas.",
"limitedCard.title": "Paslėptos kortelės",
"login.log-in-button": "Prisijungti",
"login.log-in-title": "Prisijungti",
"login.register-button": "arba susikurkite paskyrą, jei jos neturite",
"new_channel_modal.create_board.empty_board_description": "Sukurti naują tuščią lentą",
"new_channel_modal.create_board.empty_board_title": "Tuščia lenta",
"new_channel_modal.create_board.select_template_placeholder": "Pasirinkti šabloną",
"new_channel_modal.create_board.title": "Sukurti šio kanalo lentą",
"notification-box-card-limit-reached.close-tooltip": "Snausti 10 dienų",
"notification-box-card-limit-reached.contact-link": "praneškite savo administratoriui",
"notification-box-card-limit-reached.link": "Atnaujinti į mokamą planą",
"notification-box-card-limit-reached.title": "{cards} kortelės paslėptos",
"notification-box-cards-hidden.title": "Šis veiksmas paslėpė kitą kortelę",
"notification-box.card-limit-reached.not-admin.text": "Norėdami pasiekti archyvuotas korteles, galite {contactLink} naujovinti į mokamą planą.",
"notification-box.card-limit-reached.text": "Pasiektas kortelių limitas, jei norite peržiūrėti senesnes korteles, {link}",
"person.add-user-to-board": "Pridėti {username} prie lentos",
"person.add-user-to-board-confirm-button": "Pridėti prie lentos",
"person.add-user-to-board-permissions": "Leidimai",
"person.add-user-to-board-question": "Ar norite pridėti {username} prie lentos?",
"person.add-user-to-board-warning": "{username} nėra lentos narys ir negaus apie tai jokių pranešimų.",
"register.login-button": "arba prisijunkite, jei jau turite paskyrą",
"register.signup-title": "Prisiregistruokite prie paskyros",
"rhs-board-non-admin-msg": "Jūs nesate lentos administratorius",
"rhs-boards.add": "Pridėti",
"rhs-boards.dm": "AŽ",
"rhs-boards.gm": "GŽ",
"rhs-boards.header.dm": "ši tiesioginė žinutė",
"rhs-boards.header.gm": "ši grupės žinutė",
"rhs-boards.last-update-at": "Paskutinį kartą atnaujinta: {datetime}",
"rhs-boards.link-boards-to-channel": "Susieti lentas su {channelName}",
"rhs-boards.linked-boards": "Susietos lentos",
"rhs-boards.no-boards-linked-to-channel": "Su {channelName} dar nėra susietų lentų",
"rhs-boards.no-boards-linked-to-channel-description": "„Boards“ yra projektų valdymo įrankis, padedantis apibrėžti, organizuoti, sekti ir valdyti darbą įvairiose komandose, naudojant pažįstamą Kanban lentos vaizdą.",
"rhs-boards.unlink-board": "Atsieti lentą",
"rhs-boards.unlink-board1": "Atsieti lentą",
"rhs-channel-boards-header.title": "Boards",
"share-board.publish": "Paskelbti",
"share-board.share": "Bendrinti",
"shareBoard.channels-select-group": "Kanalai",
"shareBoard.confirm-change-team-role.body": "Visi šioje lentoje esantys asmenys, turintys žemesnį nei „{role}“ vaidmenį <b>, dabar bus paaukštinti į {role}</b> . Ar tikrai norite pakeisti minimalų lentos vaidmenį?",
"shareBoard.confirm-change-team-role.confirmBtnText": "Pakeiskite minimalų lentos vaidmenį",
"shareBoard.confirm-change-team-role.title": "Pakeiskite minimalų lentos vaidmenį",
"shareBoard.confirm-link-channel": "Susieti lentą su kanalu",
"shareBoard.confirm-link-channel-button": "Susieti kanalą",
"shareBoard.confirm-link-channel-button-with-other-channel": "Atsieti ir susieti čia",
"shareBoard.confirm-link-channel-subtext": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės ją redaguoti. Tai neįtraukia narių, kurie yra svečiai.",
"shareBoard.confirm-link-channel-subtext-with-other-channel": "Kai susiesite kanalą su lenta, visi kanalo nariai (esami ir nauji) galės jį redaguoti. Tai neįtraukia narių, kurie yra svečiai.{lineBreak}Ši lenta šiuo metu susieta su kitu kanalu. Jis bus atsietas, jei pasirinksite susieti jį čia.",
"shareBoard.confirm-unlink.body": "Kai atsiesite kanalą nuo lentos, visi kanalo nariai (esami ir nauji) praras prieigą prie jo, nebent jiems bus suteiktas atskiras leidimas.",
"shareBoard.confirm-unlink.confirmBtnText": "Atsieti kanalą",
"shareBoard.confirm-unlink.title": "Atsieti kanalą nuo lentos",
"shareBoard.lastAdmin": "Lentose turi būti bent vienas administratorius",
"shareBoard.members-select-group": "Nariai",
"shareBoard.unknown-channel-display-name": "Nežinomas kanalas",
"tutorial_tip.finish_tour": "Atlikta",
"tutorial_tip.got_it": "Supratau",
"tutorial_tip.ok": "Kitas",
"tutorial_tip.out": "Atsisakyti šių patarimų.",
"tutorial_tip.seen": "Matėte tai anksčiau?"
}

Просмотреть файл

@@ -20,7 +20,6 @@ const config = {
"/nanoevents/", "/nanoevents/",
"node_modules/(?!react-native|react-router|react-day-picker)" "node_modules/(?!react-native|react-router|react-day-picker)"
], ],
maxWorkers: "80%",
testEnvironment: "jsdom", testEnvironment: "jsdom",
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [

Просмотреть файл

@@ -4,7 +4,6 @@
"private": true, "private": true,
"description": "", "description": "",
"scripts": { "scripts": {
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" \"../**/*.d.ts\" --out-file i18n/tmp.json && formatjs compile i18n/tmp.json --out-file i18n/en.json && npx rimraf i18n/tmp.json",
"build": "webpack --mode=production", "build": "webpack --mode=production",
"build:watch": "webpack --mode=production --watch", "build:watch": "webpack --mode=production --watch",
"start:product": "webpack serve --mode=development", "start:product": "webpack serve --mode=development",
@@ -18,6 +17,7 @@
"check-types": "tsc -b", "check-types": "tsc -b",
"check-types:fix": "npm run check-types -- --noEmit --fix", "check-types:fix": "npm run check-types -- --noEmit --fix",
"check": "npm run check-lint && npm run check-style", "check": "npm run check-lint && npm run check-style",
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format simple --out-file i18n/en.json",
"fix": "npm run check-lint:fix && npm run check-style:fix", "fix": "npm run check-lint:fix && npm run check-style:fix",
"test": "cross-env TZ=Etc/UTC jest", "test": "cross-env TZ=Etc/UTC jest",
"test:watch": "cross-env TZ=Etc/UTC jest --watch", "test:watch": "cross-env TZ=Etc/UTC jest --watch",
@@ -58,7 +58,7 @@
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-hot-keys": "^2.7.1", "react-hot-keys": "^2.7.1",
"react-hotkeys-hook": "^3.4.4", "react-hotkeys-hook": "^3.4.4",
"react-intl": "^5.20.0", "react-intl": "*",
"react-redux": "^7.2.1", "react-redux": "^7.2.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "5.5.9", "react-select": "5.5.9",
@@ -95,7 +95,6 @@
"@types/node": "16.11.7", "@types/node": "16.11.7",
"@types/react": "17.0.53", "@types/react": "17.0.53",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-day-picker": "5.3.0",
"@types/react-dom": "17.0.19", "@types/react-dom": "17.0.19",
"@types/react-redux": "^7.1.23", "@types/react-redux": "^7.1.23",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
@@ -104,11 +103,9 @@
"@typescript-eslint/eslint-plugin": "5.57.1", "@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1", "@typescript-eslint/parser": "5.57.1",
"babel-eslint": "10.1.0", "babel-eslint": "10.1.0",
"cross-env": "^7.0.3",
"css-loader": "6.7.1", "css-loader": "6.7.1",
"eslint-import-resolver-webpack": "0.13.2", "eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
"eslint-plugin-formatjs": "4.9.0",
"eslint-plugin-header": "3.1.1", "eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
"eslint-plugin-import-newlines": "1.3.1", "eslint-plugin-import-newlines": "1.3.1",

Просмотреть файл

@@ -39,7 +39,7 @@ const BoardSwitcherDialog = (props: Props): JSX.Element => {
const team = useAppSelector(getCurrentTeam) const team = useAppSelector(getCurrentTeam)
const me = useAppSelector(getMe) const me = useAppSelector(getMe)
const title = intl.formatMessage({id: 'FindBoardsDialog.Title', defaultMessage: 'Find Boards'}) const title = intl.formatMessage({id: 'FindBoardsDialog.Title', defaultMessage: 'Find Boards'})
const subTitle = intl.formatMessage( const subTitle = intl.formatMessage<ReactNode>(
{ {
id: 'FindBoardsDialog.SubTitle', id: 'FindBoardsDialog.SubTitle',
defaultMessage: 'Type to find a board. Use <b>UP/DOWN</b> to browse. <b>ENTER</b> to select, <b>ESC</b> to dismiss', defaultMessage: 'Type to find a board. Use <b>UP/DOWN</b> to browse. <b>ENTER</b> to select, <b>ESC</b> to dismiss',

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useState} from 'react' import React, {ReactNode, useState} from 'react'
import {useIntl} from 'react-intl' import {useIntl} from 'react-intl'
import MenuWrapper from 'src/widgets/menuWrapper' import MenuWrapper from 'src/widgets/menuWrapper'
@@ -77,7 +77,7 @@ const TeamPermissionsRow = (): JSX.Element => {
id: 'shareBoard.confirm-change-team-role.title', id: 'shareBoard.confirm-change-team-role.title',
defaultMessage: 'Change minimum board role', defaultMessage: 'Change minimum board role',
}), }),
subText: intl.formatMessage({ subText: intl.formatMessage<ReactNode>({
id: 'shareBoard.confirm-change-team-role.body', id: 'shareBoard.confirm-change-team-role.body',
defaultMessage: 'Everyone on this board with a lower permission than the "{role}" role will <b>now be promoted to {role}</b>. Are you sure you want to change the minimum role for the board?', defaultMessage: 'Everyone on this board with a lower permission than the "{role}" role will <b>now be promoted to {role}</b>. Are you sure you want to change the minimum role for the board?',
}, { }, {

Просмотреть файл

@@ -193,7 +193,10 @@ describe('components/sidebarCategory', () => {
expect(mockTemplateClose).toBeCalled() expect(mockTemplateClose).toBeCalled()
}) })
test('sidebar template close other', async () => { // TODO: Remove when fetch is mocked correctly
// https://mattermost.atlassian.net/browse/MM-52212
// eslint-disable-next-line no-only-tests/no-only-tests
test.skip('sidebar template close other', async () => {
const mockStore = configureStore([]) const mockStore = configureStore([])
const store = mockStore(state) const store = mockStore(state)

Просмотреть файл

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, { import React, {
ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -167,7 +168,7 @@ const SidebarCategory = (props: Props) => {
id: 'SidebarCategories.CategoryMenu.DeleteModal.Title', id: 'SidebarCategories.CategoryMenu.DeleteModal.Title',
defaultMessage: 'Delete this category?', defaultMessage: 'Delete this category?',
}), }),
subText: intl.formatMessage( subText: intl.formatMessage<ReactNode>(
{ {
id: 'SidebarCategories.CategoryMenu.DeleteModal.Body', id: 'SidebarCategories.CategoryMenu.DeleteModal.Body',
defaultMessage: 'Boards in <b>{categoryName}</b> will move back to the Boards categories. You\'re not removed from any boards.', defaultMessage: 'Boards in <b>{categoryName}</b> will move back to the Boards categories. You\'re not removed from any boards.',

Просмотреть файл

@@ -97,7 +97,7 @@
} }
], ],
"max-lines": ["warn", {"max": 800, "skipBlankLines": true, "skipComments": true}], "max-lines": ["warn", {"max": 800, "skipBlankLines": true, "skipComments": true}],
"formatjs/no-multiple-whitespaces": ["error"] "formatjs/no-multiple-whitespaces": 2
}, },
"overrides": [ "overrides": [
{ {

Просмотреть файл

@@ -1,56 +0,0 @@
# Mattermost Web App
# [![Mattermost](https://user-images.githubusercontent.com/7205829/137170381-fe86eef0-bccc-4fdd-8e92-b258884ebdd7.png)](https://mattermost.com)
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. This repo is the primary source for core development on the Mattermost platform; it's written in Golang and React, and runs as a single Linux binary with MySQL or PostgreSQL. A new compiled version is released under an MIT license every month on the 16th.
<img width="1006" alt="mattermost-hero" src="https://user-images.githubusercontent.com/7205829/136107976-7a894c9e-290a-490d-8501-e5fdbfc3785a.png">
This repo hosts the webapp client code. If you'd like to report an issue with Mattermost, please create an issue on [mattermost-server](https://github.com/mattermost/mattermost-server), which hosts the server code. You can also look at the [Get Help With Mattermost](https://docs.mattermost.com/guides/get-help.html) guide to find the resources available to our community.
- [Product documentation](https://docs.mattermost.com/)
- [Developer documentation](https://developers.mattermost.com/)
- [Download compiled version](https://mattermost.com/download)
## Try out Mattermost
- [Join the Mattermost Contributor's server](https://community.mattermost.com/signup_user_complete/?id=codoy5s743rq5mk18i7u5ksz7e) to join community discussions about contributions, development and more
- [Get started with Mattermost Cloud](https://customers.mattermost.com/cloud/signup) to try out Mattermost
[![Deploy a Preview](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/mattermost/mattermost-heroku)
_Note: Heroku preview does not include email or persistent storage._
## Install Mattermost
- [Deploy Guide](https://docs.mattermost.com/guides/deployment.html) - Deploy Mattermost in minutes via Docker, Ubuntu, or tar.
- [Developer Machine Setup](https://developers.mattermost.com/contribute/server/developer-setup) - Follow this guide if you want to write code for Mattermost
Other Install Guides:
- [Deploy Mattermost on Docker](https://docs.mattermost.com/install/install-docker.html)
- [Mattermost Omnibus](https://docs.mattermost.com/install/installing-mattermost-omnibus.html)
- [Install Mattermost from Tar](https://docs.mattermost.com/install/install-tar.html)
- [Ubuntu 20.04 LTS](https://docs.mattermost.com/install/installing-ubuntu-2004-LTS.html)
- [Kubernetes](https://docs.mattermost.com/install/install-kubernetes.html)
- [Helm](https://docs.mattermost.com/install/install-kubernetes.html#installing-the-operators-via-helm)
- [Debian Buster](https://docs.mattermost.com/install/install-debian.html)
- [RHEL 8](https://docs.mattermost.com/install/install-rhel-8.html)
- [More server install guides](https://docs.mattermost.com/guides/deployment.html)
## Get Involved
- [Contribute to Mattermost](https://handbook.mattermost.com/contributors/contributors/ways-to-contribute)
- [Find "Help Wanted" projects](https://github.com/mattermost/mattermost-server/issues?page=1&q=is%3Aissue+is%3Aopen+%22Help+Wanted%22&utf8=%E2%9C%93)
- [Join Developer Discussion on a Mattermost Server for contributors](https://docs.mattermost.com/guides/community-chat.html)
- [Get Help With Mattermost](https://docs.mattermost.com/guides/get-help.html)
## Learn More
- [API Options - webhooks, slash commands, drivers and web service](https://api.mattermost.com/)
- [Localization Guide](https://handbook.mattermost.com/contributors/contributors/localization)
## Get the Latest News
- **Twitter** - Follow [Mattermost](https://twitter.com/Mattermost)
- **Email** - Subscribe to our [newsletter](https://mattermost.com/community-newsletter/)
Any other questions, mail us at [info@mattermost.com](mailto:info@mattermost.com). We'd love to meet you!

Просмотреть файл

@@ -13,4 +13,4 @@ test:
before_script: before_script:
- npm ci --ignore-scripts - npm ci --ignore-scripts
script: script:
- npm run test:speed - npm run test

Просмотреть файл

@@ -51,7 +51,7 @@
"localforage": "1.10.0", "localforage": "1.10.0",
"localforage-observable": "2.1.1", "localforage-observable": "2.1.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"luxon": "3.0.4", "luxon": "3.3.0",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "github:mattermost/marked#2ef7f28cc7718e3f551c4ce9ea75fdd7580c2008", "marked": "github:mattermost/marked#2ef7f28cc7718e3f551c4ce9ea75fdd7580c2008",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
@@ -70,7 +70,7 @@
"react-day-picker": "8.3.6", "react-day-picker": "8.3.6",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-hot-loader": "4.13.0", "react-hot-loader": "4.13.0",
"react-intl": "5.20.10", "react-intl": "*",
"react-is": "17.0.2", "react-is": "17.0.2",
"react-overlays": "0.9.3", "react-overlays": "0.9.3",
"react-popper": "2.3.0", "react-popper": "2.3.0",
@@ -116,6 +116,7 @@
"@redux-devtools/extension": "3.2.3", "@redux-devtools/extension": "3.2.3",
"@testing-library/jest-dom": "5.16.4", "@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.4", "@testing-library/react": "12.1.4",
"@testing-library/user-event": "12.1.4",
"@types/bootstrap": "4.5.0", "@types/bootstrap": "4.5.0",
"@types/country-list": "2.1.0", "@types/country-list": "2.1.0",
"@types/enzyme": "3.10.11", "@types/enzyme": "3.10.11",
@@ -155,14 +156,12 @@
"babel-plugin-typescript-to-proptypes": "1.4.2", "babel-plugin-typescript-to-proptypes": "1.4.2",
"bundle-loader": "0.2.0", "bundle-loader": "0.2.0",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "5.2.6", "css-loader": "5.2.6",
"dotenv-webpack": "8.0.1", "dotenv-webpack": "8.0.1",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-17-updated": "1.0.2", "enzyme-adapter-react-17-updated": "1.0.2",
"enzyme-to-json": "3.6.2", "enzyme-to-json": "3.6.2",
"eslint-import-resolver-webpack": "0.13.2", "eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-formatjs": "4.9.1",
"eslint-plugin-header": "3.1.1", "eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e", "eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
@@ -215,15 +214,13 @@
"build": "cross-env NODE_ENV=production webpack", "build": "cross-env NODE_ENV=production webpack",
"run": "webpack --progress --watch", "run": "webpack --progress --watch",
"dev-server": "webpack serve --mode development", "dev-server": "webpack serve --mode development",
"test": "cross-env TZ=Etc/UTC jest --maxWorkers=50%", "test": "cross-env TZ=Etc/UTC jest",
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
"test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose",
"test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%", "test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%",
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo", "clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json", "stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
"updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles",
"test:speed": "cross-env TZ=Etc/UTC jest",
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
"test:coverage": "cross-env TZ=Etc/UTC jest --coverage",
"mmjstool": "mmjstool", "mmjstool": "mmjstool",
"i18n-extract": "npm run mmjstool -- i18n extract-webapp --webapp-dir ./src", "i18n-extract": "npm run mmjstool -- i18n extract-webapp --webapp-dir ./src",
"i18n-clean-empty": "npm run mmjstool -- i18n clean-empty --webapp-dir ./src", "i18n-clean-empty": "npm run mmjstool -- i18n clean-empty --webapp-dir ./src",

Просмотреть файл

@@ -14,9 +14,11 @@ import {isSystemMessage, isUserAddedInChannel} from 'mattermost-redux/utils/post
import {displayUsername} from 'mattermost-redux/utils/user_utils'; import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {isThreadOpen} from 'selectors/views/threads'; import {isThreadOpen} from 'selectors/views/threads';
import {getChannelURL, getPermalinkURL} from 'selectors/urls';
import {getHistory} from 'utils/browser_history'; import {getHistory} from 'utils/browser_history';
import Constants, {NotificationLevels, UserStatuses} from 'utils/constants'; import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
import * as NotificationSounds from 'utils/notification_sounds';
import {showNotification} from 'utils/notifications'; import {showNotification} from 'utils/notifications';
import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent'; import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
@@ -178,17 +180,17 @@ export function sendDesktopNotification(post, msgProps) {
if (notify) { if (notify) {
const updatedState = getState(); const updatedState = getState();
let url = Utils.getChannelURL(updatedState, channel, teamId); let url = getChannelURL(updatedState, channel, teamId);
if (isCrtReply) { if (isCrtReply) {
url = Utils.getPermalinkURL(updatedState, teamId, post.id); url = getPermalinkURL(updatedState, teamId, post.id);
} }
dispatch(notifyMe(title, body, channel, teamId, !sound, soundName, url)); dispatch(notifyMe(title, body, channel, teamId, !sound, soundName, url));
//Don't add extra sounds on native desktop clients //Don't add extra sounds on native desktop clients
if (sound && !isDesktopApp() && !isMobileApp()) { if (sound && !isDesktopApp() && !isMobileApp()) {
Utils.ding(soundName); NotificationSounds.ding(soundName);
} }
} }
}; };

Просмотреть файл

@@ -5,8 +5,8 @@ import testConfigureStore from 'tests/test_store';
import {getHistory} from 'utils/browser_history'; import {getHistory} from 'utils/browser_history';
import Constants, {NotificationLevels, UserStatuses} from 'utils/constants'; import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
import * as NotificationSounds from 'utils/notification_sounds';
import * as utils from 'utils/notifications'; import * as utils from 'utils/notifications';
import * as baseUtils from 'utils/utils';
import {sendDesktopNotification} from './notification_actions'; import {sendDesktopNotification} from './notification_actions';
@@ -22,7 +22,7 @@ describe('notification_actions', () => {
beforeEach(() => { beforeEach(() => {
spy = jest.spyOn(utils, 'showNotification'); spy = jest.spyOn(utils, 'showNotification');
baseUtils.ding = jest.fn(); NotificationSounds.ding = jest.fn();
crt = { crt = {
user_id: 'current_user_id', user_id: 'current_user_id',
@@ -315,7 +315,7 @@ describe('notification_actions', () => {
}); });
test('should default sound when no sound is specified', () => { test('should default sound when no sound is specified', () => {
const dingSpy = jest.spyOn(baseUtils, 'ding'); const dingSpy = jest.spyOn(NotificationSounds, 'ding');
baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true'; baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true';
const store = testConfigureStore(baseState); const store = testConfigureStore(baseState);
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => { return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
@@ -324,7 +324,7 @@ describe('notification_actions', () => {
}); });
test('should use specified sound when specified', () => { test('should use specified sound when specified', () => {
const dingSpy = jest.spyOn(baseUtils, 'ding'); const dingSpy = jest.spyOn(NotificationSounds, 'ding');
baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true'; baseState.entities.users.profiles.current_user_id.notify_props.desktop_sound = 'true';
baseState.entities.users.profiles.current_user_id.notify_props.desktop_notification_sound = 'Crackle'; baseState.entities.users.profiles.current_user_id.notify_props.desktop_notification_sound = 'Crackle';
const store = testConfigureStore(baseState); const store = testConfigureStore(baseState);

Просмотреть файл

@@ -4,6 +4,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={ messages={
@@ -14,6 +15,7 @@ exports[`components/FormattedMarkdownMessage should allow to disable links 1`] =
} }
} }
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<FormattedMarkdownMessage <FormattedMarkdownMessage
@@ -41,6 +43,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={ messages={
@@ -51,6 +54,7 @@ exports[`components/FormattedMarkdownMessage should backup to default 1`] = `
} }
} }
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<FormattedMarkdownMessage <FormattedMarkdownMessage
@@ -72,6 +76,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={ messages={
@@ -82,6 +87,7 @@ exports[`components/FormattedMarkdownMessage should escape non-BR 1`] = `
} }
} }
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<FormattedMarkdownMessage <FormattedMarkdownMessage
@@ -109,6 +115,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={ messages={
@@ -119,6 +126,7 @@ exports[`components/FormattedMarkdownMessage should render message 1`] = `
} }
} }
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<FormattedMarkdownMessage <FormattedMarkdownMessage
@@ -140,6 +148,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={ messages={
@@ -150,6 +159,7 @@ exports[`components/FormattedMarkdownMessage values should work 1`] = `
} }
} }
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<FormattedMarkdownMessage <FormattedMarkdownMessage

Просмотреть файл

@@ -1,980 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/AboutBuildModal should match snapshot for cloud edition 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
},
}
}
>
<AboutBuildModalCloud
config={
Object {
"BuildDate": "21 January 2017",
"BuildEnterpriseReady": "true",
"BuildHash": "abcdef1234567890",
"BuildHashEnterprise": "0123456789abcdef",
"BuildNumber": "3.6.2",
"PrivacyPolicyLink": "https://about.custom.com/privacy-policy/",
"SQLDriverName": "Postgres",
"SchemaVersion": "77",
"TermsOfServiceLink": "https://about.custom.com/default-terms/",
"Version": "3.6.0",
}
}
doHide={[MockFunction]}
license={
Object {
"Cloud": "true",
"Company": "Mattermost Inc",
"IsLicensed": "true",
}
}
onExited={[MockFunction]}
show={true}
/>
</ContextProvider>
`;
exports[`components/AboutBuildModal should match snapshot for enterprise edition 1`] = `
<Modal
animation={true}
aria-labelledby="aboutModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal about-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="aboutModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="About {appTitle}"
id="about.title"
values={
Object {
"appTitle": "Mattermost",
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="about-modal__content"
>
<div
className="about-modal__logo"
>
<MattermostLogo />
</div>
<div>
<h3
className="about-modal__title"
>
<strong>
Mattermost
<MemoizedFormattedMessage
defaultMessage="Enterprise Edition"
id="about.enterpriseEditione1"
/>
</strong>
</h3>
<p
className="about-modal__subtitle pb-2"
>
<MemoizedFormattedMessage
defaultMessage="Modern communication from behind your firewall."
id="about.enterpriseEditionSt"
/>
</p>
<div
className="form-group less"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Mattermost Version:"
id="about.version"
/>
<span
id="versionString"
>
 3.6.2
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database Schema Version:"
id="about.dbversion"
/>
<span
id="dbversionString"
>
 77
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database:"
id="about.database"
/>
 Postgres
</div>
</div>
<div
className="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Licensed to:"
id="about.licensed"
/>
<Nbsp />
Mattermost Inc
</div>
</div>
</div>
<div
className="about-modal__footer"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Learn more about Enterprise Edition at "
id="about.enterpriseEditionLearn"
/>
<ExternalLink
href="https://mattermost.com/"
location="about_build_modal"
>
mattermost.com
</ExternalLink>
</div>
<div
className="form-group"
>
<div
className="about-modal__copyright"
>
<MemoizedFormattedMessage
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
id="about.copyright"
values={
Object {
"currentYear": 2017,
}
}
/>
</div>
<div
className="about-modal__links"
>
<ExternalLink
href="https://mattermost.com/terms-of-use/"
id="tosLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Terms of Use"
id="about.tos"
/>
</ExternalLink>
-
<ExternalLink
href="https://mattermost.com/privacy-policy/"
id="privacyLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Privacy Policy"
id="about.privacy"
/>
</ExternalLink>
</div>
</div>
</div>
<div
className="about-modal__notice form-group pt-3"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
id="about.notice"
values={
Object {
"linkDesktop": [Function],
"linkMobile": [Function],
"linkServer": [Function],
}
}
/>
</p>
</div>
<div
className="about-modal__hash"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Hash:"
id="about.hash"
/>
<Nbsp />
abcdef1234567890
<br />
<MemoizedFormattedMessage
defaultMessage="EE Build Hash:"
id="about.hashee"
/>
<Nbsp />
0123456789abcdef
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Date:"
id="about.date"
/>
<Nbsp />
21 January 2017
</p>
</div>
</ModalBody>
</Modal>
`;
exports[`components/AboutBuildModal should match snapshot for team edition 1`] = `
<Modal
animation={true}
aria-labelledby="aboutModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal about-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="aboutModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="About {appTitle}"
id="about.title"
values={
Object {
"appTitle": "Mattermost",
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="about-modal__content"
>
<div
className="about-modal__logo"
>
<MattermostLogo />
</div>
<div>
<h3
className="about-modal__title"
>
<strong>
Mattermost
<MemoizedFormattedMessage
defaultMessage="Team Edition"
id="about.teamEditiont0"
/>
</strong>
</h3>
<p
className="about-modal__subtitle pb-2"
>
<MemoizedFormattedMessage
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
id="about.teamEditionSt"
/>
</p>
<div
className="form-group less"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Mattermost Version:"
id="about.version"
/>
<span
id="versionString"
>
 3.6.2
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database Schema Version:"
id="about.dbversion"
/>
<span
id="dbversionString"
>
 77
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database:"
id="about.database"
/>
 Postgres
</div>
</div>
</div>
</div>
<div
className="about-modal__footer"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Join the Mattermost community at "
id="about.teamEditionLearn"
/>
<ExternalLink
href="https://mattermost.com/community/"
location="about_build_modal"
>
mattermost.com/community/
</ExternalLink>
</div>
<div
className="form-group"
>
<div
className="about-modal__copyright"
>
<MemoizedFormattedMessage
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
id="about.copyright"
values={
Object {
"currentYear": 2017,
}
}
/>
</div>
<div
className="about-modal__links"
>
<ExternalLink
href="https://mattermost.com/terms-of-use/"
id="tosLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Terms of Use"
id="about.tos"
/>
</ExternalLink>
-
<ExternalLink
href="https://mattermost.com/privacy-policy/"
id="privacyLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Privacy Policy"
id="about.privacy"
/>
</ExternalLink>
</div>
</div>
</div>
<div
className="about-modal__notice form-group pt-3"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
id="about.notice"
values={
Object {
"linkDesktop": [Function],
"linkMobile": [Function],
"linkServer": [Function],
}
}
/>
</p>
</div>
<div
className="about-modal__hash"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Hash:"
id="about.hash"
/>
<Nbsp />
abcdef1234567890
<br />
<MemoizedFormattedMessage
defaultMessage="EE Build Hash:"
id="about.hashee"
/>
<Nbsp />
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Date:"
id="about.date"
/>
<Nbsp />
21 January 2017
</p>
</div>
</ModalBody>
</Modal>
`;
exports[`components/AboutBuildModal should show ci if a ci build 1`] = `
<Modal
animation={true}
aria-labelledby="aboutModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal about-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="aboutModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="About {appTitle}"
id="about.title"
values={
Object {
"appTitle": "Mattermost",
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="about-modal__content"
>
<div
className="about-modal__logo"
>
<MattermostLogo />
</div>
<div>
<h3
className="about-modal__title"
>
<strong>
Mattermost
<MemoizedFormattedMessage
defaultMessage="Team Edition"
id="about.teamEditiont0"
/>
</strong>
</h3>
<p
className="about-modal__subtitle pb-2"
>
<MemoizedFormattedMessage
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
id="about.teamEditionSt"
/>
</p>
<div
className="form-group less"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Mattermost Version:"
id="about.version"
/>
<span
id="versionString"
>
 ci
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database Schema Version:"
id="about.dbversion"
/>
<span
id="dbversionString"
>
 77
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Build Number:"
id="about.buildnumber"
/>
<span
id="buildnumberString"
>
 123
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database:"
id="about.database"
/>
 Postgres
</div>
</div>
</div>
</div>
<div
className="about-modal__footer"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Join the Mattermost community at "
id="about.teamEditionLearn"
/>
<ExternalLink
href="https://mattermost.com/community/"
location="about_build_modal"
>
mattermost.com/community/
</ExternalLink>
</div>
<div
className="form-group"
>
<div
className="about-modal__copyright"
>
<MemoizedFormattedMessage
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
id="about.copyright"
values={
Object {
"currentYear": 2017,
}
}
/>
</div>
<div
className="about-modal__links"
>
<ExternalLink
href="https://mattermost.com/terms-of-use/"
id="tosLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Terms of Use"
id="about.tos"
/>
</ExternalLink>
-
<ExternalLink
href="https://mattermost.com/privacy-policy/"
id="privacyLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Privacy Policy"
id="about.privacy"
/>
</ExternalLink>
</div>
</div>
</div>
<div
className="about-modal__notice form-group pt-3"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
id="about.notice"
values={
Object {
"linkDesktop": [Function],
"linkMobile": [Function],
"linkServer": [Function],
}
}
/>
</p>
</div>
<div
className="about-modal__hash"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Hash:"
id="about.hash"
/>
<Nbsp />
abcdef1234567890
<br />
<MemoizedFormattedMessage
defaultMessage="EE Build Hash:"
id="about.hashee"
/>
<Nbsp />
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Date:"
id="about.date"
/>
<Nbsp />
21 January 2017
</p>
</div>
</ModalBody>
</Modal>
`;
exports[`components/AboutBuildModal should show dev if this is a dev build 1`] = `
<Modal
animation={true}
aria-labelledby="aboutModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal about-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="aboutModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="About {appTitle}"
id="about.title"
values={
Object {
"appTitle": "Mattermost",
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="about-modal__content"
>
<div
className="about-modal__logo"
>
<MattermostLogo />
</div>
<div>
<h3
className="about-modal__title"
>
<strong>
Mattermost
<MemoizedFormattedMessage
defaultMessage="Team Edition"
id="about.teamEditiont0"
/>
</strong>
</h3>
<p
className="about-modal__subtitle pb-2"
>
<MemoizedFormattedMessage
defaultMessage="All your team communication in one place, instantly searchable and accessible anywhere."
id="about.teamEditionSt"
/>
</p>
<div
className="form-group less"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Mattermost Version:"
id="about.version"
/>
<span
id="versionString"
>
 dev
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database Schema Version:"
id="about.dbversion"
/>
<span
id="dbversionString"
>
 77
</span>
</div>
<div>
<MemoizedFormattedMessage
defaultMessage="Database:"
id="about.database"
/>
 Postgres
</div>
</div>
</div>
</div>
<div
className="about-modal__footer"
>
<div>
<MemoizedFormattedMessage
defaultMessage="Join the Mattermost community at "
id="about.teamEditionLearn"
/>
<ExternalLink
href="https://mattermost.com/community/"
location="about_build_modal"
>
mattermost.com/community/
</ExternalLink>
</div>
<div
className="form-group"
>
<div
className="about-modal__copyright"
>
<MemoizedFormattedMessage
defaultMessage="Copyright 2015 - {currentYear} Mattermost, Inc. All rights reserved"
id="about.copyright"
values={
Object {
"currentYear": 2017,
}
}
/>
</div>
<div
className="about-modal__links"
>
<ExternalLink
href="https://mattermost.com/terms-of-use/"
id="tosLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Terms of Use"
id="about.tos"
/>
</ExternalLink>
-
<ExternalLink
href="https://mattermost.com/privacy-policy/"
id="privacyLink"
location="about_build_modal"
>
<MemoizedFormattedMessage
defaultMessage="Privacy Policy"
id="about.privacy"
/>
</ExternalLink>
</div>
</div>
</div>
<div
className="about-modal__notice form-group pt-3"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps."
id="about.notice"
values={
Object {
"linkDesktop": [Function],
"linkMobile": [Function],
"linkServer": [Function],
}
}
/>
</p>
</div>
<div
className="about-modal__hash"
>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Hash:"
id="about.hash"
/>
<Nbsp />
abcdef1234567890
<br />
<MemoizedFormattedMessage
defaultMessage="EE Build Hash:"
id="about.hashee"
/>
<Nbsp />
</p>
<p>
<MemoizedFormattedMessage
defaultMessage="Build Date:"
id="about.date"
/>
<Nbsp />
21 January 2017
</p>
</div>
</ModalBody>
</Modal>
`;

Просмотреть файл

@@ -2,8 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {Modal} from 'react-bootstrap';
import {shallow} from 'enzyme';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import mockStore from 'tests/test_store'; import mockStore from 'tests/test_store';
@@ -12,11 +10,13 @@ import {ClientConfig, ClientLicense} from '@mattermost/types/config';
import AboutBuildModal from 'components/about_build_modal/about_build_modal'; import AboutBuildModal from 'components/about_build_modal/about_build_modal';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {AboutLinks} from 'utils/constants'; import {AboutLinks} from 'utils/constants';
import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud'; import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud';
import {screen} from '@testing-library/react';
import {renderWithIntl} from 'tests/react_testing_utils';
import store from 'stores/redux_store';
import userEvent from '@testing-library/user-event';
describe('components/AboutBuildModal', () => { describe('components/AboutBuildModal', () => {
const RealDate: DateConstructor = Date; const RealDate: DateConstructor = Date;
@@ -60,10 +60,17 @@ describe('components/AboutBuildModal', () => {
}); });
test('should match snapshot for enterprise edition', () => { test('should match snapshot for enterprise edition', () => {
const wrapper = shallowAboutBuildModal({config, license}); renderAboutBuildModal({config, license});
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2'); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(wrapper).toMatchSnapshot(); expect(screen.getByText('Mattermost Enterprise Edition')).toBeInTheDocument();
expect(screen.getByText('Modern communication from behind your firewall.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com'})).toHaveAttribute('href', 'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.getByText('EE Build Hash: 0123456789abcdef', {exact: false})).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
}); });
test('should match snapshot for team edition', () => { test('should match snapshot for team edition', () => {
@@ -73,19 +80,25 @@ describe('components/AboutBuildModal', () => {
BuildHashEnterprise: '', BuildHashEnterprise: '',
}; };
const wrapper = shallowAboutBuildModal({config: teamConfig, license: {}}); renderAboutBuildModal({config: teamConfig, license: {}});
expect(wrapper.find('#versionString').text()).toBe('\u00a03.6.2'); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.2');
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(wrapper).toMatchSnapshot(); expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
}); });
test('should match snapshot for cloud edition', () => { test('should match snapshot for cloud edition', () => {
if (license !== null) { if (license !== null) {
license.Cloud = 'true'; license.Cloud = 'true';
} }
const store = mockStore();
const wrapper = shallow( renderWithIntl(
<Provider store={store}> <Provider store={store}>
<AboutBuildModalCloud <AboutBuildModalCloud
config={config} config={config}
@@ -96,7 +109,14 @@ describe('components/AboutBuildModal', () => {
/> />
</Provider>, </Provider>,
); );
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Mattermost Cloud')).toBeInTheDocument();
expect(screen.getByText('High trust messaging for the enterprise')).toBeInTheDocument();
expect(screen.getByText('0123456789abcdef', {exact: false})).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
}); });
test('should show dev if this is a dev build', () => { test('should show dev if this is a dev build', () => {
@@ -109,10 +129,18 @@ describe('components/AboutBuildModal', () => {
BuildNumber: 'dev', BuildNumber: 'dev',
}; };
const wrapper = shallowAboutBuildModal({config: sameBuildConfig, license: {}}); renderAboutBuildModal({config: sameBuildConfig, license: {}});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#versionString').text()).toBe('\u00a0dev'); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: dev');
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
}); });
test('should show ci if a ci build', () => { test('should show ci if a ci build', () => {
@@ -125,11 +153,19 @@ describe('components/AboutBuildModal', () => {
BuildNumber: '123', BuildNumber: '123',
}; };
const wrapper = shallowAboutBuildModal({config: differentBuildConfig, license: {}}); renderAboutBuildModal({config: differentBuildConfig, license: {}});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#versionString').text()).toBe('\u00a0ci'); expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: ci');
expect(wrapper.find('#dbversionString').text()).toBe('\u00a077'); expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(wrapper.find('#buildnumberString').text()).toBe('\u00a0123'); expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123');
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
}); });
test('should call onExited callback when the modal is hidden', () => { test('should call onExited callback when the modal is hidden', () => {
@@ -148,7 +184,7 @@ describe('components/AboutBuildModal', () => {
}, },
}); });
const wrapper = mountWithIntl( renderWithIntl(
<Provider store={store}> <Provider store={store}>
<AboutBuildModal <AboutBuildModal
config={config} config={config}
@@ -158,7 +194,7 @@ describe('components/AboutBuildModal', () => {
</Provider>, </Provider>,
); );
wrapper.find(Modal).first().props().onExited?.(document.createElement('div')); userEvent.click(screen.getByText('Close'));
expect(onExited).toHaveBeenCalledTimes(1); expect(onExited).toHaveBeenCalledTimes(1);
}); });
@@ -176,7 +212,7 @@ describe('components/AboutBuildModal', () => {
}, },
}, },
}); });
const wrapper = mountWithIntl( renderWithIntl(
<Provider store={store}> <Provider store={store}>
<AboutBuildModal <AboutBuildModal
config={config} config={config}
@@ -186,24 +222,15 @@ describe('components/AboutBuildModal', () => {
</Provider>, </Provider>,
); );
expect( expect(screen.getByRole('link', {name: 'Terms of Use'})).toHaveAttribute('href', `${AboutLinks.TERMS_OF_SERVICE}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
wrapper.find(AboutBuildModal).find('a#tosLink').props().href,
).toBe(
AboutLinks.TERMS_OF_SERVICE +
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
);
expect(
wrapper.find(AboutBuildModal).find('a#privacyLink').props().href,
).toBe(
AboutLinks.PRIVACY_POLICY +
'?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=',
);
expect(wrapper.find(AboutBuildModal).find('a#tosLink').props().href).not.toBe(config?.TermsOfServiceLink); expect(screen.getByRole('link', {name: 'Privacy Policy'})).toHaveAttribute('href', `${AboutLinks.PRIVACY_POLICY}?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=currentUserId&sid=`);
expect(wrapper.find(AboutBuildModal).find('a#privacyLink').props().href).not.toBe(config?.PrivacyPolicyLink);
expect(screen.getByRole('link', {name: 'Terms of Use'})).not.toHaveAttribute('href', config?.TermsOfServiceLink);
expect(screen.getByRole('link', {name: 'Privacy Policy'})).not.toHaveAttribute('href', config?.PrivacyPolicyLink);
}); });
function shallowAboutBuildModal(props = {}) { function renderAboutBuildModal(props = {}) {
const onExited = jest.fn(); const onExited = jest.fn();
const show = true; const show = true;
@@ -215,6 +242,6 @@ describe('components/AboutBuildModal', () => {
...props, ...props,
}; };
return shallow(<AboutBuildModal {...allProps}/>); return renderWithIntl(<Provider store={store}><AboutBuildModal {...allProps}/></Provider>);
} }
}); });

Просмотреть файл

@@ -49,6 +49,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
doHide = () => { doHide = () => {
this.setState({show: false}); this.setState({show: false});
this.props.onExited();
}; };
render() { render() {
@@ -172,7 +173,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
// Only show build number if it's a number (so only builds from Jenkins) // Only show build number if it's a number (so only builds from Jenkins)
let buildnumber: JSX.Element | null = ( let buildnumber: JSX.Element | null = (
<div> <div data-testid='aboutModalBuildNumber'>
<FormattedMessage <FormattedMessage
id='about.buildnumber' id='about.buildnumber'
defaultMessage='Build Number:' defaultMessage='Build Number:'
@@ -227,7 +228,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
{subTitle} {subTitle}
</p> </p>
<div className='form-group less'> <div className='form-group less'>
<div> <div data-testid='aboutModalVersion'>
<FormattedMessage <FormattedMessage
id='about.version' id='about.version'
defaultMessage='Mattermost Version:' defaultMessage='Mattermost Version:'
@@ -236,7 +237,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
{'\u00a0' + mmversion} {'\u00a0' + mmversion}
</span> </span>
</div> </div>
<div> <div data-testid='aboutModalDBVersionString'>
<FormattedMessage <FormattedMessage
id='about.dbversion' id='about.dbversion'
defaultMessage='Database Schema Version:' defaultMessage='Database Schema Version:'

Просмотреть файл

@@ -353,6 +353,36 @@ exports[`components/DatabaseSettings should match snapshot 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
>
<MemoizedFormattedMessage
defaultMessage="Active Search Backend:"
id="admin.database.search_backend.title"
/>
</label>
<div
className="col-sm-8"
>
<input
className="form-control"
disabled={true}
type="text"
value=""
/>
<div
className="help-text"
>
<MemoizedFormattedMessage
defaultMessage="Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc."
id="admin.database.search_backend.help_text"
/>
</div>
</div>
</div>
</SettingsGroup> </SettingsGroup>
<div <div
className="admin-console-save" className="admin-console-save"

Просмотреть файл

@@ -2869,7 +2869,7 @@ const AdminDefinition = {
label: t('admin.customization.enableSVGsTitle'), label: t('admin.customization.enableSVGsTitle'),
label_default: 'Enable SVGs:', label_default: 'Enable SVGs:',
help_text: t('admin.customization.enableSVGsDesc'), help_text: t('admin.customization.enableSVGsDesc'),
help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.', help_text_default: 'Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)), isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
}, },
{ {
@@ -2878,7 +2878,7 @@ const AdminDefinition = {
label: t('admin.customization.enableLatexTitle'), label: t('admin.customization.enableLatexTitle'),
label_default: 'Enable Latex Rendering:', label_default: 'Enable Latex Rendering:',
help_text: t('admin.customization.enableLatexDesc'), help_text: t('admin.customization.enableLatexDesc'),
help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.', help_text_default: 'Enable rendering of Latex in code blocks. If false, Latex code will be highlighted only.\n\nEnabling Latex is not recommended in environments where not all users are trusted.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)), isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
}, },
{ {

Просмотреть файл

@@ -2,11 +2,9 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import {screen} from '@testing-library/react';
import {Provider} from 'react-redux'; import {renderWithIntlAndStore} from 'tests/react_testing_utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {CloudProducts, RecurringIntervals} from 'utils/constants'; import {CloudProducts, RecurringIntervals} from 'utils/constants';
import {ToYearlyNudgeBanner, ToYearlyNudgeBannerDismissable} from './to_yearly_nudge_banner'; import {ToYearlyNudgeBanner, ToYearlyNudgeBannerDismissable} from './to_yearly_nudge_banner';
@@ -42,7 +40,7 @@ const initialState = {
}, },
}; };
describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () => { describe('ToYearlyNudgeBannerDismissable', () => {
test('should show for admins cloud professional monthly', () => { test('should show for admins cloud professional monthly', () => {
const state = JSON.parse(JSON.stringify(initialState)); const state = JSON.parse(JSON.stringify(initialState));
state.entities.users.profiles = { state.entities.users.profiles = {
@@ -63,14 +61,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(true); screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar');
}); });
test('should NOT show for NON admins', () => { test('should NOT show for NON admins', () => {
@@ -93,14 +86,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
}); });
test('should NOT show for admins on cloud free', () => { test('should NOT show for admins on cloud free', () => {
@@ -123,14 +111,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
}); });
test('should NOT show for admins on cloud enterprise', () => { test('should NOT show for admins on cloud enterprise', () => {
@@ -153,14 +136,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
}); });
test('should NOT show for admins on cloud pro annual', () => { test('should NOT show for admins on cloud pro annual', () => {
@@ -182,15 +160,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}, },
}; };
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const store = mockStore(state); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
}); });
test('should NOT show for admins when banner was dismissed in preferences', () => { test('should NOT show for admins when banner was dismissed in preferences', () => {
@@ -200,10 +172,10 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}; };
state.entities.preferences = { state.entities.preferences = {
myPreferences: { myPreferences: {
'cloud_yearly_nudge_banner--nudge_to_yearly_banner_dismissed': { 'to_cloud_yearly_plan_nudge--nudge_to_cloud_yearly_plan_snoozed': {
category: 'cloud_yearly_nudge_banner', category: 'to_cloud_yearly_plan_nudge',
name: 'nudge_to_yearly_banner_dismissed', name: 'nudge_to_cloud_yearly_plan_snoozed',
value: 'true', value: '{"range": 0, "show": false}',
}, },
}, },
}; };
@@ -221,19 +193,13 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
}, },
}, },
}; };
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const store = mockStore(state); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
}); });
}); });
describe('components/admin_console/billing/ToYearlyNudgeBanner', () => { describe('ToYearlyNudgeBanner', () => {
test('should show for cloud professional monthly', () => { test('should show for cloud professional monthly', () => {
const state = JSON.parse(JSON.stringify(initialState)); const state = JSON.parse(JSON.stringify(initialState));
state.entities.cloud = { state.entities.cloud = {
@@ -251,14 +217,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBanner/>
</Provider>,
);
expect(wrapper.find('AlertBanner').exists()).toBe(true); screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner');
}); });
test('should NOT show for non cloud professional monthly', () => { test('should NOT show for non cloud professional monthly', () => {
@@ -278,13 +239,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
}, },
}; };
const store = mockStore(state); renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBanner/>
</Provider>,
);
expect(wrapper.find('AlertBanner').exists()).toBe(false); expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
}); });
}); });

Просмотреть файл

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import {useIntl, FormattedMessage} from 'react-intl'; import {useIntl, FormattedMessage} from 'react-intl';
import moment from 'moment';
import AlertBanner from 'components/alert_banner'; import AlertBanner from 'components/alert_banner';
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal'; import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
@@ -22,12 +23,35 @@ import {GlobalState} from '@mattermost/types/store';
import './to_yearly_nudge_banner.scss'; import './to_yearly_nudge_banner.scss';
enum DismissShowRange {
GreaterThanEqual90 = '>=90',
BetweenNinetyAnd60 = '89-61',
SixtyTo31 = '60-31',
ThirtyTo11 = '30-11',
TenTo1 = '10-1',
Zero = '0'
}
const cloudProMonthlyCloseMoment = '20230727';
interface ToYearlyPlanDismissPreference {
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
range: DismissShowRange;
show: boolean;
}
const ToYearlyNudgeBannerDismissable = () => { const ToYearlyNudgeBannerDismissable = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const openPurchaseModal = useOpenCloudPurchaseModal({}); const openPurchaseModal = useOpenCloudPurchaseModal({});
const nudgeDismissed = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_YEARLY_NUDGE_BANNER, CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED)) === 'true'; const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE, CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED, '{"range": 0, "show": true}'));
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToYearlyPlanDismissPreference;
const show = snoozeInfo.show;
const currentUser = useSelector(getCurrentUser); const currentUser = useSelector(getCurrentUser);
const isAdmin = useSelector(isCurrentUserSystemAdmin); const isAdmin = useSelector(isCurrentUserSystemAdmin);
const product = useSelector(selectSubscriptionProduct); const product = useSelector(selectSubscriptionProduct);
@@ -35,16 +59,75 @@ const ToYearlyNudgeBannerDismissable = () => {
const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH; const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH;
const currentProductProMonthly = currentProductProfessional && currentProductIsMonthly; const currentProductProMonthly = currentProductProfessional && currentProductIsMonthly;
const savedDismissedPref = () => { const now = moment(Date.now());
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
const snoozedForRange = (range: DismissShowRange) => {
return snoozeInfo.range === range;
};
useEffect(() => {
if (!snoozeInfo.show) {
if (daysToProMonthlyEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
showBanner(true);
}
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 10) {
showBanner(true);
}
}
}, []);
const showBanner = (show = false) => {
let dRange = DismissShowRange.Zero;
if (daysToProMonthlyEnd >= 90) {
dRange = DismissShowRange.GreaterThanEqual90;
}
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60) {
dRange = DismissShowRange.BetweenNinetyAnd60;
}
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30) {
dRange = DismissShowRange.SixtyTo31;
}
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10) {
dRange = DismissShowRange.ThirtyTo11;
}
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
if (daysToProMonthlyEnd <= 10 && daysToProMonthlyEnd > 0) {
dRange = DismissShowRange.TenTo1;
}
const snoozeInfo: ToYearlyPlanDismissPreference = {
range: dRange,
show,
};
dispatch(savePreferences(currentUser.id, [{ dispatch(savePreferences(currentUser.id, [{
category: Preferences.CLOUD_YEARLY_NUDGE_BANNER, category: Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE,
name: CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED, name: CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED,
user_id: currentUser.id, user_id: currentUser.id,
value: 'true', value: JSON.stringify(snoozeInfo),
}])); }]));
}; };
if (nudgeDismissed) { if (!show) {
return null; return null;
} }
@@ -56,21 +139,30 @@ const ToYearlyNudgeBannerDismissable = () => {
return null; return null;
} }
const message = { const message = (
id: 'cloud_billing.nudge_to_yearly.announcement_bar', <FormattedMessage
defaultMessage: 'Simplify your billing and switch to an annual plan today', id='cloud_billing.nudge_to_yearly.announcement_bar'
}; defaultMessage='Monthly billing will be discontinued in {days} days . Switch to annual billing'
values={{
days: daysToProMonthlyEnd,
}}
/>
);
const announcementType = (daysToProMonthlyEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
return ( return (
<AnnouncementBar <AnnouncementBar
type={AnnouncementBarTypes.ANNOUNCEMENT} id='cloud-pro-monthly-deprecation-announcement-bar'
showCloseButton={true} type={announcementType}
showCloseButton={daysToProMonthlyEnd > 10}
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})} onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')} modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
modalButtonDefaultText='Learn more' modalButtonDefaultText='Learn more'
message={<FormattedMessage {...message}/>} message={message}
showLinkAsButton={true} showLinkAsButton={true}
handleClose={savedDismissedPref} handleClose={showBanner}
/> />
); );
}; };
@@ -90,17 +182,22 @@ const ToYearlyNudgeBanner = () => {
return null; return null;
} }
const now = moment(Date.now());
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
const title = ( const title = (
<FormattedMessage <FormattedMessage
id='cloud_billing.nudge_to_yearly.title' id='cloud_billing.nudge_to_yearly.title'
defaultMessage='Switch to an annual plan today' defaultMessage='Action required: Switch to annual billing to keep your workspace.'
/> />
); );
const description = ( const description = (
<FormattedMessage <FormattedMessage
id='cloud_billing.nudge_to_yearly.description' id='cloud_billing.nudge_to_yearly.description'
defaultMessage='Simplify your billing by switching to an annual subscription.' defaultMessage='Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.'
values={{date: moment(cloudProMonthlyCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY')}}
/> />
); );
@@ -122,9 +219,12 @@ const ToYearlyNudgeBanner = () => {
</button> </button>
); );
const bannerMode = (daysToProMonthlyEnd <= 10) ? 'danger' : 'info';
return ( return (
<AlertBanner <AlertBanner
mode='info' id='cloud-pro-monthly-deprecation-alert-banner'
mode={bannerMode}
title={title} title={title}
message={description} message={description}
className='ToYearlyNudgeBanner' className='ToYearlyNudgeBanner'

Просмотреть файл

@@ -6,7 +6,7 @@ import React from 'react';
import {injectIntl, WrappedComponentProps} from 'react-intl'; import {injectIntl, WrappedComponentProps} from 'react-intl';
import {Feedback} from '@mattermost/types/cloud'; import {Feedback} from '@mattermost/types/cloud';
import FeedbackModal from 'components/feedback_modal/feedback'; import FeedbackModal, {FeedbackOption} from 'components/feedback_modal/feedback';
type Props = { type Props = {
onSubmit: (deleteFeedback: Feedback) => void; onSubmit: (deleteFeedback: Feedback) => void;
@@ -28,23 +28,35 @@ const DeleteFeedbackModal = (props: Props) => {
defaultMessage: 'Delete Workspace', defaultMessage: 'Delete Workspace',
}); });
const deleteFeedbackOptions = [ const deleteFeedbackOptions: FeedbackOption[] = [
props.intl.formatMessage({ {
translatedMessage: props.intl.formatMessage({
id: 'feedback.deleteWorkspace.feedbackNoValue', id: 'feedback.deleteWorkspace.feedbackNoValue',
defaultMessage: 'No longer found value', defaultMessage: 'No longer found value',
}), }),
props.intl.formatMessage({ submissionValue: 'No longer found value',
},
{
translatedMessage: props.intl.formatMessage({
id: 'feedback.deleteWorkspace.feedbackMoving', id: 'feedback.deleteWorkspace.feedbackMoving',
defaultMessage: 'Moving to a different solution', defaultMessage: 'Moving to a different solution',
}), }),
props.intl.formatMessage({ submissionValue: 'Moving to a different solution',
},
{
translatedMessage: props.intl.formatMessage({
id: 'feedback.deleteWorkspace.feedbackMistake', id: 'feedback.deleteWorkspace.feedbackMistake',
defaultMessage: 'Created a workspace by mistake', defaultMessage: 'Created a workspace by mistake',
}), }),
props.intl.formatMessage({ submissionValue: 'Created a workspace by mistake',
},
{
translatedMessage: props.intl.formatMessage({
id: 'feedback.deleteWorkspace.feedbackHosting', id: 'feedback.deleteWorkspace.feedbackHosting',
defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)', defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)',
}), }),
submissionValue: 'Moving to hosting my own Mattermost instance (self-hosted)',
},
]; ];
return ( return (

Просмотреть файл

@@ -4,7 +4,7 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {recycleDatabaseConnection} from 'actions/admin_actions.jsx'; import {recycleDatabaseConnection, ping} from 'actions/admin_actions';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import {t} from 'utils/i18n'; import {t} from 'utils/i18n';
@@ -19,6 +19,15 @@ import TextSetting from './text_setting';
import MigrationsTable from './database'; import MigrationsTable from './database';
export default class DatabaseSettings extends AdminSettings { export default class DatabaseSettings extends AdminSettings {
constructor(props) {
super(props);
this.state = {
...this.state,
searchBackend: '',
};
}
getConfigFromState = (config) => { getConfigFromState = (config) => {
// driverName and dataSource are read-only from the UI // driverName and dataSource are read-only from the UI
@@ -34,6 +43,17 @@ export default class DatabaseSettings extends AdminSettings {
return config; return config;
}; };
componentDidMount() {
this.getSearchBackend().then((searchBackend) => {
this.setState({searchBackend});
});
}
async getSearchBackend() {
const res = await ping()();
return res.ActiveSearchBackend;
}
getStateFromConfig(config) { getStateFromConfig(config) {
return { return {
driverName: config.SqlSettings.DriverName, driverName: config.SqlSettings.DriverName,
@@ -368,6 +388,30 @@ export default class DatabaseSettings extends AdminSettings {
</div> </div>
</div> </div>
</div> </div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
<FormattedMessage
id='admin.database.search_backend.title'
defaultMessage='Active Search Backend:'
/>
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
value={this.state.searchBackend}
disabled={true}
/>
<div className='help-text'>
<FormattedMessage
id='admin.database.search_backend.help_text'
defaultMessage='Shows the currently active backend used for search. Values can be none, database, elasticsearch, bleve etc.'
/>
</div>
</div>
</div>
</SettingsGroup> </SettingsGroup>
); );
}; };

Просмотреть файл

@@ -7,8 +7,14 @@ import {shallow} from 'enzyme';
import DatabaseSettings from 'components/admin_console/database_settings.jsx'; import DatabaseSettings from 'components/admin_console/database_settings.jsx';
jest.mock('actions/admin_actions.jsx', () => { jest.mock('actions/admin_actions.jsx', () => {
const pingFn = () => {
return jest.fn(() => {
return {ActiveSearchBackend: 'none'};
});
};
return { return {
recycleDatabaseConnection: jest.fn(), recycleDatabaseConnection: jest.fn(),
ping: pingFn,
}; };
}); });

Просмотреть файл

@@ -671,7 +671,7 @@ export default class SchemaAdminSettings extends React.PureComponent {
}; };
handleGeneratedChange = (id, s) => { handleGeneratedChange = (id, s) => {
this.handleChange(id, s.replace('+', '-').replace('/', '_')); this.handleChange(id, s.replace(/\+/g, '-').replace(/\//g, '_'));
}; };
handleChange = (id, value, confirm = false, doSubmit = false, warning = false) => { handleChange = (id, value, confirm = false, doSubmit = false, warning = false) => {

Просмотреть файл

@@ -213,9 +213,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
content="name" content="name"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -242,6 +244,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud restore sho
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -457,9 +460,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
content="name" content="name"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -486,6 +491,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -603,9 +609,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
id="sharedTooltip" id="sharedTooltip"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -632,6 +640,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -834,9 +843,11 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
content="name" content="name"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -863,6 +874,7 @@ exports[`admin_console/team_channel_settings/team/TeamProfile__Cloud should matc
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,

Просмотреть файл

@@ -6,7 +6,7 @@ import {shallow} from 'enzyme';
import ChipsList, {ChipsInfoType} from 'components/admin_console/workspace-optimization/chips_list'; import ChipsList, {ChipsInfoType} from 'components/admin_console/workspace-optimization/chips_list';
import {ItemStatus} from './dashboard.data'; import {ItemStatus} from './dashboard.type';
describe('components/admin_console/workspace-optimization/chips_list', () => { describe('components/admin_console/workspace-optimization/chips_list', () => {
const overallScoreChips: ChipsInfoType = { const overallScoreChips: ChipsInfoType = {

Просмотреть файл

@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
import Chip from 'components/common/chip/chip'; import Chip from 'components/common/chip/chip';
import {ItemStatus} from './dashboard.data'; import {ItemStatus} from './dashboard.type';
import './dashboard.scss'; import './dashboard.scss';

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React, {useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
@@ -14,56 +14,23 @@ import {
AccountMultipleOutlineIcon, AccountMultipleOutlineIcon,
} from '@mattermost/compass-icons/components'; } from '@mattermost/compass-icons/components';
import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {getLicense, getServerVersion} from 'mattermost-redux/selectors/entities/general';
import {GlobalState} from '@mattermost/types/store'; import {GlobalState} from '@mattermost/types/store';
import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants'; import {ConsolePages} from 'utils/constants';
import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {AdminConfig} from '@mattermost/types/config';
import {runConfigChecks} from './dashboard_checks/config';
import {DataModel, ItemStatus, Options} from './dashboard.type';
import {runAccessChecks} from './dashboard_checks/access';
import {runDataPrivacyChecks} from './dashboard_checks/data_privacy';
import {runPerformanceChecks} from './dashboard_checks/performance';
import {runEaseOfUseChecks} from './dashboard_checks/easy_management';
import {runUpdateChecks} from './dashboard_checks/updates';
export type DataModel = { export const impactModifiers: Record<ItemStatus, number> = {
[key: string]: {
title: string;
description: string;
descriptionOk: string;
items: ItemModel[];
icon: React.ReactNode;
hide?: boolean;
};
}
export enum ItemStatus {
NONE = 'none',
OK = 'ok',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
}
export type ItemModel = {
id: string;
title: string;
description: string;
status: ItemStatus;
scoreImpact: number;
impactModifier: number;
configUrl?: string;
configText?: string;
telemetryAction?: string;
infoUrl?: string;
infoText?: string;
}
export type UpdatesParam = {
serverVersion: {
type: string;
status: ItemStatus;
description: string;
};
}
const impactModifiers: Record<ItemStatus, number> = {
[ItemStatus.NONE]: 1, [ItemStatus.NONE]: 1,
[ItemStatus.OK]: 1, [ItemStatus.OK]: 1,
[ItemStatus.INFO]: 0.5, [ItemStatus.INFO]: 0.5,
@@ -71,28 +38,11 @@ const impactModifiers: Record<ItemStatus, number> = {
[ItemStatus.ERROR]: 0, [ItemStatus.ERROR]: 0,
}; };
const useMetricsData = () => { const getUpdatesData = async (
const {formatMessage} = useIntl(); config: Partial<AdminConfig>,
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense); formatMessage: ReturnType<typeof useIntl>['formatMessage'],
const license = useSelector(getLicense); options: Options,
) => ({
const canStartTrial = license?.IsLicensed !== 'true' && prevTrialLicense?.IsLicensed !== 'true';
const daysUntilExpiration = daysToLicenseExpire(license) || -1;
const isLicensed = license?.IsLicensed === 'true' && daysUntilExpiration >= 0;
const isCloud = license?.Cloud === 'true';
const isEnterpriseLicense = isEnterpriseOrE20License(license);
const isStarterLicense = getIsStarterLicense(license);
const [, contactSalesLink] = useOpenSalesLink();
const trialOrEnterpriseCtaConfig = {
configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink,
configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}),
};
const getUpdatesData = (data: UpdatesParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.updates.title', id: 'admin.reporting.workspace_optimization.updates.title',
defaultMessage: 'Server updates', defaultMessage: 'Server updates',
@@ -113,36 +63,14 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runUpdateChecks(config, formatMessage, options),
{ });
id: 'server_version',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
defaultMessage: '{type} version update available.',
}, {type: data.serverVersion.type}),
description: data.serverVersion.description,
configUrl: CloudLinks.DOWNLOAD_UPDATE,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.downloadUpdate', defaultMessage: 'Download update'}),
infoUrl: DocLinks.UPGRADE_SERVER,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'server-version',
status: data.serverVersion.status,
scoreImpact: 15,
impactModifier: impactModifiers[data.serverVersion.status],
},
],
});
type ConfigurationParam = { const getConfigurationData = async (
ssl: { config: Partial<AdminConfig>,
status: ItemStatus; formatMessage: ReturnType<typeof useIntl>['formatMessage'],
}; options: Options,
sessionLength: { ) => ({
status: ItemStatus;
};
}
const getConfigurationData = (data: ConfigurationParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.title', id: 'admin.reporting.workspace_optimization.configuration.title',
defaultMessage: 'Configuration', defaultMessage: 'Configuration',
@@ -151,7 +79,7 @@ const useMetricsData = () => {
id: 'admin.reporting.workspace_optimization.configuration.description', id: 'admin.reporting.workspace_optimization.configuration.description',
defaultMessage: 'You have configuration issues to resolve', defaultMessage: 'You have configuration issues to resolve',
}), }),
hide: isCloud, hide: options.isCloud,
descriptionOk: formatMessage({ descriptionOk: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.descriptionOk', id: 'admin.reporting.workspace_optimization.configuration.descriptionOk',
defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!', defaultMessage: 'You\'ve successfully configured SSL and Session Lengths!',
@@ -164,53 +92,14 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runConfigChecks(config, formatMessage, options),
{ });
id: 'ssl',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
defaultMessage: 'Configure SSL to make your server more secure',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
}),
infoUrl: DocLinks.SSL_CERTIFICATE,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'ssl',
status: data.ssl.status,
scoreImpact: 25,
impactModifier: impactModifiers[data.ssl.status],
},
{
id: 'session-length',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
defaultMessage: 'Session lengths is set to default',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
}),
configUrl: ConsolePages.SESSION_LENGTHS,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureSessionLength', defaultMessage: 'Configure session length'}),
infoUrl: DocLinks.SESSION_LENGTHS,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'session-length',
status: data.sessionLength.status,
scoreImpact: 8,
impactModifier: impactModifiers[data.sessionLength.status],
},
],
});
type AccessParam = { const getAccessData = async (
siteUrl: { config: Partial<AdminConfig>,
status: ItemStatus; formatMessage: ReturnType<typeof useIntl>['formatMessage'],
}; options: Options,
} ) => ({
const getAccessData = (data: AccessParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.access.title', id: 'admin.reporting.workspace_optimization.access.title',
defaultMessage: 'Workspace access', defaultMessage: 'Workspace access',
@@ -219,7 +108,7 @@ const useMetricsData = () => {
id: 'admin.reporting.workspace_optimization.access.description', id: 'admin.reporting.workspace_optimization.access.description',
defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.', defaultMessage: 'Web server configuration may be affecting access to your Mattermost workspace.',
}), }),
hide: isCloud, hide: options.isCloud,
descriptionOk: formatMessage({ descriptionOk: formatMessage({
id: 'admin.reporting.workspace_optimization.access.descriptionOk', id: 'admin.reporting.workspace_optimization.access.descriptionOk',
defaultMessage: 'Your web server configuration is passing a live URL test!', defaultMessage: 'Your web server configuration is passing a live URL test!',
@@ -232,36 +121,14 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runAccessChecks(config, formatMessage),
{ });
id: 'site-url',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.access.site_url.title',
defaultMessage: 'Misconfigured web server',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.access.site_url.description',
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
}),
configUrl: ConsolePages.WEB_SERVER,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureWebServer', defaultMessage: 'Configure web server'}),
infoUrl: DocLinks.SITE_URL,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'site-url',
status: data.siteUrl.status,
scoreImpact: 12,
impactModifier: impactModifiers[data.siteUrl.status],
},
],
});
type PerformanceParam = { const getPerformanceData = async (
search: { config: Partial<AdminConfig>,
status: ItemStatus; formatMessage: ReturnType<typeof useIntl>['formatMessage'],
}; options: Options,
} ) => ({
const getPerformanceData = (data: PerformanceParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.title', id: 'admin.reporting.workspace_optimization.performance.title',
defaultMessage: 'Performance', defaultMessage: 'Performance',
@@ -270,7 +137,7 @@ const useMetricsData = () => {
id: 'admin.reporting.workspace_optimization.performance.description', id: 'admin.reporting.workspace_optimization.performance.description',
defaultMessage: 'Your server would benefit from some performance tweaks.', defaultMessage: 'Your server would benefit from some performance tweaks.',
}), }),
hide: isCloud, hide: options.isCloud,
descriptionOk: formatMessage({ descriptionOk: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.descriptionOk', id: 'admin.reporting.workspace_optimization.performance.descriptionOk',
defaultMessage: 'Your search performance suits your workspace usage!', defaultMessage: 'Your search performance suits your workspace usage!',
@@ -283,39 +150,14 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runPerformanceChecks(config, formatMessage, options),
{ });
id: 'search',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.search.title',
defaultMessage: 'Search performance',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.search.description',
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
}),
...(isLicensed && isEnterpriseLicense ? {
configUrl: ConsolePages.ELASTICSEARCH,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureElasticsearch', defaultMessage: 'Try Elasticsearch'}),
} : trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.ELASTICSEARCH,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'search-optimization',
status: data.search.status,
scoreImpact: 20,
impactModifier: impactModifiers[data.search.status],
},
],
});
type DataPrivacyParam = { const getDataPrivacyData = async (
retention: { config: Partial<AdminConfig>,
status: ItemStatus; formatMessage: ReturnType<typeof useIntl>['formatMessage'],
}; options: Options,
} ) => ({
// TBD
const getDataPrivacyData = (data: DataPrivacyParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.data_privacy.title', id: 'admin.reporting.workspace_optimization.data_privacy.title',
defaultMessage: 'Data privacy', defaultMessage: 'Data privacy',
@@ -336,42 +178,14 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runDataPrivacyChecks(config, formatMessage, options),
{ });
id: 'data-retention',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
defaultMessage: 'Become more data aware',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
}),
...(isLicensed && isEnterpriseLicense ? {
configUrl: ConsolePages.DATA_RETENTION,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureDataRetention', defaultMessage: 'Try data retention'}),
} : trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.DATA_RETENTION_POLICY,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'data-retention',
status: data.retention.status,
scoreImpact: 16,
impactModifier: impactModifiers[data.retention.status],
},
],
});
type EaseOfManagementParam = { const getEaseOfManagementData = async (
ldap: { config: Partial<AdminConfig>,
status: ItemStatus; formatMessage: ReturnType<typeof useIntl>['formatMessage'],
}; options: Options,
guestAccounts?: { ) => ({
status: ItemStatus;
};
}
// TBD
const getEaseOfManagementData = (data: EaseOfManagementParam) => ({
title: formatMessage({ title: formatMessage({
id: 'admin.reporting.workspace_optimization.ease_of_management.title', id: 'admin.reporting.workspace_optimization.ease_of_management.title',
defaultMessage: 'Ease of management', defaultMessage: 'Ease of management',
@@ -392,53 +206,74 @@ const useMetricsData = () => {
/> />
</div> </div>
), ),
items: [ items: await runEaseOfUseChecks(config, formatMessage, options),
{ });
id: 'ad-ldap',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
defaultMessage: 'AD/LDAP integration recommended',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
}),
...(isLicensed && !isStarterLicense ? {
configUrl: ConsolePages.AD_LDAP,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.configureLDAP', defaultMessage: 'Try AD/LDAP'}),
} : trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.AD_LDAP,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'ad-ldap',
status: data.ldap.status,
scoreImpact: 22,
impactModifier: impactModifiers[data.ldap.status],
},
// commented out for now. const useMetricsData = (
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385 config: Partial<AdminConfig>,
// { ) => {
// id: 'guest-accounts', const [loading, setLoading] = useState(true);
// title: formatMessage({ const [data, setData] = useState<DataModel | undefined>(undefined);
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
// defaultMessage: 'Guest Accounts recommended', const {formatMessage} = useIntl();
// }), const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
// description: formatMessage({ const license = useSelector(getLicense);
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.', // get the currently installed server version
// }), const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
// ...trialOrEnterpriseCtaConfig, const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics) as unknown as Options['analytics'];
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}), const canStartTrial = license?.IsLicensed !== 'true' && prevTrialLicense?.IsLicensed !== 'true';
// telemetryAction: 'guest-accounts', const daysUntilExpiration = daysToLicenseExpire(license) || -1;
// status: data.guestAccounts.status,
// scoreImpact: 6, const isLicensed = license?.IsLicensed === 'true' && daysUntilExpiration >= 0;
// impactModifier: impactModifiers[data.guestAccounts.status],
// }, const isCloud = license?.Cloud === 'true';
], const isEnterpriseLicense = isEnterpriseOrE20License(license);
const isStarterLicense = getIsStarterLicense(license);
const [, contactSalesLink] = useOpenSalesLink();
const trialOrEnterpriseCtaConfig = useMemo(() => ({
configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink,
configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}),
}), [canStartTrial, contactSalesLink, formatMessage]);
const options: Options = useMemo(() => ({
isLicensed,
isEnterpriseLicense,
trialOrEnterpriseCtaConfig,
isStarterLicense,
isCloud,
analytics,
installedVersion,
}), [isLicensed, isEnterpriseLicense, trialOrEnterpriseCtaConfig, isStarterLicense, isCloud, analytics, installedVersion]);
useEffect(() => {
setLoading(true);
const refreshData = async () => {
const data = {
updates: await getUpdatesData(config, formatMessage, options),
configuration: await getConfigurationData(config, formatMessage, options),
access: await getAccessData(config, formatMessage, options),
performance: await getPerformanceData(config, formatMessage, options),
dataPrivacy: await getDataPrivacyData(config, formatMessage, options),
easyManagement: await getEaseOfManagementData(config, formatMessage, options),
};
return data;
};
refreshData().then((data) => {
setData(data);
setLoading(false);
}); });
}, [config, formatMessage, options]);
return {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense}; return {
data,
loading,
};
}; };
export default useMetricsData; export default useMetricsData;

Просмотреть файл

@@ -4,17 +4,11 @@
import {CheckIcon} from '@mattermost/compass-icons/components'; import {CheckIcon} from '@mattermost/compass-icons/components';
import classNames from 'classnames'; import classNames from 'classnames';
import React, {useEffect, useState} from 'react'; import React from 'react';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import {GlobalState} from '@mattermost/types/store';
import {getServerVersion} from 'mattermost-redux/selectors/entities/general';
import {Client4} from 'mattermost-redux/client';
import Accordion, {AccordionItemType} from 'components/common/accordion/accordion'; import Accordion, {AccordionItemType} from 'components/common/accordion/accordion';
import {elasticsearchTest, ldapTest, testSiteURL} from '../../../actions/admin_actions';
import LoadingScreen from '../../loading_screen'; import LoadingScreen from '../../loading_screen';
import FormattedAdminHeader from '../../widgets/admin_console/formatted_admin_header'; import FormattedAdminHeader from '../../widgets/admin_console/formatted_admin_header';
import {Props} from '../admin_console'; import {Props} from '../admin_console';
@@ -22,10 +16,11 @@ import {Props} from '../admin_console';
import ChipsList, {ChipsInfoType} from './chips_list'; import ChipsList, {ChipsInfoType} from './chips_list';
import CtaButtons from './cta_buttons'; import CtaButtons from './cta_buttons';
import useMetricsData, {DataModel, ItemStatus, UpdatesParam} from './dashboard.data'; import useMetricsData from './dashboard.data';
import './dashboard.scss'; import './dashboard.scss';
import OverallScore from './overall-score'; import OverallScore from './overall-score';
import {ItemStatus} from './dashboard.type';
const AccordionItem = styled.div` const AccordionItem = styled.div`
padding: 12px; padding: 12px;
@@ -49,180 +44,7 @@ const successIcon = (
); );
const WorkspaceOptimizationDashboard = (props: Props) => { const WorkspaceOptimizationDashboard = (props: Props) => {
const [loading, setLoading] = useState(true); const {data, loading} = useMetricsData(props.config);
const [versionData, setVersionData] = useState<UpdatesParam['serverVersion']>({type: '', description: '', status: ItemStatus.NONE});
// const [guestAccountStatus, setGuestAccountStatus] = useState<ItemStatus>('none');
const [liveUrlStatus, setLiveUrlStatus] = useState<ItemStatus>(ItemStatus.ERROR);
const [elastisearchStatus, setElasticsearchStatus] = useState<ItemStatus>(ItemStatus.INFO);
const [ldapStatus, setLdapStatus] = useState<ItemStatus>(ItemStatus.INFO);
const [dataRetentionStatus, setDataRetentionStatus] = useState<ItemStatus>(ItemStatus.INFO);
const {formatMessage} = useIntl();
const {getAccessData, getConfigurationData, getUpdatesData, getPerformanceData, getDataPrivacyData, getEaseOfManagementData, isLicensed, isEnterpriseLicense} = useMetricsData();
// get the currently installed server version
const installedVersion = useSelector((state: GlobalState) => getServerVersion(state));
const analytics = useSelector((state: GlobalState) => state.entities.admin.analytics);
const {TOTAL_USERS: totalUsers, TOTAL_POSTS: totalPosts} = analytics!;
// gather locally available data
const {
ServiceSettings,
DataRetentionSettings,
ElasticsearchSettings,
LdapSettings,
// TeamSettings,
// GuestAccountsSettings,
} = props.config;
const {location} = document;
const sessionLengthWebInHours = ServiceSettings?.SessionLengthWebInHours || -1;
const testURL = () => {
if (!ServiceSettings?.SiteURL) {
return Promise.resolve();
}
const onSuccess = ({status}: any) => setLiveUrlStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.ERROR);
const onError = () => setLiveUrlStatus(ItemStatus.ERROR);
return testSiteURL(onSuccess, onError, ServiceSettings?.SiteURL);
};
const testDataRetention = async () => {
if (!isLicensed || !isEnterpriseLicense) {
return Promise.resolve();
}
if (DataRetentionSettings?.EnableMessageDeletion || DataRetentionSettings?.EnableFileDeletion) {
setDataRetentionStatus(ItemStatus.OK);
return Promise.resolve();
}
const result = await fetch(`${Client4.getBaseRoute()}/data_retention/policies?page=0&per_page=0`).then((result) => result.json());
setDataRetentionStatus(result.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO);
return Promise.resolve();
};
const fetchVersion = async () => {
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
if (result.tag_name) {
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
const newVersionParts = sanitizedVersion.split('.');
const installedVersionParts = installedVersion.split('.').slice(0, 3);
// quick general check if a newer version is available
let type = '';
let status: ItemStatus = ItemStatus.OK;
if (newVersionParts.join('') > installedVersionParts.join('')) {
// get correct values to be inserted into the accordion item
switch (true) {
case newVersionParts[0] > installedVersionParts[0]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
defaultMessage: 'Major',
});
status = ItemStatus.ERROR;
break;
case newVersionParts[1] > installedVersionParts[1]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
defaultMessage: 'Minor',
});
status = ItemStatus.WARNING;
break;
case newVersionParts[2] > installedVersionParts[2]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
defaultMessage: 'Patch',
});
status = ItemStatus.INFO;
break;
}
}
setVersionData({type, description: result.body, status});
}
};
const testElasticsearch = () => {
if (!isLicensed || !isEnterpriseLicense || !(ElasticsearchSettings?.EnableIndexing && ElasticsearchSettings?.EnableSearching)) {
return Promise.resolve();
}
const onSuccess = ({status}: any) => setElasticsearchStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
const onError = () => setElasticsearchStatus(ItemStatus.INFO);
return elasticsearchTest(props.config, onSuccess, onError);
};
const testLdap = () => {
if (!isLicensed || !LdapSettings?.Enable) {
return Promise.resolve();
}
const onSuccess = ({status}: any) => setLdapStatus(status === 'OK' ? ItemStatus.OK : ItemStatus.INFO);
const onError = () => setLdapStatus(ItemStatus.INFO);
return ldapTest(onSuccess, onError);
};
// commented out for now.
// @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
// const fetchGuestAccounts = async () => {
// if (TeamSettings?.EnableOpenServer && GuestAccountsSettings?.Enable) {
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
//
// // this setting is just a string with a list of domains, or an empty string
// if (GuestAccountsSettings?.RestrictCreationToDomains) {
// const domainList = GuestAccountsSettings?.RestrictCreationToDomains;
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
// }
//
// // if guest accounts make up more than 5% of the user base show the info accordion
// if (usersArray.length > (totalUsers as number * 0.05)) {
// setGuestAccountStatus(ItemStatus.INFO);
// return;
// }
// }
//
// setGuestAccountStatus(ItemStatus.OK);
// };
useEffect(() => {
const promises = [];
promises.push(testURL());
promises.push(testLdap());
promises.push(fetchVersion());
promises.push(testElasticsearch());
promises.push(testDataRetention());
// promises.push(fetchGuestAccounts());
Promise.all(promises).then(() => setLoading(false));
}, [props.config, isLicensed, isEnterpriseLicense]);
const data: DataModel = {
updates: getUpdatesData({serverVersion: versionData}),
configuration: getConfigurationData({
ssl: {status: location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR},
sessionLength: {status: sessionLengthWebInHours === 720 ? ItemStatus.INFO : ItemStatus.OK},
}),
access: getAccessData({siteUrl: {status: liveUrlStatus}}),
performance: getPerformanceData({
search: {
status: totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : elastisearchStatus,
},
}),
dataPrivacy: getDataPrivacyData({retention: {status: dataRetentionStatus}}),
easyManagement: getEaseOfManagementData({
ldap: {status: totalUsers < 100 ? ItemStatus.OK : ldapStatus},
// guestAccounts: {status: guestAccountStatus},
}),
};
const overallScoreChips: ChipsInfoType = { const overallScoreChips: ChipsInfoType = {
[ItemStatus.INFO]: 0, [ItemStatus.INFO]: 0,
@@ -236,7 +58,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const accData: AccordionItemType[] = Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => { const accordionItemsData: AccordionItemType[] | undefined = data && Object.entries(data).filter(([_, y]) => !y.hide).map(([accordionKey, accordionData]) => {
const accordionDataChips: ChipsInfoType = { const accordionDataChips: ChipsInfoType = {
[ItemStatus.INFO]: 0, [ItemStatus.INFO]: 0,
[ItemStatus.WARNING]: 0, [ItemStatus.WARNING]: 0,
@@ -297,7 +119,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
}; };
}); });
return loading ? <LoadingScreen/> : ( return loading || !accordionItemsData ? <LoadingScreen/> : (
<div className='WorkspaceOptimizationDashboard wrapper--fixed'> <div className='WorkspaceOptimizationDashboard wrapper--fixed'>
<FormattedAdminHeader <FormattedAdminHeader
id={'admin.reporting.workspace_optimization.title'} id={'admin.reporting.workspace_optimization.title'}
@@ -314,7 +136,7 @@ const WorkspaceOptimizationDashboard = (props: Props) => {
chartValue={Math.floor((overallScore.current / overallScore.max) * 100)} chartValue={Math.floor((overallScore.current / overallScore.max) * 100)}
/> />
<Accordion <Accordion
accordionItemsData={accData} accordionItemsData={accordionItemsData}
expandMultiple={true} expandMultiple={true}
/> />
</div> </div>

Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type DataModel = {
[key: string]: {
title: string;
description: string;
descriptionOk: string;
items: ItemModel[];
icon: React.ReactNode;
hide?: boolean;
};
}
export enum ItemStatus {
/** Return NONE if it's not relevant, not configured, or not an option */
NONE = 'none',
/** Return OK if all checks for this have passed */
OK = 'ok',
/** Return info if it might not be relevant to them, but they could utilize it */
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
}
export type ItemModel = {
id: string;
title: string;
description: string;
status: ItemStatus;
scoreImpact: number;
impactModifier: number;
configUrl?: string;
configText?: string;
telemetryAction?: string;
infoUrl?: string;
infoText?: string;
}
export type UpdatesParam = {
serverVersion: {
type: string;
status: ItemStatus;
description: string;
};
}
type Analytics = {
DAILY_ACTIVE_USERS: number;
MONTHLY_ACTIVE_USERS: number;
TOTAL_INACTIVE_USERS: number;
TOTAL_MASTER_DB_CONNECTIONS: number;
TOTAL_POSTS: number;
TOTAL_PRIVATE_GROUPS: number;
TOTAL_PUBLIC_CHANNELS: number;
TOTAL_READ_DB_CONNECTIONS: number;
TOTAL_TEAMS: number;
TOTAL_USERS: number;
TOTAL_WEBSOCKET_CONNECTIONS: number;
}
export type Options = {
isLicensed: boolean;
isEnterpriseLicense: boolean;
trialOrEnterpriseCtaConfig: {
configUrl: string;
configText: string;
};
isCloud: boolean;
isStarterLicense: boolean;
analytics: Analytics | undefined;
installedVersion: string;
};

Просмотреть файл

@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useIntl} from 'react-intl';
import {ItemModel, ItemStatus} from '../dashboard.type';
import {ConsolePages, DocLinks} from 'utils/constants';
import {impactModifiers} from '../dashboard.data';
import {AdminConfig} from '@mattermost/types/config';
import {testSiteURL} from 'actions/admin_actions';
/**
*
* @description Checking to see if the siteURL is configured correctly by running it through the same "check siteURL" button that exists on the webserver page.
*/
const siteURLCheck = async (config: Partial<AdminConfig>, formatMessage: ReturnType<typeof useIntl>['formatMessage']): Promise<ItemModel> => {
let status = ItemStatus.OK;
const testURL = async () => {
if (!config.ServiceSettings?.SiteURL) {
status = ItemStatus.ERROR;
}
const onSuccess = ({status: s}: any) => {
if (s === 'OK') {
status = ItemStatus.OK;
}
};
const onError = () => {
status = ItemStatus.ERROR;
};
await testSiteURL(onSuccess, onError, config.ServiceSettings?.SiteURL);
};
await testURL();
return {
id: 'site-url',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.access.site_url.title',
defaultMessage: 'Misconfigured web server',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.access.site_url.description',
defaultMessage: 'Your web server settings aren\'t passing a live URL test which means your workspace may not be accessible to users. We recommend updating your web server settings.',
}),
configUrl: ConsolePages.WEB_SERVER,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.access.site_url.cta', defaultMessage: 'Configure web server'}),
infoUrl: DocLinks.SITE_URL,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'site-url',
status,
scoreImpact: 12,
impactModifier: impactModifiers[status],
};
};
const checks = [
siteURLCheck,
];
export const runAccessChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
) => {
const results = await Promise.all(checks.map((check) => check(config, formatMessage)));
return results;
};

Просмотреть файл

@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable @typescript-eslint/no-unused-vars */
import {AdminConfig} from '@mattermost/types/config';
import {useIntl} from 'react-intl';
import {ConsolePages, DocLinks} from 'utils/constants';
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
import {impactModifiers} from '../dashboard.data';
/**
*
* @description This checks to see if the user's active session is done over https. This does not check if the server is configured to use https.
*/
const ssl = (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
): ItemModel => {
const status = document.location.protocol === 'https:' ? ItemStatus.OK : ItemStatus.ERROR;
return {
id: 'ssl',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.ssl.title',
defaultMessage: 'Configure SSL to make your server more secure',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.ssl.description',
defaultMessage: 'We strongly recommend securing your Mattermost workspace by configuring SSL in production environments.',
}),
infoUrl: DocLinks.SSL_CERTIFICATE,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'ssl',
status,
scoreImpact: 25,
impactModifier: impactModifiers[status],
};
};
/**
*
* @description This checks to see if the user has adjusted the default session lengths to something other than 720 hours.
*/
const sessionLength = (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
): ItemModel => {
const status = config.ServiceSettings?.SessionLengthMobileInHours === 720 ? ItemStatus.INFO : ItemStatus.OK;
return {
id: 'session-length',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.session_length.title',
defaultMessage: 'Session lengths is set to default',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.configuration.session_length.description',
defaultMessage: 'Your session length is set to the default of 30 days. A longer session length provides convenience, and a shorter session provides tighter security. We recommend adjusting this based on your organization\'s security policies.',
}),
configUrl: ConsolePages.SESSION_LENGTHS,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.configuration.session_length.cta', defaultMessage: 'Configure session length'}),
infoUrl: DocLinks.SESSION_LENGTHS,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'session-length',
status,
scoreImpact: 8,
impactModifier: impactModifiers[status],
};
};
export const runConfigChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const checks = [
ssl,
sessionLength,
];
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
return results;
};

Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useIntl} from 'react-intl';
import {impactModifiers} from '../dashboard.data';
import {ConsolePages, DocLinks} from 'utils/constants';
import {Client4} from 'mattermost-redux/client';
import {ItemStatus, Options} from '../dashboard.type';
import {AdminConfig} from '@mattermost/types/config';
/**
*
* @description Checks if they they have a global policy deletion enabled, or if a custom policy has been created.
*/
const dataRetentionCheck = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const testDataRetention = async (
config: Partial<AdminConfig>,
options: Options,
) => {
if (!options.isLicensed || !options.isEnterpriseLicense) {
return ItemStatus.INFO;
}
if (config.DataRetentionSettings?.EnableMessageDeletion || config.DataRetentionSettings?.EnableFileDeletion) {
return ItemStatus.OK;
}
const policyCount: {total_count: number} = await fetch(`${Client4.getBaseRoute()}/data_retention/policies_count`).then((result) => result.json());
return policyCount.total_count > 0 ? ItemStatus.OK : ItemStatus.INFO;
};
const status = await testDataRetention(config, options);
return {
id: 'data-retention',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.data_privacy.retention.title',
defaultMessage: 'Become more data aware',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.data_privacy.retention.description',
defaultMessage: 'Organizations in highly regulated industries require more control and insight with their data. We recommend enabling Data Retention and Compliance features.',
}),
...(options.isLicensed && options.isEnterpriseLicense ? {
configUrl: ConsolePages.DATA_RETENTION,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.data_privacy.retention.cta', defaultMessage: 'Try data retention'}),
} : options.trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.DATA_RETENTION_POLICY,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'data-retention',
status,
scoreImpact: 16,
impactModifier: impactModifiers[status],
};
};
export const runDataPrivacyChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const checks = [
dataRetentionCheck,
];
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
return results;
};

Просмотреть файл

@@ -0,0 +1,129 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AdminConfig} from '@mattermost/types/config';
import {useIntl} from 'react-intl';
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
import {ConsolePages, DocLinks} from 'utils/constants';
import {impactModifiers} from '../dashboard.data';
// import {Client4} from 'mattermost-redux/client';
// import {AnalyticsRow} from '@mattermost/types/admin';
import {ldapTest} from 'actions/admin_actions';
const usesLDAP = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
): Promise<ItemModel> => {
const testLdap = async (
config: Partial<AdminConfig>,
options: Options,
): Promise<ItemStatus> => {
let check = ItemStatus.INFO;
if (!options.isLicensed || !config.LdapSettings?.Enable) {
return check;
}
const onSuccess = ({status}: any) => {
if (status === 'OK') {
check = ItemStatus.OK;
}
};
await ldapTest(onSuccess);
return check;
};
// something feels flawed in this check.
const status = options.analytics?.TOTAL_USERS as number > 100 ? await testLdap(config, options) : ItemStatus.OK;
return {
id: 'ad-ldap',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.title',
defaultMessage: 'AD/LDAP integration recommended',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.description',
defaultMessage: 'You\'ve reached over 100 users! We recommend setting up AD/LDAP user authentication for easier onboarding as well as automated deactivations and role assignments.',
}),
...(options.isLicensed && !options.isStarterLicense ? {
configUrl: ConsolePages.AD_LDAP,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.ease_of_management.ldap.cta', defaultMessage: 'Try AD/LDAP'}),
} : options.trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.AD_LDAP,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'ad-ldap',
status,
scoreImpact: 22,
impactModifier: impactModifiers[status],
};
};
// // commented out for now.
// // @see discussion here: https://github.com/mattermost/mattermost-webapp/pull/9822#discussion_r806879385
// const fetchGuestAccounts = async (
// config: Partial<AdminConfig>,
// analytics: Record<string, number | AnalyticsRow[]> | undefined,
// ) => {
// if (config.TeamSettings?.EnableOpenServer && config.GuestAccountsSettings?.Enable) {
// let usersArray = await fetch(`${Client4.getBaseRoute()}/users/invalid_emails`).then((result) => result.json());
// // this setting is just a string with a list of domains, or an empty string
// if (config.GuestAccountsSettings?.RestrictCreationToDomains) {
// const domainList = config.GuestAccountsSettings?.RestrictCreationToDomains;
// usersArray = usersArray.filter(({email}: Record<string, unknown>) => domainList.includes((email as string).split('@')[1]));
// }
// // if guest accounts make up more than 5% of the user base show the info accordion
// if (analytics && usersArray.length > (analytics.totalUsers as number * 0.05)) {
// return ItemStatus.INFO;
// }
// }
// return ItemStatus.OK;
// };
// const guestAccounts = async (
// config: Partial<AdminConfig>,
// formatMessage: ReturnType<typeof useIntl>['formatMessage'],
// options: Options,
// ): Promise<ItemModel> => {
// const status = await fetchGuestAccounts(config, options.analytics);
// return {
// id: 'guest-accounts',
// title: formatMessage({
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.title',
// defaultMessage: 'Guest Accounts recommended',
// }),
// description: formatMessage({
// id: 'admin.reporting.workspace_optimization.ease_of_management.guests_accounts.description',
// defaultMessage: 'Several user accounts are using different domains than your Site URL. You can control user access to channels and teams with guest accounts. We recommend starting an Enterprise trial and enabling Guest Access.',
// }),
// ...options.trialOrEnterpriseCtaConfig,
// infoUrl: 'https://docs.mattermost.com/onboard/guest-accounts.html',
// infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
// telemetryAction: 'guest-accounts',
// status,
// scoreImpact: 6,
// impactModifier: impactModifiers[status],
// };
// };
export const runEaseOfUseChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
): Promise<ItemModel[]> => {
const checks = [
usesLDAP,
// guestAccounts,
];
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
return results;
};

Просмотреть файл

@@ -0,0 +1,72 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AdminConfig} from '@mattermost/types/config';
import {ItemModel, ItemStatus, Options} from '../dashboard.type';
import {elasticsearchTest} from 'actions/admin_actions';
import {useIntl} from 'react-intl';
import {ConsolePages, DocLinks} from 'utils/constants';
import {impactModifiers} from '../dashboard.data';
const search = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
): Promise<ItemModel> => {
const testElasticsearch = async (
config: Partial<AdminConfig>,
options: Options,
) => {
let check = ItemStatus.INFO;
if (!options.isLicensed || !options.isEnterpriseLicense || !(config.ElasticsearchSettings?.EnableIndexing && config.ElasticsearchSettings?.EnableSearching)) {
return check;
}
const onSuccess = ({status}: any) => {
if (status === 'OK') {
check = ItemStatus.OK;
}
};
await elasticsearchTest(config, onSuccess);
return check;
};
const totalPosts = options.analytics?.TOTAL_POSTS as number;
const totalUsers = options.analytics?.TOTAL_USERS as number;
const status = totalPosts < 2_000_000 && totalUsers < 500 ? ItemStatus.OK : await testElasticsearch(config, options);
return {
id: 'search',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.search.title',
defaultMessage: 'Search performance',
}),
description: formatMessage({
id: 'admin.reporting.workspace_optimization.performance.search.description',
defaultMessage: 'Your server has reached over 500 users and 2 million posts which can result in slow search performance. We recommend enabling Elasticsearch for better performance.',
}),
...(options.isLicensed && options.isEnterpriseLicense ? {
configUrl: ConsolePages.ELASTICSEARCH,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.search.cta', defaultMessage: 'Try Elasticsearch'}),
} : options.trialOrEnterpriseCtaConfig),
infoUrl: DocLinks.ELASTICSEARCH,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'search-optimization',
status,
scoreImpact: 20,
impactModifier: impactModifiers[status],
};
};
export const runPerformanceChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const checks = [
search,
];
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
return results;
};

Просмотреть файл

@@ -0,0 +1,94 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from 'mattermost-redux/client';
import {ItemStatus, Options} from '../dashboard.type';
import {useIntl} from 'react-intl';
import {AdminConfig} from '@mattermost/types/config';
import {CloudLinks, DocLinks} from 'utils/constants';
import {impactModifiers} from '../dashboard.data';
const testServerVersion = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const fetchVersion = async (
installedVersion: string,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
) => {
const result = await fetch(`${Client4.getBaseRoute()}/latest_version`).then((result) => result.json());
if (result.tag_name) {
const sanitizedVersion = result.tag_name.startsWith('v') ? result.tag_name.slice(1) : result.tag_name;
const newVersionParts = sanitizedVersion.split('.');
const installedVersionParts = installedVersion.split('.').slice(0, 3);
// quick general check if a newer version is available
let type = '';
let status: ItemStatus = ItemStatus.OK;
if (newVersionParts.join('') > installedVersionParts.join('')) {
// get correct values to be inserted into the accordion item
switch (true) {
case newVersionParts[0] > installedVersionParts[0]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.major',
defaultMessage: 'Major',
});
status = ItemStatus.ERROR;
break;
case newVersionParts[1] > installedVersionParts[1]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.minor',
defaultMessage: 'Minor',
});
status = ItemStatus.WARNING;
break;
case newVersionParts[2] > installedVersionParts[2]:
type = formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.update_type.patch',
defaultMessage: 'Patch',
});
status = ItemStatus.INFO;
break;
}
}
return {type, description: result.body, status};
}
return {type: '', description: '', status: ItemStatus.OK};
};
const serverVersion = await fetchVersion(options.installedVersion, formatMessage);
return {
id: 'server_version',
title: formatMessage({
id: 'admin.reporting.workspace_optimization.updates.server_version.status.title',
defaultMessage: '{type} version update available.',
}, {type: serverVersion.type}),
description: serverVersion.description,
configUrl: CloudLinks.DOWNLOAD_UPDATE,
configText: formatMessage({id: 'admin.reporting.workspace_optimization.updates.server_version.cta', defaultMessage: 'Download update'}),
infoUrl: DocLinks.UPGRADE_SERVER,
infoText: formatMessage({id: 'admin.reporting.workspace_optimization.cta.learnMore', defaultMessage: 'Learn more'}),
telemetryAction: 'server-version',
status: serverVersion.status,
scoreImpact: 15,
impactModifier: impactModifiers[serverVersion.status],
};
};
export const runUpdateChecks = async (
config: Partial<AdminConfig>,
formatMessage: ReturnType<typeof useIntl>['formatMessage'],
options: Options,
) => {
const checks = [
testServerVersion,
];
const results = await Promise.all(checks.map((check) => check(config, formatMessage, options)));
return results;
};

Просмотреть файл

@@ -13,6 +13,7 @@ import * as GlobalActions from 'actions/global_actions';
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants'; import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
import {PreferenceType} from '@mattermost/types/preferences'; import {PreferenceType} from '@mattermost/types/preferences';
import * as Keyboard from 'utils/keyboard';
import * as UserAgent from 'utils/user_agent'; import * as UserAgent from 'utils/user_agent';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import { import {
@@ -819,11 +820,11 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => { handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey; const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH); const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey; const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey; const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey; const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
// listen for line break key combo and insert new line character // listen for line break key combo and insert new line character
if (Utils.isUnhandledLineBreakKeyCombo(e)) { if (Utils.isUnhandledLineBreakKeyCombo(e)) {
@@ -838,7 +839,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
if ( if (
(this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) && (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
Utils.isKeyPressed(e, KeyCodes.ENTER) && Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
(e.ctrlKey || e.metaKey) (e.ctrlKey || e.metaKey)
) { ) {
this.setShowPreview(false); this.setShowPreview(false);
@@ -849,7 +850,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
const draft = this.state.draft!; const draft = this.state.draft!;
const {message} = draft; const {message} = draft;
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) { if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
this.textboxRef.current?.blur(); this.textboxRef.current?.blur();
} }
@@ -858,7 +859,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
!e.metaKey && !e.metaKey &&
!e.altKey && !e.altKey &&
!e.shiftKey && !e.shiftKey &&
Utils.isKeyPressed(e, KeyCodes.UP) && Keyboard.isKeyPressed(e, KeyCodes.UP) &&
message === '' message === ''
) { ) {
e.preventDefault(); e.preventDefault();
@@ -879,13 +880,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
} = e.target as TextboxElement; } = e.target as TextboxElement;
if (ctrlKeyCombo) { if (ctrlKeyCombo) {
if (Utils.isKeyPressed(e, KeyCodes.UP)) { if (Keyboard.isKeyPressed(e, KeyCodes.UP)) {
e.preventDefault(); e.preventDefault();
this.props.onMoveHistoryIndexBack(); this.props.onMoveHistoryIndexBack();
} else if (Utils.isKeyPressed(e, KeyCodes.DOWN)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
e.preventDefault(); e.preventDefault();
this.props.onMoveHistoryIndexForward(); this.props.onMoveHistoryIndexForward();
} else if (Utils.isKeyPressed(e, KeyCodes.B)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -894,7 +895,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.I)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -905,7 +906,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
}); });
} }
} else if (ctrlAltCombo) { } else if (ctrlAltCombo) {
if (Utils.isKeyPressed(e, KeyCodes.K)) { if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -914,7 +915,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.C)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -923,21 +924,21 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.E)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.toggleEmojiPicker(); this.toggleEmojiPicker();
} else if (Utils.isKeyPressed(e, KeyCodes.T)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.toggleAdvanceTextEditor(); this.toggleAdvanceTextEditor();
} else if (Utils.isKeyPressed(e, KeyCodes.P) && draft.message.length) { } else if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setShowPreview(!this.props.shouldShowPreview); this.setShowPreview(!this.props.shouldShowPreview);
} }
} else if (shiftAltCombo) { } else if (shiftAltCombo) {
if (Utils.isKeyPressed(e, KeyCodes.X)) { if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -946,7 +947,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'ol', markdownMode: 'ol',
@@ -954,7 +955,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'ul', markdownMode: 'ul',
@@ -962,7 +963,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'quote', markdownMode: 'quote',

Просмотреть файл

@@ -22,6 +22,7 @@ import Constants, {
Preferences, Preferences,
AdvancedTextEditor as AdvancedTextEditorConst, AdvancedTextEditor as AdvancedTextEditorConst,
} from 'utils/constants'; } from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
import { import {
containsAtChannel, containsAtChannel,
specialMentionsInText, specialMentionsInText,
@@ -34,7 +35,6 @@ import {
} from 'utils/post_utils'; } from 'utils/post_utils';
import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock} from 'utils/paste'; import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock} from 'utils/paste';
import * as UserAgent from 'utils/user_agent'; import * as UserAgent from 'utils/user_agent';
import {isMac} from 'utils/utils';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import EmojiMap from 'utils/emoji_map'; import EmojiMap from 'utils/emoji_map';
import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown'; import {applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
@@ -1096,7 +1096,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
documentKeyHandler = (e: KeyboardEvent) => { documentKeyHandler = (e: KeyboardEvent) => {
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey; const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.BACK_SLASH); const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
if (lastMessageReactionKeyCombo) { if (lastMessageReactionKeyCombo) {
this.reactToLastMessage(e); this.reactToLastMessage(e);
return; return;
@@ -1134,12 +1134,12 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey; const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
const ctrlEnterKeyCombo = (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) && const ctrlEnterKeyCombo = (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
Utils.isKeyPressed(e, KeyCodes.ENTER) && Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
ctrlOrMetaKeyPressed; ctrlOrMetaKeyPressed;
const ctrlKeyCombo = Utils.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey; const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
const ctrlAltCombo = Utils.cmdOrCtrlPressed(e, true) && e.altKey; const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
const shiftAltCombo = !Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey; const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
// listen for line break key combo and insert new line character // listen for line break key combo and insert new line character
if (Utils.isUnhandledLineBreakKeyCombo(e)) { if (Utils.isUnhandledLineBreakKeyCombo(e)) {
@@ -1155,7 +1155,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
const {message} = this.state; const {message} = this.state;
if (Utils.isKeyPressed(e, KeyCodes.ESCAPE)) { if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
this.textboxRef.current?.blur(); this.textboxRef.current?.blur();
} }
@@ -1164,7 +1164,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
!e.metaKey && !e.metaKey &&
!e.altKey && !e.altKey &&
!e.shiftKey && !e.shiftKey &&
Utils.isKeyPressed(e, KeyCodes.UP) && Keyboard.isKeyPressed(e, KeyCodes.UP) &&
message === '' message === ''
) { ) {
e.preventDefault(); e.preventDefault();
@@ -1182,15 +1182,15 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
} = e.target as TextboxElement; } = e.target as TextboxElement;
if (ctrlKeyCombo) { if (ctrlKeyCombo) {
if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.UP)) { if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.loadPrevMessage(e); this.loadPrevMessage(e);
} else if (draftMessageIsEmpty && Utils.isKeyPressed(e, KeyCodes.DOWN)) { } else if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.loadNextMessage(e); this.loadNextMessage(e);
} else if (Utils.isKeyPressed(e, KeyCodes.B)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -1199,7 +1199,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.I)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -1210,7 +1210,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
}); });
} }
} else if (ctrlAltCombo) { } else if (ctrlAltCombo) {
if (Utils.isKeyPressed(e, KeyCodes.K)) { if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -1219,7 +1219,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.C)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -1228,21 +1228,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.E)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.toggleEmojiPicker(); this.toggleEmojiPicker();
} else if (Utils.isKeyPressed(e, KeyCodes.T)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.toggleAdvanceTextEditor(); this.toggleAdvanceTextEditor();
} else if (Utils.isKeyPressed(e, KeyCodes.P) && message.length) { } else if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setShowPreview(!this.props.shouldShowPreview); this.setShowPreview(!this.props.shouldShowPreview);
} }
} else if (shiftAltCombo) { } else if (shiftAltCombo) {
if (Utils.isKeyPressed(e, KeyCodes.X)) { if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
@@ -1251,7 +1251,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.SEVEN)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'ol', markdownMode: 'ol',
@@ -1259,7 +1259,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.EIGHT)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'ul', markdownMode: 'ul',
@@ -1267,7 +1267,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
selectionEnd, selectionEnd,
message: value, message: value,
}); });
} else if (Utils.isKeyPressed(e, KeyCodes.NINE)) { } else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
e.preventDefault(); e.preventDefault();
this.applyMarkdown({ this.applyMarkdown({
markdownMode: 'quote', markdownMode: 'quote',
@@ -1277,21 +1277,21 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
}); });
} }
} }
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP); const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Utils.isKeyPressed(e, KeyCodes.UP); const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
const ctrlShiftCombo = Utils.cmdOrCtrlPressed(e, true) && e.shiftKey; const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
if (upKeyOnly && messageIsEmpty) { if (upKeyOnly && messageIsEmpty) {
this.editLastPost(e); this.editLastPost(e);
} else if (shiftUpKeyCombo && messageIsEmpty) { } else if (shiftUpKeyCombo && messageIsEmpty) {
this.replyToLastPost(e); this.replyToLastPost(e);
} else if (ctrlShiftCombo && Utils.isKeyPressed(e, KeyCodes.E)) { } else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.E)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.toggleEmojiPicker(); this.toggleEmojiPicker();
} else if (((isMac() && ctrlShiftCombo) || (!isMac() && ctrlAltCombo)) && Utils.isKeyPressed(e, KeyCodes.P) && this.state.message.length) { } else if (((UserAgent.isMac() && ctrlShiftCombo) || (!UserAgent.isMac() && ctrlAltCombo)) && Keyboard.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
this.setShowPreview(!this.props.shouldShowPreview); this.setShowPreview(!this.props.shouldShowPreview);
} else if (ctrlAltCombo && Utils.isKeyPressed(e, KeyCodes.T)) { } else if (ctrlAltCombo && Keyboard.isKeyPressed(e, KeyCodes.T)) {
this.toggleAdvanceTextEditor(); this.toggleAdvanceTextEditor();
} }
}; };

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React, {ReactNode} from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
@@ -395,7 +395,7 @@ const ConfigurationAnnouncementBar = (props: Props) => {
defaultMessage = 'Please configure your <linkSite>site URL</linkSite> on the <linkConsole>System Console</linkConsole>.'; defaultMessage = 'Please configure your <linkSite>site URL</linkSite> on the <linkConsole>System Console</linkConsole>.';
} }
const values = { const values: Record<string, ReactNode> = {
linkSite: (msg: string) => ( linkSite: (msg: string) => (
<ExternalLink <ExternalLink
href={props.siteURL} href={props.siteURL}

Просмотреть файл

@@ -12,9 +12,10 @@ import {Group} from '@mattermost/types/groups';
import ProfilePopover from 'components/profile_popover'; import ProfilePopover from 'components/profile_popover';
import {popOverOverlayPosition} from 'utils/position_utils'; import {popOverOverlayPosition} from 'utils/position_utils';
import {isKeyPressed} from 'utils/keyboard';
import {getUserOrGroupFromMentionName} from 'utils/post_utils'; import {getUserOrGroupFromMentionName} from 'utils/post_utils';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
import {getViewportSize, isKeyPressed} from 'utils/utils'; import {getViewportSize} from 'utils/utils';
import AtMentionGroup from 'components/at_mention/at_mention_group'; import AtMentionGroup from 'components/at_mention/at_mention_group';

Просмотреть файл

@@ -12,8 +12,9 @@ import ProfilePopover from 'components/profile_popover';
import UserGroupPopover from 'components/user_group_popover'; import UserGroupPopover from 'components/user_group_popover';
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants'; import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';
import {isKeyPressed} from 'utils/keyboard';
import {popOverOverlayPosition} from 'utils/position_utils'; import {popOverOverlayPosition} from 'utils/position_utils';
import {getViewportSize, isKeyPressed} from 'utils/utils'; import {getViewportSize} from 'utils/utils';
import {MAX_LIST_HEIGHT, getListHeight, VIEWPORT_SCALE_FACTOR} from 'components/user_group_popover/group_member_list/group_member_list'; import {MAX_LIST_HEIGHT, getListHeight, VIEWPORT_SCALE_FACTOR} from 'components/user_group_popover/group_member_list/group_member_list';

Просмотреть файл

@@ -6,7 +6,7 @@ import {FormattedMessage} from 'react-intl';
import styled from 'styled-components'; import styled from 'styled-components';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
import {isKeyPressed} from 'utils/utils'; import {isKeyPressed} from 'utils/keyboard';
const Title = styled.div` const Title = styled.div`
flex:1; flex:1;

Просмотреть файл

@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
forwardedRef={null} forwardedRef={null}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,

Просмотреть файл

@@ -29,9 +29,11 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
forwardedRef={null} forwardedRef={null}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -58,6 +60,7 @@ exports[`components/pricing_modal/downgrade_team_removal_modal matches snapshot
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,

Просмотреть файл

@@ -4,10 +4,12 @@ exports[`codeBlock should render html code block with proper indentation after s
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={Object {}} messages={Object {}}
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<CodeBlock <CodeBlock
@@ -68,9 +70,11 @@ exports[`codeBlock should render html code block with proper indentation after s
id="copyButton" id="copyButton"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -97,6 +101,7 @@ exports[`codeBlock should render html code block with proper indentation after s
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment), "textComponent": Symbol(react.fragment),
"timeZone": undefined, "timeZone": undefined,
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -228,10 +233,12 @@ exports[`codeBlock should render typescript code block after syntax highlighting
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={Object {}} messages={Object {}}
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<CodeBlock <CodeBlock
@@ -292,9 +299,11 @@ const myFunction = () => {
id="copyButton" id="copyButton"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -321,6 +330,7 @@ const myFunction = () => {
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment), "textComponent": Symbol(react.fragment),
"timeZone": undefined, "timeZone": undefined,
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -452,10 +462,12 @@ exports[`codeBlock should render unknown language after syntax highlighting 1`]
<IntlProvider <IntlProvider
defaultFormats={Object {}} defaultFormats={Object {}}
defaultLocale="en" defaultLocale="en"
fallbackOnEmptyString={true}
formats={Object {}} formats={Object {}}
locale="en" locale="en"
messages={Object {}} messages={Object {}}
onError={[Function]} onError={[Function]}
onWarn={[Function]}
textComponent={Symbol(react.fragment)} textComponent={Symbol(react.fragment)}
> >
<CodeBlock <CodeBlock
@@ -514,9 +526,11 @@ it shouldn't highlight, it's just garbage
id="copyButton" id="copyButton"
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -543,6 +557,7 @@ it shouldn't highlight, it's just garbage
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": Symbol(react.fragment), "textComponent": Symbol(react.fragment),
"timeZone": undefined, "timeZone": undefined,
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,

Просмотреть файл

@@ -25,7 +25,8 @@ import {GlobalState} from 'types/store';
import {getCurrentMomentForTimezone} from 'utils/timezone'; import {getCurrentMomentForTimezone} from 'utils/timezone';
import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants'; import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants';
import {t} from 'utils/i18n'; import {t} from 'utils/i18n';
import {isKeyPressed, localizeMessage} from 'utils/utils'; import {isKeyPressed} from 'utils/keyboard';
import {localizeMessage} from 'utils/utils';
import CustomStatusSuggestion from 'components/custom_status/custom_status_suggestion'; import CustomStatusSuggestion from 'components/custom_status/custom_status_suggestion';
import ExpiryMenu from 'components/custom_status/expiry_menu'; import ExpiryMenu from 'components/custom_status/expiry_menu';

Просмотреть файл

@@ -17,7 +17,8 @@ import DatePicker from 'components/date_picker';
import Menu from 'components/widgets/menu/menu'; import Menu from 'components/widgets/menu/menu';
import Timestamp from 'components/timestamp'; import Timestamp from 'components/timestamp';
import {getCurrentLocale} from 'selectors/i18n'; import {getCurrentLocale} from 'selectors/i18n';
import {isKeyPressed, localizeMessage} from 'utils/utils'; import {isKeyPressed} from 'utils/keyboard';
import {localizeMessage} from 'utils/utils';
import {getCurrentMomentForTimezone} from 'utils/timezone'; import {getCurrentMomentForTimezone} from 'utils/timezone';
import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants'; import Constants, {A11yCustomEventTypes, A11yFocusEventDetail} from 'utils/constants';

Просмотреть файл

@@ -22,7 +22,8 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import './dnd_custom_time_picker_modal.scss'; import './dnd_custom_time_picker_modal.scss';
import {toUTCUnix} from 'utils/datetime'; import {toUTCUnix} from 'utils/datetime';
import {isKeyPressed, localizeMessage} from 'utils/utils'; import {isKeyPressed} from 'utils/keyboard';
import {localizeMessage} from 'utils/utils';
import Input from 'components/widgets/inputs/input/input'; import Input from 'components/widgets/inputs/input/input';
import DatePicker from 'components/date_picker'; import DatePicker from 'components/date_picker';

Просмотреть файл

@@ -28,6 +28,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants'; import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
import DeletePostModal from 'components/delete_post_modal'; import DeletePostModal from 'components/delete_post_modal';
import DelayedAction from 'utils/delayed_action'; import DelayedAction from 'utils/delayed_action';
import * as Keyboard from 'utils/keyboard';
import * as PostUtils from 'utils/post_utils'; import * as PostUtils from 'utils/post_utils';
import * as Menu from 'components/menu'; import * as Menu from 'components/menu';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
@@ -337,61 +338,61 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
const isShiftKeyPressed = e.shiftKey; const isShiftKeyPressed = e.shiftKey;
switch (true) { switch (true) {
case Utils.isKeyPressed(e, Constants.KeyCodes.R): case Keyboard.isKeyPressed(e, Constants.KeyCodes.R):
this.handleCommentClick(e); this.handleCommentClick(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// edit post // edit post
case Utils.isKeyPressed(e, Constants.KeyCodes.E): case Keyboard.isKeyPressed(e, Constants.KeyCodes.E):
this.handleEditMenuItemActivated(e); this.handleEditMenuItemActivated(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// follow thread // follow thread
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed: case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
this.handleSetThreadFollow(e); this.handleSetThreadFollow(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// forward post // forward post
case Utils.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed: case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
this.handleForwardMenuItemActivated(e); this.handleForwardMenuItemActivated(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// copy link // copy link
case Utils.isKeyPressed(e, Constants.KeyCodes.K): case Keyboard.isKeyPressed(e, Constants.KeyCodes.K):
this.copyLink(e); this.copyLink(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// copy text // copy text
case Utils.isKeyPressed(e, Constants.KeyCodes.C): case Keyboard.isKeyPressed(e, Constants.KeyCodes.C):
this.copyText(e); this.copyText(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// delete post // delete post
case Utils.isKeyPressed(e, Constants.KeyCodes.DELETE): case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE):
this.handleDeleteMenuItemActivated(e); this.handleDeleteMenuItemActivated(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// pin / unpin // pin / unpin
case Utils.isKeyPressed(e, Constants.KeyCodes.P): case Keyboard.isKeyPressed(e, Constants.KeyCodes.P):
this.handlePinMenuItemActivated(e); this.handlePinMenuItemActivated(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// save / unsave // save / unsave
case Utils.isKeyPressed(e, Constants.KeyCodes.S): case Keyboard.isKeyPressed(e, Constants.KeyCodes.S):
this.handleFlagMenuItemActivated(e); this.handleFlagMenuItemActivated(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;
// mark as unread // mark as unread
case Utils.isKeyPressed(e, Constants.KeyCodes.U): case Keyboard.isKeyPressed(e, Constants.KeyCodes.U):
this.handleMarkPostAsUnread(e); this.handleMarkPostAsUnread(e);
this.handleDropdownOpened(false); this.handleDropdownOpened(false);
break; break;

Просмотреть файл

@@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getChannelURL} from 'utils/utils'; import {getChannelURL} from 'selectors/urls';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';

Просмотреть файл

@@ -73,9 +73,11 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
hide={[Function]} hide={[Function]}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -102,6 +104,7 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -351,9 +354,11 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
hide={[Function]} hide={[Function]}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -380,6 +385,7 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -635,9 +641,11 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
hide={[Function]} hide={[Function]}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -664,6 +672,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,
@@ -989,9 +998,11 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
hide={[Function]} hide={[Function]}
intl={ intl={
Object { Object {
"$t": [Function],
"defaultFormats": Object {}, "defaultFormats": Object {},
"defaultLocale": "en", "defaultLocale": "en",
"defaultRichTextElements": undefined, "defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function], "formatDate": [Function],
"formatDateTimeRange": [Function], "formatDateTimeRange": [Function],
"formatDateToParts": [Function], "formatDateToParts": [Function],
@@ -1018,6 +1029,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
"locale": "en", "locale": "en",
"messages": Object {}, "messages": Object {},
"onError": [Function], "onError": [Function],
"onWarn": [Function],
"textComponent": "span", "textComponent": "span",
"timeZone": "Etc/UTC", "timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined, "wrapRichTextChunksInFragment": undefined,

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше