1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-09-16 08:56:19 +02:00

fix merge conflict

This commit is contained in:
wiggin77
2023-01-17 14:54:37 -05:00
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 showFullName = *mmconfig.PrivacySettings.ShowFullName
} }
serverRoot := baseURL + "/plugins/focalboard"
if mmconfig.FeatureFlags.BoardsProduct {
serverRoot = baseURL + "/boards"
}
return &config.Configuration{ return &config.Configuration{
ServerRoot: baseURL + "/plugins/focalboard", ServerRoot: serverRoot,
Port: -1, Port: -1,
DBType: *mmconfig.SqlSettings.DriverName, DBType: *mmconfig.SqlSettings.DriverName,
DBConfigString: *mmconfig.SqlSettings.DataSource, DBConfigString: *mmconfig.SqlSettings.DataSource,

View File

@@ -6,7 +6,7 @@ exports[`components/boardSelector escape button should unmount the component 1`]
class="focalboard-body" class="focalboard-body"
> >
<div <div
class="Dialog dialog-back BoardSelector" class="Dialog dialog-back BoardSelector size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -111,7 +111,7 @@ exports[`components/boardSelector renders with no results 1`] = `
class="focalboard-body" class="focalboard-body"
> >
<div <div
class="Dialog dialog-back BoardSelector" class="Dialog dialog-back BoardSelector size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -217,7 +217,7 @@ exports[`components/boardSelector renders with some results 1`] = `
class="focalboard-body" class="focalboard-body"
> >
<div <div
class="Dialog dialog-back BoardSelector" class="Dialog dialog-back BoardSelector size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -421,7 +421,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
class="focalboard-body" class="focalboard-body"
> >
<div <div
class="Dialog dialog-back BoardSelector" class="Dialog dialog-back BoardSelector size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -9,8 +9,8 @@ require (
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94 github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.7
github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb github.com/mattermost/mattermost-plugin-api v0.1.1
github.com/mattermost/mattermost-server/v6 v6.0.0-20221214122404-8d90c7042f93 github.com/mattermost/mattermost-server/v6 v6.0.0-20230116174708-240304ad0728
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e
github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538 github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538
@@ -23,14 +23,14 @@ require (
github.com/spf13/viper v1.10.1 github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
github.com/wiggin77/merror v1.0.4 github.com/wiggin77/merror v1.0.4
golang.org/x/crypto v0.2.0 golang.org/x/crypto v0.5.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // 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/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-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-migrate/migrate/v4 v4.15.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/graph-gophers/graphql-go v1.4.0 // indirect github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a // indirect
github.com/hashicorp/go-hclog v1.3.1 // indirect github.com/hashicorp/go-hclog v1.4.0 // indirect
github.com/hashicorp/go-plugin v1.4.6 // indirect github.com/hashicorp/go-plugin v1.4.8 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.12 // indirect github.com/klauspost/compress v1.15.14 // indirect
github.com/klauspost/cpuid/v2 v2.2.1 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/magiconair/properties v1.8.6 // 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/logr/v2 v2.0.15 // indirect
github.com/mattermost/squirrel v0.2.0 // indirect github.com/mattermost/squirrel v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.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/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/minio/md5-simd v1.1.2 // 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/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // 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/modern-go/reflect2 v1.0.2 // indirect
github.com/pborman/uuid v1.2.1 // indirect github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.33.0 // indirect github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // 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/rs/xid v1.4.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
@@ -86,36 +86,36 @@ require (
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.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/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect github.com/wiggin77/srslog v1.0.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/yuin/goldmark v1.5.3 // indirect github.com/yuin/goldmark v1.5.3 // indirect
golang.org/x/mod v0.7.0 // 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/sync v0.1.0 // indirect
golang.org/x/sys v0.2.0 // indirect golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect
golang.org/x/tools v0.3.0 // indirect golang.org/x/tools v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf // indirect
google.golang.org/grpc v1.50.1 // indirect google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.36.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.16.7 // indirect modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.4.1 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.1.1 // indirect modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.1 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.18.0 // indirect modernc.org/sqlite v1.20.1 // indirect
modernc.org/strutil v1.1.1 // indirect modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.0 // 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.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 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.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/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.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= 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.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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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-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/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= 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/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 h1:JE9wveRTSXwJyjdRd6bOQ7Ob5bewTUQ58Jv4OiVdpdE=
github.com/graph-gophers/graphql-go v1.4.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= 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/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.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/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-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 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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-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-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= 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-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 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= 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-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-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 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.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 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 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.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.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 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 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.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.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= 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/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 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.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 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-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 h1:VfNz+fvJ3DxOlALM22Eea8ONp5jHrybKBCcCtDPVlss=
github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e/go.mod h1:xo0ljDknTpPxEdhhrUdwhLCexIsYyDKS6b41HqG8wGU= github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e/go.mod h1:xo0ljDknTpPxEdhhrUdwhLCexIsYyDKS6b41HqG8wGU=
github.com/mattermost/squirrel v0.2.0 h1:8ZWeyf+MWQ2cL7hu9REZgLtz2IJi51qqZEovI3T3TT8= 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.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 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.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-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.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/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/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 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= 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 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 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= 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/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 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 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/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 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= 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-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 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/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 v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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/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 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.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/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 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= 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-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-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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.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/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/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= 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.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 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 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-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-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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-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-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/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.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-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-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/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-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-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.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/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-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/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-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-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-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-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-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-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.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 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-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-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-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.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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.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 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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-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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/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.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.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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-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 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-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 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.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 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.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 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= 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/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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 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.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= 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.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-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.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= 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 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= 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 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= 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.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA= modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA=
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= 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/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= 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.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/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 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= 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.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 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 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/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= 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.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
modernc.org/sqlite v1.18.0 h1:ef66qJSgKeyLyrF4kQ2RHw/Ue3V89fyFNbGL073aDjI= modernc.org/sqlite v1.18.0 h1:ef66qJSgKeyLyrF4kQ2RHw/Ue3V89fyFNbGL073aDjI=
modernc.org/sqlite v1.18.0/go.mod h1:B9fRWZacNxJBHoCJZQr1R54zhVn3fjfl0aszflrTSxY= 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.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= 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.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.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 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-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.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= 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"). From(s.tablePrefix + "boards AS B").
Join("ChannelMembers AS CM ON B.channel_id=CM.channelId"). Join("ChannelMembers AS CM ON B.channel_id=CM.channelId").
Join("Users as U on CM.userID = U.id"). 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.Eq{"B.id": boardID}).
Where(sq.NotEq{"B.channel_id": ""}). Where(sq.NotEq{"B.channel_id": ""}).
// Filter out guests as they don't have synthetic membership // 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() rows, err := query.Query()
if err != nil { if err != nil {

View File

@@ -119,6 +119,8 @@
"ColorOption.selectColor": "Select {color} Color", "ColorOption.selectColor": "Select {color} Color",
"Comment.delete": "Delete", "Comment.delete": "Delete",
"CommentsList.send": "Send", "CommentsList.send": "Send",
"ConfirmPerson.empty": "Empty",
"ConfirmPerson.search": "Search...",
"ConfirmationDialog.cancel-action": "Cancel", "ConfirmationDialog.cancel-action": "Cancel",
"ConfirmationDialog.confirm-action": "Confirm", "ConfirmationDialog.confirm-action": "Confirm",
"ContentBlock.Delete": "Delete", "ContentBlock.Delete": "Delete",
@@ -166,6 +168,7 @@
"FilterByText.placeholder": "filter text", "FilterByText.placeholder": "filter text",
"FilterComponent.add-filter": "+ Add filter", "FilterComponent.add-filter": "+ Add filter",
"FilterComponent.delete": "Delete", "FilterComponent.delete": "Delete",
"FilterValue.empty": "(empty)",
"FindBoardsDialog.IntroText": "Search for boards", "FindBoardsDialog.IntroText": "Search for boards",
"FindBoardsDialog.NoResultsFor": "No results for \"{searchQuery}\"", "FindBoardsDialog.NoResultsFor": "No results for \"{searchQuery}\"",
"FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.", "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.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", "OnboardingTour.ShareBoard.Title": "Share board",
"PersonProperty.board-members": "Board members", "PersonProperty.board-members": "Board members",
"PersonProperty.me": "Me",
"PersonProperty.non-board-members": "Not board members", "PersonProperty.non-board-members": "Not board members",
"PropertyMenu.Delete": "Delete", "PropertyMenu.Delete": "Delete",
"PropertyMenu.changeType": "Change property type", "PropertyMenu.changeType": "Change property type",
@@ -285,8 +289,8 @@
"TableHeaderMenu.insert-right": "Insert right", "TableHeaderMenu.insert-right": "Insert right",
"TableHeaderMenu.sort-ascending": "Sort ascending", "TableHeaderMenu.sort-ascending": "Sort ascending",
"TableHeaderMenu.sort-descending": "Sort descending", "TableHeaderMenu.sort-descending": "Sort descending",
"TableRow.DuplicateCard": "duplicate card",
"TableRow.MoreOption": "More actions", "TableRow.MoreOption": "More actions",
"TableRow.delete": "Delete",
"TableRow.open": "Open", "TableRow.open": "Open",
"TopBar.give-feedback": "Give feedback", "TopBar.give-feedback": "Give feedback",
"URLProperty.copiedLink": "Copied!", "URLProperty.copiedLink": "Copied!",
@@ -368,6 +372,8 @@
"calendar.month": "Month", "calendar.month": "Month",
"calendar.today": "TODAY", "calendar.today": "TODAY",
"calendar.week": "Week", "calendar.week": "Week",
"centerPanel.undefined": "No {propertyName}",
"centerPanel.unknown-user": "Unknown user",
"cloudMessage.learn-more": "Learn more", "cloudMessage.learn-more": "Learn more",
"createImageBlock.failed": "Unable to upload the file. File size limit reached.", "createImageBlock.failed": "Unable to upload the file. File size limit reached.",
"default-properties.badges": "Comments and description", "default-properties.badges": "Comments and description",

View File

@@ -1,11 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Utils} from './utils' import {Utils} from './utils'
import {Card} from './blocks/card' import {Card} from './blocks/card'
import {IPropertyTemplate, IPropertyOption, BoardGroup} from './blocks/board' 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 = [] const groups = []
for (const optionId of optionIds) { for (const optionId of optionIds) {
if (optionId) { if (optionId) {
@@ -36,7 +35,7 @@ export function groupCardsByOptions(cards: Card[], optionIds: string[], groupByP
return groups 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[] = [] let unassignedOptionIds: string[] = []
if (groupByProperty) { if (groupByProperty) {
unassignedOptionIds = groupByProperty.options. unassignedOptionIds = groupByProperty.options.
@@ -54,3 +53,37 @@ export function getVisibleAndHiddenGroups(cards: Card[], visibleOptionIds: strin
const hiddenGroups = groupCardsByOptions(cards, hiddenOptionIds, groupByProperty) const hiddenGroups = groupCardsByOptions(cards, hiddenOptionIds, groupByProperty)
return {visible: visibleGroups, hidden: hiddenGroups} 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', () => { describe('verify isClauseMet method - single date property', () => {
// Date Properties are stored as 12PM UTC. // Date Properties are stored as 12PM UTC.
const now = new Date(Date.now()) const now = new Date(Date.now())
@@ -429,6 +569,32 @@ describe('src/cardFilter', () => {
expect(result.value).toBeFalsy() 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', () => { describe('verify propertiesThatMeetFilterGroup method', () => {
test('should return {} with undefined filterGroup', () => { test('should return {} with undefined filterGroup', () => {
const result = CardFilter.propertiesThatMeetFilterGroup(undefined, []) const result = CardFilter.propertiesThatMeetFilterGroup(undefined, [])

View File

@@ -76,9 +76,12 @@ class CardFilter {
if (template?.type === 'date') { if (template?.type === 'date') {
dateValue = this.createDatePropertyFromString(value as string) dateValue = this.createDatePropertyFromString(value as string)
} }
if (!value) { if (!value && template) {
// const template = templates.find((o) => o.id === filter.propertyId) if (template.type === 'createdBy') {
if (template && template.type === 'createdTime') { value = card.createdBy
} else if (template.type === 'updatedBy') {
value = card.modifiedBy
} else if (template && template.type === 'createdTime') {
value = card.createAt.toString() value = card.createAt.toString()
dateValue = this.createDatePropertyFromString(value as string) dateValue = this.createDatePropertyFromString(value as string)
} else if (template && template.type === 'updatedTime') { } else if (template && template.type === 'updatedTime') {
@@ -255,6 +258,10 @@ class CardFilter {
return {id: filterClause.propertyId} return {id: filterClause.propertyId}
} }
if (template.type === 'createdBy' || template.type === 'updatedBy') {
return {id: filterClause.propertyId}
}
switch (filterClause.condition) { switch (filterClause.condition) {
case 'includes': { case 'includes': {
if (filterClause.values.length < 1) { if (filterClause.values.length < 1) {
@@ -281,7 +288,7 @@ class CardFilter {
return {id: filterClause.propertyId} return {id: filterClause.propertyId}
} }
default: { default: {
Utils.assertFailure(`Unexpected filter condition: ${filterClause.condition}`) // Handle filter clause that cannot be set
return {id: filterClause.propertyId} return {id: filterClause.propertyId}
} }
} }

View File

@@ -3,7 +3,7 @@
exports[`components/cardDialog already following card 1`] = ` exports[`components/cardDialog already following card 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -215,7 +215,7 @@ exports[`components/cardDialog already following card 1`] = `
exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] = ` exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`components/cardDialog return a cardDialog readonly 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -578,7 +578,7 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
exports[`components/cardDialog return cardDialog menu content 1`] = ` exports[`components/cardDialog return cardDialog menu content 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`components/cardDialog return cardDialog menu content and cancel delete confirmation do nothing 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -1139,7 +1139,7 @@ exports[`components/cardDialog return cardDialog menu content and cancel delete
exports[`components/cardDialog should match snapshot 1`] = ` exports[`components/cardDialog should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -1351,7 +1351,7 @@ exports[`components/cardDialog should match snapshot 1`] = `
exports[`components/cardDialog should match snapshot without permissions 1`] = ` exports[`components/cardDialog should match snapshot without permissions 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back cardDialog" class="Dialog dialog-back cardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -3,7 +3,7 @@
exports[`/components/confirmAddUserForNotifications should match snapshot 1`] = ` exports[`/components/confirmAddUserForNotifications should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back confirmation-dialog-box" class="Dialog dialog-back confirmation-dialog-box size--small"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -3,7 +3,7 @@
exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1`] = ` exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back confirmation-dialog-box" class="Dialog dialog-back confirmation-dialog-box size--small"
> >
<div <div
class="backdrop" 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`] = ` exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Text should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back confirmation-dialog-box" class="Dialog dialog-back confirmation-dialog-box size--small"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -3,7 +3,7 @@
exports[`components/dialog should match snapshot 1`] = ` exports[`components/dialog should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back undefined" class="Dialog dialog-back undefined size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`components/dialog should return dialog and click on cancel button 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back undefined" class="Dialog dialog-back undefined size--medium"
> >
<div <div
class="backdrop" 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> <div>
<div <div
class="Dialog dialog-back DeleteBoardDialog" class="Dialog dialog-back DeleteBoardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -3,7 +3,7 @@
exports[`component/BoardSwitcherDialog base case 1`] = ` exports[`component/BoardSwitcherDialog base case 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back BoardSwitcherDialog" class="Dialog dialog-back BoardSwitcherDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -1,10 +1,25 @@
.Dialog { .cardDialog {
&.cardDialog { .dialog {
.dialog { width: 100%;
top: 0;
height: 100%;
>.CardDetail {
display: flex;
flex-direction: column;
align-items: flex-start;
@media not screen and (max-width: 975px) { @media not screen and (max-width: 975px) {
width: 800px; padding: 10px 126px;
max-width: 100%;
} }
@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 {Block} from '../blocks/block'
import {BlockIcons} from '../blockIcons' import {BlockIcons} from '../blockIcons'
import {Card, createCard} from '../blocks/card' 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 {BoardView} from '../blocks/boardView'
import {CardFilter} from '../cardFilter' import {CardFilter} from '../cardFilter'
import mutator from '../mutator' import mutator from '../mutator'
@@ -22,12 +22,15 @@ import {updateView} from '../store/views'
import {getVisibleAndHiddenGroups} from '../boardUtils' import {getVisibleAndHiddenGroups} from '../boardUtils'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient' import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
import {getClientConfig} from '../store/clientConfig'
import './centerPanel.scss' import './centerPanel.scss'
import {useAppSelector, useAppDispatch} from '../store/hooks' import {useAppSelector, useAppDispatch} from '../store/hooks'
import { import {
getMe, getMe,
getBoardUsers,
getOnboardingTourCategory, getOnboardingTourCategory,
getOnboardingTourStarted, getOnboardingTourStarted,
getOnboardingTourStep, getOnboardingTourStep,
@@ -84,8 +87,11 @@ const CenterPanel = (props: Props) => {
const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp) const cardLimitTimestamp = useAppSelector(getCardLimitTimestamp)
const me = useAppSelector(getMe) const me = useAppSelector(getMe)
const currentCard = useAppSelector(getCurrentCard) const currentCard = useAppSelector(getCurrentCard)
const boardUsers = useAppSelector(getBoardUsers)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
// empty dependency array yields behavior like `componentDidMount`, it only runs _once_ // empty dependency array yields behavior like `componentDidMount`, it only runs _once_
// https://stackoverflow.com/a/58579462 // https://stackoverflow.com/a/58579462
useEffect(() => { useEffect(() => {
@@ -361,10 +367,35 @@ const CenterPanel = (props: Props) => {
const showShareLoginButton = props.readonly && me?.id !== 'single-user' const showShareLoginButton = props.readonly && me?.id !== 'single-user'
const {groupByProperty, activeView, board, views, cards} = props const {groupByProperty, activeView, board, views, cards} = props
const {visible: visibleGroups, hidden: hiddenGroups} = useMemo(
() => getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty), const getUserDisplayName = (boardGroup: BoardGroup) => {
[cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty], 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 ( return (
<div <div
className='BoardComponent' className='BoardComponent'

View File

@@ -3,30 +3,11 @@
.confirmation-dialog-box { .confirmation-dialog-box {
.dialog { .dialog {
@include z-index(confirmation-dialog-box); @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 { > .toolbar {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
padding: 16px;
} }
} }
} }

View File

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

View File

@@ -3,7 +3,7 @@
exports[`components/createCategory/CreateCategory base case should match snapshot 1`] = ` exports[`components/createCategory/CreateCategory base case should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back CreateCategoryModal" class="Dialog dialog-back CreateCategoryModal size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -6,62 +6,71 @@
width: 600px; width: 600px;
height: auto; 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; display: flex;
flex-direction: column; flex-direction: row;
padding: 0 32px 24px; justify-content: flex-end;
gap: 24px; }
.inputWrapper { .createCategoryActions {
position: relative; display: flex;
flex-direction: row;
.CloseCircle { justify-content: flex-end;
cursor: pointer; margin-top: auto;
position: absolute; gap: 12px;
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;
}
} }
} }
} }

View File

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

View File

@@ -10,6 +10,14 @@
bottom: 0; bottom: 0;
} }
&.size--small {
.dialog {
max-width: 512px;
width: 100%;
height: max-content;
}
}
.dialog-title { .dialog-title {
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
@@ -22,10 +30,6 @@
margin-top: 8px; margin-top: 8px;
} }
.dialog__close {
margin: 0 -14px 0 0;
}
.backdrop { .backdrop {
@include z-index(dialog-backdrop); @include z-index(dialog-backdrop);
position: fixed; 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; flex-shrink: 0;
} }
@@ -106,7 +102,7 @@
.toolbar { .toolbar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 24px 32px; padding: 24px 0;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
} }
@@ -116,24 +112,7 @@
gap: 8px; gap: 8px;
align-items: center; align-items: center;
height: 28px; height: 28px;
} margin-right: 16px;
> .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;
} }
} }

View File

@@ -12,6 +12,7 @@ import './dialog.scss'
type Props = { type Props = {
children: React.ReactNode children: React.ReactNode
size?: string
toolsMenu?: React.ReactNode // some dialogs may not require a toolmenu toolsMenu?: React.ReactNode // some dialogs may not require a toolmenu
toolbar?: React.ReactNode toolbar?: React.ReactNode
hideCloseButton?: boolean hideCloseButton?: boolean
@@ -22,7 +23,7 @@ type Props = {
} }
const Dialog = (props: Props) => { const Dialog = (props: Props) => {
const {toolsMenu, toolbar, title, subtitle} = props const {toolsMenu, toolbar, title, subtitle, size} = props
const intl = useIntl() const intl = useIntl()
const closeDialogText = intl.formatMessage({ const closeDialogText = intl.formatMessage({
@@ -35,7 +36,7 @@ const Dialog = (props: Props) => {
const isBackdropClickedRef = useRef(false) const isBackdropClickedRef = useRef(false)
return ( return (
<div className={`Dialog dialog-back ${props.className}`}> <div className={`Dialog dialog-back ${props.className} size--${size || 'medium'}`}>
<div className='backdrop'/> <div className='backdrop'/>
<div <div
className='wrapper' className='wrapper'

View File

@@ -905,7 +905,7 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
</div> </div>
</div> </div>
<div <div
class="Dialog dialog-back confirmation-dialog-box" class="Dialog dialog-back confirmation-dialog-box size--small"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -49,6 +49,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
const {board, activeView, intl, group, groupByProperty} = props const {board, activeView, intl, group, groupByProperty} = props
const [groupTitle, setGroupTitle] = useState(group.option.value) const [groupTitle, setGroupTitle] = useState(group.option.value)
const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties]) const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties])
const canEditOption = groupByProperty?.type !== 'person' && group.option.id
const headerRef = useRef<HTMLDivElement>(null) const headerRef = useRef<HTMLDivElement>(null)
@@ -108,7 +109,11 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
}} }}
/> />
</Label>} </Label>}
{group.option.id && {groupByProperty?.type === 'person' &&
<Label>
{groupTitle}
</Label>}
{canEditOption &&
<Label color={group.option.color}> <Label color={group.option.color}>
<Editable <Editable
value={groupTitle} value={groupTitle}
@@ -165,7 +170,7 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')} onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')}
/> />
{group.option.id && {canEditOption &&
<> <>
<Menu.Text <Menu.Text
id='delete' id='delete'

View File

@@ -1,8 +1,9 @@
.Person { .Person {
padding: 4px 8px;
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
flex-wrap: wrap;
gap: 8px;
&.readonly { &.readonly {
overflow: hidden; overflow: hidden;
@@ -13,7 +14,6 @@
.Person-item { .Person-item {
display: flex; display: flex;
align-items: center; align-items: center;
color: rgba(var(--center-channel-color-rgb), 1);
img { img {
border-radius: 50px; border-radius: 50px;
@@ -23,10 +23,6 @@
} }
} }
.react-select__input {
margin-left: -5px !important;
}
.react-select__menu { .react-select__menu {
background: rgba(var(--center-channel-bg-rgb), 1); background: rgba(var(--center-channel-bg-rgb), 1);
box-shadow: var(--elevation-4); box-shadow: var(--elevation-4);
@@ -41,11 +37,52 @@
max-width: 100%; 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 { .react-select__option {
display: flex; display: flex;
align-items: center; align-items: center;
height: 40px; height: 40px;
padding: 0 40px 0 10px; padding: 0 40px 0 20px;
&:hover { &:hover {
background: rgba(var(--center-channel-color-rgb), 0.08); background: rgba(var(--center-channel-color-rgb), 0.08);
@@ -71,5 +108,3 @@
border: 0; 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`] = ` exports[`src/components/shareBoard/shareBoard confirm unlinking linked channel 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy link 2`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Regenerate token 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Select 2`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard and click Select, non-plugin mode 2`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard template and click Select 2`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoard, and click switch 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard return shareBoardComponent and click Switch without sharing 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"
@@ -3281,7 +3281,7 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli
exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = ` exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard should match snapshot with sharing 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and subpath 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard should match snapshot with sharing and without workspaceId and subpath 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`src/components/shareBoard/shareBoard should match snapshot, with template 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ShareBoardDialog" class="Dialog dialog-back ShareBoardDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -54,6 +54,8 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
className += ' expanded' className += ' expanded'
} }
const canEditOption = groupByProperty?.type !== 'person' && group.option.id
return ( return (
<div <div
key={group.option.id + 'header'} key={group.option.id + 'header'}
@@ -90,7 +92,11 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
}} }}
/> />
</Label>} </Label>}
{group.option.id && {groupByProperty?.type === 'person' &&
<Label>
{groupTitle}
</Label>}
{canEditOption &&
<Label color={group.option.color}> <Label color={group.option.color}>
<Editable <Editable
value={groupTitle} value={groupTitle}
@@ -122,7 +128,7 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})} name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')} onClick={() => mutator.hideViewColumn(board.id, activeView, group.option.id || '')}
/> />
{group.option.id && {canEditOption &&
<> <>
<Menu.Text <Menu.Text
id='delete' id='delete'

View File

@@ -95,7 +95,15 @@ const TableRow = (props: Props) => {
} }
if (isGrouped) { if (isGrouped) {
const groupID = groupById || '' 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) { if (collapsedOptionIds.indexOf(groupValue) > -1) {
className += ' hidden' className += ' hidden'
} }

View File

@@ -711,6 +711,8 @@ exports[`components/viewHeader/filterComponent return filterComponent and click
<div /> <div />
<div /> <div />
<div /> <div />
<div />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@@ -453,6 +455,474 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on delet
</div> </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`] = ` exports[`components/viewHeader/filterEntry return filterEntry and click on doesn't include 1`] = `
<div> <div>
<div <div
@@ -600,6 +1070,8 @@ exports[`components/viewHeader/filterEntry return filterEntry and click on doesn
<div /> <div />
<div /> <div />
<div /> <div />
<div />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@@ -1617,6 +2095,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for boolean field
<div <div
class="menu-options" class="menu-options"
> >
<div />
<div />
<div /> <div />
<div> <div>
<div <div
@@ -1825,6 +2305,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for date field 2`]
<div <div
class="menu-options" class="menu-options"
> >
<div />
<div />
<div /> <div />
<div /> <div />
<div /> <div />
@@ -2115,6 +2597,8 @@ exports[`components/viewHeader/filterEntry return filterEntry for text field 2`]
<div <div
class="menu-options" class="menu-options"
> >
<div />
<div />
<div /> <div />
<div /> <div />
<div> <div>

View File

@@ -270,4 +270,57 @@ describe('components/viewHeader/filterEntry', () => {
userEvent.click(allButton[allButton.length - 1]) userEvent.click(allButton[allButton.length - 1])
expect(mockedMutator.changeViewFilter).toBeCalledTimes(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}`) Utils.assert(newFilter, `No filter at index ${filterIndex}`)
if (newFilter.propertyId !== optionId) { if (newFilter.propertyId !== optionId) {
newFilter.propertyId = optionId newFilter.propertyId = optionId
newFilter.condition = OctoUtils.filterConditionValidOrDefault(propsRegistry.get(o.type).filterValueType, newFilter.condition)
newFilter.values = [] newFilter.values = []
mutator.changeViewFilter(props.board.id, view.id, view.fields.filter, filterGroup) 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)} 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' && {propertyType.filterValueType === 'boolean' &&
<> <>
<Menu.Text <Menu.Text

View File

@@ -19,6 +19,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
import DateFilter from './dateFilter' import DateFilter from './dateFilter'
import './filterValue.scss' import './filterValue.scss'
import MultiPersonFilterValue from './multipersonFilterValue'
type Props = { type Props = {
view: BoardView view: BoardView
@@ -40,7 +41,7 @@ const filterValue = (props: Props): JSX.Element|null => {
return 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 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 (propertyType.filterValueType === 'date') {
if (filter.condition === 'isSet' || filter.condition === 'isNotSet') { if (filter.condition === 'isSet' || filter.condition === 'isNotSet') {
return null return null
@@ -85,12 +94,13 @@ const filterValue = (props: Props): JSX.Element|null => {
return option?.value || '(Unknown)' return option?.value || '(Unknown)'
}).join(', ') }).join(', ')
} else { } else {
displayValue = '(empty)' displayValue = intl.formatMessage({id: 'FilterValue.empty', defaultMessage: '(empty)'})
} }
return ( return (
<MenuWrapper className='filterValue'> <MenuWrapper className='filterValue'>
<Button>{displayValue}</Button> <Button>{displayValue}</Button>
<Menu> <Menu>
{template?.options.map((o) => ( {template?.options.map((o) => (
<Menu.Switch <Menu.Switch

View File

@@ -3,7 +3,6 @@
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
flex-wrap: wrap; flex-wrap: wrap;
padding: 8px 0 0;
gap: 8px; gap: 8px;
&.readonly { &.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`] = ` exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button for non sys admin user 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ViewLimitDialog" class="Dialog dialog-back ViewLimitDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`components/viewLimitDialog/ViewLiimitDialog show upgrade button for sys admin user 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ViewLimitDialog" class="Dialog dialog-back ViewLimitDialog size--medium"
> >
<div <div
class="backdrop" class="backdrop"

View File

@@ -3,7 +3,7 @@
exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmation msg 1`] = ` exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmation msg 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ViewLimitDialog" class="Dialog dialog-back ViewLimitDialog size--medium"
> >
<div <div
class="backdrop" 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`] = ` exports[`components/viewLimitDialog/ViewL]imitDialog show view limit dialog 1`] = `
<div> <div>
<div <div
class="Dialog dialog-back ViewLimitDialog" class="Dialog dialog-back ViewLimitDialog size--medium"
> >
<div <div
class="backdrop" 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] { function createCardTree(): [Block[], Block] {
const blocks: Block[] = [] const blocks: Block[] = []

View File

@@ -3,6 +3,7 @@
import {IntlShape} from 'react-intl' import {IntlShape} from 'react-intl'
import {FilterValueType} from './properties/types'
import {Block, createBlock} from './blocks/block' import {Block, createBlock} from './blocks/block'
import {BoardView, createBoardView} from './blocks/boardView' import {BoardView, createBoardView} from './blocks/boardView'
import {Card, createCard} from './blocks/card' import {Card, createCard} from './blocks/card'
@@ -105,7 +106,7 @@ class OctoUtils {
} }
static filterConditionDisplayString(filterCondition: FilterCondition, intl: IntlShape, filterValueType: string): string { static filterConditionDisplayString(filterCondition: FilterCondition, intl: IntlShape, filterValueType: string): string {
if (filterValueType === 'options') { if (filterValueType === 'options' || filterValueType === 'person') {
switch (filterCondition) { switch (filterCondition) {
case 'includes': return intl.formatMessage({id: 'Filter.includes', defaultMessage: 'includes'}) case 'includes': return intl.formatMessage({id: 'Filter.includes', defaultMessage: 'includes'})
case 'notIncludes': return intl.formatMessage({id: 'Filter.not-includes', defaultMessage: 'doesn\'t include'}) case 'notIncludes': return intl.formatMessage({id: 'Filter.not-includes', defaultMessage: 'doesn\'t include'})
@@ -152,6 +153,57 @@ class OctoUtils {
return '(unknown)' 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} export {OctoUtils}

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {IntlShape} from 'react-intl' import {IntlShape} from 'react-intl'
import {PropertyType, PropertyTypeEnum} from '../types' import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
import CreatedBy from './createdBy' import CreatedBy from './createdBy'
@@ -12,4 +12,7 @@ export default class CreatedByProperty extends PropertyType {
type = 'createdBy' as PropertyTypeEnum type = 'createdBy' as PropertyTypeEnum
isReadOnly = true isReadOnly = true
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.CreatedBy', defaultMessage: 'Created by'}) 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']} propertyValue={['user-id-4']}
readOnly={false} readOnly={false}
showEmptyPlaceholder={false} showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate} propertyTemplate={{
id: 'personPropertyID',
name: 'My Person Property',
type: 'multiPerson',
options: [],
} as IPropertyTemplate}
board={{} as Board} board={{} as Board}
card={{} as Card} card={{} as Card}
/> />
@@ -97,7 +102,12 @@ describe('properties/multiperson', () => {
propertyValue={['user-id-1', 'user-id-2']} propertyValue={['user-id-1', 'user-id-2']}
readOnly={false} readOnly={false}
showEmptyPlaceholder={false} showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate} propertyTemplate={{
id: 'personPropertyID',
name: 'My Person Property',
type: 'multiPerson',
options: [],
} as IPropertyTemplate}
board={{} as Board} board={{} as Board}
card={{} as Card} card={{} as Card}
/> />
@@ -123,7 +133,12 @@ describe('properties/multiperson', () => {
propertyValue={['user-id-1', 'user-id-2']} propertyValue={['user-id-1', 'user-id-2']}
readOnly={true} readOnly={true}
showEmptyPlaceholder={false} showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate} propertyTemplate={{
id: 'personPropertyID',
name: 'My Person Property',
type: 'multiPerson',
options: [],
} as IPropertyTemplate}
board={{} as Board} board={{} as Board}
card={{} as Card} card={{} as Card}
/> />
@@ -149,7 +164,12 @@ describe('properties/multiperson', () => {
propertyValue={['user-id-1', 'user-id-2']} propertyValue={['user-id-1', 'user-id-2']}
readOnly={false} readOnly={false}
showEmptyPlaceholder={false} showEmptyPlaceholder={false}
propertyTemplate={{} as IPropertyTemplate} propertyTemplate={{
id: 'personPropertyID',
name: 'My Person Property',
type: 'multiPerson',
options: [],
} as IPropertyTemplate}
board={{} as Board} board={{} as Board}
card={{} as Card} card={{} as Card}
/> />

View File

@@ -1,213 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react' import React 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 {PropertyProps} from '../types' import {PropertyProps} from '../types'
import {ClientConfig} from '../../config/clientConfig' import ConfirmPerson from '../person/confirmPerson'
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',
}),
}
const MultiPerson = (props: PropertyProps): JSX.Element => { 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 ( return (
<> <ConfirmPerson
{confirmAddUser && {...props}
<ConfirmAddUserForNotifications showEmptyPlaceholder={true}
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) || [])
}
}}
/>
</>
) )
} }

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {IntlShape} from 'react-intl' import {IntlShape} from 'react-intl'
import {PropertyType, PropertyTypeEnum} from '../types' import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
import MultiPerson from './multiperson' import MultiPerson from './multiperson'
@@ -11,4 +11,6 @@ export default class MultiPersonProperty extends PropertyType {
name = 'MultiPerson' name = 'MultiPerson'
type = 'multiPerson' as PropertyTypeEnum type = 'multiPerson' as PropertyTypeEnum
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.MultiPerson', defaultMessage: 'Multi person'}) 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. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react' import React 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 {PropertyProps} from '../types' import {PropertyProps} from '../types'
import './person.scss' import ConfirmPerson from './confirmPerson'
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',
}),
}
const Person = (props: PropertyProps): JSX.Element => { 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 ( return (
<> <ConfirmPerson
{confirmAddUser && {...props}
<ConfirmAddUserForNotifications showEmptyPlaceholder={true}
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('')
}
}}
/>
</>
) )
} }

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {IntlShape} from 'react-intl' import {IntlShape} from 'react-intl'
import {PropertyType, PropertyTypeEnum} from '../types' import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
import Person from './person' import Person from './person'
@@ -11,4 +11,7 @@ export default class PersonProperty extends PropertyType {
name = 'Person' name = 'Person'
type = 'person' as PropertyTypeEnum type = 'person' as PropertyTypeEnum
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.Person', defaultMessage: 'Person'}) 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 PropertyTypeEnum = BoardPropertyTypeEnum
export type FilterValueType = 'none'|'options'|'boolean'|'text'|'date' export type FilterValueType = 'none'|'options'|'boolean'|'text'|'date'|'person'
export type FilterCondition = { export type FilterCondition = {
id: string id: string

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {IntlShape} from 'react-intl' import {IntlShape} from 'react-intl'
import {PropertyType, PropertyTypeEnum} from '../types' import {PropertyType, PropertyTypeEnum, FilterValueType} from '../types'
import UpdatedBy from './updatedBy' import UpdatedBy from './updatedBy'
@@ -12,4 +12,7 @@ export default class UpdatedByProperty extends PropertyType {
type = 'updatedBy' as PropertyTypeEnum type = 'updatedBy' as PropertyTypeEnum
isReadOnly = true isReadOnly = true
displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.UpdatedBy', defaultMessage: 'Last updated by'}) displayName = (intl: IntlShape) => intl.formatMessage({id: 'PropertyType.UpdatedBy', defaultMessage: 'Last updated by'})
canFilter = true
filterValueType = 'person' as FilterValueType
canGroup = true
} }