[GH-26715] Added Pagination Support for IncomingWebHooks (#27502)
* Added Pagination Support for IncomingWebHooks * Incorporated feedback from reviews * Removed trailing spaces * Restored deleted server en.json entries, fixed order in webapp en.json --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -83,6 +83,13 @@
|
|||||||
description: The ID of the team to get hooks for.
|
description: The ID of the team to get hooks for.
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- name: include_total_count
|
||||||
|
description: >-
|
||||||
|
Appends a total count of returned hooks inside the response object - ex: `{ "incoming_webhooks": [], "total_count": 0 }`.
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Incoming webhooks retrieval successful
|
description: Incoming webhooks retrieval successful
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
hooks []*model.IncomingWebhook
|
hooks []*model.IncomingWebhook
|
||||||
appErr *model.AppError
|
appErr *model.AppError
|
||||||
|
js []byte
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if teamID != "" {
|
if teamID != "" {
|
||||||
@@ -217,7 +219,20 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
js, err := json.Marshal(hooks)
|
if c.Params.IncludeTotalCount {
|
||||||
|
totalCount, appErr := c.App.GetIncomingWebhooksCount(teamID, userID)
|
||||||
|
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hooksWithCount := model.IncomingWebhooksWithCount{Webhooks: hooks, TotalCount: totalCount}
|
||||||
|
js, err = json.Marshal(hooksWithCount)
|
||||||
|
} else {
|
||||||
|
js, err = json.Marshal(hooks)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = model.NewAppError("getIncomingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
c.Err = model.NewAppError("getIncomingHooks", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -304,6 +304,60 @@ func TestGetIncomingWebhooksByTeam(t *testing.T) {
|
|||||||
assert.Equal(t, basicHook.Id, filteredHooks[0].Id)
|
assert.Equal(t, basicHook.Id, filteredHooks[0].Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetIncomingWebhooksWithCount(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
BasicClient := th.Client
|
||||||
|
th.LoginBasic()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
||||||
|
|
||||||
|
defaultRolePermissions := th.SaveDefaultRolePermissions()
|
||||||
|
defer func() {
|
||||||
|
th.RestoreDefaultRolePermissions(defaultRolePermissions)
|
||||||
|
}()
|
||||||
|
th.AddPermissionToRole(model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId)
|
||||||
|
th.AddPermissionToRole(model.PermissionManageIncomingWebhooks.Id, model.SystemUserRoleId)
|
||||||
|
|
||||||
|
// Basic user webhook
|
||||||
|
bHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id}
|
||||||
|
basicHook, _, err := BasicClient.CreateIncomingWebhook(context.Background(), bHook)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
basicHooksWithCount, _, err := BasicClient.GetIncomingWebhooksWithCount(context.Background(), 0, 1000, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(basicHooksWithCount.Webhooks))
|
||||||
|
assert.Equal(t, int64(1), basicHooksWithCount.TotalCount)
|
||||||
|
assert.Equal(t, basicHook.Id, basicHooksWithCount.Webhooks[0].Id)
|
||||||
|
|
||||||
|
// Admin User webhook
|
||||||
|
aHook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, TeamId: th.BasicTeam.Id, UserId: th.SystemAdminUser.Id}
|
||||||
|
adminHook, _, err := th.SystemAdminClient.CreateIncomingWebhook(context.Background(), aHook)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||||
|
adminHooksWithCount, _, err2 := client.GetIncomingWebhooksWithCount(context.Background(), 0, 1000, "")
|
||||||
|
require.NoError(t, err2)
|
||||||
|
assert.Equal(t, 2, len(adminHooksWithCount.Webhooks))
|
||||||
|
assert.Equal(t, int64(2), adminHooksWithCount.TotalCount)
|
||||||
|
|
||||||
|
foundBasicHook := false
|
||||||
|
foundAdminHook := false
|
||||||
|
|
||||||
|
for _, h := range adminHooksWithCount.Webhooks {
|
||||||
|
if basicHook.Id == h.Id {
|
||||||
|
foundBasicHook = true
|
||||||
|
}
|
||||||
|
if adminHook.Id == h.Id {
|
||||||
|
foundAdminHook = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.True(t, foundBasicHook, "missing basic user hook")
|
||||||
|
require.True(t, foundAdminHook, "missing admin user hook")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetIncomingWebhook(t *testing.T) {
|
func TestGetIncomingWebhook(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ func (a *App) getAnalytics(rctx request.CTX, name string, teamID string, forSupp
|
|||||||
var incomingWebhookCount int64
|
var incomingWebhookCount int64
|
||||||
g2.Go(func() error {
|
g2.Go(func() error {
|
||||||
var err error
|
var err error
|
||||||
if incomingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID); err != nil {
|
if incomingWebhookCount, err = a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID, ""); err != nil {
|
||||||
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_incoming_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return model.NewAppError("GetAnalytics", "app.webhooks.analytics_incoming_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -717,6 +717,7 @@ type AppIface interface {
|
|||||||
GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError)
|
GetGroupsByUserId(userID string) ([]*model.Group, *model.AppError)
|
||||||
GetHubForUserId(userID string) *platform.Hub
|
GetHubForUserId(userID string) *platform.Hub
|
||||||
GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError)
|
GetIncomingWebhook(hookID string) (*model.IncomingWebhook, *model.AppError)
|
||||||
|
GetIncomingWebhooksCount(teamID string, userID string) (int64, *model.AppError)
|
||||||
GetIncomingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
GetIncomingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
||||||
GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
GetIncomingWebhooksForTeamPageByUser(teamID string, userID string, page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
||||||
GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebhook, *model.AppError)
|
||||||
|
|||||||
@@ -7220,6 +7220,28 @@ func (a *OpenTracingAppLayer) GetIncomingWebhook(hookID string) (*model.Incoming
|
|||||||
return resultVar0, resultVar1
|
return resultVar0, resultVar1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) GetIncomingWebhooksCount(teamID string, userID string) (int64, *model.AppError) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksCount")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.GetIncomingWebhooksCount(teamID, userID)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
func (a *OpenTracingAppLayer) GetIncomingWebhooksForTeamPage(teamID string, page int, perPage int) ([]*model.IncomingWebhook, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPage")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIncomingWebhooksForTeamPage")
|
||||||
|
|||||||
@@ -507,6 +507,19 @@ func (a *App) GetIncomingWebhooksPage(page, perPage int) ([]*model.IncomingWebho
|
|||||||
return a.GetIncomingWebhooksPageByUser("", page, perPage)
|
return a.GetIncomingWebhooksPageByUser("", page, perPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetIncomingWebhooksCount(teamID string, userID string) (int64, *model.AppError) {
|
||||||
|
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
|
||||||
|
return 0, model.NewAppError("GetIncomingWebhooksCount", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount(teamID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, model.NewAppError("GetIncomingWebhooksCount", "app.webhooks.get_incoming_count.app_error", map[string]any{"TeamID": teamID, "UserID": userID, "Error": err.Error()}, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
|
func (a *App) CreateOutgoingWebhook(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError) {
|
||||||
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
if !*a.Config().ServiceSettings.EnableOutgoingWebhooks {
|
||||||
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
return nil, model.NewAppError("CreateOutgoingWebhook", "api.outgoing_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
|||||||
@@ -12908,7 +12908,7 @@ func (s *OpenTracingLayerUserTermsOfServiceStore) Save(userTermsOfService *model
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
|
func (s *OpenTracingLayerWebhookStore) AnalyticsIncomingCount(teamID string, userID string) (int64, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsIncomingCount")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "WebhookStore.AnalyticsIncomingCount")
|
||||||
s.Root.Store.SetContext(newCtx)
|
s.Root.Store.SetContext(newCtx)
|
||||||
@@ -12917,7 +12917,7 @@ func (s *OpenTracingLayerWebhookStore) AnalyticsIncomingCount(teamID string) (in
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
|
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.LogFields(spanlog.Error(err))
|
span.LogFields(spanlog.Error(err))
|
||||||
ext.Error.Set(span, true)
|
ext.Error.Set(span, true)
|
||||||
|
|||||||
@@ -14744,11 +14744,11 @@ func (s *RetryLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserT
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RetryLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
|
func (s *RetryLayerWebhookStore) AnalyticsIncomingCount(teamID string, userID string) (int64, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
for {
|
for {
|
||||||
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
|
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID, userID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,15 +352,19 @@ func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) (*model.Out
|
|||||||
return hook, nil
|
return hook, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) (int64, error) {
|
func (s SqlWebhookStore) AnalyticsIncomingCount(teamID string, userID string) (int64, error) {
|
||||||
queryBuilder :=
|
queryBuilder :=
|
||||||
s.getQueryBuilder().
|
s.getQueryBuilder().
|
||||||
Select("COUNT(*)").
|
Select("COUNT(*)").
|
||||||
From("IncomingWebhooks").
|
From("IncomingWebhooks").
|
||||||
Where("DeleteAt = 0")
|
Where("DeleteAt = 0")
|
||||||
|
|
||||||
if teamId != "" {
|
if teamID != "" {
|
||||||
queryBuilder = queryBuilder.Where("TeamId", teamId)
|
queryBuilder = queryBuilder.Where(sq.Eq{"TeamId": teamID})
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID != "" {
|
||||||
|
queryBuilder = queryBuilder.Where(sq.Eq{"UserId": userID})
|
||||||
}
|
}
|
||||||
|
|
||||||
queryString, args, err := queryBuilder.ToSql()
|
queryString, args, err := queryBuilder.ToSql()
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ type WebhookStore interface {
|
|||||||
PermanentDeleteOutgoingByUser(userID string) error
|
PermanentDeleteOutgoingByUser(userID string) error
|
||||||
UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
|
UpdateOutgoing(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, error)
|
||||||
|
|
||||||
AnalyticsIncomingCount(teamID string) (int64, error)
|
AnalyticsIncomingCount(teamID string, userID string) (int64, error)
|
||||||
AnalyticsOutgoingCount(teamID string) (int64, error)
|
AnalyticsOutgoingCount(teamID string) (int64, error)
|
||||||
InvalidateWebhookCache(webhook string)
|
InvalidateWebhookCache(webhook string)
|
||||||
ClearCaches()
|
ClearCaches()
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ type WebhookStore struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnalyticsIncomingCount provides a mock function with given fields: teamID
|
// AnalyticsIncomingCount provides a mock function with given fields: teamID, userID
|
||||||
func (_m *WebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
|
func (_m *WebhookStore) AnalyticsIncomingCount(teamID string, userID string) (int64, error) {
|
||||||
ret := _m.Called(teamID)
|
ret := _m.Called(teamID, userID)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for AnalyticsIncomingCount")
|
panic("no return value specified for AnalyticsIncomingCount")
|
||||||
@@ -24,17 +24,17 @@ func (_m *WebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
|
|||||||
|
|
||||||
var r0 int64
|
var r0 int64
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func(string) (int64, error)); ok {
|
if rf, ok := ret.Get(0).(func(string, string) (int64, error)); ok {
|
||||||
return rf(teamID)
|
return rf(teamID, userID)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func(string) int64); ok {
|
if rf, ok := ret.Get(0).(func(string, string) int64); ok {
|
||||||
r0 = rf(teamID)
|
r0 = rf(teamID, userID)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(int64)
|
r0 = ret.Get(0).(int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||||
r1 = rf(teamID)
|
r1 = rf(teamID, userID)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -577,10 +577,28 @@ func testWebhookStoreCountIncoming(t *testing.T, rctx request.CTX, ss store.Stor
|
|||||||
|
|
||||||
_, _ = ss.Webhook().SaveIncoming(o1)
|
_, _ = ss.Webhook().SaveIncoming(o1)
|
||||||
|
|
||||||
c, err := ss.Webhook().AnalyticsIncomingCount("")
|
c, err := ss.Webhook().AnalyticsIncomingCount("", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NotEqual(t, 0, c, "should have at least 1 incoming hook")
|
require.NotEqual(t, 0, c, "should have at least 1 incoming hook")
|
||||||
|
|
||||||
|
o2 := &model.IncomingWebhook{}
|
||||||
|
o2.ChannelId = model.NewId()
|
||||||
|
o2.UserId = model.NewId()
|
||||||
|
o2.TeamId = model.NewId()
|
||||||
|
|
||||||
|
_, _ = ss.Webhook().SaveIncoming(o2)
|
||||||
|
|
||||||
|
c, err = ss.Webhook().AnalyticsIncomingCount("", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, 1, c, "should have at least 2 incoming hooks")
|
||||||
|
|
||||||
|
c, err = ss.Webhook().AnalyticsIncomingCount(o1.TeamId, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, 0, c, "should have at least 1 incoming hook when filtering by TeamID")
|
||||||
|
|
||||||
|
c, err = ss.Webhook().AnalyticsIncomingCount("", o2.UserId)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, 0, c, "should have at least 1 incoming hook when filtering by UserID")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWebhookStoreCountOutgoing(t *testing.T, rctx request.CTX, ss store.Store) {
|
func testWebhookStoreCountOutgoing(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||||
|
|||||||
@@ -11618,10 +11618,10 @@ func (s *TimerLayerUserTermsOfServiceStore) Save(userTermsOfService *model.UserT
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TimerLayerWebhookStore) AnalyticsIncomingCount(teamID string) (int64, error) {
|
func (s *TimerLayerWebhookStore) AnalyticsIncomingCount(teamID string, userID string) (int64, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID)
|
result, err := s.WebhookStore.AnalyticsIncomingCount(teamID, userID)
|
||||||
|
|
||||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
if s.Root.Metrics != nil {
|
if s.Root.Metrics != nil {
|
||||||
|
|||||||
@@ -7046,6 +7046,10 @@
|
|||||||
"id": "app.webhooks.get_incoming_by_user.app_error",
|
"id": "app.webhooks.get_incoming_by_user.app_error",
|
||||||
"translation": "Unable to get the webhook."
|
"translation": "Unable to get the webhook."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.webhooks.get_incoming_count.app_error",
|
||||||
|
"translation": "Unable to get the webhook for teamID={{.TeamID}}, userID={{.UserID}}, err={{.Error}}."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.webhooks.get_outgoing.app_error",
|
"id": "app.webhooks.get_outgoing.app_error",
|
||||||
"translation": "Unable to get the webhook."
|
"translation": "Unable to get the webhook."
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ func (ts *TelemetryService) trackActivity() {
|
|||||||
|
|
||||||
slashCommandsCount, _ = ts.dbStore.Command().AnalyticsCommandCount("")
|
slashCommandsCount, _ = ts.dbStore.Command().AnalyticsCommandCount("")
|
||||||
|
|
||||||
if c, err := ts.dbStore.Webhook().AnalyticsIncomingCount(""); err == nil {
|
if c, err := ts.dbStore.Webhook().AnalyticsIncomingCount("", ""); err == nil {
|
||||||
incomingWebhooksCount = c
|
incomingWebhooksCount = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func initializeMocks(cfg *model.Config, cloudLicense bool) (*mocks.ServerIface,
|
|||||||
commandStore.On("AnalyticsCommandCount", "").Return(int64(15), nil)
|
commandStore.On("AnalyticsCommandCount", "").Return(int64(15), nil)
|
||||||
|
|
||||||
webhookStore := storeMocks.WebhookStore{}
|
webhookStore := storeMocks.WebhookStore{}
|
||||||
webhookStore.On("AnalyticsIncomingCount", "").Return(int64(16), nil)
|
webhookStore.On("AnalyticsIncomingCount", "", "").Return(int64(16), nil)
|
||||||
webhookStore.On("AnalyticsOutgoingCount", "").Return(int64(17), nil)
|
webhookStore.On("AnalyticsOutgoingCount", "").Return(int64(17), nil)
|
||||||
|
|
||||||
groupStore := storeMocks.GroupStore{}
|
groupStore := storeMocks.GroupStore{}
|
||||||
|
|||||||
@@ -4983,6 +4983,24 @@ func (c *Client4) GetIncomingWebhooks(ctx context.Context, page int, perPage int
|
|||||||
return iwl, BuildResponse(r), nil
|
return iwl, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIncomingWebhooksWithCount returns a page of incoming webhooks on the system including the total count. Page counting starts at 0.
|
||||||
|
func (c *Client4) GetIncomingWebhooksWithCount(ctx context.Context, page int, perPage int, etag string) (*IncomingWebhooksWithCount, *Response, error) {
|
||||||
|
query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count="+c.boolString(true), page, perPage)
|
||||||
|
r, err := c.DoAPIGet(ctx, c.incomingWebhooksRoute()+query, etag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
var iwl *IncomingWebhooksWithCount
|
||||||
|
if r.StatusCode == http.StatusNotModified {
|
||||||
|
return iwl, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&iwl); err != nil {
|
||||||
|
return nil, nil, NewAppError("GetIncomingWebhooksWithCount", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return iwl, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetIncomingWebhooksForTeam returns a page of incoming webhooks for a team. Page counting starts at 0.
|
// GetIncomingWebhooksForTeam returns a page of incoming webhooks for a team. Page counting starts at 0.
|
||||||
func (c *Client4) GetIncomingWebhooksForTeam(ctx context.Context, teamId string, page int, perPage int, etag string) ([]*IncomingWebhook, *Response, error) {
|
func (c *Client4) GetIncomingWebhooksForTeam(ctx context.Context, teamId string, page int, perPage int, etag string) ([]*IncomingWebhook, *Response, error) {
|
||||||
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
|
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ type IncomingWebhookRequest struct {
|
|||||||
Priority *PostPriority `json:"priority"`
|
Priority *PostPriority `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IncomingWebhooksWithCount struct {
|
||||||
|
Webhooks []*IncomingWebhook `json:"incoming_webhooks"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
func (o *IncomingWebhook) IsValid() *AppError {
|
func (o *IncomingWebhook) IsValid() *AppError {
|
||||||
if !IsValidId(o.Id) {
|
if !IsValidId(o.Id) {
|
||||||
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest)
|
return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {IncomingWebhook, OutgoingWebhook, Command, OAuthApp, OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
import type {IncomingWebhook, IncomingWebhooksWithCount, OutgoingWebhook, Command, OAuthApp, OutgoingOAuthConnection} from '@mattermost/types/integrations';
|
||||||
|
|
||||||
import * as IntegrationActions from 'mattermost-redux/actions/integrations';
|
import * as IntegrationActions from 'mattermost-redux/actions/integrations';
|
||||||
import {getProfilesByIds} from 'mattermost-redux/actions/users';
|
import {getProfilesByIds} from 'mattermost-redux/actions/users';
|
||||||
@@ -11,11 +11,13 @@ import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
|||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 100;
|
const DEFAULT_PAGE_SIZE = 100;
|
||||||
|
|
||||||
export function loadIncomingHooksAndProfilesForTeam(teamId: string, page = 0, perPage = DEFAULT_PAGE_SIZE): ActionFuncAsync<IncomingWebhook[]> {
|
export function loadIncomingHooksAndProfilesForTeam(teamId: string, page = 0, perPage = DEFAULT_PAGE_SIZE, includeTotalCount = false): ActionFuncAsync<IncomingWebhook[] | IncomingWebhooksWithCount> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const {data} = await dispatch(IntegrationActions.getIncomingHooks(teamId, page, perPage));
|
const {data} = await dispatch(IntegrationActions.getIncomingHooks(teamId, page, perPage, includeTotalCount));
|
||||||
if (data) {
|
if (data) {
|
||||||
dispatch(loadProfilesForIncomingHooks(data));
|
const isWebhooksWithCount = IntegrationActions.isIncomingWebhooksWithCount(data);
|
||||||
|
const hooks = isWebhooksWithCount ? (data as IncomingWebhooksWithCount).incoming_webhooks : data;
|
||||||
|
dispatch(loadProfilesForIncomingHooks(hooks as IncomingWebhook[]));
|
||||||
}
|
}
|
||||||
return {data};
|
return {data};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
@import 'utils/variables';
|
||||||
|
|
||||||
|
#searchInput {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backstage-footer {
|
||||||
|
display: flex;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid $light-gray;
|
||||||
|
border-top: none;
|
||||||
|
background: $white;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 1.1em;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.backstage-footer__cell {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,20 @@
|
|||||||
|
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import type {ChangeEvent, ReactNode} from 'react';
|
import type {ChangeEvent, ReactNode} from 'react';
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
|
import NextIcon from 'components/widgets/icons/fa_next_icon';
|
||||||
|
import PreviousIcon from 'components/widgets/icons/fa_previous_icon';
|
||||||
import SearchIcon from 'components/widgets/icons/fa_search_icon';
|
import SearchIcon from 'components/widgets/icons/fa_search_icon';
|
||||||
|
|
||||||
import {localizeMessage} from 'utils/utils';
|
import {localizeMessage} from 'utils/utils';
|
||||||
|
|
||||||
|
import './backstage_list.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: ReactNode | ((filter: string) => void);
|
children?: JSX.Element[] | ((filter: string) => [JSX.Element[], boolean]);
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
addLink?: string;
|
addLink?: string;
|
||||||
addText?: ReactNode;
|
addText?: ReactNode;
|
||||||
@@ -21,23 +26,52 @@ type Props = {
|
|||||||
helpText?: ReactNode;
|
helpText?: ReactNode;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
|
nextPage?: () => void;
|
||||||
|
previousPage?: () => void;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaging = (remainingProps: Props, childCount: number, hasFilter: boolean) => {
|
||||||
|
const page = (hasFilter || !remainingProps.page) ? 0 : remainingProps.page;
|
||||||
|
const pageSize = (hasFilter || !remainingProps.pageSize) ? childCount : remainingProps.pageSize;
|
||||||
|
const total = (hasFilter || !remainingProps.total) ? childCount : remainingProps.total;
|
||||||
|
|
||||||
|
let startCount = (page * pageSize) + 1;
|
||||||
|
let endCount = (page + 1) * pageSize;
|
||||||
|
endCount = endCount > total ? total : endCount;
|
||||||
|
if (endCount === 0) {
|
||||||
|
startCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstPage = startCount <= 1;
|
||||||
|
const isLastPage = endCount >= total;
|
||||||
|
|
||||||
|
return {startCount, endCount, total, isFirstPage, isLastPage};
|
||||||
};
|
};
|
||||||
|
|
||||||
const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.search', 'Search'), ...remainingProps}: Props) => {
|
const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.search', 'Search'), ...remainingProps}: Props) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
const updateFilter = (e: ChangeEvent<HTMLInputElement>) => setFilter(e.target.value);
|
const updateFilter = (e: ChangeEvent<HTMLInputElement>) => setFilter(e.target.value);
|
||||||
|
|
||||||
const filterLowered = filter.toLowerCase();
|
const filterLowered = filter.toLowerCase();
|
||||||
|
|
||||||
let children;
|
let children = [];
|
||||||
|
let childCount = 0;
|
||||||
if (remainingProps.loading) {
|
if (remainingProps.loading) {
|
||||||
children = <LoadingScreen/>;
|
children = [
|
||||||
|
<LoadingScreen
|
||||||
|
key='loading'
|
||||||
|
/>,
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
children = remainingProps.children;
|
|
||||||
let hasChildren = true;
|
let hasChildren = true;
|
||||||
if (typeof children === 'function') {
|
if (typeof remainingProps.children === 'function') {
|
||||||
[children, hasChildren] = children(filterLowered);
|
[children, hasChildren] = remainingProps.children(filterLowered);
|
||||||
|
} else {
|
||||||
|
children = remainingProps.children as JSX.Element[];
|
||||||
}
|
}
|
||||||
children = React.Children.map(children, (child) => {
|
children = React.Children.map(children, (child) => {
|
||||||
return React.cloneElement(child, {filterLowered});
|
return React.cloneElement(child, {filterLowered});
|
||||||
@@ -45,22 +79,28 @@ const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.sear
|
|||||||
if (children.length === 0 || !hasChildren) {
|
if (children.length === 0 || !hasChildren) {
|
||||||
if (!filterLowered) {
|
if (!filterLowered) {
|
||||||
if (remainingProps.emptyText) {
|
if (remainingProps.emptyText) {
|
||||||
children = (
|
children = [(
|
||||||
<div className='backstage-list__item backstage-list__empty'>
|
<div
|
||||||
|
className='backstage-list__item backstage-list__empty'
|
||||||
|
key='emptyText'
|
||||||
|
>
|
||||||
{remainingProps.emptyText}
|
{remainingProps.emptyText}
|
||||||
</div>
|
</div>
|
||||||
);
|
)];
|
||||||
}
|
}
|
||||||
} else if (remainingProps.emptyTextSearch) {
|
} else if (remainingProps.emptyTextSearch) {
|
||||||
children = (
|
children = [(
|
||||||
<div
|
<div
|
||||||
className='backstage-list__item backstage-list__empty'
|
className='backstage-list__item backstage-list__empty'
|
||||||
id='emptySearchResultsMessage'
|
id='emptySearchResultsMessage'
|
||||||
|
key='emptyTextSearch'
|
||||||
>
|
>
|
||||||
{React.cloneElement(remainingProps.emptyTextSearch, {values: {searchTerm: filterLowered}})}
|
{React.cloneElement(remainingProps.emptyTextSearch, {values: {searchTerm: filterLowered}})}
|
||||||
</div>
|
</div>
|
||||||
);
|
)];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
childCount = children.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +125,19 @@ const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.sear
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFilter = filter.length > 0;
|
||||||
|
const {startCount, endCount, total, isFirstPage, isLastPage} = getPaging(remainingProps, childCount, hasFilter);
|
||||||
|
const childrenToDisplay = childCount > 0 ? children.slice(startCount - 1, endCount) : children;
|
||||||
|
|
||||||
|
let previousPageFn = remainingProps.previousPage;
|
||||||
|
let nextPageFn = remainingProps.nextPage;
|
||||||
|
if (isFirstPage) {
|
||||||
|
previousPageFn = () => {};
|
||||||
|
}
|
||||||
|
if (isLastPage) {
|
||||||
|
nextPageFn = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='backstage-content'>
|
<div className='backstage-content'>
|
||||||
<div className='backstage-header'>
|
<div className='backstage-header'>
|
||||||
@@ -102,7 +155,6 @@ const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.sear
|
|||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={updateFilter}
|
onChange={updateFilter}
|
||||||
style={style.search}
|
|
||||||
id='searchInput'
|
id='searchInput'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,14 +163,39 @@ const BackstageList = ({searchPlaceholder = localizeMessage('backstage_list.sear
|
|||||||
{remainingProps.helpText}
|
{remainingProps.helpText}
|
||||||
</span>
|
</span>
|
||||||
<div className='backstage-list'>
|
<div className='backstage-list'>
|
||||||
{children}
|
{childrenToDisplay}
|
||||||
|
</div>
|
||||||
|
<div className='backstage-footer'>
|
||||||
|
<div className='backstage-footer__cell'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='backstage_list.paginatorCount'
|
||||||
|
defaultMessage='{startCount, number} - {endCount, number} of {total, number}'
|
||||||
|
values={{
|
||||||
|
startCount,
|
||||||
|
endCount,
|
||||||
|
total,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={'btn btn-quaternary btn-icon btn-sm ml-2 prev ' + (isFirstPage ? 'disabled' : '')}
|
||||||
|
onClick={previousPageFn}
|
||||||
|
aria-label={formatMessage({id: 'backstage_list.previousButton.ariaLabel', defaultMessage: 'Previous'})}
|
||||||
|
>
|
||||||
|
<PreviousIcon/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={'btn btn-quaternary btn-icon btn-sm next ' + (isLastPage ? 'disabled' : '')}
|
||||||
|
onClick={nextPageFn}
|
||||||
|
aria-label={formatMessage({id: 'backstage_list.nextButton.ariaLabel', defaultMessage: 'Next'})}
|
||||||
|
>
|
||||||
|
<NextIcon/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const style = {
|
|
||||||
search: {flexGrow: 0, flexShrink: 0},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BackstageList;
|
export default BackstageList;
|
||||||
|
|||||||
@@ -192,12 +192,12 @@ export default class Bots extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
bots = (filter?: string): Array<boolean | JSX.Element> => {
|
bots = (filter?: string): [JSX.Element[], boolean] => {
|
||||||
const bots = Object.values(this.props.bots).sort((a, b) => a.username.localeCompare(b.username));
|
const bots = Object.values(this.props.bots).sort((a, b) => a.username.localeCompare(b.username));
|
||||||
const match = (bot: BotType) => matchesFilter(bot, filter, this.props.owners[bot.user_id]);
|
const match = (bot: BotType) => matchesFilter(bot, filter, this.props.owners[bot.user_id]);
|
||||||
const enabledBots = bots.filter((bot) => bot.delete_at === 0).filter(match).map(this.botToJSX);
|
const enabledBots = bots.filter((bot) => bot.delete_at === 0).filter(match).map(this.botToJSX);
|
||||||
const disabledBots = bots.filter((bot) => bot.delete_at > 0).filter(match).map(this.botToJSX);
|
const disabledBots = bots.filter((bot) => bot.delete_at > 0).filter(match).map(this.botToJSX);
|
||||||
const sections = (
|
const sections = [(
|
||||||
<div key='sections'>
|
<div key='sections'>
|
||||||
<this.EnabledSection
|
<this.EnabledSection
|
||||||
enabledBots={enabledBots}
|
enabledBots={enabledBots}
|
||||||
@@ -207,7 +207,7 @@ export default class Bots extends React.PureComponent<Props, State> {
|
|||||||
disabledBots={disabledBots}
|
disabledBots={disabledBots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)];
|
||||||
|
|
||||||
return [sections, enabledBots.length > 0 || disabledBots.length > 0];
|
return [sections, enabledBots.length > 0 || disabledBots.length > 0];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {removeIncomingHook} from 'mattermost-redux/actions/integrations';
|
|||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {getAllChannels} from 'mattermost-redux/selectors/entities/channels';
|
import {getAllChannels} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getIncomingHooks} from 'mattermost-redux/selectors/entities/integrations';
|
import {getFilteredIncomingHooks, getIncomingHooksTotalCount} from 'mattermost-redux/selectors/entities/integrations';
|
||||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getUsers} from 'mattermost-redux/selectors/entities/users';
|
import {getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||||
@@ -21,17 +21,16 @@ import {loadIncomingHooksAndProfilesForTeam} from 'actions/integration_actions';
|
|||||||
import InstalledIncomingWebhooks from './installed_incoming_webhooks';
|
import InstalledIncomingWebhooks from './installed_incoming_webhooks';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const config = getConfig(state);
|
|
||||||
const teamId = getCurrentTeamId(state);
|
const teamId = getCurrentTeamId(state);
|
||||||
|
const incomingHooks = getFilteredIncomingHooks(state);
|
||||||
|
const incomingHooksTotalCount = getIncomingHooksTotalCount(state);
|
||||||
|
const config = getConfig(state);
|
||||||
const canManageOthersWebhooks = haveITeamPermission(state, teamId, Permissions.MANAGE_OTHERS_INCOMING_WEBHOOKS);
|
const canManageOthersWebhooks = haveITeamPermission(state, teamId, Permissions.MANAGE_OTHERS_INCOMING_WEBHOOKS);
|
||||||
const incomingHooks = getIncomingHooks(state);
|
|
||||||
const incomingWebhooks = Object.keys(incomingHooks).
|
|
||||||
map((key) => incomingHooks[key]).
|
|
||||||
filter((incomingWebhook) => incomingWebhook.team_id === teamId);
|
|
||||||
const enableIncomingWebhooks = config.EnableIncomingWebhooks === 'true';
|
const enableIncomingWebhooks = config.EnableIncomingWebhooks === 'true';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
incomingWebhooks,
|
incomingHooks,
|
||||||
|
incomingHooksTotalCount,
|
||||||
channels: getAllChannels(state),
|
channels: getAllChannels(state),
|
||||||
users: getUsers(state),
|
users: getUsers(state),
|
||||||
canManageOthersWebhooks,
|
canManageOthersWebhooks,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
import type {IncomingWebhook} from '@mattermost/types/integrations';
|
import type {IncomingWebhook, IncomingWebhooksWithCount} from '@mattermost/types/integrations';
|
||||||
import type {Team} from '@mattermost/types/teams';
|
import type {Team} from '@mattermost/types/teams';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||||
@@ -17,25 +17,29 @@ import ExternalLink from 'components/external_link';
|
|||||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||||
import InstalledIncomingWebhook, {matchesFilter} from 'components/integrations/installed_incoming_webhook';
|
import InstalledIncomingWebhook, {matchesFilter} from 'components/integrations/installed_incoming_webhook';
|
||||||
|
|
||||||
import {Constants, DeveloperLinks} from 'utils/constants';
|
import {DeveloperLinks} from 'utils/constants';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 200;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
team: Team;
|
team: Team;
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
canManageOthersWebhooks: boolean;
|
incomingHooks: IncomingWebhook[];
|
||||||
incomingWebhooks: IncomingWebhook[];
|
incomingHooksTotalCount: number;
|
||||||
channels: IDMappedObjects<Channel>;
|
channels: IDMappedObjects<Channel>;
|
||||||
users: IDMappedObjects<UserProfile>;
|
users: IDMappedObjects<UserProfile>;
|
||||||
|
canManageOthersWebhooks: boolean;
|
||||||
enableIncomingWebhooks: boolean;
|
enableIncomingWebhooks: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
removeIncomingHook: (hookId: string) => Promise<ActionResult>;
|
removeIncomingHook: (hookId: string) => Promise<ActionResult>;
|
||||||
loadIncomingHooksAndProfilesForTeam: (teamId: string, startPageNumber: number,
|
loadIncomingHooksAndProfilesForTeam: (teamId: string, startPageNumber: number,
|
||||||
pageSize: number) => Promise<ActionResult>;
|
pageSize: number, includeTotalCount: boolean) => Promise<ActionResult<IncomingWebhook[] | IncomingWebhooksWithCount>>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,26 +48,43 @@ export default class InstalledIncomingWebhooks extends React.PureComponent<Props
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
page: 0,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.enableIncomingWebhooks) {
|
this.loadPage(0);
|
||||||
this.props.actions.loadIncomingHooksAndProfilesForTeam(
|
|
||||||
this.props.team.id,
|
|
||||||
Constants.Integrations.START_PAGE_NUM,
|
|
||||||
Constants.Integrations.PAGE_SIZE,
|
|
||||||
).then(
|
|
||||||
() => this.setState({loading: false}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteIncomingWebhook = (incomingWebhook: IncomingWebhook) => {
|
deleteIncomingWebhook = (incomingWebhook: IncomingWebhook) => {
|
||||||
this.props.actions.removeIncomingHook(incomingWebhook.id);
|
this.props.actions.removeIncomingHook(incomingWebhook.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadPage = async (pageToLoad: number) => {
|
||||||
|
if (this.props.enableIncomingWebhooks) {
|
||||||
|
this.setState({loading: true},
|
||||||
|
async () => {
|
||||||
|
await this.props.actions.loadIncomingHooksAndProfilesForTeam(
|
||||||
|
this.props.team.id,
|
||||||
|
pageToLoad,
|
||||||
|
PAGE_SIZE,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.setState({page: pageToLoad, loading: false});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nextPage = () => {
|
||||||
|
this.loadPage(this.state.page + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
previousPage = () => {
|
||||||
|
this.loadPage(this.state.page - 1);
|
||||||
|
};
|
||||||
|
|
||||||
incomingWebhookCompare = (a: IncomingWebhook, b: IncomingWebhook) => {
|
incomingWebhookCompare = (a: IncomingWebhook, b: IncomingWebhook) => {
|
||||||
let displayNameA = a.display_name;
|
let displayNameA = a.display_name;
|
||||||
if (!displayNameA) {
|
if (!displayNameA) {
|
||||||
@@ -76,11 +97,10 @@ export default class InstalledIncomingWebhooks extends React.PureComponent<Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayNameB = b.display_name;
|
const displayNameB = b.display_name;
|
||||||
|
|
||||||
return displayNameA.localeCompare(displayNameB);
|
return displayNameA.localeCompare(displayNameB);
|
||||||
};
|
};
|
||||||
|
|
||||||
incomingWebhooks = (filter: string) => this.props.incomingWebhooks.
|
incomingWebhooks = (filter: string) => this.props.incomingHooks.
|
||||||
sort(this.incomingWebhookCompare).
|
sort(this.incomingWebhookCompare).
|
||||||
filter((incomingWebhook: IncomingWebhook) => matchesFilter(incomingWebhook, this.props.channels[incomingWebhook.channel_id], filter)).
|
filter((incomingWebhook: IncomingWebhook) => matchesFilter(incomingWebhook, this.props.channels[incomingWebhook.channel_id], filter)).
|
||||||
map((incomingWebhook: IncomingWebhook) => {
|
map((incomingWebhook: IncomingWebhook) => {
|
||||||
@@ -160,6 +180,11 @@ export default class InstalledIncomingWebhooks extends React.PureComponent<Props
|
|||||||
}
|
}
|
||||||
searchPlaceholder={Utils.localizeMessage('installed_incoming_webhooks.search', 'Search Incoming Webhooks')}
|
searchPlaceholder={Utils.localizeMessage('installed_incoming_webhooks.search', 'Search Incoming Webhooks')}
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
nextPage={this.nextPage}
|
||||||
|
previousPage={this.previousPage}
|
||||||
|
page={this.state.page}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
total={this.props.incomingHooksTotalCount}
|
||||||
>
|
>
|
||||||
{(filter: string) => {
|
{(filter: string) => {
|
||||||
const children = this.incomingWebhooks(filter);
|
const children = this.incomingWebhooks(filter);
|
||||||
|
|||||||
@@ -161,12 +161,6 @@ exports[`components/integrations/InstalledOutgoingOAuthConnections should match
|
|||||||
id="searchInput"
|
id="searchInput"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
placeholder="Search Outgoing OAuth Connections"
|
placeholder="Search Outgoing OAuth Connections"
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flexGrow": 0,
|
|
||||||
"flexShrink": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type="search"
|
type="search"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@@ -223,7 +217,9 @@ exports[`components/integrations/InstalledOutgoingOAuthConnections should match
|
|||||||
<div
|
<div
|
||||||
className="backstage-list"
|
className="backstage-list"
|
||||||
>
|
>
|
||||||
<LoadingScreen>
|
<LoadingScreen
|
||||||
|
key="loading"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="loading-screen"
|
className="loading-screen"
|
||||||
style={
|
style={
|
||||||
@@ -251,6 +247,55 @@ exports[`components/integrations/InstalledOutgoingOAuthConnections should match
|
|||||||
</div>
|
</div>
|
||||||
</LoadingScreen>
|
</LoadingScreen>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="backstage-footer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="backstage-footer__cell"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{startCount, number} - {endCount, number} of {total, number}"
|
||||||
|
id="backstage_list.paginatorCount"
|
||||||
|
values={
|
||||||
|
Object {
|
||||||
|
"endCount": 0,
|
||||||
|
"startCount": 0,
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
0 - 0 of 0
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
<button
|
||||||
|
aria-label="Previous"
|
||||||
|
className="btn btn-quaternary btn-icon btn-sm ml-2 prev disabled"
|
||||||
|
onClick={[Function]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Memo(PreviousIcon)>
|
||||||
|
<i
|
||||||
|
className="icon icon-chevron-left"
|
||||||
|
title="Previous Icon"
|
||||||
|
/>
|
||||||
|
</Memo(PreviousIcon)>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="Next"
|
||||||
|
className="btn btn-quaternary btn-icon btn-sm next disabled"
|
||||||
|
onClick={[Function]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Memo(NextIcon)>
|
||||||
|
<i
|
||||||
|
className="icon icon-chevron-right"
|
||||||
|
title="Next Icon"
|
||||||
|
/>
|
||||||
|
</Memo(NextIcon)>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BackstageList>
|
</BackstageList>
|
||||||
</InstalledOutgoingOAuthConnections>
|
</InstalledOutgoingOAuthConnections>
|
||||||
|
|||||||
@@ -2961,6 +2961,9 @@
|
|||||||
"avatar.alt": "{username} profile image",
|
"avatar.alt": "{username} profile image",
|
||||||
"avatars.overflowUnnamedOnly": "{overflowUnnamedCount, plural, =1 {one other} other {# others}}",
|
"avatars.overflowUnnamedOnly": "{overflowUnnamedCount, plural, =1 {one other} other {# others}}",
|
||||||
"avatars.overflowUsers": "{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}",
|
"avatars.overflowUsers": "{overflowUnnamedCount, plural, =0 {{names}} =1 {{names} and one other} other {{names} and # others}}",
|
||||||
|
"backstage_list.nextButton.ariaLabel": "Next",
|
||||||
|
"backstage_list.paginatorCount": "{startCount, number} - {endCount, number} of {total, number}",
|
||||||
|
"backstage_list.previousButton.ariaLabel": "Previous",
|
||||||
"backstage_list.search": "Search",
|
"backstage_list.search": "Search",
|
||||||
"backstage_navbar.back": "Back",
|
"backstage_navbar.back": "Back",
|
||||||
"backstage_navbar.backToMattermost": "Back to {siteName}",
|
"backstage_navbar.backToMattermost": "Back to {siteName}",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default keyMirror({
|
|||||||
|
|
||||||
RECEIVED_INCOMING_HOOK: null,
|
RECEIVED_INCOMING_HOOK: null,
|
||||||
RECEIVED_INCOMING_HOOKS: null,
|
RECEIVED_INCOMING_HOOKS: null,
|
||||||
|
RECEIVED_INCOMING_HOOKS_TOTAL_COUNT: null,
|
||||||
DELETED_INCOMING_HOOK: null,
|
DELETED_INCOMING_HOOK: null,
|
||||||
RECEIVED_OUTGOING_HOOK: null,
|
RECEIVED_OUTGOING_HOOK: null,
|
||||||
RECEIVED_OUTGOING_HOOKS: null,
|
RECEIVED_OUTGOING_HOOKS: null,
|
||||||
|
|||||||
@@ -86,17 +86,31 @@ describe('Actions.Integrations', () => {
|
|||||||
} as IncomingWebhook,
|
} as IncomingWebhook,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
/* Test with include_total_count being set to false */
|
||||||
nock(Client4.getBaseRoute()).
|
nock(Client4.getBaseRoute()).
|
||||||
get('/hooks/incoming').
|
get('/hooks/incoming').
|
||||||
query(true).
|
query(true).
|
||||||
reply(200, [created]);
|
reply(200, [created]);
|
||||||
|
|
||||||
await store.dispatch(Actions.getIncomingHooks(TestHelper.basicTeam!.id));
|
const response = await store.dispatch(Actions.getIncomingHooks(TestHelper.basicTeam!.id));
|
||||||
const state = store.getState();
|
expect(response.data).toBeTruthy();
|
||||||
|
expect(response.data[0].id === created.id).toBeTruthy();
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
const hooks = state.entities.integrations.incomingHooks;
|
const hooks = state.entities.integrations.incomingHooks;
|
||||||
expect(hooks).toBeTruthy();
|
expect(hooks).toBeTruthy();
|
||||||
expect(hooks[created.id]).toBeTruthy();
|
expect(hooks[created.id]).toBeTruthy();
|
||||||
|
|
||||||
|
/* Test with include_total_count being set to true */
|
||||||
|
nock(Client4.getBaseRoute()).
|
||||||
|
get('/hooks/incoming').
|
||||||
|
query(true).
|
||||||
|
reply(200, {incoming_webhooks: [created], total_count: 1});
|
||||||
|
|
||||||
|
const responseWithCount = await store.dispatch(Actions.getIncomingHooks(TestHelper.basicTeam!.id, 0, 10, true));
|
||||||
|
expect(responseWithCount.data.incoming_webhooks).toBeTruthy();
|
||||||
|
expect(responseWithCount.data.incoming_webhooks[0].id === created.id).toBeTruthy();
|
||||||
|
expect(responseWithCount.data.total_count === 1).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeIncomingHook', async () => {
|
it('removeIncomingHook', async () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {AnyAction} from 'redux';
|
||||||
import {batchActions} from 'redux-batched-actions';
|
import {batchActions} from 'redux-batched-actions';
|
||||||
|
|
||||||
import type {Command, CommandArgs, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook, SubmitDialogResponse} from '@mattermost/types/integrations';
|
import type {Command, CommandArgs, DialogSubmission, IncomingWebhook, IncomingWebhooksWithCount, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook, SubmitDialogResponse} from '@mattermost/types/integrations';
|
||||||
|
|
||||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@@ -37,16 +38,41 @@ export function getIncomingHook(hookId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIncomingHooks(teamId = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT) {
|
export function getIncomingHooks(teamId = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, includeTotalCount = false): ActionFuncAsync<IncomingWebhook[] | IncomingWebhooksWithCount> {
|
||||||
return bindClientFunc({
|
return async (dispatch, getState) => {
|
||||||
clientFunc: Client4.getIncomingWebhooks,
|
let data;
|
||||||
onSuccess: [IntegrationTypes.RECEIVED_INCOMING_HOOKS],
|
|
||||||
params: [
|
try {
|
||||||
teamId,
|
data = await Client4.getIncomingWebhooks(teamId, page, perPage, includeTotalCount);
|
||||||
page,
|
} catch (error) {
|
||||||
perPage,
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
],
|
dispatch(logError(error));
|
||||||
});
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWebhooksWithCount = isIncomingWebhooksWithCount(data);
|
||||||
|
|
||||||
|
const actions: AnyAction[] = [{
|
||||||
|
type: IntegrationTypes.RECEIVED_INCOMING_HOOKS,
|
||||||
|
data: isWebhooksWithCount ? (data as IncomingWebhooksWithCount).incoming_webhooks : data,
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (isWebhooksWithCount) {
|
||||||
|
actions.push({
|
||||||
|
type: IntegrationTypes.RECEIVED_INCOMING_HOOKS_TOTAL_COUNT,
|
||||||
|
data: (data as IncomingWebhooksWithCount).total_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(batchActions(actions));
|
||||||
|
return {data};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIncomingWebhooksWithCount(data: any): data is IncomingWebhooksWithCount {
|
||||||
|
return typeof data.incoming_webhooks !== 'undefined' &&
|
||||||
|
Array.isArray(data.incoming_webhooks) &&
|
||||||
|
typeof data.total_count === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeIncomingHook(hookId: string): ActionFuncAsync {
|
export function removeIncomingHook(hookId: string): ActionFuncAsync {
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ function incomingHooks(state: IDMappedObjects<IncomingWebhook> = {}, action: Any
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function incomingHooksTotalCount(state: number = 0, action: AnyAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case IntegrationTypes.RECEIVED_INCOMING_HOOKS_TOTAL_COUNT: {
|
||||||
|
return action.data;
|
||||||
|
}
|
||||||
|
case IntegrationTypes.DELETED_INCOMING_HOOK: {
|
||||||
|
return Math.max(state - 1, 0);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function outgoingHooks(state: IDMappedObjects<OutgoingWebhook> = {}, action: AnyAction) {
|
function outgoingHooks(state: IDMappedObjects<OutgoingWebhook> = {}, action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case IntegrationTypes.RECEIVED_OUTGOING_HOOK: {
|
case IntegrationTypes.RECEIVED_OUTGOING_HOOK: {
|
||||||
@@ -306,6 +319,9 @@ export default combineReducers({
|
|||||||
// object where every key is the hook id and has an object with the incoming hook details
|
// object where every key is the hook id and has an object with the incoming hook details
|
||||||
incomingHooks,
|
incomingHooks,
|
||||||
|
|
||||||
|
// object to represent total amount of incoming hooks
|
||||||
|
incomingHooksTotalCount,
|
||||||
|
|
||||||
// object where every key is the hook id and has an object with the outgoing hook details
|
// object where every key is the hook id and has an object with the outgoing hook details
|
||||||
outgoingHooks,
|
outgoingHooks,
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {OutgoingWebhook, Command} from '@mattermost/types/integrations';
|
import type {IncomingWebhook, OutgoingWebhook, Command} from '@mattermost/types/integrations';
|
||||||
import type {GlobalState} from '@mattermost/types/store';
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
@@ -14,6 +14,10 @@ export function getIncomingHooks(state: GlobalState) {
|
|||||||
return state.entities.integrations.incomingHooks;
|
return state.entities.integrations.incomingHooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIncomingHooksTotalCount(state: GlobalState) {
|
||||||
|
return state.entities.integrations.incomingHooksTotalCount;
|
||||||
|
}
|
||||||
|
|
||||||
export function getOutgoingHooks(state: GlobalState) {
|
export function getOutgoingHooks(state: GlobalState) {
|
||||||
return state.entities.integrations.outgoingHooks;
|
return state.entities.integrations.outgoingHooks;
|
||||||
}
|
}
|
||||||
@@ -30,6 +34,17 @@ export function getOutgoingOAuthConnections(state: GlobalState) {
|
|||||||
return state.entities.integrations.outgoingOAuthConnections;
|
return state.entities.integrations.outgoingOAuthConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFilteredIncomingHooks: (state: GlobalState) => IncomingWebhook[] = createSelector(
|
||||||
|
'getFilteredIncomingHooks',
|
||||||
|
getCurrentTeamId,
|
||||||
|
getIncomingHooks,
|
||||||
|
(teamId, hooks) => {
|
||||||
|
return Object.keys(hooks).
|
||||||
|
map((key) => hooks[key]).
|
||||||
|
filter((incomingHook) => incomingHook.team_id === teamId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const getAppsOAuthAppIDs: (state: GlobalState) => string[] = createSelector(
|
export const getAppsOAuthAppIDs: (state: GlobalState) => string[] = createSelector(
|
||||||
'getAppsOAuthAppIDs',
|
'getAppsOAuthAppIDs',
|
||||||
appsEnabled,
|
appsEnabled,
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ const state: GlobalState = {
|
|||||||
},
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
incomingHooks: {},
|
incomingHooks: {},
|
||||||
|
incomingHooksTotalCount: 0,
|
||||||
outgoingHooks: {},
|
outgoingHooks: {},
|
||||||
oauthApps: {},
|
oauthApps: {},
|
||||||
systemCommands: {},
|
systemCommands: {},
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import type {
|
|||||||
CommandResponse,
|
CommandResponse,
|
||||||
DialogSubmission,
|
DialogSubmission,
|
||||||
IncomingWebhook,
|
IncomingWebhook,
|
||||||
|
IncomingWebhooksWithCount,
|
||||||
OAuthApp,
|
OAuthApp,
|
||||||
OutgoingOAuthConnection,
|
OutgoingOAuthConnection,
|
||||||
OutgoingWebhook,
|
OutgoingWebhook,
|
||||||
@@ -2625,17 +2626,18 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getIncomingWebhooks = (teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
|
getIncomingWebhooks = (teamId = '', page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => {
|
||||||
const queryParams: any = {
|
const queryParams: any = {
|
||||||
page,
|
page,
|
||||||
per_page: perPage,
|
per_page: perPage,
|
||||||
|
include_total_count: includeTotalCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
queryParams.team_id = teamId;
|
queryParams.team_id = teamId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.doFetch<IncomingWebhook[]>(
|
return this.doFetch<IncomingWebhook[] | IncomingWebhooksWithCount>(
|
||||||
`${this.getIncomingHooksRoute()}${buildQueryString(queryParams)}`,
|
`${this.getIncomingHooksRoute()}${buildQueryString(queryParams)}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export type IncomingWebhook = {
|
|||||||
channel_locked: boolean;
|
channel_locked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IncomingWebhooksWithCount = {
|
||||||
|
incoming_webhooks: IncomingWebhook[];
|
||||||
|
total_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type OutgoingWebhook = {
|
export type OutgoingWebhook = {
|
||||||
id: string;
|
id: string;
|
||||||
token: string;
|
token: string;
|
||||||
@@ -121,6 +126,7 @@ export type OutgoingOAuthConnection = {
|
|||||||
|
|
||||||
export type IntegrationsState = {
|
export type IntegrationsState = {
|
||||||
incomingHooks: IDMappedObjects<IncomingWebhook>;
|
incomingHooks: IDMappedObjects<IncomingWebhook>;
|
||||||
|
incomingHooksTotalCount: number;
|
||||||
outgoingHooks: IDMappedObjects<OutgoingWebhook>;
|
outgoingHooks: IDMappedObjects<OutgoingWebhook>;
|
||||||
oauthApps: IDMappedObjects<OAuthApp>;
|
oauthApps: IDMappedObjects<OAuthApp>;
|
||||||
outgoingOAuthConnections: IDMappedObjects<OutgoingOAuthConnection>;
|
outgoingOAuthConnections: IDMappedObjects<OutgoingOAuthConnection>;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user