mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-24 13:43:12 +02:00
fix merge conflict
This commit is contained in:
commit
ca704a2760
@ -80,8 +80,12 @@ func createBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID strin
|
||||
showFullName = *mmconfig.PrivacySettings.ShowFullName
|
||||
}
|
||||
|
||||
serverRoot := baseURL + "/plugins/focalboard"
|
||||
if mmconfig.FeatureFlags.BoardsProduct {
|
||||
serverRoot = baseURL + "/boards"
|
||||
}
|
||||
return &config.Configuration{
|
||||
ServerRoot: baseURL + "/plugins/focalboard",
|
||||
ServerRoot: serverRoot,
|
||||
Port: -1,
|
||||
DBType: *mmconfig.SqlSettings.DriverName,
|
||||
DBConfigString: *mmconfig.SqlSettings.DataSource,
|
||||
|
@ -6,7 +6,7 @@ exports[`components/boardSelector escape button should unmount the component 1`]
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector"
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -111,7 +111,7 @@ exports[`components/boardSelector renders with no results 1`] = `
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector"
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -217,7 +217,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector"
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -421,7 +421,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
|
||||
class="focalboard-body"
|
||||
>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSelector"
|
||||
class="Dialog dialog-back BoardSelector size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -9,8 +9,8 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93
|
||||
github.com/mattermost/mattermost-plugin-api v0.1.1
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20230116174708-240304ad0728
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538
|
||||
@ -23,14 +23,14 @@ require (
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/wiggin77/merror v1.0.4
|
||||
golang.org/x/crypto v0.2.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||
@ -38,20 +38,20 @@ require (
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.15.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.3.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.6 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a // indirect
|
||||
github.com/hashicorp/go-hclog v1.4.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.8 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.15.14 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
@ -60,10 +60,10 @@ require (
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
github.com/mattermost/squirrel v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.43 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.45 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
@ -71,12 +71,12 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/philhofer/fwd v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
@ -86,36 +86,36 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.6 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/yuin/goldmark v1.5.3 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/tools v0.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/sqlite v1.18.0 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.20.1 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
@ -201,6 +201,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
|
||||
@ -483,6 +484,7 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||
@ -651,6 +653,7 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/graph-gophers/graphql-go v1.4.0 h1:JE9wveRTSXwJyjdRd6bOQ7Ob5bewTUQ58Jv4OiVdpdE=
|
||||
github.com/graph-gophers/graphql-go v1.4.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
@ -670,6 +673,7 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
|
||||
github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
|
||||
@ -678,6 +682,7 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
|
||||
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
@ -801,10 +806,12 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
||||
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
|
||||
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -861,8 +868,17 @@ github.com/mattermost/logr/v2 v2.0.15 h1:+WNbGcsc3dBao65eXlceB6dTILNJRIrvubnsTl3
|
||||
github.com/mattermost/logr/v2 v2.0.15/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg=
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb h1:q1qXKVv59rA2gcQ7lVLc5OlWBmfsR3i8mdGD5EZesyk=
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb/go.mod h1:PIeo40t9VTA4Wu1FwjzH7QmcgC3SRyk/ohCwJw4/oSo=
|
||||
github.com/mattermost/mattermost-plugin-api v0.1.1/go.mod h1:9yZhtg0bBj3kqSTjXnjYBMZoTsWbe3ajdFMdl9/Jz34=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933 h1:h7EibO8cwWeK8dLhC/A5tKGbkYSuJKZ0+2EXW7jDHoA=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933/go.mod h1:otnBnKY9Y0eNkUKeD161de+BUBlESwANTnrkPT/392Y=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221130200243-06e964b86b0d h1:CKJXDUCkRrfy1U9sZHOpvACOtkthV5iWt2boHUK720I=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221130200243-06e964b86b0d/go.mod h1:U3gSM0I15WSMHPpDEU30mmc4JrbSDk+8F1+MFLOHWD0=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93 h1:mGN2D6KhjKosQdZ+BHzmWxsA/tRK9FiR+nUd38nSZQY=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93/go.mod h1:U3gSM0I15WSMHPpDEU30mmc4JrbSDk+8F1+MFLOHWD0=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20230116174708-240304ad0728 h1:fegj7GaXjiVH+/j1DsPtkobczafvUJynfFSwNeqIA84=
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20230116174708-240304ad0728/go.mod h1:FPN2+SAU9ndEpMFcjClvdillSpvS2eQ+i1qiSgAUxPI=
|
||||
github.com/mattermost/morph v0.0.0-20220401091636-39f834798da8 h1:gwliVjCTqAC01mSCNqa5nJ/4MmGq50vrjsottIhQ4d8=
|
||||
github.com/mattermost/morph v0.0.0-20220401091636-39f834798da8/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw=
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e h1:VfNz+fvJ3DxOlALM22Eea8ONp5jHrybKBCcCtDPVlss=
|
||||
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e/go.mod h1:xo0ljDknTpPxEdhhrUdwhLCexIsYyDKS6b41HqG8wGU=
|
||||
github.com/mattermost/squirrel v0.2.0 h1:8ZWeyf+MWQ2cL7hu9REZgLtz2IJi51qqZEovI3T3TT8=
|
||||
@ -887,6 +903,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
@ -910,6 +927,7 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
|
||||
github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
|
||||
github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
|
||||
@ -1029,6 +1047,7 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
@ -1092,7 +1111,9 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
@ -1226,6 +1247,7 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb
|
||||
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
@ -1234,6 +1256,7 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@ -1281,7 +1304,9 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
|
||||
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
|
||||
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||
@ -1379,6 +1404,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1500,8 +1526,12 @@ golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 h1:0qjDla5xICC2suMtyRH/QqX3B1btXTfNsIt/i4LFgO0=
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@ -1536,7 +1566,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1665,17 +1697,24 @@ golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 h1:PgOr27OhUx2IRqGJ2RxAWI4dJQ7bi9cSrB82uzFzfUA=
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1687,6 +1726,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -1778,8 +1819,12 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1917,6 +1962,7 @@ google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ6
|
||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ=
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
@ -1955,6 +2001,7 @@ google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@ -2078,12 +2125,17 @@ modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
|
||||
modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
|
||||
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo=
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
|
||||
@ -2100,30 +2152,40 @@ modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||
modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA=
|
||||
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
|
||||
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
|
||||
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
|
||||
modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
|
||||
modernc.org/sqlite v1.18.0 h1:ef66qJSgKeyLyrF4kQ2RHw/Ue3V89fyFNbGL073aDjI=
|
||||
modernc.org/sqlite v1.18.0/go.mod h1:B9fRWZacNxJBHoCJZQr1R54zhVn3fjfl0aszflrTSxY=
|
||||
modernc.org/sqlite v1.20.1/go.mod h1:fODt+bFmc/j8LcoCbMSkAuKuGmhxjG45KGc25N2705M=
|
||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
|
||||
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
|
||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
|
||||
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
|
||||
|
@ -1114,10 +1114,12 @@ func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.Board
|
||||
From(s.tablePrefix + "boards AS B").
|
||||
Join("ChannelMembers AS CM ON B.channel_id=CM.channelId").
|
||||
Join("Users as U on CM.userID = U.id").
|
||||
LeftJoin("Bots as bo on U.id = bo.UserID").
|
||||
Where(sq.Eq{"B.id": boardID}).
|
||||
Where(sq.NotEq{"B.channel_id": ""}).
|
||||
// Filter out guests as they don't have synthetic membership
|
||||
Where(sq.NotEq{"U.roles": "system_guest"})
|
||||
Where(sq.NotEq{"U.roles": "system_guest"}).
|
||||
Where(sq.Eq{"bo.UserId IS NOT NULL": false})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
|
@ -119,6 +119,8 @@
|
||||
"ColorOption.selectColor": "Select {color} Color",
|
||||
"Comment.delete": "Delete",
|
||||
"CommentsList.send": "Send",
|
||||
"ConfirmPerson.empty": "Empty",
|
||||
"ConfirmPerson.search": "Search...",
|
||||
"ConfirmationDialog.cancel-action": "Cancel",
|
||||
"ConfirmationDialog.confirm-action": "Confirm",
|
||||
"ContentBlock.Delete": "Delete",
|
||||
@ -166,6 +168,7 @@
|
||||
"FilterByText.placeholder": "filter text",
|
||||
"FilterComponent.add-filter": "+ Add filter",
|
||||
"FilterComponent.delete": "Delete",
|
||||
"FilterValue.empty": "(empty)",
|
||||
"FindBoardsDialog.IntroText": "Search for boards",
|
||||
"FindBoardsDialog.NoResultsFor": "No results for \"{searchQuery}\"",
|
||||
"FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.",
|
||||
@ -195,6 +198,7 @@
|
||||
"OnboardingTour.ShareBoard.Body": "You can share your board internally, within your team, or publish it publicly for visibility outside of your organization.",
|
||||
"OnboardingTour.ShareBoard.Title": "Share board",
|
||||
"PersonProperty.board-members": "Board members",
|
||||
"PersonProperty.me": "Me",
|
||||
"PersonProperty.non-board-members": "Not board members",
|
||||
"PropertyMenu.Delete": "Delete",
|
||||
"PropertyMenu.changeType": "Change property type",
|
||||
@ -285,8 +289,8 @@
|
||||
"TableHeaderMenu.insert-right": "Insert right",
|
||||
"TableHeaderMenu.sort-ascending": "Sort ascending",
|
||||
"TableHeaderMenu.sort-descending": "Sort descending",
|
||||
"TableRow.DuplicateCard": "duplicate card",
|
||||
"TableRow.MoreOption": "More actions",
|
||||
"TableRow.delete": "Delete",
|
||||
"TableRow.open": "Open",
|
||||
"TopBar.give-feedback": "Give feedback",
|
||||
"URLProperty.copiedLink": "Copied!",
|
||||
@ -368,6 +372,8 @@
|
||||
"calendar.month": "Month",
|
||||
"calendar.today": "TODAY",
|
||||
"calendar.week": "Week",
|
||||
"centerPanel.undefined": "No {propertyName}",
|
||||
"centerPanel.unknown-user": "Unknown user",
|
||||
"cloudMessage.learn-more": "Learn more",
|
||||
"createImageBlock.failed": "Unable to upload the file. File size limit reached.",
|
||||
"default-properties.badges": "Comments and description",
|
||||
|
@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Utils} from './utils'
|
||||
import {Card} from './blocks/card'
|
||||
import {IPropertyTemplate, IPropertyOption, BoardGroup} from './blocks/board'
|
||||
|
||||
export function groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] {
|
||||
function groupCardsByOptions(cards: Card[], optionIds: string[], groupByProperty?: IPropertyTemplate): BoardGroup[] {
|
||||
const groups = []
|
||||
for (const optionId of optionIds) {
|
||||
if (optionId) {
|
||||
@ -36,7 +35,7 @@ export function groupCardsByOptions(cards: Card[], optionIds: string[], groupByP
|
||||
return groups
|
||||
}
|
||||
|
||||
export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
|
||||
function getOptionGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
|
||||
let unassignedOptionIds: string[] = []
|
||||
if (groupByProperty) {
|
||||
unassignedOptionIds = groupByProperty.options.
|
||||
@ -54,3 +53,37 @@ export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: strin
|
||||
const hiddenGroups = groupCardsByOptions(cards, hiddenOptionIds, groupByProperty)
|
||||
return {visible: visibleGroups, hidden: hiddenGroups}
|
||||
}
|
||||
export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: string[], hiddenOptionIds: string[], groupByProperty?: IPropertyTemplate): {visible: BoardGroup[], hidden: BoardGroup[]} {
|
||||
if (groupByProperty?.type === 'createdBy' || groupByProperty?.type === 'updatedBy' || groupByProperty?.type === 'person') {
|
||||
return getPersonGroups(cards, groupByProperty, hiddenOptionIds)
|
||||
}
|
||||
|
||||
return getOptionGroups(cards, visibleOptionIds, hiddenOptionIds, groupByProperty)
|
||||
}
|
||||
|
||||
function getPersonGroups(cards: Card[], groupByProperty: IPropertyTemplate, hiddenOptionIds: string[]): {visible: BoardGroup[], hidden: BoardGroup[]} {
|
||||
const groups = cards.reduce((unique: {[key: string]: Card[]}, item: Card): {[key: string]: Card[]} => {
|
||||
let key = item.fields.properties[groupByProperty.id] as string
|
||||
if (groupByProperty?.type === 'createdBy') {
|
||||
key = item.createdBy
|
||||
} else if (groupByProperty?.type === 'updatedBy') {
|
||||
key = item.modifiedBy
|
||||
}
|
||||
|
||||
const curGroup = unique[key] ?? []
|
||||
return {...unique, [key]: [...curGroup, item]}
|
||||
}, {})
|
||||
|
||||
const hiddenGroups: BoardGroup[] = []
|
||||
const visibleGroups: BoardGroup[] = []
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
const propertyOption = {id: key, value: key, color: ''} as IPropertyOption
|
||||
if (hiddenOptionIds.find((e) => e === key)) {
|
||||
hiddenGroups.push({option: propertyOption, cards: value})
|
||||
} else {
|
||||
visibleGroups.push({option: propertyOption, cards: value})
|
||||
}
|
||||
})
|
||||
|
||||
return {visible: visibleGroups, hidden: hiddenGroups}
|
||||
}
|
||||
|
@ -59,6 +59,146 @@ describe('src/cardFilter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - person property', () => {
|
||||
const personCard = TestBlockFactory.createCard(board)
|
||||
personCard.id = '1'
|
||||
personCard.title = 'card1'
|
||||
personCard.fields.properties.personPropertyID = 'personid1'
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'personPropertyID',
|
||||
name: 'myPerson',
|
||||
type: 'person',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('should be true with isNotEmpty clause', () => {
|
||||
const filterClauseIsNotEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isNotEmpty', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsNotEmpty, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('should be false with isEmpty clause', () => {
|
||||
const filterClauseIsEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isEmpty', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsEmpty, [template], personCard)
|
||||
expect(result).toBeFalsy()
|
||||
})
|
||||
test('verify empty includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause multiple values', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid2', 'personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify not includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid2']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - multi-person property', () => {
|
||||
const personCard = TestBlockFactory.createCard(board)
|
||||
personCard.id = '1'
|
||||
personCard.title = 'card1'
|
||||
personCard.fields.properties.personPropertyID = ['personid1', 'personid2']
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'personPropertyID',
|
||||
name: 'myPerson',
|
||||
type: 'multiPerson',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('should be true with isNotEmpty clause', () => {
|
||||
const filterClauseIsNotEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isNotEmpty', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsNotEmpty, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('should be false with isEmpty clause', () => {
|
||||
const filterClauseIsEmpty = createFilterClause({propertyId: 'personPropertyID', condition: 'isEmpty', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIsEmpty, [template], personCard)
|
||||
expect(result).toBeFalsy()
|
||||
})
|
||||
test('verify empty includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause 2', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid2']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause multiple values', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause multiple values 2', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid2']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify not includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid3']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify not includes clause, multiple values', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid3', 'personid4']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - (createdBy) person property', () => {
|
||||
const personCard = TestBlockFactory.createCard(board)
|
||||
personCard.id = '1'
|
||||
personCard.title = 'card1'
|
||||
personCard.createdBy = 'personid1'
|
||||
|
||||
const template: IPropertyTemplate = {
|
||||
id: 'personPropertyID',
|
||||
name: 'myPerson',
|
||||
type: 'createdBy',
|
||||
options: [],
|
||||
}
|
||||
|
||||
test('verify empty includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: []})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify includes clause multiple values', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'includes', values: ['personid3', 'personid1']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
test('verify not includes clause', () => {
|
||||
const filterClauseIncludes = createFilterClause({propertyId: 'personPropertyID', condition: 'notIncludes', values: ['personid2']})
|
||||
const result = CardFilter.isClauseMet(filterClauseIncludes, [template], personCard)
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('verify isClauseMet method - single date property', () => {
|
||||
// Date Properties are stored as 12PM UTC.
|
||||
const now = new Date(Date.now())
|
||||
@ -429,6 +569,32 @@ describe('src/cardFilter', () => {
|
||||
expect(result.value).toBeFalsy()
|
||||
})
|
||||
})
|
||||
describe('verify propertyThatMeetsFilterClause method - Person properties', () => {
|
||||
test('should return filterClause propertyId with template, and isEmpty clause', () => {
|
||||
const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'is', values: []})
|
||||
const templateFilter: IPropertyTemplate = {
|
||||
id: filterClauseIsEmpty.propertyId,
|
||||
name: 'template',
|
||||
type: 'createdBy',
|
||||
options: [],
|
||||
}
|
||||
const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsEmpty, [templateFilter])
|
||||
expect(result.id).toEqual(filterClauseIsEmpty.propertyId)
|
||||
expect(result.value).toBeFalsy()
|
||||
})
|
||||
test('should return filterClause propertyId with template, and isEmpty clause', () => {
|
||||
const filterClauseIsEmpty = createFilterClause({propertyId: 'propertyId', condition: 'is', values: []})
|
||||
const templateFilter: IPropertyTemplate = {
|
||||
id: filterClauseIsEmpty.propertyId,
|
||||
name: 'template',
|
||||
type: 'createdBy',
|
||||
options: [],
|
||||
}
|
||||
const result = CardFilter.propertyThatMeetsFilterClause(filterClauseIsEmpty, [templateFilter])
|
||||
expect(result.id).toEqual(filterClauseIsEmpty.propertyId)
|
||||
expect(result.value).toBeFalsy()
|
||||
})
|
||||
})
|
||||
describe('verify propertiesThatMeetFilterGroup method', () => {
|
||||
test('should return {} with undefined filterGroup', () => {
|
||||
const result = CardFilter.propertiesThatMeetFilterGroup(undefined, [])
|
||||
|
@ -76,9 +76,12 @@ class CardFilter {
|
||||
if (template?.type === 'date') {
|
||||
dateValue = this.createDatePropertyFromString(value as string)
|
||||
}
|
||||
if (!value) {
|
||||
// const template = templates.find((o) => o.id === filter.propertyId)
|
||||
if (template && template.type === 'createdTime') {
|
||||
if (!value && template) {
|
||||
if (template.type === 'createdBy') {
|
||||
value = card.createdBy
|
||||
} else if (template.type === 'updatedBy') {
|
||||
value = card.modifiedBy
|
||||
} else if (template && template.type === 'createdTime') {
|
||||
value = card.createAt.toString()
|
||||
dateValue = this.createDatePropertyFromString(value as string)
|
||||
} else if (template && template.type === 'updatedTime') {
|
||||
@ -255,6 +258,10 @@ class CardFilter {
|
||||
return {id: filterClause.propertyId}
|
||||
}
|
||||
|
||||
if (template.type === 'createdBy' || template.type === 'updatedBy') {
|
||||
return {id: filterClause.propertyId}
|
||||
}
|
||||
|
||||
switch (filterClause.condition) {
|
||||
case 'includes': {
|
||||
if (filterClause.values.length < 1) {
|
||||
@ -281,7 +288,7 @@ class CardFilter {
|
||||
return {id: filterClause.propertyId}
|
||||
}
|
||||
default: {
|
||||
Utils.assertFailure(`Unexpected filter condition: ${filterClause.condition}`)
|
||||
// Handle filter clause that cannot be set
|
||||
return {id: filterClause.propertyId}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`components/cardDialog already following card 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -215,7 +215,7 @@ exports[`components/cardDialog already following card 1`] = `
|
||||
exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -447,7 +447,7 @@ exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] =
|
||||
exports[`components/cardDialog return a cardDialog readonly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -578,7 +578,7 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
|
||||
exports[`components/cardDialog return cardDialog menu content 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -927,7 +927,7 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
|
||||
exports[`components/cardDialog return cardDialog menu content and cancel delete confirmation do nothing 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -1139,7 +1139,7 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
|
||||
exports[`components/cardDialog should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -1351,7 +1351,7 @@ exports[`components/cardDialog should match snapshot 1`] = `
|
||||
exports[`components/cardDialog should match snapshot without permissions 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back cardDialog"
|
||||
class="Dialog dialog-back cardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`/components/confirmAddUserForNotifications should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back confirmation-dialog-box"
|
||||
class="Dialog dialog-back confirmation-dialog-box size--small"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back confirmation-dialog-box"
|
||||
class="Dialog dialog-back confirmation-dialog-box size--small"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -84,7 +84,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
|
||||
exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Text should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back confirmation-dialog-box"
|
||||
class="Dialog dialog-back confirmation-dialog-box size--small"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`components/dialog should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back undefined"
|
||||
class="Dialog dialog-back undefined size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -50,7 +50,7 @@ exports[`components/dialog should match snapshot 1`] = `
|
||||
exports[`components/dialog should return dialog and click on cancel button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back undefined"
|
||||
class="Dialog dialog-back undefined size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
1204
webapp/src/components/__snapshots__/personSelector.test.tsx.snap
Normal file
1204
webapp/src/components/__snapshots__/personSelector.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -143,7 +143,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorItem should trigg
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back DeleteBoardDialog"
|
||||
class="Dialog dialog-back DeleteBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`component/BoardSwitcherDialog base case 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back BoardSwitcherDialog"
|
||||
class="Dialog dialog-back BoardSwitcherDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -1,10 +1,25 @@
|
||||
.Dialog {
|
||||
&.cardDialog {
|
||||
.dialog {
|
||||
.cardDialog {
|
||||
.dialog {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
>.CardDetail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
padding: 10px 126px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 975px) {
|
||||
padding: 16px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
>.CardDetail--fullwidth {
|
||||
padding-left: 78px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {ClientConfig} from '../config/clientConfig'
|
||||
import {Block} from '../blocks/block'
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {Card, createCard} from '../blocks/card'
|
||||
import {Board, IPropertyTemplate} from '../blocks/board'
|
||||
import {Board, IPropertyTemplate, BoardGroup} from '../blocks/board'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
import {CardFilter} from '../cardFilter'
|
||||
import mutator from '../mutator'
|
||||
@ -22,12 +22,15 @@ import {updateView} from '../store/views'
|
||||
import {getVisibleAndHiddenGroups} from '../boardUtils'
|
||||
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
import {getClientConfig} from '../store/clientConfig'
|
||||
|
||||
import './centerPanel.scss'
|
||||
|
||||
import {useAppSelector, useAppDispatch} from '../store/hooks'
|
||||
|
||||
import {
|
||||
getMe,
|
||||
getBoardUsers,
|
||||
getOnboardingTourCategory,
|
||||
getOnboardingTourStarted,
|
||||
getOnboardingTourStep,
|
||||
@ -84,8 +87,11 @@ const CenterPanel = (props: Props) => {
|
||||
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
|
||||
const me = useAppSelector(getMe)
|
||||
const currentCard = useAppSelector(getCurrentCard)
|
||||
const boardUsers = useAppSelector(getBoardUsers)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
|
||||
// empty dependency array yields behavior like `componentDidMount`, it only runs _once_
|
||||
// https://stackoverflow.com/a/58579462
|
||||
useEffect(() => {
|
||||
@ -361,10 +367,35 @@ const CenterPanel = (props: Props) => {
|
||||
const showShareLoginButton = props.readonly && me?.id !== 'single-user'
|
||||
|
||||
const {groupByProperty, activeView, board, views, cards} = props
|
||||
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(
|
||||
() => getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty),
|
||||
[cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty],
|
||||
)
|
||||
|
||||
const getUserDisplayName = (boardGroup: BoardGroup) => {
|
||||
const user = boardUsers[boardGroup.option.id]
|
||||
if (user) {
|
||||
return Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)
|
||||
} else if (boardGroup.option.id === 'undefined') {
|
||||
return intl.formatMessage({
|
||||
id: 'centerPanel.undefined',
|
||||
defaultMessage: 'No {propertyName}',
|
||||
}, {propertyName: groupByProperty?.name})
|
||||
}
|
||||
return intl.formatMessage({id: 'centerPanel.unknown-user', defaultMessage: 'Unknown user'})
|
||||
}
|
||||
|
||||
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(() => {
|
||||
const {visible: vg, hidden: hg} = getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty)
|
||||
if (groupByProperty?.type === 'createdBy' || groupByProperty?.type === 'updatedBy' || groupByProperty?.type === 'person') {
|
||||
if (boardUsers) {
|
||||
vg.forEach((value) => {
|
||||
value.option.value = getUserDisplayName(value)
|
||||
})
|
||||
hg.forEach((value) => {
|
||||
value.option.value = getUserDisplayName(value)
|
||||
})
|
||||
}
|
||||
}
|
||||
return {visible: vg, hidden: hg}
|
||||
}, [cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty, boardUsers])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='BoardComponent'
|
||||
|
@ -3,30 +3,11 @@
|
||||
.confirmation-dialog-box {
|
||||
.dialog {
|
||||
@include z-index(confirmation-dialog-box);
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
max-width: 512px;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: max-content;
|
||||
|
||||
background-color: rgb(var(--center-channel-bg-rgb));
|
||||
box-shadow: rgba(var(--center-channel-color-rgb), 0.1) 0 0 0 1px,
|
||||
rgba(var(--center-channel-color-rgb), 0.1) 0 2px 4px;
|
||||
|
||||
border-radius: var(--modal-rad);
|
||||
padding: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
> .toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ export const ConfirmationDialogBox = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
size='small'
|
||||
className='confirmation-dialog-box'
|
||||
onClose={handleOnClose}
|
||||
>
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`components/createCategory/CreateCategory base case should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back CreateCategoryModal"
|
||||
class="Dialog dialog-back CreateCategoryModal size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -6,62 +6,71 @@
|
||||
width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.CreateCategory {
|
||||
.CreateCategory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 32px 24px;
|
||||
gap: 24px;
|
||||
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
|
||||
.inputWrapper__close-wrapper {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.CloseCircle {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categoryNameInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color);
|
||||
padding: 0 16px;
|
||||
flex: 1;
|
||||
transition: border 0.15s ease-in;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 32px 24px;
|
||||
gap: 24px;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
|
||||
.CloseCircle {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
right: 16px;
|
||||
top: 6px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
}
|
||||
|
||||
.categoryNameInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color);
|
||||
padding: 0 16px;
|
||||
flex: 1;
|
||||
transition: border 0.15s ease-in;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.createCategoryActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.createCategoryActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ const CreateCategory = (props: Props): JSX.Element => {
|
||||
{
|
||||
Boolean(name) &&
|
||||
<div
|
||||
className='clearBtn'
|
||||
className='clearBtn inputWrapper__close-wrapper'
|
||||
onClick={() => setName('')}
|
||||
>
|
||||
<CloseCircle/>
|
||||
|
@ -10,6 +10,14 @@
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.size--small {
|
||||
.dialog {
|
||||
max-width: 512px;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
@ -22,10 +30,6 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dialog__close {
|
||||
margin: 0 -14px 0 0;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
@include z-index(dialog-backdrop);
|
||||
position: fixed;
|
||||
@ -66,14 +70,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 975px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -106,7 +102,7 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 24px 32px;
|
||||
padding: 24px 0;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@ -116,24 +112,7 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
> .CardDetail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@media not screen and (max-width: 975px) {
|
||||
padding: 10px 126px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 975px) {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> .CardDetail--fullwidth {
|
||||
padding-left: 78px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import './dialog.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
size?: string
|
||||
toolsMenu?: React.ReactNode // some dialogs may not require a toolmenu
|
||||
toolbar?: React.ReactNode
|
||||
hideCloseButton?: boolean
|
||||
@ -22,7 +23,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const Dialog = (props: Props) => {
|
||||
const {toolsMenu, toolbar, title, subtitle} = props
|
||||
const {toolsMenu, toolbar, title, subtitle, size} = props
|
||||
const intl = useIntl()
|
||||
|
||||
const closeDialogText = intl.formatMessage({
|
||||
@ -35,7 +36,7 @@ const Dialog = (props: Props) => {
|
||||
const isBackdropClickedRef = useRef(false)
|
||||
|
||||
return (
|
||||
<div className={`Dialog dialog-back ${props.className}`}>
|
||||
<div className={`Dialog dialog-back ${props.className} size--${size || 'medium'}`}>
|
||||
<div className='backdrop'/>
|
||||
<div
|
||||
className='wrapper'
|
||||
|
@ -905,7 +905,7 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Dialog dialog-back confirmation-dialog-box"
|
||||
class="Dialog dialog-back confirmation-dialog-box size--small"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -49,6 +49,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
const {board, activeView, intl, group, groupByProperty} = props
|
||||
const [groupTitle, setGroupTitle] = useState(group.option.value)
|
||||
const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties])
|
||||
const canEditOption = groupByProperty?.type !== 'person' && group.option.id
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@ -108,7 +109,11 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
</Label>}
|
||||
{group.option.id &&
|
||||
{groupByProperty?.type === 'person' &&
|
||||
<Label>
|
||||
{groupTitle}
|
||||
</Label>}
|
||||
{canEditOption &&
|
||||
<Label color={group.option.color}>
|
||||
<Editable
|
||||
value={groupTitle}
|
||||
@ -165,7 +170,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')}
|
||||
/>
|
||||
{group.option.id &&
|
||||
{canEditOption &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='delete'
|
||||
|
@ -1,8 +1,9 @@
|
||||
.Person {
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
&.readonly {
|
||||
overflow: hidden;
|
||||
@ -13,7 +14,6 @@
|
||||
.Person-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--center-channel-color-rgb), 1);
|
||||
|
||||
img {
|
||||
border-radius: 50px;
|
||||
@ -23,10 +23,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.react-select__input {
|
||||
margin-left: -5px !important;
|
||||
}
|
||||
|
||||
.react-select__menu {
|
||||
background: rgba(var(--center-channel-bg-rgb), 1);
|
||||
box-shadow: var(--elevation-4);
|
||||
@ -41,11 +37,52 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.react-select__value-container--is-multi {
|
||||
gap: 4px;
|
||||
display: inline-flex;
|
||||
|
||||
.react-select__multi-value__label {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.react-select__multi-value {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: 24px;
|
||||
display: inline-flex;
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
|
||||
.MultiPerson-item,
|
||||
.react-select__multi-value__label {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-select__multi-value__remove {
|
||||
font-size: 18px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
margin: 6px;
|
||||
border-radius: 100%;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.26);
|
||||
}
|
||||
}
|
||||
|
||||
.react-select__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 40px 0 10px;
|
||||
padding: 0 40px 0 20px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
@ -71,5 +108,3 @@
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
333
webapp/src/components/personSelector.test.tsx
Normal file
333
webapp/src/components/personSelector.test.tsx
Normal file
@ -0,0 +1,333 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {render, waitFor} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {act} from 'react-dom/test-utils'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {wrapIntl} from '../testUtils'
|
||||
|
||||
import PersonProperty from '../properties/person/property'
|
||||
|
||||
import PersonSelector from './personSelector'
|
||||
|
||||
describe('properties/person', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
'user-id-1': {
|
||||
id: 'user-id-1',
|
||||
username: 'username-1',
|
||||
email: 'user-1@example.com',
|
||||
firstname: 'test',
|
||||
lastname: 'user',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
boardUsers: {
|
||||
'user-id-1': {
|
||||
id: 'user-id-1',
|
||||
username: 'username-1',
|
||||
email: 'user-1@example.com',
|
||||
firstname: 'test',
|
||||
lastname: 'user',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
'user-id-2': {
|
||||
id: 'user-id-2',
|
||||
username: 'username-2',
|
||||
email: 'user-2@example.com',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
'user-id-3': {
|
||||
id: 'user-id-3',
|
||||
username: 'username-3',
|
||||
email: 'user-3@example.com',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
teammateNameDisplay: 'username',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test('not readOnly, show username', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
userIDs={['user-id-1']}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={false}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('not readOnly, show firstname', async () => {
|
||||
const store = mockStore({
|
||||
...state,
|
||||
clientConfig: {
|
||||
value: {
|
||||
teammateNameDisplay: 'full_name',
|
||||
},
|
||||
},
|
||||
})
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
userIDs={['user-id-1']}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={false}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('not readOnly, show modal', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
userIDs={[]}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={false}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.Person > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
userEvent.click(userProperty as Element)
|
||||
})
|
||||
|
||||
const userList = container.querySelector('.Person-item')
|
||||
expect(userList).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
})
|
||||
|
||||
test('readOnly view', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={true}
|
||||
userIDs={['user-id-1']}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={false}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.Person > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).toBeNull()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
})
|
||||
|
||||
test('show multiple', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
userIDs={['user-id-1', 'user-id-2']}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={true}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
test('show multiple, display modal', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
userIDs={['user-id-1', 'user-id-2']}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'(empty)'}
|
||||
isMulti={true}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.MultiPerson > div > div:nth-child(1) > div:nth-child(3) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
userEvent.click(userProperty as Element)
|
||||
})
|
||||
const userList = container.querySelector('.MultiPerson-item')
|
||||
expect(userList).not.toBeNull()
|
||||
expect(container).toMatchSnapshot()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
})
|
||||
|
||||
test('not readOnly, show me', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<PersonSelector
|
||||
readOnly={false}
|
||||
showMe={true}
|
||||
userIDs={[]}
|
||||
allowAddUsers={false}
|
||||
property={new PersonProperty()}
|
||||
emptyDisplayValue={'Empty'}
|
||||
isMulti={false}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
|
||||
// expect(container).toMatchSnapshot()
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.Person > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
act(() => {
|
||||
userEvent.click(userProperty as Element)
|
||||
})
|
||||
|
||||
const userList = container.querySelector('.Person-item')
|
||||
expect(userList).not.toBeNull()
|
||||
console.log('Text content ' + userList?.textContent)
|
||||
expect(userList?.textContent).toBe('Me')
|
||||
expect(container).toMatchSnapshot()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
205
webapp/src/components/personSelector.tsx
Normal file
205
webapp/src/components/personSelector.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import Select from 'react-select/async'
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
import {ActionMeta} from 'react-select'
|
||||
|
||||
import {getSelectBaseStyle} from '../theme'
|
||||
import {IUser} from '../user'
|
||||
import {Utils} from '../utils'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
import {getBoardUsers, getBoardUsersList, getMe} from '../store/users'
|
||||
|
||||
import {ClientConfig} from '../config/clientConfig'
|
||||
import {getClientConfig} from '../store/clientConfig'
|
||||
import client from '../octoClient'
|
||||
|
||||
import GuestBadge from '../widgets/guestBadge'
|
||||
import {PropertyType} from '../properties/types'
|
||||
|
||||
import './personSelector.scss'
|
||||
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
userIDs: string[]
|
||||
allowAddUsers: boolean
|
||||
property?: PropertyType
|
||||
emptyDisplayValue: string
|
||||
isMulti: boolean
|
||||
closeMenuOnSelect?: boolean
|
||||
showMe?: boolean
|
||||
onChange: (items: any, action: ActionMeta<IUser>) => void
|
||||
}
|
||||
|
||||
const selectStyles = {
|
||||
...getSelectBaseStyle(),
|
||||
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
|
||||
...provided,
|
||||
background: state.isFocused ? 'rgba(var(--center-channel-color-rgb), 0.1)' : 'rgb(var(--center-channel-bg-rgb))',
|
||||
color: state.isFocused ? 'rgb(var(--center-channel-color-rgb))' : 'rgb(var(--center-channel-color-rgb))',
|
||||
padding: '8px',
|
||||
}),
|
||||
control: (): CSSObject => ({
|
||||
border: 0,
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
}),
|
||||
valueContainer: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
padding: 'unset',
|
||||
overflow: 'unset',
|
||||
}),
|
||||
singleValue: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
position: 'static',
|
||||
top: 'unset',
|
||||
transform: 'unset',
|
||||
}),
|
||||
menu: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
width: 'unset',
|
||||
background: 'rgb(var(--center-channel-bg-rgb))',
|
||||
minWidth: '260px',
|
||||
}),
|
||||
}
|
||||
|
||||
const PersonSelector = (props: Props): JSX.Element => {
|
||||
const {readOnly, userIDs, allowAddUsers, isMulti, closeMenuOnSelect = true, emptyDisplayValue, showMe = false, onChange} = props
|
||||
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
const intl = useIntl()
|
||||
|
||||
const boardUsersById = useAppSelector<{[key: string]: IUser}>(getBoardUsers)
|
||||
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
|
||||
const boardUsersKey = Object.keys(boardUsersById) ? Utils.hashCode(JSON.stringify(Object.keys(boardUsersById))) : 0
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
|
||||
const formatOptionLabel = (user: any): JSX.Element => {
|
||||
if (!user) {
|
||||
return <div/>
|
||||
}
|
||||
let profileImg
|
||||
if (imageURLForUser) {
|
||||
profileImg = imageURLForUser(user.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className={isMulti ? 'MultiPerson-item' : 'Person-item'}
|
||||
>
|
||||
{profileImg && (
|
||||
<img
|
||||
alt='Person-avatar'
|
||||
src={profileImg}
|
||||
/>
|
||||
)}
|
||||
{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
||||
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let users: IUser[] = []
|
||||
if (Object.keys(boardUsersById).length > 0) {
|
||||
users = userIDs.map((id) => boardUsersById[id])
|
||||
}
|
||||
|
||||
const loadOptions = useCallback(async (value: string) => {
|
||||
if (!allowAddUsers) {
|
||||
const returnUsers: IUser[] = []
|
||||
if (showMe && me) {
|
||||
returnUsers.push({
|
||||
id: me.id,
|
||||
username: intl.formatMessage({id: 'PersonProperty.me', defaultMessage: 'Me'}),
|
||||
email: '',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: me.create_at,
|
||||
update_at: me.update_at,
|
||||
is_bot: false,
|
||||
is_guest: me.is_guest,
|
||||
roles: me.roles,
|
||||
})
|
||||
returnUsers.push(...boardUsers.filter((u) => u.id !== me.id))
|
||||
} else {
|
||||
returnUsers.push(...boardUsers)
|
||||
}
|
||||
if (value) {
|
||||
return returnUsers.filter((u) => {
|
||||
return u.username.toLowerCase().includes(value.toLowerCase()) ||
|
||||
u.lastname.toLowerCase().includes(value.toLowerCase()) ||
|
||||
u.firstname.toLowerCase().includes(value.toLowerCase()) ||
|
||||
u.nickname.toLowerCase().includes(value.toLowerCase())
|
||||
})
|
||||
}
|
||||
return returnUsers
|
||||
}
|
||||
const excludeBots = true
|
||||
const allUsers = await client.searchTeamUsers(value, excludeBots)
|
||||
const usersInsideBoard: IUser[] = []
|
||||
const usersOutsideBoard: IUser[] = []
|
||||
for (const u of allUsers) {
|
||||
if (boardUsersById[u.id]) {
|
||||
usersInsideBoard.push(u)
|
||||
} else {
|
||||
usersOutsideBoard.push(u)
|
||||
}
|
||||
}
|
||||
return [
|
||||
{label: intl.formatMessage({id: 'PersonProperty.board-members', defaultMessage: 'Board members'}), options: usersInsideBoard},
|
||||
{label: intl.formatMessage({id: 'PersonProperty.non-board-members', defaultMessage: 'Not board members'}), options: usersOutsideBoard},
|
||||
]
|
||||
}, [boardUsers, allowAddUsers, boardUsersById, me])
|
||||
|
||||
let primaryClass = 'Person'
|
||||
if (isMulti) {
|
||||
primaryClass = 'MultiPerson'
|
||||
}
|
||||
let secondaryClass = ''
|
||||
if (props.property) {
|
||||
secondaryClass = ` ${props.property.valueClassName(readOnly)}`
|
||||
}
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<div className={`${primaryClass}${secondaryClass}`}>
|
||||
{users.map((user) => formatOptionLabel(user))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
key={boardUsersKey}
|
||||
loadOptions={loadOptions}
|
||||
isMulti={isMulti}
|
||||
defaultOptions={true}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
backspaceRemovesValue={true}
|
||||
closeMenuOnSelect={closeMenuOnSelect}
|
||||
className={`${primaryClass}${secondaryClass}`}
|
||||
classNamePrefix={'react-select'}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectStyles}
|
||||
placeholder={emptyDisplayValue}
|
||||
getOptionLabel={(o: IUser) => o.username}
|
||||
getOptionValue={(a: IUser) => a.id}
|
||||
value={users}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PersonSelector
|
@ -3,7 +3,7 @@
|
||||
exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -253,7 +253,7 @@ exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -462,7 +462,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -671,7 +671,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Regenerate token 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -903,7 +903,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -1139,7 +1139,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -1607,7 +1607,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -1843,7 +1843,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -2311,7 +2311,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -2496,7 +2496,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -2817,7 +2817,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard template and cli
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoard, and click switch 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -3049,7 +3049,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc
|
||||
exports[`src/components/shareBoard/shareBoard return shareBoardComponent and click Switch without sharing 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -3281,7 +3281,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
|
||||
exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -3490,7 +3490,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
|
||||
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -3699,7 +3699,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and subpath 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -3908,7 +3908,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and without workspaceId and subpath 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -4117,7 +4117,7 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing
|
||||
exports[`src/components/shareBoard/shareBoard should match snapshot, with template 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ShareBoardDialog"
|
||||
class="Dialog dialog-back ShareBoardDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -54,6 +54,8 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
className += ' expanded'
|
||||
}
|
||||
|
||||
const canEditOption = groupByProperty?.type !== 'person' && group.option.id
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.option.id + 'header'}
|
||||
@ -90,7 +92,11 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
</Label>}
|
||||
{group.option.id &&
|
||||
{groupByProperty?.type === 'person' &&
|
||||
<Label>
|
||||
{groupTitle}
|
||||
</Label>}
|
||||
{canEditOption &&
|
||||
<Label color={group.option.color}>
|
||||
<Editable
|
||||
value={groupTitle}
|
||||
@ -122,7 +128,7 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
|
||||
onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')}
|
||||
/>
|
||||
{group.option.id &&
|
||||
{canEditOption &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='delete'
|
||||
|
@ -95,7 +95,15 @@ const TableRow = (props: Props) => {
|
||||
}
|
||||
if (isGrouped) {
|
||||
const groupID = groupById || ''
|
||||
const groupValue = card.fields.properties[groupID] as string || 'undefined'
|
||||
let groupValue = card.fields.properties[groupID] as string || 'undefined'
|
||||
if (groupValue === 'undefined') {
|
||||
const template = board.cardProperties.find((p) => p.id === groupById) //templates.find((o) => o.id === groupById)
|
||||
if (template && template.type === 'createdBy') {
|
||||
groupValue = card.createdBy
|
||||
} else if (template && template.type === 'updatedBy') {
|
||||
groupValue = card.modifiedBy
|
||||
}
|
||||
}
|
||||
if (collapsedOptionIds.indexOf(groupValue) > -1) {
|
||||
className += ' hidden'
|
||||
}
|
||||
|
@ -711,6 +711,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
|
@ -388,6 +388,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -453,6 +455,474 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry and click on different property type 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="FilterEntry"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Status
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Title"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Title
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Status"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Status
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 1"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 2"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 2
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 3"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 3
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
includes
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper filterValue"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Status
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry and click on different property type, but same filterOperation 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="FilterEntry"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Property 1
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect bottom "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Title"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Title
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Status"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Status
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 1"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 2"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 2
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="Property 3"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Property 3
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-option__content"
|
||||
>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
is set
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="octo-spacer"
|
||||
/>
|
||||
<button
|
||||
class="Button"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/filterEntry return filterEntry and click on doesn't include 1`] = `
|
||||
<div>
|
||||
<div
|
||||
@ -600,6 +1070,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -812,6 +1284,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on inclu
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1024,6 +1498,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is em
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1236,6 +1712,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on is no
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -1617,6 +2095,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for boolean field
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
@ -1825,6 +2305,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for date field 2`]
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
@ -2115,6 +2597,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for text field 2`]
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div>
|
||||
|
@ -270,4 +270,57 @@ describe('components/viewHeader/filterEntry', () => {
|
||||
userEvent.click(allButton[allButton.length - 1])
|
||||
expect(mockedMutator.changeViewFilter).toBeCalledTimes(1)
|
||||
})
|
||||
test('return filterEntry and click on different property type', () => {
|
||||
activeView.fields.filter.filters = [statusFilter]
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<FilterEntry
|
||||
board={board}
|
||||
view={activeView}
|
||||
conditionClicked={mockedConditionClicked}
|
||||
filter={statusFilter}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
const buttonDate = screen.getByRole('button', {name: 'Property 3'})
|
||||
userEvent.click(buttonDate)
|
||||
expect(mockedMutator.changeViewFilter).toBeCalledWith(
|
||||
board.id, activeView.id,
|
||||
{operation: 'and', filters: [statusFilter]},
|
||||
{operation: 'and', filters: [dateFilter]})
|
||||
})
|
||||
test('return filterEntry and click on different property type, but same filterOperation', () => {
|
||||
activeView.fields.filter.filters = [booleanFilter]
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<FilterEntry
|
||||
board={board}
|
||||
view={activeView}
|
||||
conditionClicked={mockedConditionClicked}
|
||||
filter={booleanFilter}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
const buttonDate = screen.getByRole('button', {name: 'Property 3'})
|
||||
userEvent.click(buttonDate)
|
||||
expect(mockedMutator.changeViewFilter).toBeCalledWith(
|
||||
board.id, activeView.id,
|
||||
{operation: 'and', filters: [booleanFilter]},
|
||||
{operation: 'and',
|
||||
filters: [{
|
||||
propertyId: board.cardProperties[3].id,
|
||||
condition: 'isSet',
|
||||
values: [],
|
||||
}]})
|
||||
})
|
||||
})
|
||||
|
@ -76,6 +76,7 @@ const FilterEntry = (props: Props): JSX.Element => {
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
if (newFilter.propertyId !== optionId) {
|
||||
newFilter.propertyId = optionId
|
||||
newFilter.condition = OctoUtils.filterConditionValidOrDefault(propsRegistry.get(o.type).filterValueType, newFilter.condition)
|
||||
newFilter.values = []
|
||||
mutator.changeViewFilter(props.board.id, view.id, view.fields.filter, filterGroup)
|
||||
}
|
||||
@ -109,6 +110,32 @@ const FilterEntry = (props: Props): JSX.Element => {
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
{propertyType.filterValueType === 'person' &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='includes'
|
||||
name={intl.formatMessage({id: 'Filter.includes', defaultMessage: 'includes'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='notIncludes'
|
||||
name={intl.formatMessage({id: 'Filter.not-includes', defaultMessage: 'doesn\'t include'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
{(propertyType.type === 'person' || propertyType.type === 'multiPerson') &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='isEmpty'
|
||||
name={intl.formatMessage({id: 'Filter.is-empty', defaultMessage: 'is empty'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='isNotEmpty'
|
||||
name={intl.formatMessage({id: 'Filter.is-not-empty', defaultMessage: 'is not empty'})}
|
||||
onClick={(id) => props.conditionClicked(id, filter)}
|
||||
/>
|
||||
</>}
|
||||
{propertyType.filterValueType === 'boolean' &&
|
||||
<>
|
||||
<Menu.Text
|
||||
|
@ -19,6 +19,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import DateFilter from './dateFilter'
|
||||
|
||||
import './filterValue.scss'
|
||||
import MultiPersonFilterValue from './multipersonFilterValue'
|
||||
|
||||
type Props = {
|
||||
view: BoardView
|
||||
@ -40,7 +41,7 @@ const filterValue = (props: Props): JSX.Element|null => {
|
||||
return null
|
||||
}
|
||||
|
||||
if (propertyType.filterValueType === 'options' && filter.condition !== 'includes' && filter.condition !== 'notIncludes') {
|
||||
if ((propertyType.filterValueType === 'options' || propertyType.filterValueType === 'person') && filter.condition !== 'includes' && filter.condition !== 'notIncludes') {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -65,6 +66,14 @@ const filterValue = (props: Props): JSX.Element|null => {
|
||||
)
|
||||
}
|
||||
|
||||
if (propertyType.filterValueType === 'person') {
|
||||
return (
|
||||
<MultiPersonFilterValue
|
||||
view={view}
|
||||
filter={filter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (propertyType.filterValueType === 'date') {
|
||||
if (filter.condition === 'isSet' || filter.condition === 'isNotSet') {
|
||||
return null
|
||||
@ -85,12 +94,13 @@ const filterValue = (props: Props): JSX.Element|null => {
|
||||
return option?.value || '(Unknown)'
|
||||
}).join(', ')
|
||||
} else {
|
||||
displayValue = '(empty)'
|
||||
displayValue = intl.formatMessage({id: 'FilterValue.empty', defaultMessage: '(empty)'})
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuWrapper className='filterValue'>
|
||||
<Button>{displayValue}</Button>
|
||||
|
||||
<Menu>
|
||||
{template?.options.map((o) => (
|
||||
<Menu.Switch
|
||||
|
@ -3,7 +3,6 @@
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0 0;
|
||||
gap: 8px;
|
||||
|
||||
&.readonly {
|
61
webapp/src/components/viewHeader/multipersonFilterValue.tsx
Normal file
61
webapp/src/components/viewHeader/multipersonFilterValue.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {MultiValue} from 'react-select'
|
||||
|
||||
import {Utils} from '../../utils'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
import {createFilterGroup} from '../../blocks/filterGroup'
|
||||
|
||||
import PersonSelector from '../personSelector'
|
||||
import {IUser} from '../../user'
|
||||
|
||||
import './multiperson.scss'
|
||||
|
||||
type Props = {
|
||||
view: BoardView
|
||||
filter: FilterClause
|
||||
}
|
||||
|
||||
const MultiPersonFilterValue = (props: Props): JSX.Element => {
|
||||
const {filter, view} = props
|
||||
const intl = useIntl()
|
||||
const emptyDisplayValue = intl.formatMessage({id: 'ConfirmPerson.search', defaultMessage: 'Search...'})
|
||||
|
||||
return (
|
||||
<PersonSelector
|
||||
userIDs={filter.values}
|
||||
allowAddUsers={false}
|
||||
isMulti={true}
|
||||
readOnly={false}
|
||||
emptyDisplayValue={emptyDisplayValue}
|
||||
showMe={true}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(items: MultiValue<IUser>, action) => {
|
||||
const filterIndex = view.fields.filter.filters.indexOf(filter)
|
||||
Utils.assert(filterIndex >= 0, "Can't find filter")
|
||||
|
||||
const filterGroup = createFilterGroup(view.fields.filter)
|
||||
const newFilter = filterGroup.filters[filterIndex] as FilterClause
|
||||
Utils.assert(newFilter, `No filter at index ${filterIndex}`)
|
||||
|
||||
if (action.action === 'select-option') {
|
||||
newFilter.values = items.map((a) => a.id)
|
||||
} else if (action.action === 'clear') {
|
||||
newFilter.values = []
|
||||
} else if (action.action === 'remove-value') {
|
||||
newFilter.values = items.filter((a) => a.id !== action.removedValue.id).map((b) => b.id) || []
|
||||
}
|
||||
mutator.changeViewFilter(view.boardId, view.id, view.fields.filter, filterGroup)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiPersonFilterValue
|
@ -3,7 +3,7 @@
|
||||
exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button for non sys admin user 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ViewLimitDialog"
|
||||
class="Dialog dialog-back ViewLimitDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -85,7 +85,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button
|
||||
exports[`components/viewLimitDialog/ViewLiimitDialog show upgrade button for sys admin user 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ViewLimitDialog"
|
||||
class="Dialog dialog-back ViewLimitDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmation msg 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ViewLimitDialog"
|
||||
class="Dialog dialog-back ViewLimitDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
@ -119,7 +119,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmat
|
||||
exports[`components/viewLimitDialog/ViewL]imitDialog show view limit dialog 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Dialog dialog-back ViewLimitDialog"
|
||||
class="Dialog dialog-back ViewLimitDialog size--medium"
|
||||
>
|
||||
<div
|
||||
class="backdrop"
|
||||
|
@ -28,6 +28,35 @@ test('duplicateBlockTree: Card', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('filterConditionValidOrDefault', async () => {
|
||||
// Test 'options'
|
||||
expect(OctoUtils.filterConditionValidOrDefault('options', 'includes')).toBe('includes')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('options', 'notIncludes')).toBe('notIncludes')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('options', 'isEmpty')).toBe('isEmpty')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('options', 'isNotEmpty')).toBe('isNotEmpty')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('options', 'is')).toBe('includes')
|
||||
|
||||
expect(OctoUtils.filterConditionValidOrDefault('boolean', 'isSet')).toBe('isSet')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('boolean', 'isNotSet')).toBe('isNotSet')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('boolean', 'includes')).toBe('isSet')
|
||||
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'is')).toBe('is')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'contains')).toBe('contains')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'notContains')).toBe('notContains')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'startsWith')).toBe('startsWith')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'notStartsWith')).toBe('notStartsWith')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'endsWith')).toBe('endsWith')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'notEndsWith')).toBe('notEndsWith')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('text', 'isEmpty')).toBe('is')
|
||||
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'is')).toBe('is')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'isBefore')).toBe('isBefore')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'isAfter')).toBe('isAfter')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'isSet')).toBe('isSet')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'isNotSet')).toBe('isNotSet')
|
||||
expect(OctoUtils.filterConditionValidOrDefault('date', 'isEmpty')).toBe('is')
|
||||
})
|
||||
|
||||
function createCardTree(): [Block[], Block] {
|
||||
const blocks: Block[] = []
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {FilterValueType} from './properties/types'
|
||||
import {Block, createBlock} from './blocks/block'
|
||||
import {BoardView, createBoardView} from './blocks/boardView'
|
||||
import {Card, createCard} from './blocks/card'
|
||||
@ -105,7 +106,7 @@ class OctoUtils {
|
||||
}
|
||||
|
||||
static filterConditionDisplayString(filterCondition: FilterCondition, intl: IntlShape, filterValueType: string): string {
|
||||
if (filterValueType === 'options') {
|
||||
if (filterValueType === 'options' || filterValueType === 'person') {
|
||||
switch (filterCondition) {
|
||||
case 'includes': return intl.formatMessage({id: 'Filter.includes', defaultMessage: 'includes'})
|
||||
case 'notIncludes': return intl.formatMessage({id: 'Filter.not-includes', defaultMessage: 'doesn\'t include'})
|
||||
@ -152,6 +153,57 @@ class OctoUtils {
|
||||
return '(unknown)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static filterConditionValidOrDefault(filterValueType: FilterValueType, currentFilterCondition: FilterCondition): FilterCondition {
|
||||
if (filterValueType === 'options') {
|
||||
switch (currentFilterCondition) {
|
||||
case 'includes':
|
||||
case 'notIncludes':
|
||||
case 'isEmpty':
|
||||
case 'isNotEmpty':
|
||||
return currentFilterCondition
|
||||
default: {
|
||||
return 'includes'
|
||||
}
|
||||
}
|
||||
} else if (filterValueType === 'boolean') {
|
||||
switch (currentFilterCondition) {
|
||||
case 'isSet':
|
||||
case 'isNotSet':
|
||||
return currentFilterCondition
|
||||
default: {
|
||||
return 'isSet'
|
||||
}
|
||||
}
|
||||
} else if (filterValueType === 'text') {
|
||||
switch (currentFilterCondition) {
|
||||
case 'is':
|
||||
case 'contains':
|
||||
case 'notContains':
|
||||
case 'startsWith':
|
||||
case 'notStartsWith':
|
||||
case 'endsWith':
|
||||
case 'notEndsWith':
|
||||
return currentFilterCondition
|
||||
default: {
|
||||
return 'is'
|
||||
}
|
||||
}
|
||||
} else if (filterValueType === 'date') {
|
||||
switch (currentFilterCondition) {
|
||||
case 'is':
|
||||
case 'isBefore':
|
||||
case 'isAfter':
|
||||
case 'isSet':
|
||||
case 'isNotSet':
|
||||
return currentFilterCondition
|
||||
default: {
|
||||
return 'is'
|
||||
}
|
||||
}
|
||||
}
|
||||
Utils.assertFailure()
|
||||
return 'includes'
|
||||
}
|
||||
}
|
||||
export {OctoUtils}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
|
||||
|
||||
import CreatedBy from './createdBy'
|
||||
|
||||
@ -12,4 +12,7 @@ export default class CreatedByProperty extends PropertyType {
|
||||
type = 'createdBy' as PropertyTypeEnum
|
||||
isReadOnly = true
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.CreatedBy', defaultMessage: 'Created by'})
|
||||
canFilter = true
|
||||
filterValueType = 'person' as FilterValueType
|
||||
canGroup = true
|
||||
}
|
||||
|
@ -71,7 +71,12 @@ describe('properties/multiperson', () => {
|
||||
propertyValue={['user-id-4']}
|
||||
readOnly={false}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
propertyTemplate={{
|
||||
id: 'personPropertyID',
|
||||
name: 'My Person Property',
|
||||
type: 'multiPerson',
|
||||
options: [],
|
||||
} as IPropertyTemplate}
|
||||
board={{} as Board}
|
||||
card={{} as Card}
|
||||
/>
|
||||
@ -97,7 +102,12 @@ describe('properties/multiperson', () => {
|
||||
propertyValue={['user-id-1', 'user-id-2']}
|
||||
readOnly={false}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
propertyTemplate={{
|
||||
id: 'personPropertyID',
|
||||
name: 'My Person Property',
|
||||
type: 'multiPerson',
|
||||
options: [],
|
||||
} as IPropertyTemplate}
|
||||
board={{} as Board}
|
||||
card={{} as Card}
|
||||
/>
|
||||
@ -123,7 +133,12 @@ describe('properties/multiperson', () => {
|
||||
propertyValue={['user-id-1', 'user-id-2']}
|
||||
readOnly={true}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
propertyTemplate={{
|
||||
id: 'personPropertyID',
|
||||
name: 'My Person Property',
|
||||
type: 'multiPerson',
|
||||
options: [],
|
||||
} as IPropertyTemplate}
|
||||
board={{} as Board}
|
||||
card={{} as Card}
|
||||
/>
|
||||
@ -149,7 +164,12 @@ describe('properties/multiperson', () => {
|
||||
propertyValue={['user-id-1', 'user-id-2']}
|
||||
readOnly={false}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
propertyTemplate={{
|
||||
id: 'personPropertyID',
|
||||
name: 'My Person Property',
|
||||
type: 'multiPerson',
|
||||
options: [],
|
||||
} as IPropertyTemplate}
|
||||
board={{} as Board}
|
||||
card={{} as Card}
|
||||
/>
|
||||
|
@ -1,213 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
import Select from 'react-select/async'
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
import {getSelectBaseStyle} from '../../theme'
|
||||
import {IUser} from '../../user'
|
||||
import {Utils} from '../../utils'
|
||||
import mutator from '../../mutator'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getBoardUsers, getBoardUsersList, getMe} from '../../store/users'
|
||||
import {BoardMember, BoardTypeOpen, MemberRole} from '../../blocks/board'
|
||||
import React from 'react'
|
||||
|
||||
import {PropertyProps} from '../types'
|
||||
import {ClientConfig} from '../../config/clientConfig'
|
||||
import {getClientConfig} from '../../store/clientConfig'
|
||||
import {useHasPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
import client from '../../octoClient'
|
||||
import ConfirmAddUserForNotifications from '../../components/confirmAddUserForNotifications'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
|
||||
import './multiperson.scss'
|
||||
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser
|
||||
|
||||
const selectStyles = {
|
||||
...getSelectBaseStyle(),
|
||||
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
|
||||
...provided,
|
||||
background: state.isFocused ? 'rgba(var(--center-channel-color-rgb), 0.1)' : 'rgb(var(--center-channel-bg-rgb))',
|
||||
color: state.isFocused ? 'rgb(var(--center-channel-color-rgb))' : 'rgb(var(--center-channel-color-rgb))',
|
||||
padding: '8px',
|
||||
}),
|
||||
control: (): CSSObject => ({
|
||||
border: 0,
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
}),
|
||||
valueContainer: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
padding: 'unset',
|
||||
overflow: 'unset',
|
||||
}),
|
||||
singleValue: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
position: 'static',
|
||||
top: 'unset',
|
||||
transform: 'unset',
|
||||
}),
|
||||
menu: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
width: 'unset',
|
||||
background: 'rgb(var(--center-channel-bg-rgb))',
|
||||
minWidth: '260px',
|
||||
}),
|
||||
}
|
||||
import ConfirmPerson from '../person/confirmPerson'
|
||||
|
||||
const MultiPerson = (props: PropertyProps): JSX.Element => {
|
||||
const {card, board, propertyTemplate, propertyValue, readOnly} = props
|
||||
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
const intl = useIntl()
|
||||
|
||||
const boardUsersById = useAppSelector<{[key: string]: IUser}>(getBoardUsers)
|
||||
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
|
||||
const boardUsersKey = Object.keys(boardUsersById) ? Utils.hashCode(JSON.stringify(Object.keys(boardUsersById))) : 0
|
||||
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
|
||||
const allowManageBoardRoles = useHasPermissions(board.teamId, board.id, [Permission.ManageBoardRoles])
|
||||
const allowAddUsers = !me?.is_guest && (allowManageBoardRoles || board.type === BoardTypeOpen)
|
||||
|
||||
const onChange = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
|
||||
|
||||
const formatOptionLabel = (user: any): JSX.Element => {
|
||||
if (!user) {
|
||||
return <div/>
|
||||
}
|
||||
let profileImg
|
||||
if (imageURLForUser) {
|
||||
profileImg = imageURLForUser(user.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className='MultiPerson-item'
|
||||
>
|
||||
{profileImg && (
|
||||
<img
|
||||
alt='MultiPerson-avatar'
|
||||
src={profileImg}
|
||||
/>
|
||||
)}
|
||||
{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
||||
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let users: IUser[] = []
|
||||
|
||||
if (typeof propertyValue === 'string' && propertyValue !== '') {
|
||||
users = [boardUsersById[propertyValue as string]]
|
||||
} else if (Array.isArray(propertyValue) && propertyValue.length > 0) {
|
||||
users = propertyValue.map((id) => boardUsersById[id])
|
||||
}
|
||||
|
||||
const addUser = useCallback(async (userId: string, role: string) => {
|
||||
const newRole = role || MemberRole.Viewer
|
||||
const newMember = {
|
||||
boardId: board.id,
|
||||
userId,
|
||||
roles: role,
|
||||
schemeAdmin: newRole === MemberRole.Admin,
|
||||
schemeEditor: newRole === MemberRole.Admin || newRole === MemberRole.Editor,
|
||||
schemeCommenter: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter,
|
||||
schemeViewer: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter || newRole === MemberRole.Viewer,
|
||||
} as BoardMember
|
||||
|
||||
setConfirmAddUser(null)
|
||||
await mutator.createBoardMember(newMember)
|
||||
|
||||
if (users) {
|
||||
const userIds = users.map((a) => a.id)
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, [...userIds, newMember.userId])
|
||||
} else {
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, newMember.userId)
|
||||
}
|
||||
}, [board, card, propertyTemplate, users])
|
||||
|
||||
const loadOptions = useCallback(async (value: string) => {
|
||||
if (!allowAddUsers) {
|
||||
return boardUsers.filter((u) => u.username.toLowerCase().includes(value.toLowerCase()))
|
||||
}
|
||||
const excludeBots = true
|
||||
const allUsers = await client.searchTeamUsers(value, excludeBots)
|
||||
const usersInsideBoard: IUser[] = []
|
||||
const usersOutsideBoard: IUser[] = []
|
||||
for (const u of allUsers) {
|
||||
if (boardUsersById[u.id]) {
|
||||
usersInsideBoard.push(u)
|
||||
} else {
|
||||
usersOutsideBoard.push(u)
|
||||
}
|
||||
}
|
||||
return [
|
||||
{label: intl.formatMessage({id: 'PersonProperty.board-members', defaultMessage: 'Board members'}), options: usersInsideBoard},
|
||||
{label: intl.formatMessage({id: 'PersonProperty.non-board-members', defaultMessage: 'Not board members'}), options: usersOutsideBoard},
|
||||
]
|
||||
}, [boardUsers, allowAddUsers, boardUsersById])
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<div className={`MultiPerson ${props.property.valueClassName(true)}`}>
|
||||
{users ? users.map((user) => formatOptionLabel(user)) : propertyValue}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{confirmAddUser &&
|
||||
<ConfirmAddUserForNotifications
|
||||
allowManageBoardRoles={allowManageBoardRoles}
|
||||
minimumRole={board.minimumRole}
|
||||
user={confirmAddUser}
|
||||
onConfirm={addUser}
|
||||
onClose={() => setConfirmAddUser(null)}
|
||||
/>}
|
||||
<Select
|
||||
key={boardUsersKey}
|
||||
loadOptions={loadOptions}
|
||||
isMulti={true}
|
||||
defaultOptions={true}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
backspaceRemovesValue={true}
|
||||
className={`MultiPerson ${props.property.valueClassName(props.readOnly)}`}
|
||||
classNamePrefix={'react-select'}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectStyles}
|
||||
placeholder={'Empty'}
|
||||
getOptionLabel={(o: IUser) => o.username}
|
||||
getOptionValue={(a: IUser) => a.id}
|
||||
value={users}
|
||||
onChange={(items, action) => {
|
||||
if (action.action === 'select-option') {
|
||||
const confirmedIds: string[] = []
|
||||
items.forEach((item) => {
|
||||
if (boardUsersById[item.id]) {
|
||||
confirmedIds.push(item.id)
|
||||
} else {
|
||||
setConfirmAddUser(item)
|
||||
}
|
||||
})
|
||||
onChange(confirmedIds)
|
||||
} else if (action.action === 'clear') {
|
||||
onChange([])
|
||||
} else if (action.action === 'remove-value') {
|
||||
onChange(items.filter((a) => a.id !== action.removedValue.id).map((b) => b.id) || [])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<ConfirmPerson
|
||||
{...props}
|
||||
showEmptyPlaceholder={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
|
||||
|
||||
import MultiPerson from './multiperson'
|
||||
|
||||
@ -11,4 +11,6 @@ export default class MultiPersonProperty extends PropertyType {
|
||||
name = 'MultiPerson'
|
||||
type = 'multiPerson' as PropertyTypeEnum
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.MultiPerson', defaultMessage: 'Multi person'})
|
||||
canFilter = true
|
||||
filterValueType = 'person' as FilterValueType
|
||||
}
|
||||
|
@ -0,0 +1,505 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`properties/person select user - cancel 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Person octo-propertyvalue css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
/>
|
||||
<div
|
||||
class="react-select__control css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
id="react-select-4-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__clear-indicator css-tpaeio-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-19sxey8-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`properties/person select user - cancel 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Person octo-propertyvalue css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-4-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
>
|
||||
<span
|
||||
id="aria-selection"
|
||||
/>
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
option username-4 focused, 0 of 2. 2 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="react-select__control react-select__control--is-focused react-select__control--menu-is-open css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-4-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-4-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
id="react-select-4-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__clear-indicator css-13eygzs-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-hl9mox-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__menu css-10b6da7-menu"
|
||||
id="react-select-4-listbox"
|
||||
>
|
||||
<div
|
||||
class="react-select__menu-list css-g29tl0-MenuList"
|
||||
>
|
||||
<div
|
||||
class="react-select__group css-syji7d-Group"
|
||||
>
|
||||
<div
|
||||
class="react-select__group-heading css-18ng2q5-group"
|
||||
id="react-select-4-group-1-heading"
|
||||
>
|
||||
Not board members
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="react-select__option react-select__option--is-focused css-1bwtvog-option"
|
||||
id="react-select-4-option-1-0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-4
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="react-select__option css-nyiims-option"
|
||||
id="react-select-4-option-1-1"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`properties/person select user - confirm 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Person octo-propertyvalue css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
/>
|
||||
<div
|
||||
class="react-select__control css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__clear-indicator css-tpaeio-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-19sxey8-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`properties/person select user - confirm 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Person octo-propertyvalue css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-2-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
>
|
||||
<span
|
||||
id="aria-selection"
|
||||
/>
|
||||
<span
|
||||
id="aria-context"
|
||||
>
|
||||
option username-4 focused, 0 of 2. 2 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="react-select__control react-select__control--is-focused react-select__control--menu-is-open css-18140j1-Control"
|
||||
>
|
||||
<div
|
||||
class="react-select__value-container react-select__value-container--has-value css-433wy7-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="react-select__single-value css-1lixa2z-singleValue"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-1
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__input-container css-ox1y69-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls="react-select-2-listbox"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
aria-owns="react-select-2-listbox"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="react-select__input"
|
||||
id="react-select-2-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__clear-indicator css-13eygzs-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="react-select__indicator-separator css-43ykx9-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="react-select__indicator react-select__dropdown-indicator css-hl9mox-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="react-select__menu css-10b6da7-menu"
|
||||
id="react-select-2-listbox"
|
||||
>
|
||||
<div
|
||||
class="react-select__menu-list css-g29tl0-MenuList"
|
||||
>
|
||||
<div
|
||||
class="react-select__group css-syji7d-Group"
|
||||
>
|
||||
<div
|
||||
class="react-select__group-heading css-18ng2q5-group"
|
||||
id="react-select-2-group-1-heading"
|
||||
>
|
||||
Not board members
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="react-select__option react-select__option--is-focused css-1bwtvog-option"
|
||||
id="react-select-2-option-1-0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-4
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="react-select__option css-nyiims-option"
|
||||
id="react-select-2-option-1-1"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Person-item"
|
||||
>
|
||||
username-5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
237
webapp/src/properties/person/confirmPerson.test.tsx
Normal file
237
webapp/src/properties/person/confirmPerson.test.tsx
Normal file
@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {render, screen, waitFor, within} from '@testing-library/react'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {act} from 'react-dom/test-utils'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
|
||||
import client from '../../octoClient'
|
||||
|
||||
import mutator from '../../mutator'
|
||||
|
||||
import PersonProperty from './property'
|
||||
|
||||
// import {IPropertyTemplate, Board} from '../blocks/board'
|
||||
|
||||
import ConfirmPerson from './confirmPerson'
|
||||
jest.mock('../../mutator')
|
||||
jest.mock('../../octoClient')
|
||||
|
||||
const mockedMutator = mocked(mutator, true)
|
||||
const mockedOctoClient = mocked(client, true)
|
||||
|
||||
const board = TestBlockFactory.createBoard()
|
||||
board.teamId = 'team-id-1'
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
|
||||
describe('properties/person', () => {
|
||||
const mockStore = configureStore([])
|
||||
const state = {
|
||||
boards: {
|
||||
boards: {
|
||||
[board.id]: board,
|
||||
},
|
||||
current: board.id,
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user-id-1', schemeAdmin: true},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1',
|
||||
roles: 'system_user',
|
||||
},
|
||||
boardUsers: {
|
||||
'user-id-1': {
|
||||
id: 'user-id-1',
|
||||
username: 'username-1',
|
||||
email: 'user-1@example.com',
|
||||
firstname: 'test',
|
||||
lastname: 'user',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
'user-id-2': {
|
||||
id: 'user-id-2',
|
||||
username: 'username-2',
|
||||
email: 'user-2@example.com',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
'user-id-3': {
|
||||
id: 'user-id-3',
|
||||
username: 'username-3',
|
||||
email: 'user-3@example.com',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
teammateNameDisplay: 'username',
|
||||
},
|
||||
},
|
||||
}
|
||||
const additionalUsers = [
|
||||
{
|
||||
id: 'user-id-4',
|
||||
username: 'username-4',
|
||||
email: 'user-4@example.com',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
is_bot: false,
|
||||
is_guest: false,
|
||||
roles: 'system_user',
|
||||
},
|
||||
{
|
||||
id: 'user-id-5',
|
||||
username: 'username-5',
|
||||
email: 'user-5@example.com',
|
||||
nickname: '',
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
props: {},
|
||||
create_at: 1621315184,
|
||||
update_at: 1621315184,
|
||||
delete_at: 0,
|
||||
is_bot: false,
|
||||
is_guest: false,
|
||||
roles: 'system_user',
|
||||
},
|
||||
]
|
||||
|
||||
mockedOctoClient.searchTeamUsers.mockResolvedValue(additionalUsers)
|
||||
|
||||
test('select user - confirm', async () => {
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ConfirmPerson
|
||||
property={new PersonProperty()}
|
||||
propertyValue={'user-id-1'}
|
||||
readOnly={false}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
board={board}
|
||||
card={card}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.Person > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
userEvent.click(userProperty as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
const option = renderResult.getByText('username-4')
|
||||
expect(option).not.toBeNull()
|
||||
act(() => {
|
||||
userEvent.click(option as Element)
|
||||
})
|
||||
|
||||
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
|
||||
expect(confirmDialog).toBeDefined()
|
||||
const confirmButton = within(confirmDialog).getByRole('button', {name: 'Add to board'})
|
||||
expect(confirmButton).toBeDefined()
|
||||
userEvent.click(confirmButton)
|
||||
|
||||
expect(mockedMutator.createBoardMember).toBeCalled()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
})
|
||||
|
||||
test('select user - cancel', async () => {
|
||||
mockedMutator.createBoardMember.mockClear()
|
||||
const store = mockStore(state)
|
||||
const component = wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ConfirmPerson
|
||||
property={new PersonProperty()}
|
||||
propertyValue={'user-id-1'}
|
||||
readOnly={false}
|
||||
showEmptyPlaceholder={false}
|
||||
propertyTemplate={{} as IPropertyTemplate}
|
||||
board={board}
|
||||
card={card}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
)
|
||||
const renderResult = render(component)
|
||||
const container = await waitFor(() => {
|
||||
if (!renderResult.container) {
|
||||
return Promise.reject(new Error('container not found'))
|
||||
}
|
||||
return Promise.resolve(renderResult.container)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
if (container) {
|
||||
// this is the actual element where the click event triggers
|
||||
// opening of the dropdown
|
||||
const userProperty = container.querySelector('.Person > div > div:nth-child(1) > div:nth-child(2) > input')
|
||||
expect(userProperty).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
userEvent.click(userProperty as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
const option = renderResult.getByText('username-4')
|
||||
expect(option).not.toBeNull()
|
||||
act(() => {
|
||||
userEvent.click(option as Element)
|
||||
})
|
||||
|
||||
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
|
||||
expect(confirmDialog).toBeDefined()
|
||||
const cancelButton = within(confirmDialog).getByRole('button', {name: 'Cancel'})
|
||||
expect(cancelButton).toBeDefined()
|
||||
userEvent.click(cancelButton)
|
||||
|
||||
expect(mockedMutator.createBoardMember).not.toBeCalled()
|
||||
} else {
|
||||
throw new Error('container should have been initialized')
|
||||
}
|
||||
})
|
||||
})
|
118
webapp/src/properties/person/confirmPerson.tsx
Normal file
118
webapp/src/properties/person/confirmPerson.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import {ActionMeta, SingleValue, MultiValue} from 'react-select'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
import mutator from '../../mutator'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getBoardUsers, getMe} from '../../store/users'
|
||||
import {BoardMember, BoardTypeOpen, MemberRole} from '../../blocks/board'
|
||||
|
||||
import {PropertyProps} from '../types'
|
||||
import {useHasPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
import ConfirmAddUserForNotifications from '../../components/confirmAddUserForNotifications'
|
||||
import PersonSelector from '../../components/personSelector'
|
||||
|
||||
const ConfirmPerson = (props: PropertyProps): JSX.Element => {
|
||||
const {card, board, propertyTemplate, propertyValue, property, readOnly} = props
|
||||
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||
const intl = useIntl()
|
||||
|
||||
const boardUsersById = useAppSelector<{[key: string]: IUser}>(getBoardUsers)
|
||||
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
|
||||
const allowManageBoardRoles = useHasPermissions(board.teamId, board.id, [Permission.ManageBoardRoles])
|
||||
const allowAddUsers = !me?.is_guest && (allowManageBoardRoles || board.type === BoardTypeOpen)
|
||||
const changePropertyValue = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
|
||||
const emptyDisplayValue = props.showEmptyPlaceholder ? intl.formatMessage({id: 'ConfirmPerson.empty', defaultMessage: 'Empty'}) : ''
|
||||
|
||||
let userIDs: string[] = []
|
||||
if (typeof propertyValue === 'string' && propertyValue !== '') {
|
||||
userIDs.push(propertyValue as string)
|
||||
} else if (Array.isArray(propertyValue) && propertyValue.length > 0) {
|
||||
userIDs = propertyValue
|
||||
}
|
||||
|
||||
const onChange = (items: SingleValue<IUser> | MultiValue<IUser>, action: ActionMeta<IUser>) => {
|
||||
if (Array.isArray(items)) {
|
||||
if (action.action === 'select-option') {
|
||||
const confirmedIds: string[] = []
|
||||
items.forEach((item) => {
|
||||
if (boardUsersById[item.id]) {
|
||||
confirmedIds.push(item.id)
|
||||
} else {
|
||||
setConfirmAddUser(item)
|
||||
}
|
||||
})
|
||||
changePropertyValue(confirmedIds)
|
||||
} else if (action.action === 'clear') {
|
||||
changePropertyValue([])
|
||||
} else if (action.action === 'remove-value') {
|
||||
changePropertyValue(items.filter((a) => a.id !== action.removedValue.id).map((b) => b.id) || [])
|
||||
}
|
||||
} else {
|
||||
const item = items as IUser
|
||||
if (action.action === 'select-option') {
|
||||
if (boardUsersById[item?.id || '']) {
|
||||
changePropertyValue(item?.id || '')
|
||||
} else {
|
||||
setConfirmAddUser(item)
|
||||
}
|
||||
} else if (action.action === 'clear') {
|
||||
changePropertyValue('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addUser = useCallback(async (userId: string, role: string) => {
|
||||
const newRole = role || MemberRole.Viewer
|
||||
const newMember = {
|
||||
boardId: board.id,
|
||||
userId,
|
||||
roles: role,
|
||||
schemeAdmin: newRole === MemberRole.Admin,
|
||||
schemeEditor: newRole === MemberRole.Admin || newRole === MemberRole.Editor,
|
||||
schemeCommenter: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter,
|
||||
schemeViewer: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter || newRole === MemberRole.Viewer,
|
||||
} as BoardMember
|
||||
|
||||
setConfirmAddUser(null)
|
||||
await mutator.createBoardMember(newMember)
|
||||
|
||||
if (userIDs) {
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, [...userIDs, newMember.userId])
|
||||
} else {
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, newMember.userId)
|
||||
}
|
||||
}, [board, card, propertyTemplate, userIDs])
|
||||
|
||||
return (
|
||||
<>
|
||||
{confirmAddUser &&
|
||||
<ConfirmAddUserForNotifications
|
||||
allowManageBoardRoles={allowManageBoardRoles}
|
||||
minimumRole={board.minimumRole}
|
||||
user={confirmAddUser}
|
||||
onConfirm={addUser}
|
||||
onClose={() => setConfirmAddUser(null)}
|
||||
/>}
|
||||
<PersonSelector
|
||||
userIDs={userIDs}
|
||||
allowAddUsers={allowAddUsers}
|
||||
isMulti={propertyTemplate.type === 'multiPerson'}
|
||||
readOnly={readOnly}
|
||||
emptyDisplayValue={emptyDisplayValue}
|
||||
property={property}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmPerson
|
@ -1,186 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import Select from 'react-select/async'
|
||||
import {useIntl} from 'react-intl'
|
||||
import {CSSObject} from '@emotion/serialize'
|
||||
|
||||
import {Utils} from '../../utils'
|
||||
import {IUser} from '../../user'
|
||||
import {getBoardUsersList, getBoardUsers, getMe} from '../../store/users'
|
||||
import {BoardMember, BoardTypeOpen, MemberRole} from '../../blocks/board'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import mutator from '../../mutator'
|
||||
import {getSelectBaseStyle} from '../../theme'
|
||||
import {ClientConfig} from '../../config/clientConfig'
|
||||
import {getClientConfig} from '../../store/clientConfig'
|
||||
import {useHasPermissions} from '../../hooks/permissions'
|
||||
import {Permission} from '../../constants'
|
||||
import client from '../../octoClient'
|
||||
import ConfirmAddUserForNotifications from '../../components/confirmAddUserForNotifications'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
import React from 'react'
|
||||
|
||||
import {PropertyProps} from '../types'
|
||||
|
||||
import './person.scss'
|
||||
|
||||
const imageURLForUser = (window as any).Components?.imageURLForUser
|
||||
|
||||
const selectStyles = {
|
||||
...getSelectBaseStyle(),
|
||||
option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
|
||||
...provided,
|
||||
background: state.isFocused ? 'rgba(var(--center-channel-color-rgb), 0.1)' : 'rgb(var(--center-channel-bg-rgb))',
|
||||
color: state.isFocused ? 'rgb(var(--center-channel-color-rgb))' : 'rgb(var(--center-channel-color-rgb))',
|
||||
padding: '8px',
|
||||
}),
|
||||
control: (): CSSObject => ({
|
||||
border: 0,
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
}),
|
||||
valueContainer: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
padding: 'unset',
|
||||
overflow: 'unset',
|
||||
}),
|
||||
singleValue: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
position: 'static',
|
||||
top: 'unset',
|
||||
transform: 'unset',
|
||||
}),
|
||||
menu: (provided: CSSObject): CSSObject => ({
|
||||
...provided,
|
||||
width: 'unset',
|
||||
background: 'rgb(var(--center-channel-bg-rgb))',
|
||||
minWidth: '260px',
|
||||
}),
|
||||
}
|
||||
import ConfirmPerson from './confirmPerson'
|
||||
|
||||
const Person = (props: PropertyProps): JSX.Element => {
|
||||
const {card, board, propertyTemplate, propertyValue, readOnly} = props
|
||||
const [confirmAddUser, setConfirmAddUser] = useState<IUser|null>(null)
|
||||
|
||||
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
|
||||
const boardUsersById = useAppSelector<{[key: string]: IUser}>(getBoardUsers)
|
||||
const boardUsersKey = Object.keys(boardUsersById) ? Utils.hashCode(JSON.stringify(Object.keys(boardUsersById))) : 0
|
||||
const onChange = useCallback((newValue) => mutator.changePropertyValue(board.id, card, propertyTemplate.id, newValue), [board.id, card, propertyTemplate.id])
|
||||
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
const intl = useIntl()
|
||||
|
||||
const formatOptionLabel = (user: IUser) => {
|
||||
let profileImg
|
||||
if (imageURLForUser) {
|
||||
profileImg = imageURLForUser(user.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='Person-item'>
|
||||
{profileImg && (
|
||||
<img
|
||||
alt='Person-avatar'
|
||||
src={profileImg}
|
||||
/>
|
||||
)}
|
||||
{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
||||
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const addUser = useCallback(async (userId: string, role: string) => {
|
||||
const newRole = role || MemberRole.Viewer
|
||||
const newMember = {
|
||||
boardId: board.id,
|
||||
userId,
|
||||
roles: role,
|
||||
schemeAdmin: newRole === MemberRole.Admin,
|
||||
schemeEditor: newRole === MemberRole.Admin || newRole === MemberRole.Editor,
|
||||
schemeCommenter: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter,
|
||||
schemeViewer: newRole === MemberRole.Admin || newRole === MemberRole.Editor || newRole === MemberRole.Commenter || newRole === MemberRole.Viewer,
|
||||
} as BoardMember
|
||||
|
||||
setConfirmAddUser(null)
|
||||
await mutator.createBoardMember(newMember)
|
||||
await mutator.changePropertyValue(board.id, card, propertyTemplate.id, newMember.userId)
|
||||
mutator.updateBoardMember(newMember, {...newMember, schemeAdmin: false, schemeEditor: true, schemeCommenter: true, schemeViewer: true})
|
||||
}, [board, card, propertyTemplate])
|
||||
|
||||
const allowManageBoardRoles = useHasPermissions(board.teamId, board.id, [Permission.ManageBoardRoles])
|
||||
const allowAddUsers = !me?.is_guest && (allowManageBoardRoles || board.type === BoardTypeOpen)
|
||||
|
||||
const loadOptions = useCallback(async (value: string) => {
|
||||
if (!allowAddUsers) {
|
||||
return boardUsers.filter((u) => u.username.toLowerCase().includes(value.toLowerCase()))
|
||||
}
|
||||
const excludeBots = true
|
||||
const allUsers = await client.searchTeamUsers(value, excludeBots)
|
||||
const usersInsideBoard: IUser[] = []
|
||||
const usersOutsideBoard: IUser[] = []
|
||||
for (const u of allUsers) {
|
||||
if (boardUsersById[u.id]) {
|
||||
usersInsideBoard.push(u)
|
||||
} else {
|
||||
usersOutsideBoard.push(u)
|
||||
}
|
||||
}
|
||||
return [
|
||||
{label: intl.formatMessage({id: 'PersonProperty.board-members', defaultMessage: 'Board members'}), options: usersInsideBoard},
|
||||
{label: intl.formatMessage({id: 'PersonProperty.non-board-members', defaultMessage: 'Not board members'}), options: usersOutsideBoard},
|
||||
]
|
||||
}, [boardUsers, allowAddUsers, boardUsersById])
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<div className={`Person ${props.property.valueClassName(true)}`}>
|
||||
{boardUsersById[propertyValue as string] ? formatOptionLabel(boardUsersById[propertyValue as string]) : propertyValue}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{confirmAddUser &&
|
||||
<ConfirmAddUserForNotifications
|
||||
allowManageBoardRoles={allowManageBoardRoles}
|
||||
minimumRole={board.minimumRole}
|
||||
user={confirmAddUser}
|
||||
onConfirm={addUser}
|
||||
onClose={() => setConfirmAddUser(null)}
|
||||
/>}
|
||||
<Select
|
||||
key={boardUsersKey}
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={true}
|
||||
isSearchable={true}
|
||||
isClearable={true}
|
||||
backspaceRemovesValue={true}
|
||||
className={`Person ${props.property.valueClassName(props.readOnly)}`}
|
||||
classNamePrefix={'react-select'}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={selectStyles}
|
||||
placeholder={'Empty'}
|
||||
getOptionLabel={(o: IUser) => o.username}
|
||||
getOptionValue={(a: IUser) => a.id}
|
||||
value={boardUsersById[propertyValue as string] || null}
|
||||
onChange={(item, action) => {
|
||||
if (action.action === 'select-option') {
|
||||
if (boardUsersById[item?.id || '']) {
|
||||
onChange(item?.id || '')
|
||||
} else {
|
||||
setConfirmAddUser(item)
|
||||
}
|
||||
} else if (action.action === 'clear') {
|
||||
onChange('')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<ConfirmPerson
|
||||
{...props}
|
||||
showEmptyPlaceholder={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
|
||||
|
||||
import Person from './person'
|
||||
|
||||
@ -11,4 +11,7 @@ export default class PersonProperty extends PropertyType {
|
||||
name = 'Person'
|
||||
type = 'person' as PropertyTypeEnum
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.Person', defaultMessage: 'Person'})
|
||||
canFilter = true
|
||||
filterValueType = 'person' as FilterValueType
|
||||
canGroup = true
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ function encodeText(text: string): string {
|
||||
|
||||
export type PropertyTypeEnum = BoardPropertyTypeEnum
|
||||
|
||||
export type FilterValueType = 'none'|'options'|'boolean'|'text'|'date'
|
||||
export type FilterValueType = 'none'|'options'|'boolean'|'text'|'date'|'person'
|
||||
|
||||
export type FilterCondition = {
|
||||
id: string
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {PropertyType, PropertyTypeEnum} from '../types'
|
||||
import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
|
||||
|
||||
import UpdatedBy from './updatedBy'
|
||||
|
||||
@ -12,4 +12,7 @@ export default class UpdatedByProperty extends PropertyType {
|
||||
type = 'updatedBy' as PropertyTypeEnum
|
||||
isReadOnly = true
|
||||
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.UpdatedBy', defaultMessage: 'Last updated by'})
|
||||
canFilter = true
|
||||
filterValueType = 'person' as FilterValueType
|
||||
canGroup = true
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user