1
0
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:
wiggin77 2023-01-17 14:54:37 -05:00
commit ca704a2760
55 changed files with 3944 additions and 607 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -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, [])

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -28,6 +28,7 @@ export const ConfirmationDialogBox = (props: Props) => {
return (
<Dialog
size='small'
className='confirmation-dialog-box'
onClose={handleOnClose}
>

View File

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

View File

@ -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;
}
}
}

View File

@ -97,7 +97,7 @@ const CreateCategory = (props: Props): JSX.Element => {
{
Boolean(name) &&
<div
className='clearBtn'
className='clearBtn inputWrapper__close-wrapper'
onClick={() => setName('')}
>
<CloseCircle/>

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -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;
}
}

View 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()
})
})

View 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

View File

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

View File

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

View File

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

View File

@ -711,6 +711,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
<div />
<div />
<div />
<div />
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"

View File

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

View File

@ -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: [],
}]})
})
})

View File

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

View File

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

View File

@ -3,7 +3,6 @@
align-items: center;
border-radius: 4px;
flex-wrap: wrap;
padding: 8px 0 0;
gap: 8px;
&.readonly {

View 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

View File

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

View File

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

View File

@ -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[] = []

View File

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

View File

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

View File

@ -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}
/>

View File

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

View File

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

View File

@ -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>
`;

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

View 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

View File

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

View File

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

View File

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

View File

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