mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' into MM-60555
This commit is contained in:
commit
396beac61d
@ -129,12 +129,10 @@ test('Post actions tab support', async ({pw, axe}) => {
|
|||||||
// # Press arrow right
|
// # Press arrow right
|
||||||
await channelsPage.postDotMenu.remindMenuItem.press('ArrowRight');
|
await channelsPage.postDotMenu.remindMenuItem.press('ArrowRight');
|
||||||
|
|
||||||
// * Reminder menu should be visible and have focused
|
// * Reminder menu should be visible
|
||||||
channelsPage.postReminderMenu.toBeVisible();
|
expect(channelsPage.postReminderMenu.container).toBeVisible();
|
||||||
await expect(channelsPage.postReminderMenu.container).toBeFocused();
|
|
||||||
|
|
||||||
// * Should move focus to 30 mins after arrow down
|
// * Should have focus on 30 mins after submenu opens
|
||||||
await channelsPage.postReminderMenu.container.press('ArrowDown');
|
|
||||||
expect(await channelsPage.postReminderMenu.thirtyMinsMenuItem).toBeFocused();
|
expect(await channelsPage.postReminderMenu.thirtyMinsMenuItem).toBeFocused();
|
||||||
|
|
||||||
// * Should move focus to 1 hour after arrow down
|
// * Should move focus to 1 hour after arrow down
|
||||||
|
@ -8,15 +8,6 @@ else
|
|||||||
PLATFORM := $(shell uname)
|
PLATFORM := $(shell uname)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Set an environment variable on Linux used to resolve `docker.host.internal` inconsistencies with
|
|
||||||
# docker. This can be reworked once https://github.com/docker/for-linux/issues/264 is resolved
|
|
||||||
# satisfactorily.
|
|
||||||
ifeq ($(PLATFORM),Linux)
|
|
||||||
export IS_LINUX = -linux
|
|
||||||
else
|
|
||||||
export IS_LINUX =
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Detect Apple Silicon and set a flag.
|
# Detect Apple Silicon and set a flag.
|
||||||
ifeq ($(shell uname)/$(shell uname -m),Darwin/arm64)
|
ifeq ($(shell uname)/$(shell uname -m),Darwin/arm64)
|
||||||
ARM_BASED_MAC = true
|
ARM_BASED_MAC = true
|
||||||
|
@ -144,10 +144,12 @@ services:
|
|||||||
image: "prom/prometheus:v2.46.0"
|
image: "prom/prometheus:v2.46.0"
|
||||||
user: root
|
user: root
|
||||||
volumes:
|
volumes:
|
||||||
- "./docker/prometheus${IS_LINUX}.yml:/etc/prometheus/prometheus.yml"
|
- "./docker/prometheus.yml:/etc/prometheus/prometheus.yml"
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
networks:
|
networks:
|
||||||
- mm-test
|
- mm-test
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
grafana:
|
grafana:
|
||||||
image: "grafana/grafana:10.4.2"
|
image: "grafana/grafana:10.4.2"
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
global:
|
|
||||||
scrape_interval: 5s
|
|
||||||
evaluation_interval: 60s
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: 'mattermost'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['172.17.0.1:8067']
|
|
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -54,6 +55,8 @@ func TestCreateCPAField(t *testing.T) {
|
|||||||
}, "an invalid field should be rejected")
|
}, "an invalid field should be rejected")
|
||||||
|
|
||||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||||
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
||||||
|
|
||||||
name := model.NewId()
|
name := model.NewId()
|
||||||
field := &model.PropertyField{
|
field := &model.PropertyField{
|
||||||
Name: fmt.Sprintf(" %s\t", name), // name should be sanitized
|
Name: fmt.Sprintf(" %s\t", name), // name should be sanitized
|
||||||
@ -67,6 +70,27 @@ func TestCreateCPAField(t *testing.T) {
|
|||||||
require.NotZero(t, createdField.ID)
|
require.NotZero(t, createdField.ID)
|
||||||
require.Equal(t, name, createdField.Name)
|
require.Equal(t, name, createdField.Name)
|
||||||
require.Equal(t, "default", createdField.Attrs["visibility"])
|
require.Equal(t, "default", createdField.Attrs["visibility"])
|
||||||
|
|
||||||
|
t.Run("a websocket event should be fired as part of the field creation", func(t *testing.T) {
|
||||||
|
var wsField model.PropertyField
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case event := <-webSocketClient.EventChannel:
|
||||||
|
if event.EventType() == model.WebsocketEventCPAFieldCreated {
|
||||||
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
require.NotEmpty(t, wsField.ID)
|
||||||
|
require.Equal(t, createdField, &wsField)
|
||||||
|
})
|
||||||
}, "a user with admin permissions should be able to create the field")
|
}, "a user with admin permissions should be able to create the field")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +173,8 @@ func TestPatchCPAField(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||||
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
||||||
|
|
||||||
field := &model.PropertyField{
|
field := &model.PropertyField{
|
||||||
Name: model.NewId(),
|
Name: model.NewId(),
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
@ -163,6 +189,27 @@ func TestPatchCPAField(t *testing.T) {
|
|||||||
CheckOKStatus(t, resp)
|
CheckOKStatus(t, resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, newName, patchedField.Name)
|
require.Equal(t, newName, patchedField.Name)
|
||||||
|
|
||||||
|
t.Run("a websocket event should be fired as part of the field patch", func(t *testing.T) {
|
||||||
|
var wsField model.PropertyField
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case event := <-webSocketClient.EventChannel:
|
||||||
|
if event.EventType() == model.WebsocketEventCPAFieldUpdated {
|
||||||
|
fieldData, err := json.Marshal(event.GetData()["field"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, json.Unmarshal(fieldData, &wsField))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
require.NotEmpty(t, wsField.ID)
|
||||||
|
require.Equal(t, patchedField, &wsField)
|
||||||
|
})
|
||||||
}, "a user with admin permissions should be able to patch the field")
|
}, "a user with admin permissions should be able to patch the field")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +244,8 @@ func TestDeleteCPAField(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||||
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
||||||
|
|
||||||
field := &model.PropertyField{
|
field := &model.PropertyField{
|
||||||
Name: model.NewId(),
|
Name: model.NewId(),
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
@ -213,6 +262,26 @@ func TestDeleteCPAField(t *testing.T) {
|
|||||||
deletedField, appErr := th.App.GetCPAField(createdField.ID)
|
deletedField, appErr := th.App.GetCPAField(createdField.ID)
|
||||||
require.Nil(t, appErr)
|
require.Nil(t, appErr)
|
||||||
require.NotZero(t, deletedField.DeleteAt)
|
require.NotZero(t, deletedField.DeleteAt)
|
||||||
|
|
||||||
|
t.Run("a websocket event should be fired as part of the field deletion", func(t *testing.T) {
|
||||||
|
var fieldID string
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case event := <-webSocketClient.EventChannel:
|
||||||
|
if event.EventType() == model.WebsocketEventCPAFieldDeleted {
|
||||||
|
var ok bool
|
||||||
|
fieldID, ok = event.GetData()["field_id"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
require.Equal(t, createdField.ID, fieldID)
|
||||||
|
})
|
||||||
}, "a user with admin permissions should be able to delete the field")
|
}, "a user with admin permissions should be able to delete the field")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,6 +539,8 @@ func TestPatchCPAValues(t *testing.T) {
|
|||||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||||
|
|
||||||
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
t.Run("any team member should be able to create their own values", func(t *testing.T) {
|
||||||
|
webSocketClient := th.CreateConnectedWebSocketClient(t)
|
||||||
|
|
||||||
values := map[string]json.RawMessage{}
|
values := map[string]json.RawMessage{}
|
||||||
value := "Field Value"
|
value := "Field Value"
|
||||||
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
values[createdField.ID] = json.RawMessage(fmt.Sprintf(`" %s "`, value)) // value should be sanitized
|
||||||
@ -490,6 +561,27 @@ func TestPatchCPAValues(t *testing.T) {
|
|||||||
actualValue = ""
|
actualValue = ""
|
||||||
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
require.NoError(t, json.Unmarshal(values[createdField.ID], &actualValue))
|
||||||
require.Equal(t, value, actualValue)
|
require.Equal(t, value, actualValue)
|
||||||
|
|
||||||
|
t.Run("a websocket event should be fired as part of the value changes", func(t *testing.T) {
|
||||||
|
var wsValues map[string]json.RawMessage
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case event := <-webSocketClient.EventChannel:
|
||||||
|
if event.EventType() == model.WebsocketEventCPAValuesUpdated {
|
||||||
|
valuesData, err := json.Marshal(event.GetData()["values"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, json.Unmarshal(valuesData, &wsValues))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
require.NotEmpty(t, wsValues)
|
||||||
|
require.Equal(t, patchedValues, wsValues)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
t.Run("any team member should be able to patch their own values", func(t *testing.T) {
|
||||||
|
@ -91,7 +91,11 @@ func TestSubmitMetrics(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("metrics enabled and valid", func(t *testing.T) {
|
t.Run("metrics enabled and valid", func(t *testing.T) {
|
||||||
metricsMock := setupMetricsMock()
|
metricsMock := setupMetricsMock()
|
||||||
metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
metricsMock.On("IncrementClientLongTasks",
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("float64")).Return()
|
||||||
|
|
||||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||||
return metricsMock
|
return metricsMock
|
||||||
@ -159,7 +163,11 @@ func TestSubmitMetrics(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("metrics recorded for API errors", func(t *testing.T) {
|
t.Run("metrics recorded for API errors", func(t *testing.T) {
|
||||||
metricsMock := setupMetricsMock()
|
metricsMock := setupMetricsMock()
|
||||||
metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
metricsMock.On("IncrementClientLongTasks",
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("float64")).Return()
|
||||||
|
|
||||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||||
return metricsMock
|
return metricsMock
|
||||||
@ -190,7 +198,11 @@ func TestSubmitMetrics(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("metrics recorded for URL length limit errors", func(t *testing.T) {
|
t.Run("metrics recorded for URL length limit errors", func(t *testing.T) {
|
||||||
metricsMock := setupMetricsMock()
|
metricsMock := setupMetricsMock()
|
||||||
metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
metricsMock.On("IncrementClientLongTasks",
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("string"),
|
||||||
|
mock.AnythingOfType("float64")).Return()
|
||||||
|
|
||||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||||
return metricsMock
|
return metricsMock
|
||||||
|
@ -3475,7 +3475,9 @@ func TestWebHubMembership(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWebHubCloseConnOnDBFail(t *testing.T) {
|
func TestWebHubCloseConnOnDBFail(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := SetupConfig(t, func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.EnableWebHubChannelIteration = true
|
||||||
|
}).InitBasic()
|
||||||
defer func() {
|
defer func() {
|
||||||
th.TearDown()
|
th.TearDown()
|
||||||
_, err := th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE dummy RENAME to ChannelMembers`)
|
_, err := th.Server.Store().GetInternalMasterDB().Exec(`ALTER TABLE dummy RENAME to ChannelMembers`)
|
||||||
|
@ -55,7 +55,7 @@ func (a *App) UpdateChannelBookmark(c request.CTX, updateBookmark *model.Channel
|
|||||||
isAnotherFile := updateBookmark.FileInfo != nil && updateBookmark.FileId != "" && updateBookmark.FileId != updateBookmark.FileInfo.Id
|
isAnotherFile := updateBookmark.FileInfo != nil && updateBookmark.FileId != "" && updateBookmark.FileId != updateBookmark.FileInfo.Id
|
||||||
|
|
||||||
if isAnotherFile {
|
if isAnotherFile {
|
||||||
if fileAlreadyAttachedErr := a.Srv().Store().ChannelBookmark().ErrorIfBookmarkFileInfoAlreadyAttached(updateBookmark.FileId); fileAlreadyAttachedErr != nil {
|
if fileAlreadyAttachedErr := a.Srv().Store().ChannelBookmark().ErrorIfBookmarkFileInfoAlreadyAttached(updateBookmark.FileId, updateBookmark.ChannelId); fileAlreadyAttachedErr != nil {
|
||||||
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.update.app_error", nil, "", http.StatusInternalServerError).Wrap(fileAlreadyAttachedErr)
|
return nil, model.NewAppError("UpdateChannelBookmark", "app.channel.bookmark.update.app_error", nil, "", http.StatusInternalServerError).Wrap(fileAlreadyAttachedErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,7 @@ func TestUpdateBookmark(t *testing.T) {
|
|||||||
var testUpdateAnotherFile = func(th *TestHelper, t *testing.T) {
|
var testUpdateAnotherFile = func(th *TestHelper, t *testing.T) {
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -116,6 +117,7 @@ func TestUpdateBookmark(t *testing.T) {
|
|||||||
|
|
||||||
file2 := &model.FileInfo{
|
file2 := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -144,6 +146,106 @@ func TestUpdateBookmark(t *testing.T) {
|
|||||||
require.Nil(t, bookmarkResp)
|
require.Nil(t, bookmarkResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testUpdateInvalidFiles = func(th *TestHelper, t *testing.T, creatingUserId string, updatingUserId string) {
|
||||||
|
file := &model.FileInfo{
|
||||||
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
CreatorId: model.BookmarkFileOwner,
|
||||||
|
Path: "somepath",
|
||||||
|
ThumbnailPath: "thumbpath",
|
||||||
|
PreviewPath: "prevPath",
|
||||||
|
Name: "test file",
|
||||||
|
Extension: "png",
|
||||||
|
MimeType: "images/png",
|
||||||
|
Size: 873182,
|
||||||
|
Width: 3076,
|
||||||
|
Height: 2200,
|
||||||
|
HasPreviewImage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := th.App.Srv().Store().FileInfo().Save(th.Context, file)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, file.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
th.Context.Session().UserId = creatingUserId
|
||||||
|
|
||||||
|
bookmark := createBookmark("File to be updated", model.ChannelBookmarkFile, th.BasicChannel.Id, file.Id)
|
||||||
|
bookmarkToEdit, appErr := th.App.CreateChannelBookmark(th.Context, bookmark, "")
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
require.NotNil(t, bookmarkToEdit)
|
||||||
|
|
||||||
|
otherChannel := th.CreateChannel(th.Context, th.BasicTeam)
|
||||||
|
|
||||||
|
createAt := time.Now().Add(-1 * time.Minute)
|
||||||
|
deleteAt := createAt.Add(1 * time.Second)
|
||||||
|
|
||||||
|
deletedFile := &model.FileInfo{
|
||||||
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
CreatorId: model.BookmarkFileOwner,
|
||||||
|
Path: "somepath",
|
||||||
|
ThumbnailPath: "thumbpath",
|
||||||
|
PreviewPath: "prevPath",
|
||||||
|
Name: "test file",
|
||||||
|
Extension: "png",
|
||||||
|
MimeType: "images/png",
|
||||||
|
Size: 873182,
|
||||||
|
Width: 3076,
|
||||||
|
Height: 2200,
|
||||||
|
HasPreviewImage: true,
|
||||||
|
CreateAt: createAt.UnixMilli(),
|
||||||
|
UpdateAt: createAt.UnixMilli(),
|
||||||
|
DeleteAt: deleteAt.UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = th.App.Srv().Store().FileInfo().Save(th.Context, deletedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, deletedFile.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
th.Context.Session().UserId = updatingUserId
|
||||||
|
|
||||||
|
updateBookmarkPending := bookmarkToEdit.Clone()
|
||||||
|
updateBookmarkPending.FileId = deletedFile.Id
|
||||||
|
bookmarkEdited, appErr := th.App.UpdateChannelBookmark(th.Context, updateBookmarkPending, "")
|
||||||
|
assert.NotNil(t, appErr)
|
||||||
|
require.Nil(t, bookmarkEdited)
|
||||||
|
|
||||||
|
anotherChannelFile := &model.FileInfo{
|
||||||
|
Id: model.NewId(),
|
||||||
|
ChannelId: otherChannel.Id,
|
||||||
|
CreatorId: model.BookmarkFileOwner,
|
||||||
|
Path: "somepath",
|
||||||
|
ThumbnailPath: "thumbpath",
|
||||||
|
PreviewPath: "prevPath",
|
||||||
|
Name: "test file",
|
||||||
|
Extension: "png",
|
||||||
|
MimeType: "images/png",
|
||||||
|
Size: 873182,
|
||||||
|
Width: 3076,
|
||||||
|
Height: 2200,
|
||||||
|
HasPreviewImage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = th.App.Srv().Store().FileInfo().Save(th.Context, anotherChannelFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, anotherChannelFile.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
updateBookmarkPending = bookmarkToEdit.Clone()
|
||||||
|
updateBookmarkPending.FileId = anotherChannelFile.Id
|
||||||
|
bookmarkEdited, appErr = th.App.UpdateChannelBookmark(th.Context, updateBookmarkPending, "")
|
||||||
|
assert.NotNil(t, appErr)
|
||||||
|
require.Nil(t, bookmarkEdited)
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("same user update a channel bookmark", func(t *testing.T) {
|
t.Run("same user update a channel bookmark", func(t *testing.T) {
|
||||||
bookmark1 := &model.ChannelBookmark{
|
bookmark1 := &model.ChannelBookmark{
|
||||||
ChannelId: th.BasicChannel.Id,
|
ChannelId: th.BasicChannel.Id,
|
||||||
@ -166,6 +268,8 @@ func TestUpdateBookmark(t *testing.T) {
|
|||||||
assert.Greater(t, response.Updated.UpdateAt, response.Updated.CreateAt)
|
assert.Greater(t, response.Updated.UpdateAt, response.Updated.CreateAt)
|
||||||
|
|
||||||
testUpdateAnotherFile(th, t)
|
testUpdateAnotherFile(th, t)
|
||||||
|
|
||||||
|
testUpdateInvalidFiles(th, t, th.BasicUser.Id, th.BasicUser.Id)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("another user update a channel bookmark", func(t *testing.T) {
|
t.Run("another user update a channel bookmark", func(t *testing.T) {
|
||||||
@ -181,6 +285,8 @@ func TestUpdateBookmark(t *testing.T) {
|
|||||||
assert.Equal(t, "New name", response.Deleted.DisplayName)
|
assert.Equal(t, "New name", response.Deleted.DisplayName)
|
||||||
|
|
||||||
testUpdateAnotherFile(th, t)
|
testUpdateAnotherFile(th, t)
|
||||||
|
|
||||||
|
testUpdateInvalidFiles(th, t, th.BasicUser.Id, th.BasicUser.Id)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("update an already deleted channel bookmark", func(t *testing.T) {
|
t.Run("update an already deleted channel bookmark", func(t *testing.T) {
|
||||||
@ -265,6 +371,7 @@ func TestGetChannelBookmarks(t *testing.T) {
|
|||||||
|
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -346,6 +453,7 @@ func TestUpdateChannelBookmarkSortOrder(t *testing.T) {
|
|||||||
|
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
|
@ -6,13 +6,16 @@ package app
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CustomProfileAttributesFieldLimit = 20
|
const (
|
||||||
|
CustomProfileAttributesFieldLimit = 20
|
||||||
|
)
|
||||||
|
|
||||||
var cpaGroupID string
|
var cpaGroupID string
|
||||||
|
|
||||||
@ -58,7 +61,6 @@ func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
|
|||||||
|
|
||||||
opts := model.PropertyFieldSearchOpts{
|
opts := model.PropertyFieldSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: CustomProfileAttributesFieldLimit,
|
PerPage: CustomProfileAttributesFieldLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +69,10 @@ func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
|
|||||||
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(fields, func(i, j int) bool {
|
||||||
|
return model.CustomProfileAttributesPropertySortOrder(fields[i]) < model.CustomProfileAttributesPropertySortOrder(fields[j])
|
||||||
|
})
|
||||||
|
|
||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +82,12 @@ func (a *App) CreateCPAField(field *model.PropertyField) (*model.PropertyField,
|
|||||||
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
existingFields, appErr := a.ListCPAFields()
|
fieldCount, err := a.Srv().propertyService.CountActivePropertyFieldsForGroup(groupID)
|
||||||
if appErr != nil {
|
if err != nil {
|
||||||
return nil, appErr
|
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.count_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(existingFields) >= CustomProfileAttributesFieldLimit {
|
if fieldCount >= CustomProfileAttributesFieldLimit {
|
||||||
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
|
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +103,10 @@ func (a *App) CreateCPAField(field *model.PropertyField) (*model.PropertyField,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldCreated, "", "", "", nil, "")
|
||||||
|
message.Add("field", newField)
|
||||||
|
a.Publish(message)
|
||||||
|
|
||||||
return newField, nil
|
return newField, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +132,10 @@ func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldUpdated, "", "", "", nil, "")
|
||||||
|
message.Add("field", patchedField)
|
||||||
|
a.Publish(message)
|
||||||
|
|
||||||
return patchedField, nil
|
return patchedField, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +164,10 @@ func (a *App) DeleteCPAField(id string) *model.AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldDeleted, "", "", "", nil, "")
|
||||||
|
message.Add("field_id", id)
|
||||||
|
a.Publish(message)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,19 +177,16 @@ func (a *App) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppEr
|
|||||||
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := model.PropertyValueSearchOpts{
|
values, err := a.Srv().propertyService.SearchPropertyValues(model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
TargetID: userID,
|
TargetID: userID,
|
||||||
Page: 0,
|
PerPage: CustomProfileAttributesFieldLimit,
|
||||||
PerPage: 999999,
|
})
|
||||||
IncludeDeleted: false,
|
|
||||||
}
|
|
||||||
fields, err := a.Srv().propertyService.SearchPropertyValues(opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
|
func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
|
||||||
@ -193,49 +208,54 @@ func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) PatchCPAValue(userID string, fieldID string, value json.RawMessage) (*model.PropertyValue, *model.AppError) {
|
func (a *App) PatchCPAValue(userID string, fieldID string, value json.RawMessage) (*model.PropertyValue, *model.AppError) {
|
||||||
|
values, appErr := a.PatchCPAValues(userID, map[string]json.RawMessage{fieldID: value})
|
||||||
|
if appErr != nil {
|
||||||
|
return nil, appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return values[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PatchCPAValues(userID string, fieldValueMap map[string]json.RawMessage) ([]*model.PropertyValue, *model.AppError) {
|
||||||
groupID, err := a.cpaGroupID()
|
groupID, err := a.cpaGroupID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure field exists in this group
|
valuesToUpdate := []*model.PropertyValue{}
|
||||||
existingField, appErr := a.GetCPAField(fieldID)
|
for fieldID, value := range fieldValueMap {
|
||||||
if appErr != nil {
|
// make sure field exists in this group
|
||||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
|
existingField, appErr := a.GetCPAField(fieldID)
|
||||||
} else if existingField.DeleteAt > 0 {
|
if appErr != nil {
|
||||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
|
||||||
}
|
} else if existingField.DeleteAt > 0 {
|
||||||
|
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
|
||||||
existingValues, appErr := a.ListCPAValues(userID)
|
|
||||||
if appErr != nil {
|
|
||||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_list.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
||||||
}
|
|
||||||
var existingValue *model.PropertyValue
|
|
||||||
for key, value := range existingValues {
|
|
||||||
if value.FieldID == fieldID {
|
|
||||||
existingValue = existingValues[key]
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if existingValue != nil {
|
value := &model.PropertyValue{
|
||||||
existingValue.Value = value
|
|
||||||
_, err = a.ch.srv.propertyService.UpdatePropertyValue(existingValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
propertyValue := &model.PropertyValue{
|
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
TargetType: "user",
|
TargetType: "user",
|
||||||
TargetID: userID,
|
TargetID: userID,
|
||||||
FieldID: fieldID,
|
FieldID: fieldID,
|
||||||
Value: value,
|
Value: value,
|
||||||
}
|
}
|
||||||
existingValue, err = a.ch.srv.propertyService.CreatePropertyValue(propertyValue)
|
valuesToUpdate = append(valuesToUpdate, value)
|
||||||
if err != nil {
|
|
||||||
return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_creation.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return existingValue, nil
|
|
||||||
|
updatedValues, err := a.Srv().propertyService.UpsertPropertyValues(valuesToUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_value_upsert.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedFieldValueMap := map[string]json.RawMessage{}
|
||||||
|
for _, value := range updatedValues {
|
||||||
|
updatedFieldValueMap[value.FieldID] = value.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WebsocketEventCPAValuesUpdated, "", "", "", nil, "")
|
||||||
|
message.Add("user_id", userID)
|
||||||
|
message.Add("values", updatedFieldValueMap)
|
||||||
|
a.Publish(message)
|
||||||
|
|
||||||
|
return updatedValues, nil
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func TestGetCPAField(t *testing.T) {
|
|||||||
GroupID: cpaGroupID,
|
GroupID: cpaGroupID,
|
||||||
Name: "Test Field",
|
Name: "Test Field",
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
Attrs: map[string]any{"visibility": "hidden"},
|
Attrs: model.StringInterface{"visibility": "hidden"},
|
||||||
}
|
}
|
||||||
|
|
||||||
createdField, err := th.App.CreateCPAField(field)
|
createdField, err := th.App.CreateCPAField(field)
|
||||||
@ -76,13 +76,14 @@ func TestListCPAFields(t *testing.T) {
|
|||||||
require.NoError(t, cErr)
|
require.NoError(t, cErr)
|
||||||
|
|
||||||
t.Run("should list the CPA property fields", func(t *testing.T) {
|
t.Run("should list the CPA property fields", func(t *testing.T) {
|
||||||
field1 := &model.PropertyField{
|
field1 := model.PropertyField{
|
||||||
GroupID: cpaGroupID,
|
GroupID: cpaGroupID,
|
||||||
Name: "Field 1",
|
Name: "Field 1",
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
|
Attrs: model.StringInterface{model.CustomProfileAttributesPropertyAttrsSortOrder: 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := th.App.Srv().propertyService.CreatePropertyField(field1)
|
_, err := th.App.Srv().propertyService.CreatePropertyField(&field1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
field2 := &model.PropertyField{
|
field2 := &model.PropertyField{
|
||||||
@ -93,23 +94,20 @@ func TestListCPAFields(t *testing.T) {
|
|||||||
_, err = th.App.Srv().propertyService.CreatePropertyField(field2)
|
_, err = th.App.Srv().propertyService.CreatePropertyField(field2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
field3 := &model.PropertyField{
|
field3 := model.PropertyField{
|
||||||
GroupID: cpaGroupID,
|
GroupID: cpaGroupID,
|
||||||
Name: "Field 3",
|
Name: "Field 3",
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
|
Attrs: model.StringInterface{model.CustomProfileAttributesPropertyAttrsSortOrder: 0},
|
||||||
}
|
}
|
||||||
_, err = th.App.Srv().propertyService.CreatePropertyField(field3)
|
_, err = th.App.Srv().propertyService.CreatePropertyField(&field3)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fields, appErr := th.App.ListCPAFields()
|
fields, appErr := th.App.ListCPAFields()
|
||||||
require.Nil(t, appErr)
|
require.Nil(t, appErr)
|
||||||
require.Len(t, fields, 2)
|
require.Len(t, fields, 2)
|
||||||
|
require.Equal(t, "Field 3", fields[0].Name)
|
||||||
fieldNames := []string{}
|
require.Equal(t, "Field 1", fields[1].Name)
|
||||||
for _, field := range fields {
|
|
||||||
fieldNames = append(fieldNames, field.Name)
|
|
||||||
}
|
|
||||||
require.ElementsMatch(t, []string{"Field 1", "Field 3"}, fieldNames)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +144,7 @@ func TestCreateCPAField(t *testing.T) {
|
|||||||
GroupID: cpaGroupID,
|
GroupID: cpaGroupID,
|
||||||
Name: model.NewId(),
|
Name: model.NewId(),
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
Attrs: map[string]any{"visibility": "hidden"},
|
Attrs: model.StringInterface{"visibility": "hidden"},
|
||||||
}
|
}
|
||||||
|
|
||||||
createdField, err := th.App.CreateCPAField(field)
|
createdField, err := th.App.CreateCPAField(field)
|
||||||
@ -226,14 +224,14 @@ func TestPatchCPAField(t *testing.T) {
|
|||||||
GroupID: cpaGroupID,
|
GroupID: cpaGroupID,
|
||||||
Name: model.NewId(),
|
Name: model.NewId(),
|
||||||
Type: model.PropertyFieldTypeText,
|
Type: model.PropertyFieldTypeText,
|
||||||
Attrs: map[string]any{"visibility": "hidden"},
|
Attrs: model.StringInterface{"visibility": "hidden"},
|
||||||
}
|
}
|
||||||
createdField, err := th.App.CreateCPAField(newField)
|
createdField, err := th.App.CreateCPAField(newField)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
patch := &model.PropertyFieldPatch{
|
patch := &model.PropertyFieldPatch{
|
||||||
Name: model.NewPointer("Patched name"),
|
Name: model.NewPointer("Patched name"),
|
||||||
Attrs: model.NewPointer(map[string]any{"visibility": "default"}),
|
Attrs: model.NewPointer(model.StringInterface{"visibility": "default"}),
|
||||||
TargetID: model.NewPointer(model.NewId()),
|
TargetID: model.NewPointer(model.NewId()),
|
||||||
TargetType: model.NewPointer(model.NewId()),
|
TargetType: model.NewPointer(model.NewId()),
|
||||||
}
|
}
|
||||||
@ -517,3 +515,60 @@ func TestPatchCPAValue(t *testing.T) {
|
|||||||
require.Equal(t, userID, updatedValue.TargetID)
|
require.Equal(t, userID, updatedValue.TargetID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListCPAValues(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
cpaGroupID, cErr := th.App.cpaGroupID()
|
||||||
|
require.NoError(t, cErr)
|
||||||
|
|
||||||
|
userID := model.NewId()
|
||||||
|
|
||||||
|
t.Run("should return empty list when user has no values", func(t *testing.T) {
|
||||||
|
values, appErr := th.App.ListCPAValues(userID)
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
require.Empty(t, values)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should list all values for a user", func(t *testing.T) {
|
||||||
|
var expectedValues []json.RawMessage
|
||||||
|
|
||||||
|
for i := 1; i <= CustomProfileAttributesFieldLimit; i++ {
|
||||||
|
field := &model.PropertyField{
|
||||||
|
GroupID: cpaGroupID,
|
||||||
|
Name: fmt.Sprintf("Field %d", i),
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := th.App.Srv().propertyService.CreatePropertyField(field)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
value := &model.PropertyValue{
|
||||||
|
TargetID: userID,
|
||||||
|
TargetType: "user",
|
||||||
|
GroupID: cpaGroupID,
|
||||||
|
FieldID: field.ID,
|
||||||
|
Value: json.RawMessage(fmt.Sprintf(`"Value %d"`, i)),
|
||||||
|
}
|
||||||
|
_, err = th.App.Srv().propertyService.CreatePropertyValue(value)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedValues = append(expectedValues, value.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List values for original user
|
||||||
|
values, appErr := th.App.ListCPAValues(userID)
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
require.Len(t, values, CustomProfileAttributesFieldLimit)
|
||||||
|
|
||||||
|
actualValues := make([]json.RawMessage, len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
require.Equal(t, userID, value.TargetID)
|
||||||
|
require.Equal(t, "user", value.TargetType)
|
||||||
|
require.Equal(t, cpaGroupID, value.GroupID)
|
||||||
|
actualValues[i] = value.Value
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, expectedValues, actualValues)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa
|
|||||||
for _, c := range report.Counters {
|
for _, c := range report.Counters {
|
||||||
switch c.Metric {
|
switch c.Metric {
|
||||||
case model.ClientLongTasks:
|
case model.ClientLongTasks:
|
||||||
a.Metrics().IncrementClientLongTasks(commonLabels["platform"], commonLabels["agent"], c.Value)
|
a.Metrics().IncrementClientLongTasks(commonLabels["platform"], commonLabels["agent"], userID, c.Value)
|
||||||
default:
|
default:
|
||||||
// we intentionally skip unknown metrics
|
// we intentionally skip unknown metrics
|
||||||
}
|
}
|
||||||
@ -50,22 +50,26 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa
|
|||||||
case model.ClientFirstContentfulPaint:
|
case model.ClientFirstContentfulPaint:
|
||||||
a.Metrics().ObserveClientFirstContentfulPaint(commonLabels["platform"],
|
a.Metrics().ObserveClientFirstContentfulPaint(commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientLargestContentfulPaint:
|
case model.ClientLargestContentfulPaint:
|
||||||
a.Metrics().ObserveClientLargestContentfulPaint(
|
a.Metrics().ObserveClientLargestContentfulPaint(
|
||||||
commonLabels["platform"],
|
commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
h.GetLabelValue("region", model.AcceptedLCPRegions, "other"),
|
h.GetLabelValue("region", model.AcceptedLCPRegions, "other"),
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientInteractionToNextPaint:
|
case model.ClientInteractionToNextPaint:
|
||||||
a.Metrics().ObserveClientInteractionToNextPaint(
|
a.Metrics().ObserveClientInteractionToNextPaint(
|
||||||
commonLabels["platform"],
|
commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
h.GetLabelValue("interaction", model.AcceptedInteractions, "other"),
|
h.GetLabelValue("interaction", model.AcceptedInteractions, "other"),
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientCumulativeLayoutShift:
|
case model.ClientCumulativeLayoutShift:
|
||||||
a.Metrics().ObserveClientCumulativeLayoutShift(commonLabels["platform"],
|
a.Metrics().ObserveClientCumulativeLayoutShift(commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
|
userID,
|
||||||
h.Value)
|
h.Value)
|
||||||
case model.ClientPageLoadDuration:
|
case model.ClientPageLoadDuration:
|
||||||
a.Metrics().ObserveClientPageLoadDuration(commonLabels["platform"],
|
a.Metrics().ObserveClientPageLoadDuration(commonLabels["platform"],
|
||||||
@ -76,20 +80,24 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa
|
|||||||
commonLabels["platform"],
|
commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""),
|
h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""),
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientTeamSwitchDuration:
|
case model.ClientTeamSwitchDuration:
|
||||||
a.Metrics().ObserveClientTeamSwitchDuration(
|
a.Metrics().ObserveClientTeamSwitchDuration(
|
||||||
commonLabels["platform"],
|
commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""),
|
h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""),
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientRHSLoadDuration:
|
case model.ClientRHSLoadDuration:
|
||||||
a.Metrics().ObserveClientRHSLoadDuration(commonLabels["platform"],
|
a.Metrics().ObserveClientRHSLoadDuration(commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.ClientGlobalThreadsLoadDuration:
|
case model.ClientGlobalThreadsLoadDuration:
|
||||||
a.Metrics().ObserveGlobalThreadsLoadDuration(commonLabels["platform"],
|
a.Metrics().ObserveGlobalThreadsLoadDuration(commonLabels["platform"],
|
||||||
commonLabels["agent"],
|
commonLabels["agent"],
|
||||||
|
userID,
|
||||||
h.Value/1000)
|
h.Value/1000)
|
||||||
case model.MobileClientLoadDuration:
|
case model.MobileClientLoadDuration:
|
||||||
a.Metrics().ObserveMobileClientLoadDuration(commonLabels["platform"],
|
a.Metrics().ObserveMobileClientLoadDuration(commonLabels["platform"],
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
|
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -95,8 +96,10 @@ type WebConn struct {
|
|||||||
UserId string
|
UserId string
|
||||||
PostedAck bool
|
PostedAck bool
|
||||||
|
|
||||||
lastUserActivityAt int64
|
allChannelMembers map[string]string
|
||||||
send chan model.WebSocketMessage
|
lastAllChannelMembersTime int64
|
||||||
|
lastUserActivityAt int64
|
||||||
|
send chan model.WebSocketMessage
|
||||||
// deadQueue behaves like a queue of a finite size
|
// deadQueue behaves like a queue of a finite size
|
||||||
// which is used to store all messages that are sent via the websocket.
|
// which is used to store all messages that are sent via the websocket.
|
||||||
// It basically acts as the user-space socket buffer, and is used
|
// It basically acts as the user-space socket buffer, and is used
|
||||||
@ -758,6 +761,8 @@ func (wc *WebConn) drainDeadQueue(index int) error {
|
|||||||
|
|
||||||
// InvalidateCache resets all internal data of the WebConn.
|
// InvalidateCache resets all internal data of the WebConn.
|
||||||
func (wc *WebConn) InvalidateCache() {
|
func (wc *WebConn) InvalidateCache() {
|
||||||
|
wc.allChannelMembers = nil
|
||||||
|
wc.lastAllChannelMembersTime = 0
|
||||||
wc.SetSession(nil)
|
wc.SetSession(nil)
|
||||||
wc.SetSessionExpiresAt(0)
|
wc.SetSessionExpiresAt(0)
|
||||||
}
|
}
|
||||||
@ -938,9 +943,36 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to do any further checks because this is already scoped
|
if *wc.Platform.Config().ServiceSettings.EnableWebHubChannelIteration {
|
||||||
// to channel members from web_hub.
|
// We don't need to do any further checks because this is already scoped
|
||||||
return true
|
// to channel members from web_hub.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime {
|
||||||
|
wc.allChannelMembers = nil
|
||||||
|
wc.lastAllChannelMembersTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if wc.allChannelMembers == nil {
|
||||||
|
result, err := wc.Platform.Store.Channel().GetAllChannelMembersForUser(
|
||||||
|
sqlstore.RequestContextWithMaster(request.EmptyContext(wc.Platform.logger)),
|
||||||
|
wc.UserId,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
wc.allChannelMembers = result
|
||||||
|
wc.lastAllChannelMembersTime = model.GetMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := wc.allChannelMembers[chID]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only report events to users who are in the team for the event
|
// Only report events to users who are in the team for the event
|
||||||
|
@ -486,6 +486,7 @@ func (h *Hub) Start() {
|
|||||||
connIndex := newHubConnectionIndex(inactiveConnReaperInterval,
|
connIndex := newHubConnectionIndex(inactiveConnReaperInterval,
|
||||||
h.platform.Store,
|
h.platform.Store,
|
||||||
h.platform.logger,
|
h.platform.logger,
|
||||||
|
*h.platform.Config().ServiceSettings.EnableWebHubChannelIteration,
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -599,6 +600,11 @@ func (h *Hub) Start() {
|
|||||||
for _, webConn := range connIndex.ForUser(userID) {
|
for _, webConn := range connIndex.ForUser(userID) {
|
||||||
webConn.InvalidateCache()
|
webConn.InvalidateCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*h.platform.Config().ServiceSettings.EnableWebHubChannelIteration {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
err := connIndex.InvalidateCMCacheForUser(userID)
|
err := connIndex.InvalidateCMCacheForUser(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.platform.Log().Error("Error while invalidating channel member cache", mlog.String("user_id", userID), mlog.Err(err))
|
h.platform.Log().Error("Error while invalidating channel member cache", mlog.String("user_id", userID), mlog.Err(err))
|
||||||
@ -666,7 +672,7 @@ func (h *Hub) Start() {
|
|||||||
}
|
}
|
||||||
} else if userID := msg.GetBroadcast().UserId; userID != "" {
|
} else if userID := msg.GetBroadcast().UserId; userID != "" {
|
||||||
targetConns = connIndex.ForUser(userID)
|
targetConns = connIndex.ForUser(userID)
|
||||||
} else if channelID := msg.GetBroadcast().ChannelId; channelID != "" {
|
} else if channelID := msg.GetBroadcast().ChannelId; channelID != "" && *h.platform.Config().ServiceSettings.EnableWebHubChannelIteration {
|
||||||
targetConns = connIndex.ForChannel(channelID)
|
targetConns = connIndex.ForChannel(channelID)
|
||||||
}
|
}
|
||||||
if targetConns != nil {
|
if targetConns != nil {
|
||||||
@ -733,6 +739,10 @@ func closeAndRemoveConn(connIndex *hubConnectionIndex, conn *WebConn) {
|
|||||||
connIndex.Remove(conn)
|
connIndex.Remove(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type connMetadata struct {
|
||||||
|
channelIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
|
// hubConnectionIndex provides fast addition, removal, and iteration of web connections.
|
||||||
// It requires 4 functionalities which need to be very fast:
|
// It requires 4 functionalities which need to be very fast:
|
||||||
// - check if a connection exists or not.
|
// - check if a connection exists or not.
|
||||||
@ -740,90 +750,95 @@ func closeAndRemoveConn(connIndex *hubConnectionIndex, conn *WebConn) {
|
|||||||
// - get all connections for a given channelID.
|
// - get all connections for a given channelID.
|
||||||
// - get all connections.
|
// - get all connections.
|
||||||
type hubConnectionIndex struct {
|
type hubConnectionIndex struct {
|
||||||
// byUserId stores the list of connections for a given userID
|
// byUserId stores the set of connections for a given userID
|
||||||
byUserId map[string][]*WebConn
|
byUserId map[string]map[*WebConn]struct{}
|
||||||
// byChannelID stores the list of connections for a given channelID.
|
// byChannelID stores the set of connections for a given channelID
|
||||||
byChannelID map[string][]*WebConn
|
byChannelID map[string]map[*WebConn]struct{}
|
||||||
// byConnection serves the dual purpose of storing the index of the webconn
|
// byConnection serves the dual purpose of storing the channelIDs
|
||||||
// in the value of byUserId map, and also to get all connections.
|
// and also to get all connections
|
||||||
byConnection map[*WebConn]int
|
byConnection map[*WebConn]connMetadata
|
||||||
byConnectionId map[string]*WebConn
|
byConnectionId map[string]*WebConn
|
||||||
// staleThreshold is the limit beyond which inactive connections
|
// staleThreshold is the limit beyond which inactive connections
|
||||||
// will be deleted.
|
// will be deleted.
|
||||||
staleThreshold time.Duration
|
staleThreshold time.Duration
|
||||||
|
|
||||||
store store.Store
|
fastIteration bool
|
||||||
logger mlog.LoggerIFace
|
store store.Store
|
||||||
|
logger mlog.LoggerIFace
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHubConnectionIndex(interval time.Duration,
|
func newHubConnectionIndex(interval time.Duration,
|
||||||
store store.Store,
|
store store.Store,
|
||||||
logger mlog.LoggerIFace,
|
logger mlog.LoggerIFace,
|
||||||
|
fastIteration bool,
|
||||||
) *hubConnectionIndex {
|
) *hubConnectionIndex {
|
||||||
return &hubConnectionIndex{
|
return &hubConnectionIndex{
|
||||||
byUserId: make(map[string][]*WebConn),
|
byUserId: make(map[string]map[*WebConn]struct{}),
|
||||||
byChannelID: make(map[string][]*WebConn),
|
byChannelID: make(map[string]map[*WebConn]struct{}),
|
||||||
byConnection: make(map[*WebConn]int),
|
byConnection: make(map[*WebConn]connMetadata),
|
||||||
byConnectionId: make(map[string]*WebConn),
|
byConnectionId: make(map[string]*WebConn),
|
||||||
staleThreshold: interval,
|
staleThreshold: interval,
|
||||||
store: store,
|
store: store,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
fastIteration: fastIteration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *hubConnectionIndex) Add(wc *WebConn) error {
|
func (i *hubConnectionIndex) Add(wc *WebConn) error {
|
||||||
cm, err := i.store.Channel().GetAllChannelMembersForUser(request.EmptyContext(i.logger), wc.UserId, false, false)
|
var channelIDs []string
|
||||||
if err != nil {
|
if i.fastIteration {
|
||||||
return fmt.Errorf("error getChannelMembersForUser: %v", err)
|
cm, err := i.store.Channel().GetAllChannelMembersForUser(request.EmptyContext(i.logger), wc.UserId, false, false)
|
||||||
}
|
if err != nil {
|
||||||
for chID := range cm {
|
return fmt.Errorf("error getChannelMembersForUser: %v", err)
|
||||||
i.byChannelID[chID] = append(i.byChannelID[chID], wc)
|
}
|
||||||
|
|
||||||
|
// Store channel IDs and add to byChannelID
|
||||||
|
channelIDs = make([]string, 0, len(cm))
|
||||||
|
for chID := range cm {
|
||||||
|
channelIDs = append(channelIDs, chID)
|
||||||
|
|
||||||
|
// Initialize the channel's map if it doesn't exist
|
||||||
|
if _, ok := i.byChannelID[chID]; !ok {
|
||||||
|
i.byChannelID[chID] = make(map[*WebConn]struct{})
|
||||||
|
}
|
||||||
|
i.byChannelID[chID][wc] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i.byUserId[wc.UserId] = append(i.byUserId[wc.UserId], wc)
|
// Initialize the user's map if it doesn't exist
|
||||||
i.byConnection[wc] = len(i.byUserId[wc.UserId]) - 1
|
if _, ok := i.byUserId[wc.UserId]; !ok {
|
||||||
|
i.byUserId[wc.UserId] = make(map[*WebConn]struct{})
|
||||||
|
}
|
||||||
|
i.byUserId[wc.UserId][wc] = struct{}{}
|
||||||
|
i.byConnection[wc] = connMetadata{
|
||||||
|
channelIDs: channelIDs,
|
||||||
|
}
|
||||||
i.byConnectionId[wc.GetConnectionID()] = wc
|
i.byConnectionId[wc.GetConnectionID()] = wc
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *hubConnectionIndex) Remove(wc *WebConn) {
|
func (i *hubConnectionIndex) Remove(wc *WebConn) {
|
||||||
userConnIndex, ok := i.byConnection[wc]
|
connMeta, ok := i.byConnection[wc]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the wc from i.byUserId
|
// Remove from byUserId
|
||||||
// get the conn slice.
|
if userConns, ok := i.byUserId[wc.UserId]; ok {
|
||||||
userConnections := i.byUserId[wc.UserId]
|
delete(userConns, wc)
|
||||||
// get the last connection.
|
}
|
||||||
last := userConnections[len(userConnections)-1]
|
|
||||||
// https://go.dev/wiki/SliceTricks#delete-without-preserving-order
|
|
||||||
userConnections[userConnIndex] = last
|
|
||||||
userConnections[len(userConnections)-1] = nil
|
|
||||||
i.byUserId[wc.UserId] = userConnections[:len(userConnections)-1]
|
|
||||||
// set the index of the connection that was moved to the new index.
|
|
||||||
i.byConnection[last] = userConnIndex
|
|
||||||
|
|
||||||
connectionID := wc.GetConnectionID()
|
if i.fastIteration {
|
||||||
// Remove webconns from i.byChannelID
|
// Remove from byChannelID for each channel
|
||||||
// This has O(n) complexity. We are trading off speed while removing
|
for _, chID := range connMeta.channelIDs {
|
||||||
// a connection, to improve broadcasting a message.
|
if channelConns, ok := i.byChannelID[chID]; ok {
|
||||||
for chID, webConns := range i.byChannelID {
|
delete(channelConns, wc)
|
||||||
// https://go.dev/wiki/SliceTricks#filtering-without-allocating
|
|
||||||
filtered := webConns[:0]
|
|
||||||
for _, conn := range webConns {
|
|
||||||
if conn.GetConnectionID() != connectionID {
|
|
||||||
filtered = append(filtered, conn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := len(filtered); i < len(webConns); i++ {
|
|
||||||
webConns[i] = nil
|
|
||||||
}
|
|
||||||
i.byChannelID[chID] = filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(i.byConnection, wc)
|
delete(i.byConnection, wc)
|
||||||
delete(i.byConnectionId, connectionID)
|
delete(i.byConnectionId, wc.GetConnectionID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *hubConnectionIndex) InvalidateCMCacheForUser(userID string) error {
|
func (i *hubConnectionIndex) InvalidateCMCacheForUser(userID string) error {
|
||||||
@ -833,25 +848,40 @@ func (i *hubConnectionIndex) InvalidateCMCacheForUser(userID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out all user entries which belong to channels.
|
// Get all connections for this user
|
||||||
for chID, webConns := range i.byChannelID {
|
conns := i.ForUser(userID)
|
||||||
// https://go.dev/wiki/SliceTricks#filtering-without-allocating
|
|
||||||
filtered := webConns[:0]
|
// Remove all user connections from existing channels
|
||||||
for _, conn := range webConns {
|
for _, conn := range conns {
|
||||||
if conn.UserId != userID {
|
if meta, ok := i.byConnection[conn]; ok {
|
||||||
filtered = append(filtered, conn)
|
// Remove from old channels
|
||||||
|
for _, chID := range meta.channelIDs {
|
||||||
|
if channelConns, ok := i.byChannelID[chID]; ok {
|
||||||
|
delete(channelConns, conn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := len(filtered); i < len(webConns); i++ {
|
|
||||||
webConns[i] = nil
|
|
||||||
}
|
|
||||||
i.byChannelID[chID] = filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-populate the cache
|
// Add connections to new channels
|
||||||
for chID := range cm {
|
for _, conn := range conns {
|
||||||
i.byChannelID[chID] = append(i.byChannelID[chID], i.ForUser(userID)...)
|
newChannelIDs := make([]string, 0, len(cm))
|
||||||
|
for chID := range cm {
|
||||||
|
newChannelIDs = append(newChannelIDs, chID)
|
||||||
|
// Initialize channel map if needed
|
||||||
|
if _, ok := i.byChannelID[chID]; !ok {
|
||||||
|
i.byChannelID[chID] = make(map[*WebConn]struct{})
|
||||||
|
}
|
||||||
|
i.byChannelID[chID][conn] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection metadata
|
||||||
|
if meta, ok := i.byConnection[conn]; ok {
|
||||||
|
meta.channelIDs = newChannelIDs
|
||||||
|
i.byConnection[conn] = meta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -862,26 +892,31 @@ func (i *hubConnectionIndex) Has(wc *WebConn) bool {
|
|||||||
|
|
||||||
// ForUser returns all connections for a user ID.
|
// ForUser returns all connections for a user ID.
|
||||||
func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
|
func (i *hubConnectionIndex) ForUser(id string) []*WebConn {
|
||||||
// Fast path if there is only one or fewer connection.
|
userConns, ok := i.byUserId[id]
|
||||||
if len(i.byUserId[id]) <= 1 {
|
if !ok {
|
||||||
return i.byUserId[id]
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to using maps.Keys to use the iterator pattern with 1.23.
|
||||||
|
// This saves the additional slice copy.
|
||||||
|
conns := make([]*WebConn, 0, len(userConns))
|
||||||
|
for conn := range userConns {
|
||||||
|
conns = append(conns, conn)
|
||||||
}
|
}
|
||||||
// If there are multiple connections per user,
|
|
||||||
// then we have to return a clone of the slice
|
|
||||||
// to allow connIndex.Remove to be safely called while
|
|
||||||
// iterating the slice.
|
|
||||||
conns := make([]*WebConn, len(i.byUserId[id]))
|
|
||||||
copy(conns, i.byUserId[id])
|
|
||||||
return conns
|
return conns
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForChannel returns all connections for a channelID.
|
// ForChannel returns all connections for a channelID.
|
||||||
func (i *hubConnectionIndex) ForChannel(channelID string) []*WebConn {
|
func (i *hubConnectionIndex) ForChannel(channelID string) []*WebConn {
|
||||||
// Note: this is expensive because usually there will be
|
channelConns, ok := i.byChannelID[channelID]
|
||||||
// more than 1 member for a channel, and broadcasting
|
if !ok {
|
||||||
// is a hot path, but worth it.
|
return nil
|
||||||
conns := make([]*WebConn, len(i.byChannelID[channelID]))
|
}
|
||||||
copy(conns, i.byChannelID[channelID])
|
|
||||||
|
conns := make([]*WebConn, 0, len(channelConns))
|
||||||
|
for conn := range channelConns {
|
||||||
|
conns = append(conns, conn)
|
||||||
|
}
|
||||||
return conns
|
return conns
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -902,7 +937,7 @@ func (i *hubConnectionIndex) ForConnection(id string) *WebConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All returns the full webConn index.
|
// All returns the full webConn index.
|
||||||
func (i *hubConnectionIndex) All() map[*WebConn]int {
|
func (i *hubConnectionIndex) All() map[*WebConn]connMetadata {
|
||||||
return i.byConnection
|
return i.byConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ package platform
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -197,154 +198,158 @@ func TestHubConnIndex(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Basic", func(t *testing.T) {
|
for _, fastIterate := range []bool{true, false} {
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
t.Run(fmt.Sprintf("fastIterate=%t", fastIterate), func(t *testing.T) {
|
||||||
|
t.Run("Basic", func(t *testing.T) {
|
||||||
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, fastIterate)
|
||||||
|
|
||||||
// User1
|
// User1
|
||||||
wc1 := &WebConn{
|
wc1 := &WebConn{
|
||||||
Platform: th.Service,
|
Platform: th.Service,
|
||||||
Suite: th.Suite,
|
Suite: th.Suite,
|
||||||
UserId: model.NewId(),
|
UserId: model.NewId(),
|
||||||
}
|
}
|
||||||
wc1.SetConnectionID(model.NewId())
|
wc1.SetConnectionID(model.NewId())
|
||||||
wc1.SetSession(&model.Session{})
|
wc1.SetSession(&model.Session{})
|
||||||
|
|
||||||
// User2
|
// User2
|
||||||
wc2 := &WebConn{
|
wc2 := &WebConn{
|
||||||
Platform: th.Service,
|
Platform: th.Service,
|
||||||
Suite: th.Suite,
|
Suite: th.Suite,
|
||||||
UserId: model.NewId(),
|
UserId: model.NewId(),
|
||||||
}
|
}
|
||||||
wc2.SetConnectionID(model.NewId())
|
wc2.SetConnectionID(model.NewId())
|
||||||
wc2.SetSession(&model.Session{})
|
wc2.SetSession(&model.Session{})
|
||||||
|
|
||||||
wc3 := &WebConn{
|
wc3 := &WebConn{
|
||||||
Platform: th.Service,
|
Platform: th.Service,
|
||||||
Suite: th.Suite,
|
Suite: th.Suite,
|
||||||
UserId: wc2.UserId,
|
UserId: wc2.UserId,
|
||||||
}
|
}
|
||||||
wc3.SetConnectionID(model.NewId())
|
wc3.SetConnectionID(model.NewId())
|
||||||
wc3.SetSession(&model.Session{})
|
wc3.SetSession(&model.Session{})
|
||||||
|
|
||||||
wc4 := &WebConn{
|
wc4 := &WebConn{
|
||||||
Platform: th.Service,
|
Platform: th.Service,
|
||||||
Suite: th.Suite,
|
Suite: th.Suite,
|
||||||
UserId: wc2.UserId,
|
UserId: wc2.UserId,
|
||||||
}
|
}
|
||||||
wc4.SetConnectionID(model.NewId())
|
wc4.SetConnectionID(model.NewId())
|
||||||
wc4.SetSession(&model.Session{})
|
wc4.SetSession(&model.Session{})
|
||||||
|
|
||||||
connIndex.Add(wc1)
|
connIndex.Add(wc1)
|
||||||
connIndex.Add(wc2)
|
connIndex.Add(wc2)
|
||||||
connIndex.Add(wc3)
|
connIndex.Add(wc3)
|
||||||
connIndex.Add(wc4)
|
connIndex.Add(wc4)
|
||||||
|
|
||||||
t.Run("Basic", func(t *testing.T) {
|
t.Run("Basic", func(t *testing.T) {
|
||||||
assert.True(t, connIndex.Has(wc1))
|
assert.True(t, connIndex.Has(wc1))
|
||||||
assert.True(t, connIndex.Has(wc2))
|
assert.True(t, connIndex.Has(wc2))
|
||||||
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc3, wc4})
|
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc3, wc4})
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{wc1})
|
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{wc1})
|
||||||
assert.True(t, connIndex.Has(wc2))
|
assert.True(t, connIndex.Has(wc2))
|
||||||
assert.True(t, connIndex.Has(wc1))
|
assert.True(t, connIndex.Has(wc1))
|
||||||
assert.Len(t, connIndex.All(), 4)
|
assert.Len(t, connIndex.All(), 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveMiddleUser2", func(t *testing.T) {
|
||||||
|
connIndex.Remove(wc3) // Remove from middle from user2
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc4})
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{wc1})
|
||||||
|
assert.True(t, connIndex.Has(wc2))
|
||||||
|
assert.False(t, connIndex.Has(wc3))
|
||||||
|
assert.True(t, connIndex.Has(wc4))
|
||||||
|
assert.Len(t, connIndex.All(), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveUser1", func(t *testing.T) {
|
||||||
|
connIndex.Remove(wc1) // Remove sole connection from user1
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc4})
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{})
|
||||||
|
assert.Len(t, connIndex.ForUser(wc1.UserId), 0)
|
||||||
|
assert.Len(t, connIndex.All(), 2)
|
||||||
|
assert.False(t, connIndex.Has(wc1))
|
||||||
|
assert.True(t, connIndex.Has(wc2))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveEndUser2", func(t *testing.T) {
|
||||||
|
connIndex.Remove(wc4) // Remove from end from user2
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2})
|
||||||
|
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{})
|
||||||
|
assert.True(t, connIndex.Has(wc2))
|
||||||
|
assert.False(t, connIndex.Has(wc3))
|
||||||
|
assert.False(t, connIndex.Has(wc4))
|
||||||
|
assert.Len(t, connIndex.All(), 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ByConnectionId", func(t *testing.T) {
|
||||||
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, fastIterate)
|
||||||
|
|
||||||
|
// User1
|
||||||
|
wc1ID := model.NewId()
|
||||||
|
wc1 := &WebConn{
|
||||||
|
Platform: th.Service,
|
||||||
|
Suite: th.Suite,
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
}
|
||||||
|
wc1.SetConnectionID(wc1ID)
|
||||||
|
wc1.SetSession(&model.Session{})
|
||||||
|
|
||||||
|
// User2
|
||||||
|
wc2ID := model.NewId()
|
||||||
|
wc2 := &WebConn{
|
||||||
|
Platform: th.Service,
|
||||||
|
Suite: th.Suite,
|
||||||
|
UserId: th.BasicUser2.Id,
|
||||||
|
}
|
||||||
|
wc2.SetConnectionID(wc2ID)
|
||||||
|
wc2.SetSession(&model.Session{})
|
||||||
|
|
||||||
|
wc3ID := model.NewId()
|
||||||
|
wc3 := &WebConn{
|
||||||
|
Platform: th.Service,
|
||||||
|
Suite: th.Suite,
|
||||||
|
UserId: wc2.UserId,
|
||||||
|
}
|
||||||
|
wc3.SetConnectionID(wc3ID)
|
||||||
|
wc3.SetSession(&model.Session{})
|
||||||
|
|
||||||
|
t.Run("no connections", func(t *testing.T) {
|
||||||
|
assert.False(t, connIndex.Has(wc1))
|
||||||
|
assert.False(t, connIndex.Has(wc2))
|
||||||
|
assert.False(t, connIndex.Has(wc3))
|
||||||
|
assert.Empty(t, connIndex.byConnectionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding", func(t *testing.T) {
|
||||||
|
connIndex.Add(wc1)
|
||||||
|
connIndex.Add(wc3)
|
||||||
|
|
||||||
|
assert.Len(t, connIndex.byConnectionId, 2)
|
||||||
|
assert.Equal(t, wc1, connIndex.ForConnection(wc1ID))
|
||||||
|
assert.Equal(t, wc3, connIndex.ForConnection(wc3ID))
|
||||||
|
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc2ID))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("removing", func(t *testing.T) {
|
||||||
|
connIndex.Remove(wc3)
|
||||||
|
|
||||||
|
assert.Len(t, connIndex.byConnectionId, 1)
|
||||||
|
assert.Equal(t, wc1, connIndex.ForConnection(wc1ID))
|
||||||
|
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc3ID))
|
||||||
|
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc2ID))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
t.Run("RemoveMiddleUser2", func(t *testing.T) {
|
|
||||||
connIndex.Remove(wc3) // Remove from middle from user2
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc4})
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{wc1})
|
|
||||||
assert.True(t, connIndex.Has(wc2))
|
|
||||||
assert.False(t, connIndex.Has(wc3))
|
|
||||||
assert.True(t, connIndex.Has(wc4))
|
|
||||||
assert.Len(t, connIndex.All(), 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("RemoveUser1", func(t *testing.T) {
|
|
||||||
connIndex.Remove(wc1) // Remove sole connection from user1
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2, wc4})
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{})
|
|
||||||
assert.Len(t, connIndex.ForUser(wc1.UserId), 0)
|
|
||||||
assert.Len(t, connIndex.All(), 2)
|
|
||||||
assert.False(t, connIndex.Has(wc1))
|
|
||||||
assert.True(t, connIndex.Has(wc2))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("RemoveEndUser2", func(t *testing.T) {
|
|
||||||
connIndex.Remove(wc4) // Remove from end from user2
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc2.UserId), []*WebConn{wc2})
|
|
||||||
assert.ElementsMatch(t, connIndex.ForUser(wc1.UserId), []*WebConn{})
|
|
||||||
assert.True(t, connIndex.Has(wc2))
|
|
||||||
assert.False(t, connIndex.Has(wc3))
|
|
||||||
assert.False(t, connIndex.Has(wc4))
|
|
||||||
assert.Len(t, connIndex.All(), 1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ByConnectionId", func(t *testing.T) {
|
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
|
||||||
|
|
||||||
// User1
|
|
||||||
wc1ID := model.NewId()
|
|
||||||
wc1 := &WebConn{
|
|
||||||
Platform: th.Service,
|
|
||||||
Suite: th.Suite,
|
|
||||||
UserId: th.BasicUser.Id,
|
|
||||||
}
|
|
||||||
wc1.SetConnectionID(wc1ID)
|
|
||||||
wc1.SetSession(&model.Session{})
|
|
||||||
|
|
||||||
// User2
|
|
||||||
wc2ID := model.NewId()
|
|
||||||
wc2 := &WebConn{
|
|
||||||
Platform: th.Service,
|
|
||||||
Suite: th.Suite,
|
|
||||||
UserId: th.BasicUser2.Id,
|
|
||||||
}
|
|
||||||
wc2.SetConnectionID(wc2ID)
|
|
||||||
wc2.SetSession(&model.Session{})
|
|
||||||
|
|
||||||
wc3ID := model.NewId()
|
|
||||||
wc3 := &WebConn{
|
|
||||||
Platform: th.Service,
|
|
||||||
Suite: th.Suite,
|
|
||||||
UserId: wc2.UserId,
|
|
||||||
}
|
|
||||||
wc3.SetConnectionID(wc3ID)
|
|
||||||
wc3.SetSession(&model.Session{})
|
|
||||||
|
|
||||||
t.Run("no connections", func(t *testing.T) {
|
|
||||||
assert.False(t, connIndex.Has(wc1))
|
|
||||||
assert.False(t, connIndex.Has(wc2))
|
|
||||||
assert.False(t, connIndex.Has(wc3))
|
|
||||||
assert.Empty(t, connIndex.byConnectionId)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("adding", func(t *testing.T) {
|
|
||||||
connIndex.Add(wc1)
|
|
||||||
connIndex.Add(wc3)
|
|
||||||
|
|
||||||
assert.Len(t, connIndex.byConnectionId, 2)
|
|
||||||
assert.Equal(t, wc1, connIndex.ForConnection(wc1ID))
|
|
||||||
assert.Equal(t, wc3, connIndex.ForConnection(wc3ID))
|
|
||||||
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc2ID))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("removing", func(t *testing.T) {
|
|
||||||
connIndex.Remove(wc3)
|
|
||||||
|
|
||||||
assert.Len(t, connIndex.byConnectionId, 1)
|
|
||||||
assert.Equal(t, wc1, connIndex.ForConnection(wc1ID))
|
|
||||||
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc3ID))
|
|
||||||
assert.Equal(t, (*WebConn)(nil), connIndex.ForConnection(wc2ID))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ByChannelId", func(t *testing.T) {
|
t.Run("ByChannelId", func(t *testing.T) {
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, true)
|
||||||
|
|
||||||
// User1
|
// User1
|
||||||
wc1ID := model.NewId()
|
wc1ID := model.NewId()
|
||||||
@ -414,7 +419,7 @@ func TestHubConnIndexIncorrectRemoval(t *testing.T) {
|
|||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, false)
|
||||||
|
|
||||||
// User2
|
// User2
|
||||||
wc2 := &WebConn{
|
wc2 := &WebConn{
|
||||||
@ -461,7 +466,7 @@ func TestHubConnIndexInactive(t *testing.T) {
|
|||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
connIndex := newHubConnectionIndex(2*time.Second, th.Service.Store, th.Service.logger)
|
connIndex := newHubConnectionIndex(2*time.Second, th.Service.Store, th.Service.logger, false)
|
||||||
|
|
||||||
// User1
|
// User1
|
||||||
wc1 := &WebConn{
|
wc1 := &WebConn{
|
||||||
@ -621,7 +626,7 @@ func TestHubWebConnCount(t *testing.T) {
|
|||||||
func BenchmarkHubConnIndex(b *testing.B) {
|
func BenchmarkHubConnIndex(b *testing.B) {
|
||||||
th := Setup(b).InitBasic()
|
th := Setup(b).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, false)
|
||||||
|
|
||||||
// User1
|
// User1
|
||||||
wc1 := &WebConn{
|
wc1 := &WebConn{
|
||||||
@ -666,7 +671,7 @@ func TestHubConnIndexRemoveMemLeak(t *testing.T) {
|
|||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger)
|
connIndex := newHubConnectionIndex(1*time.Second, th.Service.Store, th.Service.logger, false)
|
||||||
|
|
||||||
wc := &WebConn{
|
wc := &WebConn{
|
||||||
Platform: th.Service,
|
Platform: th.Service,
|
||||||
|
@ -19,6 +19,10 @@ func (ps *PropertyService) GetPropertyFields(ids []string) ([]*model.PropertyFie
|
|||||||
return ps.fieldStore.GetMany(ids)
|
return ps.fieldStore.GetMany(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *PropertyService) CountActivePropertyFieldsForGroup(groupID string) (int64, error) {
|
||||||
|
return ps.fieldStore.CountForGroup(groupID, false)
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *PropertyService) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
|
func (ps *PropertyService) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
|
||||||
return ps.fieldStore.SearchPropertyFields(opts)
|
return ps.fieldStore.SearchPropertyFields(opts)
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,19 @@ func (ps *PropertyService) UpdatePropertyValues(values []*model.PropertyValue) (
|
|||||||
return ps.valueStore.Update(values)
|
return ps.valueStore.Update(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *PropertyService) UpsertPropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
|
||||||
|
values, err := ps.UpsertPropertyValues([]*model.PropertyValue{value})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return values[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PropertyService) UpsertPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
|
return ps.valueStore.Upsert(values)
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *PropertyService) DeletePropertyValue(id string) error {
|
func (ps *PropertyService) DeletePropertyValue(id string) error {
|
||||||
return ps.valueStore.Delete(id)
|
return ps.valueStore.Delete(id)
|
||||||
}
|
}
|
||||||
|
@ -257,6 +257,10 @@ channels/db/migrations/mysql/000129_add_property_system_architecture.down.sql
|
|||||||
channels/db/migrations/mysql/000129_add_property_system_architecture.up.sql
|
channels/db/migrations/mysql/000129_add_property_system_architecture.up.sql
|
||||||
channels/db/migrations/mysql/000130_system_console_stats.down.sql
|
channels/db/migrations/mysql/000130_system_console_stats.down.sql
|
||||||
channels/db/migrations/mysql/000130_system_console_stats.up.sql
|
channels/db/migrations/mysql/000130_system_console_stats.up.sql
|
||||||
|
channels/db/migrations/mysql/000131_create_index_pagination_on_property_values.down.sql
|
||||||
|
channels/db/migrations/mysql/000131_create_index_pagination_on_property_values.up.sql
|
||||||
|
channels/db/migrations/mysql/000132_create_index_pagination_on_property_fields.down.sql
|
||||||
|
channels/db/migrations/mysql/000132_create_index_pagination_on_property_fields.up.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||||
@ -515,3 +519,7 @@ channels/db/migrations/postgres/000129_add_property_system_architecture.down.sql
|
|||||||
channels/db/migrations/postgres/000129_add_property_system_architecture.up.sql
|
channels/db/migrations/postgres/000129_add_property_system_architecture.up.sql
|
||||||
channels/db/migrations/postgres/000130_system_console_stats.down.sql
|
channels/db/migrations/postgres/000130_system_console_stats.down.sql
|
||||||
channels/db/migrations/postgres/000130_system_console_stats.up.sql
|
channels/db/migrations/postgres/000130_system_console_stats.up.sql
|
||||||
|
channels/db/migrations/postgres/000131_create_index_pagination_on_property_values.down.sql
|
||||||
|
channels/db/migrations/postgres/000131_create_index_pagination_on_property_values.up.sql
|
||||||
|
channels/db/migrations/postgres/000132_create_index_pagination_on_property_fields.down.sql
|
||||||
|
channels/db/migrations/postgres/000132_create_index_pagination_on_property_fields.up.sql
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE table_name = 'PropertyValues'
|
||||||
|
AND table_schema = DATABASE()
|
||||||
|
AND index_name = 'idx_propertyvalues_create_at_id'
|
||||||
|
) > 0,
|
||||||
|
'DROP INDEX idx_propertyvalues_create_at_id ON PropertyValues;',
|
||||||
|
'SELECT 1'
|
||||||
|
));
|
||||||
|
|
||||||
|
PREPARE removeIndexIfExists FROM @preparedStatement;
|
||||||
|
EXECUTE removeIndexIfExists;
|
||||||
|
DEALLOCATE PREPARE removeIndexIfExists;
|
@ -0,0 +1,14 @@
|
|||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE table_name = 'PropertyValues'
|
||||||
|
AND table_schema = DATABASE()
|
||||||
|
AND index_name = 'idx_propertyvalues_create_at_id'
|
||||||
|
) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'CREATE INDEX idx_propertyvalues_create_at_id ON PropertyValues(CreateAt, ID);'
|
||||||
|
));
|
||||||
|
|
||||||
|
PREPARE createIndexIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE createIndexIfNotExists;
|
||||||
|
DEALLOCATE PREPARE createIndexIfNotExists;
|
@ -0,0 +1,14 @@
|
|||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE table_name = 'PropertyFields'
|
||||||
|
AND table_schema = DATABASE()
|
||||||
|
AND index_name = 'idx_propertyfields_create_at_id'
|
||||||
|
) > 0,
|
||||||
|
'DROP INDEX idx_propertyfields_create_at_id ON PropertyFields;',
|
||||||
|
'SELECT 1'
|
||||||
|
));
|
||||||
|
|
||||||
|
PREPARE removeIndexIfExists FROM @preparedStatement;
|
||||||
|
EXECUTE removeIndexIfExists;
|
||||||
|
DEALLOCATE PREPARE removeIndexIfExists;
|
@ -0,0 +1,14 @@
|
|||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE table_name = 'PropertyFields'
|
||||||
|
AND table_schema = DATABASE()
|
||||||
|
AND index_name = 'idx_propertyfields_create_at_id'
|
||||||
|
) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'CREATE INDEX idx_propertyfields_create_at_id ON PropertyFields(CreateAt, ID);'
|
||||||
|
));
|
||||||
|
|
||||||
|
PREPARE createIndexIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE createIndexIfNotExists;
|
||||||
|
DEALLOCATE PREPARE createIndexIfNotExists;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- morph:nontransactional
|
||||||
|
DROP INDEX CONCURRENTLY IF EXISTS idx_propertyvalues_create_at_id;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- morph:nontransactional
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_propertyvalues_create_at_id ON PropertyValues(CreateAt, ID)
|
@ -0,0 +1,2 @@
|
|||||||
|
-- morph:nontransactional
|
||||||
|
DROP INDEX CONCURRENTLY IF EXISTS idx_propertyfields_create_at_id;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- morph:nontransactional
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_propertyfields_create_at_id ON PropertyFields(CreateAt, ID)
|
@ -3153,11 +3153,11 @@ func (s *RetryLayerChannelBookmarkStore) Delete(bookmarkID string, deleteFile bo
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RetryLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string) error {
|
func (s *RetryLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string, channelID string) error {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
for {
|
for {
|
||||||
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileID)
|
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileID, channelID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -8949,6 +8949,27 @@ func (s *RetryLayerProductNoticesStore) View(userID string, notices []string) er
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerPropertyFieldStore) CountForGroup(groupID string, includeDeleted bool) (int64, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.PropertyFieldStore.CountForGroup(groupID, includeDeleted)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
func (s *RetryLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
@ -9054,11 +9075,11 @@ func (s *RetryLayerPropertyFieldStore) SearchPropertyFields(opts model.PropertyF
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RetryLayerPropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
|
func (s *RetryLayerPropertyFieldStore) Update(fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
for {
|
for {
|
||||||
result, err := s.PropertyFieldStore.Update(field)
|
result, err := s.PropertyFieldStore.Update(fields)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@ -9243,11 +9264,32 @@ func (s *RetryLayerPropertyValueStore) SearchPropertyValues(opts model.PropertyV
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RetryLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
func (s *RetryLayerPropertyValueStore) Update(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
for {
|
for {
|
||||||
result, err := s.PropertyValueStore.Update(field)
|
result, err := s.PropertyValueStore.Update(values)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerPropertyValueStore) Upsert(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.PropertyValueStore.Upsert(values)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func bookmarkWithFileInfoSliceColumns() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error {
|
func (s *SqlChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId string, channelId string) error {
|
||||||
existingQuery := s.getSubQueryBuilder().
|
existingQuery := s.getSubQueryBuilder().
|
||||||
Select("FileInfoId").
|
Select("FileInfoId").
|
||||||
From("ChannelBookmarks").
|
From("ChannelBookmarks").
|
||||||
@ -66,11 +66,13 @@ func (s *SqlChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileId
|
|||||||
Where(sq.Or{
|
Where(sq.Or{
|
||||||
sq.Expr("Id IN (?)", existingQuery),
|
sq.Expr("Id IN (?)", existingQuery),
|
||||||
sq.And{
|
sq.And{
|
||||||
|
sq.Eq{"Id": fileId},
|
||||||
sq.Or{
|
sq.Or{
|
||||||
sq.NotEq{"PostId": ""},
|
sq.NotEq{"PostId": ""},
|
||||||
sq.NotEq{"CreatorId": model.BookmarkFileOwner},
|
sq.NotEq{"CreatorId": model.BookmarkFileOwner},
|
||||||
|
sq.NotEq{"ChannelId": channelId},
|
||||||
|
sq.NotEq{"DeleteAt": 0},
|
||||||
},
|
},
|
||||||
sq.Eq{"Id": fileId},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -139,7 +141,7 @@ func (s *SqlChannelBookmarkStore) Save(bookmark *model.ChannelBookmark, increase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bookmark.FileId != "" {
|
if bookmark.FileId != "" {
|
||||||
err = s.ErrorIfBookmarkFileInfoAlreadyAttached(bookmark.FileId)
|
err = s.ErrorIfBookmarkFileInfoAlreadyAttached(bookmark.FileId, bookmark.ChannelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "unable_to_save_channel_bookmark")
|
return nil, errors.Wrap(err, "unable_to_save_channel_bookmark")
|
||||||
}
|
}
|
||||||
|
@ -78,9 +78,26 @@ func (s *SqlPropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, e
|
|||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SqlPropertyFieldStore) CountForGroup(groupID string, includeDeleted bool) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
builder := s.getQueryBuilder().
|
||||||
|
Select("COUNT(id)").
|
||||||
|
From("PropertyFields").
|
||||||
|
Where(sq.Eq{"GroupID": groupID})
|
||||||
|
|
||||||
|
if !includeDeleted {
|
||||||
|
builder = builder.Where(sq.Eq{"DeleteAt": 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.GetReplica().GetBuilder(&count, builder); err != nil {
|
||||||
|
return int64(0), errors.Wrap(err, "failed to count Sessions")
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SqlPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
|
func (s *SqlPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
|
||||||
if opts.Page < 0 {
|
if err := opts.Cursor.IsValid(); err != nil {
|
||||||
return nil, errors.New("page must be positive integer")
|
return nil, fmt.Errorf("cursor is invalid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PerPage < 1 {
|
if opts.PerPage < 1 {
|
||||||
@ -88,10 +105,19 @@ func (s *SqlPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSea
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder := s.tableSelectQuery.
|
builder := s.tableSelectQuery.
|
||||||
OrderBy("CreateAt ASC").
|
OrderBy("CreateAt ASC, Id ASC").
|
||||||
Offset(uint64(opts.Page * opts.PerPage)).
|
|
||||||
Limit(uint64(opts.PerPage))
|
Limit(uint64(opts.PerPage))
|
||||||
|
|
||||||
|
if !opts.Cursor.IsEmpty() {
|
||||||
|
builder = builder.Where(sq.Or{
|
||||||
|
sq.Gt{"CreateAt": opts.Cursor.CreateAt},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"CreateAt": opts.Cursor.CreateAt},
|
||||||
|
sq.Gt{"Id": opts.Cursor.PropertyFieldID},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.IncludeDeleted {
|
if !opts.IncludeDeleted {
|
||||||
builder = builder.Where(sq.Eq{"DeleteAt": 0})
|
builder = builder.Where(sq.Eq{"DeleteAt": 0})
|
||||||
}
|
}
|
||||||
@ -128,44 +154,66 @@ func (s *SqlPropertyFieldStore) Update(fields []*model.PropertyField) (_ []*mode
|
|||||||
defer finalizeTransactionX(transaction, &err)
|
defer finalizeTransactionX(transaction, &err)
|
||||||
|
|
||||||
updateTime := model.GetMillis()
|
updateTime := model.GetMillis()
|
||||||
for _, field := range fields {
|
isPostgres := s.DriverName() == model.DatabaseDriverPostgres
|
||||||
field.UpdateAt = updateTime
|
nameCase := sq.Case("id")
|
||||||
|
typeCase := sq.Case("id")
|
||||||
|
attrsCase := sq.Case("id")
|
||||||
|
targetIDCase := sq.Case("id")
|
||||||
|
targetTypeCase := sq.Case("id")
|
||||||
|
deleteAtCase := sq.Case("id")
|
||||||
|
ids := make([]string, len(fields))
|
||||||
|
|
||||||
|
for i, field := range fields {
|
||||||
|
field.UpdateAt = updateTime
|
||||||
if vErr := field.IsValid(); vErr != nil {
|
if vErr := field.IsValid(); vErr != nil {
|
||||||
return nil, errors.Wrap(vErr, "property_field_update_isvalid")
|
return nil, errors.Wrap(vErr, "property_field_update_isvalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
queryString, args, err := s.getQueryBuilder().
|
ids[i] = field.ID
|
||||||
Update("PropertyFields").
|
whenID := sq.Expr("?", field.ID)
|
||||||
Set("Name", field.Name).
|
if isPostgres {
|
||||||
Set("Type", field.Type).
|
nameCase = nameCase.When(whenID, sq.Expr("?::text", field.Name))
|
||||||
Set("Attrs", field.Attrs).
|
typeCase = typeCase.When(whenID, sq.Expr("?::property_field_type", field.Type))
|
||||||
Set("TargetID", field.TargetID).
|
attrsCase = attrsCase.When(whenID, sq.Expr("?::jsonb", field.Attrs))
|
||||||
Set("TargetType", field.TargetType).
|
targetIDCase = targetIDCase.When(whenID, sq.Expr("?::text", field.TargetID))
|
||||||
Set("UpdateAt", field.UpdateAt).
|
targetTypeCase = targetTypeCase.When(whenID, sq.Expr("?::text", field.TargetType))
|
||||||
Set("DeleteAt", field.DeleteAt).
|
deleteAtCase = deleteAtCase.When(whenID, sq.Expr("?::bigint", field.DeleteAt))
|
||||||
Where(sq.Eq{"id": field.ID}).
|
} else {
|
||||||
ToSql()
|
nameCase = nameCase.When(whenID, sq.Expr("?", field.Name))
|
||||||
if err != nil {
|
typeCase = typeCase.When(whenID, sq.Expr("?", field.Type))
|
||||||
return nil, errors.Wrap(err, "property_field_update_tosql")
|
attrsCase = attrsCase.When(whenID, sq.Expr("?", field.Attrs))
|
||||||
}
|
targetIDCase = targetIDCase.When(whenID, sq.Expr("?", field.TargetID))
|
||||||
|
targetTypeCase = targetTypeCase.When(whenID, sq.Expr("?", field.TargetType))
|
||||||
result, err := transaction.Exec(queryString, args...)
|
deleteAtCase = deleteAtCase.When(whenID, sq.Expr("?", field.DeleteAt))
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to update property field with id: %s", field.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "property_field_update_rowsaffected")
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
return nil, store.NewErrNotFound("PropertyField", field.ID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder := s.getQueryBuilder().
|
||||||
|
Update("PropertyFields").
|
||||||
|
Set("Name", nameCase).
|
||||||
|
Set("Type", typeCase).
|
||||||
|
Set("Attrs", attrsCase).
|
||||||
|
Set("TargetID", targetIDCase).
|
||||||
|
Set("TargetType", targetTypeCase).
|
||||||
|
Set("UpdateAt", updateTime).
|
||||||
|
Set("DeleteAt", deleteAtCase).
|
||||||
|
Where(sq.Eq{"id": ids})
|
||||||
|
|
||||||
|
result, err := transaction.ExecBuilder(builder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_field_update_exec")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_field_update_rowsaffected")
|
||||||
|
}
|
||||||
|
if count != int64(len(fields)) {
|
||||||
|
return nil, errors.Errorf("failed to update, some property fields were not found, got %d of %d", count, len(fields))
|
||||||
|
}
|
||||||
|
|
||||||
if err := transaction.Commit(); err != nil {
|
if err := transaction.Commit(); err != nil {
|
||||||
return nil, errors.Wrap(err, "property_field_update_commit")
|
return nil, errors.Wrap(err, "property_field_update_commit_transaction")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields, nil
|
return fields, nil
|
||||||
|
@ -83,8 +83,8 @@ func (s *SqlPropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
|
func (s *SqlPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
|
||||||
if opts.Page < 0 {
|
if err := opts.Cursor.IsValid(); err != nil {
|
||||||
return nil, errors.New("page must be positive integer")
|
return nil, fmt.Errorf("cursor is invalid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PerPage < 1 {
|
if opts.PerPage < 1 {
|
||||||
@ -92,10 +92,19 @@ func (s *SqlPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSea
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder := s.tableSelectQuery.
|
builder := s.tableSelectQuery.
|
||||||
OrderBy("CreateAt ASC").
|
OrderBy("CreateAt ASC, Id ASC").
|
||||||
Offset(uint64(opts.Page * opts.PerPage)).
|
|
||||||
Limit(uint64(opts.PerPage))
|
Limit(uint64(opts.PerPage))
|
||||||
|
|
||||||
|
if !opts.Cursor.IsEmpty() {
|
||||||
|
builder = builder.Where(sq.Or{
|
||||||
|
sq.Gt{"CreateAt": opts.Cursor.CreateAt},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"CreateAt": opts.Cursor.CreateAt},
|
||||||
|
sq.Gt{"Id": opts.Cursor.PropertyValueID},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.IncludeDeleted {
|
if !opts.IncludeDeleted {
|
||||||
builder = builder.Where(sq.Eq{"DeleteAt": 0})
|
builder = builder.Where(sq.Eq{"DeleteAt": 0})
|
||||||
}
|
}
|
||||||
@ -136,11 +145,78 @@ func (s *SqlPropertyValueStore) Update(values []*model.PropertyValue) (_ []*mode
|
|||||||
defer finalizeTransactionX(transaction, &err)
|
defer finalizeTransactionX(transaction, &err)
|
||||||
|
|
||||||
updateTime := model.GetMillis()
|
updateTime := model.GetMillis()
|
||||||
for _, value := range values {
|
isPostgres := s.DriverName() == model.DatabaseDriverPostgres
|
||||||
|
valueCase := sq.Case("id")
|
||||||
|
deleteAtCase := sq.Case("id")
|
||||||
|
ids := make([]string, len(values))
|
||||||
|
|
||||||
|
for i, value := range values {
|
||||||
|
value.UpdateAt = updateTime
|
||||||
|
if vErr := value.IsValid(); vErr != nil {
|
||||||
|
return nil, errors.Wrap(vErr, "property_value_update_isvalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
ids[i] = value.ID
|
||||||
|
valueJSON := value.Value
|
||||||
|
if s.IsBinaryParamEnabled() {
|
||||||
|
valueJSON = AppendBinaryFlag(valueJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPostgres {
|
||||||
|
valueCase = valueCase.When(sq.Expr("?", value.ID), sq.Expr("?::jsonb", valueJSON))
|
||||||
|
deleteAtCase = deleteAtCase.When(sq.Expr("?", value.ID), sq.Expr("?::bigint", value.DeleteAt))
|
||||||
|
} else {
|
||||||
|
valueCase = valueCase.When(sq.Expr("?", value.ID), sq.Expr("?", valueJSON))
|
||||||
|
deleteAtCase = deleteAtCase.When(sq.Expr("?", value.ID), sq.Expr("?", value.DeleteAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := s.getQueryBuilder().
|
||||||
|
Update("PropertyValues").
|
||||||
|
Set("Value", valueCase).
|
||||||
|
Set("DeleteAt", deleteAtCase).
|
||||||
|
Set("UpdateAt", updateTime).
|
||||||
|
Where(sq.Eq{"id": ids})
|
||||||
|
|
||||||
|
result, err := transaction.ExecBuilder(builder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_value_update_exec")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_value_update_rowsaffected")
|
||||||
|
}
|
||||||
|
if count != int64(len(values)) {
|
||||||
|
return nil, errors.Errorf("failed to update, some property values were not found, got %d of %d", count, len(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := transaction.Commit(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_value_update_commit_transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SqlPropertyValueStore) Upsert(values []*model.PropertyValue) (_ []*model.PropertyValue, err error) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction, err := s.GetMaster().Beginx()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "property_value_upsert_begin_transaction")
|
||||||
|
}
|
||||||
|
defer finalizeTransactionX(transaction, &err)
|
||||||
|
|
||||||
|
updatedValues := make([]*model.PropertyValue, len(values))
|
||||||
|
updateTime := model.GetMillis()
|
||||||
|
for i, value := range values {
|
||||||
|
value.PreSave()
|
||||||
value.UpdateAt = updateTime
|
value.UpdateAt = updateTime
|
||||||
|
|
||||||
if err := value.IsValid(); err != nil {
|
if err := value.IsValid(); err != nil {
|
||||||
return nil, errors.Wrap(err, "property_value_update_isvalid")
|
return nil, errors.Wrap(err, "property_value_upsert_isvalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
valueJSON := value.Value
|
valueJSON := value.Value
|
||||||
@ -148,36 +224,69 @@ func (s *SqlPropertyValueStore) Update(values []*model.PropertyValue) (_ []*mode
|
|||||||
valueJSON = AppendBinaryFlag(valueJSON)
|
valueJSON = AppendBinaryFlag(valueJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
queryString, args, err := s.getQueryBuilder().
|
builder := s.getQueryBuilder().
|
||||||
Update("PropertyValues").
|
Insert("PropertyValues").
|
||||||
Set("Value", valueJSON).
|
Columns("ID", "TargetID", "TargetType", "GroupID", "FieldID", "Value", "CreateAt", "UpdateAt", "DeleteAt").
|
||||||
Set("UpdateAt", value.UpdateAt).
|
Values(value.ID, value.TargetID, value.TargetType, value.GroupID, value.FieldID, valueJSON, value.CreateAt, value.UpdateAt, value.DeleteAt)
|
||||||
Set("DeleteAt", value.DeleteAt).
|
|
||||||
Where(sq.Eq{"id": value.ID}).
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "property_value_update_tosql")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := transaction.Exec(queryString, args...)
|
if s.DriverName() == model.DatabaseDriverMysql {
|
||||||
if err != nil {
|
builder = builder.SuffixExpr(sq.Expr(
|
||||||
return nil, errors.Wrapf(err, "failed to update property value with id: %s", value.ID)
|
"ON DUPLICATE KEY UPDATE Value = ?, UpdateAt = ?, DeleteAt = ?",
|
||||||
}
|
valueJSON,
|
||||||
|
value.UpdateAt,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
|
||||||
count, err := result.RowsAffected()
|
if _, err := transaction.ExecBuilder(builder); err != nil {
|
||||||
if err != nil {
|
return nil, errors.Wrap(err, "property_value_upsert_exec")
|
||||||
return nil, errors.Wrap(err, "property_value_update_rowsaffected")
|
}
|
||||||
}
|
|
||||||
if count == 0 {
|
// MySQL doesn't support RETURNING, so we need to fetch
|
||||||
return nil, store.NewErrNotFound("PropertyValue", value.ID)
|
// the new field to get its ID in case we hit a DUPLICATED
|
||||||
|
// KEY and the value.ID we have is not the right one
|
||||||
|
gBuilder := s.tableSelectQuery.Where(sq.Eq{
|
||||||
|
"GroupID": value.GroupID,
|
||||||
|
"TargetID": value.TargetID,
|
||||||
|
"FieldID": value.FieldID,
|
||||||
|
"DeleteAt": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
var values []*model.PropertyValue
|
||||||
|
if gErr := transaction.SelectBuilder(&values, gBuilder); gErr != nil {
|
||||||
|
return nil, errors.Wrap(gErr, "property_value_upsert_select")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) != 1 {
|
||||||
|
return nil, errors.New("property_value_upsert_select_length")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedValues[i] = values[0]
|
||||||
|
} else {
|
||||||
|
builder = builder.SuffixExpr(sq.Expr(
|
||||||
|
"ON CONFLICT (GroupID, TargetID, FieldID) WHERE DeleteAt = 0 DO UPDATE SET Value = ?, UpdateAt = ?, DeleteAt = ? RETURNING *",
|
||||||
|
valueJSON,
|
||||||
|
value.UpdateAt,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
|
||||||
|
var values []*model.PropertyValue
|
||||||
|
if err := transaction.SelectBuilder(&values, builder); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to upsert property value with id: %s", value.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) != 1 {
|
||||||
|
return nil, errors.New("property_value_upsert_select_length")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedValues[i] = values[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := transaction.Commit(); err != nil {
|
if err := transaction.Commit(); err != nil {
|
||||||
return nil, errors.Wrap(err, "property_value_update_commit")
|
return nil, errors.Wrap(err, "property_value_upsert_commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
return values, nil
|
return updatedValues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlPropertyValueStore) Delete(id string) error {
|
func (s *SqlPropertyValueStore) Delete(id string) error {
|
||||||
|
@ -1059,7 +1059,7 @@ type PostPersistentNotificationStore interface {
|
|||||||
DeleteByTeam(teamIds []string) error
|
DeleteByTeam(teamIds []string) error
|
||||||
}
|
}
|
||||||
type ChannelBookmarkStore interface {
|
type ChannelBookmarkStore interface {
|
||||||
ErrorIfBookmarkFileInfoAlreadyAttached(fileID string) error
|
ErrorIfBookmarkFileInfoAlreadyAttached(fileID string, channelID string) error
|
||||||
Get(Id string, includeDeleted bool) (b *model.ChannelBookmarkWithFileInfo, err error)
|
Get(Id string, includeDeleted bool) (b *model.ChannelBookmarkWithFileInfo, err error)
|
||||||
Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (b *model.ChannelBookmarkWithFileInfo, err error)
|
Save(bookmark *model.ChannelBookmark, increaseSortOrder bool) (b *model.ChannelBookmarkWithFileInfo, err error)
|
||||||
Update(bookmark *model.ChannelBookmark) error
|
Update(bookmark *model.ChannelBookmark) error
|
||||||
@ -1089,8 +1089,9 @@ type PropertyFieldStore interface {
|
|||||||
Create(field *model.PropertyField) (*model.PropertyField, error)
|
Create(field *model.PropertyField) (*model.PropertyField, error)
|
||||||
Get(id string) (*model.PropertyField, error)
|
Get(id string) (*model.PropertyField, error)
|
||||||
GetMany(ids []string) ([]*model.PropertyField, error)
|
GetMany(ids []string) ([]*model.PropertyField, error)
|
||||||
|
CountForGroup(groupID string, includeDeleted bool) (int64, error)
|
||||||
SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)
|
SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)
|
||||||
Update(field []*model.PropertyField) ([]*model.PropertyField, error)
|
Update(fields []*model.PropertyField) ([]*model.PropertyField, error)
|
||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1099,7 +1100,8 @@ type PropertyValueStore interface {
|
|||||||
Get(id string) (*model.PropertyValue, error)
|
Get(id string) (*model.PropertyValue, error)
|
||||||
GetMany(ids []string) ([]*model.PropertyValue, error)
|
GetMany(ids []string) ([]*model.PropertyValue, error)
|
||||||
SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error)
|
SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error)
|
||||||
Update(field []*model.PropertyValue) ([]*model.PropertyValue, error)
|
Update(values []*model.PropertyValue) ([]*model.PropertyValue, error)
|
||||||
|
Upsert(values []*model.PropertyValue) ([]*model.PropertyValue, error)
|
||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
DeleteForField(id string) error
|
DeleteForField(id string) error
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,12 @@ func TestChannelBookmarkStore(t *testing.T, rctx request.CTX, ss store.Store, s
|
|||||||
|
|
||||||
func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||||
channelID := model.NewId()
|
channelID := model.NewId()
|
||||||
|
otherChannelID := model.NewId()
|
||||||
userID := model.NewId()
|
userID := model.NewId()
|
||||||
|
|
||||||
|
createAt := time.Now().Add(-1 * time.Minute)
|
||||||
|
deleteAt := createAt.Add(1 * time.Second)
|
||||||
|
|
||||||
bookmark1 := &model.ChannelBookmark{
|
bookmark1 := &model.ChannelBookmark{
|
||||||
ChannelId: channelID,
|
ChannelId: channelID,
|
||||||
OwnerId: userID,
|
OwnerId: userID,
|
||||||
@ -47,6 +51,7 @@ func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
|
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: channelID,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -80,6 +85,7 @@ func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
|
|
||||||
file2 := &model.FileInfo{
|
file2 := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: channelID,
|
||||||
CreatorId: userID,
|
CreatorId: userID,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -102,6 +108,60 @@ func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
Emoji: ":smile:",
|
Emoji: ":smile:",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletedFile := &model.FileInfo{
|
||||||
|
Id: model.NewId(),
|
||||||
|
ChannelId: channelID,
|
||||||
|
CreatorId: model.BookmarkFileOwner,
|
||||||
|
Path: "somepath",
|
||||||
|
ThumbnailPath: "thumbpath",
|
||||||
|
PreviewPath: "prevPath",
|
||||||
|
Name: "test file",
|
||||||
|
Extension: "png",
|
||||||
|
MimeType: "images/png",
|
||||||
|
Size: 873182,
|
||||||
|
Width: 3076,
|
||||||
|
Height: 2200,
|
||||||
|
HasPreviewImage: true,
|
||||||
|
CreateAt: createAt.UnixMilli(),
|
||||||
|
UpdateAt: createAt.UnixMilli(),
|
||||||
|
DeleteAt: deleteAt.UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkFileDeleted := &model.ChannelBookmark{
|
||||||
|
ChannelId: channelID,
|
||||||
|
OwnerId: userID,
|
||||||
|
DisplayName: "file deleted",
|
||||||
|
FileId: deletedFile.Id,
|
||||||
|
Type: model.ChannelBookmarkFile,
|
||||||
|
Emoji: ":smile:",
|
||||||
|
}
|
||||||
|
|
||||||
|
// another channel
|
||||||
|
anotherChannelFile := &model.FileInfo{
|
||||||
|
Id: model.NewId(),
|
||||||
|
ChannelId: otherChannelID,
|
||||||
|
CreatorId: model.BookmarkFileOwner,
|
||||||
|
Path: "somepath",
|
||||||
|
ThumbnailPath: "thumbpath",
|
||||||
|
PreviewPath: "prevPath",
|
||||||
|
Name: "test file",
|
||||||
|
Extension: "png",
|
||||||
|
MimeType: "images/png",
|
||||||
|
Size: 873182,
|
||||||
|
Width: 3076,
|
||||||
|
Height: 2200,
|
||||||
|
HasPreviewImage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkFileAnotherChannel := &model.ChannelBookmark{
|
||||||
|
ChannelId: channelID,
|
||||||
|
OwnerId: userID,
|
||||||
|
DisplayName: "file another channel",
|
||||||
|
FileId: anotherChannelFile.Id,
|
||||||
|
Type: model.ChannelBookmarkFile,
|
||||||
|
Emoji: ":smile:",
|
||||||
|
}
|
||||||
|
|
||||||
_, err := ss.FileInfo().Save(rctx, file)
|
_, err := ss.FileInfo().Save(rctx, file)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
|
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
|
||||||
@ -113,6 +173,14 @@ func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
err = ss.FileInfo().AttachToPost(rctx, file2.Id, model.NewId(), channelID, userID)
|
err = ss.FileInfo().AttachToPost(rctx, file2.Id, model.NewId(), channelID, userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = ss.FileInfo().Save(rctx, deletedFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer ss.FileInfo().PermanentDelete(rctx, deletedFile.Id)
|
||||||
|
|
||||||
|
_, err = ss.FileInfo().Save(rctx, anotherChannelFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer ss.FileInfo().PermanentDelete(rctx, anotherChannelFile.Id)
|
||||||
|
|
||||||
t.Run("save bookmarks", func(t *testing.T) {
|
t.Run("save bookmarks", func(t *testing.T) {
|
||||||
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
|
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -137,6 +205,12 @@ func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
|
|
||||||
_, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
|
_, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
|
||||||
assert.Error(t, err) // Error as the file is attached to a post
|
assert.Error(t, err) // Error as the file is attached to a post
|
||||||
|
|
||||||
|
_, err = ss.ChannelBookmark().Save(bookmarkFileDeleted.Clone(), true)
|
||||||
|
assert.Error(t, err) // Error as the file is deleted
|
||||||
|
|
||||||
|
_, err = ss.ChannelBookmark().Save(bookmarkFileAnotherChannel.Clone(), true)
|
||||||
|
assert.Error(t, err) // Error as the file is from another channel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +278,7 @@ func testUpdateSortOrderChannelBookmark(t *testing.T, rctx request.CTX, ss store
|
|||||||
|
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: channelID,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
@ -391,6 +466,7 @@ func testDeleteChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
|
|
||||||
file := &model.FileInfo{
|
file := &model.FileInfo{
|
||||||
Id: model.NewId(),
|
Id: model.NewId(),
|
||||||
|
ChannelId: channelID,
|
||||||
CreatorId: model.BookmarkFileOwner,
|
CreatorId: model.BookmarkFileOwner,
|
||||||
Path: "somepath",
|
Path: "somepath",
|
||||||
ThumbnailPath: "thumbpath",
|
ThumbnailPath: "thumbpath",
|
||||||
|
@ -32,17 +32,17 @@ func (_m *ChannelBookmarkStore) Delete(bookmarkID string, deleteFile bool) error
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorIfBookmarkFileInfoAlreadyAttached provides a mock function with given fields: fileID
|
// ErrorIfBookmarkFileInfoAlreadyAttached provides a mock function with given fields: fileID, channelID
|
||||||
func (_m *ChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string) error {
|
func (_m *ChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string, channelID string) error {
|
||||||
ret := _m.Called(fileID)
|
ret := _m.Called(fileID, channelID)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for ErrorIfBookmarkFileInfoAlreadyAttached")
|
panic("no return value specified for ErrorIfBookmarkFileInfoAlreadyAttached")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 error
|
var r0 error
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||||
r0 = rf(fileID)
|
r0 = rf(fileID, channelID)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Error(0)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,34 @@ type PropertyFieldStore struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountForGroup provides a mock function with given fields: groupID, includeDeleted
|
||||||
|
func (_m *PropertyFieldStore) CountForGroup(groupID string, includeDeleted bool) (int64, error) {
|
||||||
|
ret := _m.Called(groupID, includeDeleted)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for CountForGroup")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 int64
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, bool) (int64, error)); ok {
|
||||||
|
return rf(groupID, includeDeleted)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(string, bool) int64); ok {
|
||||||
|
r0 = rf(groupID, includeDeleted)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
|
||||||
|
r1 = rf(groupID, includeDeleted)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Create provides a mock function with given fields: field
|
// Create provides a mock function with given fields: field
|
||||||
func (_m *PropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
func (_m *PropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
||||||
ret := _m.Called(field)
|
ret := _m.Called(field)
|
||||||
@ -152,9 +180,9 @@ func (_m *PropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearc
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update provides a mock function with given fields: field
|
// Update provides a mock function with given fields: fields
|
||||||
func (_m *PropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
|
func (_m *PropertyFieldStore) Update(fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
||||||
ret := _m.Called(field)
|
ret := _m.Called(fields)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for Update")
|
panic("no return value specified for Update")
|
||||||
@ -163,10 +191,10 @@ func (_m *PropertyFieldStore) Update(field []*model.PropertyField) ([]*model.Pro
|
|||||||
var r0 []*model.PropertyField
|
var r0 []*model.PropertyField
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func([]*model.PropertyField) ([]*model.PropertyField, error)); ok {
|
if rf, ok := ret.Get(0).(func([]*model.PropertyField) ([]*model.PropertyField, error)); ok {
|
||||||
return rf(field)
|
return rf(fields)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func([]*model.PropertyField) []*model.PropertyField); ok {
|
if rf, ok := ret.Get(0).(func([]*model.PropertyField) []*model.PropertyField); ok {
|
||||||
r0 = rf(field)
|
r0 = rf(fields)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*model.PropertyField)
|
r0 = ret.Get(0).([]*model.PropertyField)
|
||||||
@ -174,7 +202,7 @@ func (_m *PropertyFieldStore) Update(field []*model.PropertyField) ([]*model.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func([]*model.PropertyField) error); ok {
|
if rf, ok := ret.Get(1).(func([]*model.PropertyField) error); ok {
|
||||||
r1 = rf(field)
|
r1 = rf(fields)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
@ -170,9 +170,9 @@ func (_m *PropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearc
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update provides a mock function with given fields: field
|
// Update provides a mock function with given fields: values
|
||||||
func (_m *PropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
func (_m *PropertyValueStore) Update(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
ret := _m.Called(field)
|
ret := _m.Called(values)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for Update")
|
panic("no return value specified for Update")
|
||||||
@ -181,10 +181,10 @@ func (_m *PropertyValueStore) Update(field []*model.PropertyValue) ([]*model.Pro
|
|||||||
var r0 []*model.PropertyValue
|
var r0 []*model.PropertyValue
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) ([]*model.PropertyValue, error)); ok {
|
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) ([]*model.PropertyValue, error)); ok {
|
||||||
return rf(field)
|
return rf(values)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) []*model.PropertyValue); ok {
|
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) []*model.PropertyValue); ok {
|
||||||
r0 = rf(field)
|
r0 = rf(values)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*model.PropertyValue)
|
r0 = ret.Get(0).([]*model.PropertyValue)
|
||||||
@ -192,7 +192,37 @@ func (_m *PropertyValueStore) Update(field []*model.PropertyValue) ([]*model.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func([]*model.PropertyValue) error); ok {
|
if rf, ok := ret.Get(1).(func([]*model.PropertyValue) error); ok {
|
||||||
r1 = rf(field)
|
r1 = rf(values)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert provides a mock function with given fields: values
|
||||||
|
func (_m *PropertyValueStore) Upsert(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
|
ret := _m.Called(values)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for Upsert")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 []*model.PropertyValue
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) ([]*model.PropertyValue, error)); ok {
|
||||||
|
return rf(values)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) []*model.PropertyValue); ok {
|
||||||
|
r0 = rf(values)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.PropertyValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func([]*model.PropertyValue) error); ok {
|
||||||
|
r1 = rf(values)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ package storetest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ func TestPropertyFieldStore(t *testing.T, rctx request.CTX, ss store.Store, s Sq
|
|||||||
t.Run("UpdatePropertyField", func(t *testing.T) { testUpdatePropertyField(t, rctx, ss) })
|
t.Run("UpdatePropertyField", func(t *testing.T) { testUpdatePropertyField(t, rctx, ss) })
|
||||||
t.Run("DeletePropertyField", func(t *testing.T) { testDeletePropertyField(t, rctx, ss) })
|
t.Run("DeletePropertyField", func(t *testing.T) { testDeletePropertyField(t, rctx, ss) })
|
||||||
t.Run("SearchPropertyFields", func(t *testing.T) { testSearchPropertyFields(t, rctx, ss) })
|
t.Run("SearchPropertyFields", func(t *testing.T) { testSearchPropertyFields(t, rctx, ss) })
|
||||||
|
t.Run("CountForGroup", func(t *testing.T) { testCountForGroup(t, rctx, ss) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
func testCreatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
@ -146,8 +148,7 @@ func testUpdatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
}
|
}
|
||||||
updatedField, err := ss.PropertyField().Update([]*model.PropertyField{field})
|
updatedField, err := ss.PropertyField().Update([]*model.PropertyField{field})
|
||||||
require.Zero(t, updatedField)
|
require.Zero(t, updatedField)
|
||||||
var enf *store.ErrNotFound
|
require.ErrorContains(t, err, "failed to update, some property fields were not found, got 0 of 1")
|
||||||
require.ErrorAs(t, err, &enf)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should fail if the property field is not valid", func(t *testing.T) {
|
t.Run("should fail if the property field is not valid", func(t *testing.T) {
|
||||||
@ -280,6 +281,46 @@ func testUpdatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
require.Equal(t, groupID, updated2.GroupID)
|
require.Equal(t, groupID, updated2.GroupID)
|
||||||
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
|
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should not update any fields if one update points to a nonexisting one", func(t *testing.T) {
|
||||||
|
// Create a valid field
|
||||||
|
field1 := &model.PropertyField{
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
Name: "First field",
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.PropertyField().Create(field1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
originalUpdateAt := field1.UpdateAt
|
||||||
|
|
||||||
|
// Try to update both the valid field and a nonexistent one
|
||||||
|
field2 := &model.PropertyField{
|
||||||
|
ID: model.NewId(),
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
Name: "Second field",
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
CreateAt: 1,
|
||||||
|
Attrs: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
field1.Name = "Updated First"
|
||||||
|
|
||||||
|
_, err = ss.PropertyField().Update([]*model.PropertyField{field1, field2})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, "failed to update, some property fields were not found")
|
||||||
|
|
||||||
|
// Check that the valid field was not updated
|
||||||
|
updated1, err := ss.PropertyField().Get(field1.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "First field", updated1.Name)
|
||||||
|
require.Equal(t, originalUpdateAt, updated1.UpdateAt)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeletePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
func testDeletePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
@ -317,6 +358,97 @@ func testDeletePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCountForGroup(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
|
t.Run("should return 0 for group with no properties", func(t *testing.T) {
|
||||||
|
count, err := ss.PropertyField().CountForGroup(model.NewId(), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(0), count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return correct count for group with properties", func(t *testing.T) {
|
||||||
|
groupID := model.NewId()
|
||||||
|
|
||||||
|
// Create 5 property fields
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
field := &model.PropertyField{
|
||||||
|
GroupID: groupID,
|
||||||
|
Name: fmt.Sprintf("Field %d", i),
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyField().Create(field)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := ss.PropertyField().CountForGroup(groupID, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(5), count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not count deleted properties when includeDeleted is false", func(t *testing.T) {
|
||||||
|
groupID := model.NewId()
|
||||||
|
|
||||||
|
// Create 5 property fields
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
field := &model.PropertyField{
|
||||||
|
GroupID: groupID,
|
||||||
|
Name: fmt.Sprintf("Field %d", i),
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyField().Create(field)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one more and delete it
|
||||||
|
deletedField := &model.PropertyField{
|
||||||
|
GroupID: groupID,
|
||||||
|
Name: "To be deleted",
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyField().Create(deletedField)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ss.PropertyField().Delete(deletedField.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Count should be 5 since the deleted field shouldn't be counted
|
||||||
|
count, err := ss.PropertyField().CountForGroup(groupID, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(5), count)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should count deleted properties when includeDeleted is true", func(t *testing.T) {
|
||||||
|
groupID := model.NewId()
|
||||||
|
|
||||||
|
// Create 5 property fields
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
field := &model.PropertyField{
|
||||||
|
GroupID: groupID,
|
||||||
|
Name: fmt.Sprintf("Field %d", i),
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyField().Create(field)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one more and delete it
|
||||||
|
deletedField := &model.PropertyField{
|
||||||
|
GroupID: groupID,
|
||||||
|
Name: "To be deleted",
|
||||||
|
Type: model.PropertyFieldTypeText,
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyField().Create(deletedField)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ss.PropertyField().Delete(deletedField.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Count should be 6 since we're including deleted fields
|
||||||
|
count, err := ss.PropertyField().CountForGroup(groupID, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(6), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
groupID := model.NewId()
|
groupID := model.NewId()
|
||||||
targetID := model.NewId()
|
targetID := model.NewId()
|
||||||
@ -367,18 +499,9 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
expectedError bool
|
expectedError bool
|
||||||
expectedIDs []string
|
expectedIDs []string
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "negative page",
|
|
||||||
opts: model.PropertyFieldSearchOpts{
|
|
||||||
Page: -1,
|
|
||||||
PerPage: 10,
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "negative per_page",
|
name: "negative per_page",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
Page: 0,
|
|
||||||
PerPage: -1,
|
PerPage: -1,
|
||||||
},
|
},
|
||||||
expectedError: true,
|
expectedError: true,
|
||||||
@ -387,7 +510,6 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by group_id",
|
name: "filter by group_id",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{field1.ID, field2.ID},
|
expectedIDs: []string{field1.ID, field2.ID},
|
||||||
@ -396,7 +518,6 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by group_id including deleted",
|
name: "filter by group_id including deleted",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
IncludeDeleted: true,
|
IncludeDeleted: true,
|
||||||
},
|
},
|
||||||
@ -406,7 +527,6 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by target_type",
|
name: "filter by target_type",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
TargetType: "test_type",
|
TargetType: "test_type",
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{field1.ID, field3.ID},
|
expectedIDs: []string{field1.ID, field3.ID},
|
||||||
@ -415,7 +535,6 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by target_id",
|
name: "filter by target_id",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{field1.ID, field2.ID},
|
expectedIDs: []string{field1.ID, field2.ID},
|
||||||
@ -424,7 +543,6 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "pagination page 0",
|
name: "pagination page 0",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 2,
|
PerPage: 2,
|
||||||
IncludeDeleted: true,
|
IncludeDeleted: true,
|
||||||
},
|
},
|
||||||
@ -433,8 +551,11 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
{
|
{
|
||||||
name: "pagination page 1",
|
name: "pagination page 1",
|
||||||
opts: model.PropertyFieldSearchOpts{
|
opts: model.PropertyFieldSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 1,
|
Cursor: model.PropertyFieldSearchCursor{
|
||||||
|
CreateAt: field2.CreateAt,
|
||||||
|
PropertyFieldID: field2.ID,
|
||||||
|
},
|
||||||
PerPage: 2,
|
PerPage: 2,
|
||||||
IncludeDeleted: true,
|
IncludeDeleted: true,
|
||||||
},
|
},
|
||||||
@ -451,7 +572,7 @@ func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var ids = make([]string, len(results))
|
ids := make([]string, len(results))
|
||||||
for i, field := range results {
|
for i, field := range results {
|
||||||
ids[i] = field.ID
|
ids[i] = field.ID
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ func TestPropertyValueStore(t *testing.T, rctx request.CTX, ss store.Store, s Sq
|
|||||||
t.Run("GetPropertyValue", func(t *testing.T) { testGetPropertyValue(t, rctx, ss) })
|
t.Run("GetPropertyValue", func(t *testing.T) { testGetPropertyValue(t, rctx, ss) })
|
||||||
t.Run("GetManyPropertyValues", func(t *testing.T) { testGetManyPropertyValues(t, rctx, ss) })
|
t.Run("GetManyPropertyValues", func(t *testing.T) { testGetManyPropertyValues(t, rctx, ss) })
|
||||||
t.Run("UpdatePropertyValue", func(t *testing.T) { testUpdatePropertyValue(t, rctx, ss) })
|
t.Run("UpdatePropertyValue", func(t *testing.T) { testUpdatePropertyValue(t, rctx, ss) })
|
||||||
|
t.Run("UpsertPropertyValue", func(t *testing.T) { testUpsertPropertyValue(t, rctx, ss) })
|
||||||
t.Run("DeletePropertyValue", func(t *testing.T) { testDeletePropertyValue(t, rctx, ss) })
|
t.Run("DeletePropertyValue", func(t *testing.T) { testDeletePropertyValue(t, rctx, ss) })
|
||||||
t.Run("SearchPropertyValues", func(t *testing.T) { testSearchPropertyValues(t, rctx, ss) })
|
t.Run("SearchPropertyValues", func(t *testing.T) { testSearchPropertyValues(t, rctx, ss) })
|
||||||
t.Run("DeleteForField", func(t *testing.T) { testDeleteForField(t, rctx, ss) })
|
t.Run("DeleteForField", func(t *testing.T) { testDeleteForField(t, rctx, ss) })
|
||||||
@ -149,8 +150,7 @@ func testUpdatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
}
|
}
|
||||||
updatedValue, err := ss.PropertyValue().Update([]*model.PropertyValue{value})
|
updatedValue, err := ss.PropertyValue().Update([]*model.PropertyValue{value})
|
||||||
require.Zero(t, updatedValue)
|
require.Zero(t, updatedValue)
|
||||||
var enf *store.ErrNotFound
|
require.ErrorContains(t, err, "failed to update, some property values were not found, got 0 of 1")
|
||||||
require.ErrorAs(t, err, &enf)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
||||||
@ -220,7 +220,7 @@ func testUpdatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
|
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should not update any fields if one update is invalid", func(t *testing.T) {
|
t.Run("should not update any values if one update is invalid", func(t *testing.T) {
|
||||||
// Create two valid values
|
// Create two valid values
|
||||||
groupID := model.NewId()
|
groupID := model.NewId()
|
||||||
value1 := &model.PropertyValue{
|
value1 := &model.PropertyValue{
|
||||||
@ -266,6 +266,208 @@ func testUpdatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
require.Equal(t, groupID, updated2.GroupID)
|
require.Equal(t, groupID, updated2.GroupID)
|
||||||
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
|
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should not update any values if one update points to a nonexisting one", func(t *testing.T) {
|
||||||
|
// Create a valid value
|
||||||
|
value1 := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"Value 1"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.PropertyValue().Create(value1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
originalUpdateAt := value1.UpdateAt
|
||||||
|
|
||||||
|
// Try to update both the valid value and a nonexistent one
|
||||||
|
value2 := &model.PropertyValue{
|
||||||
|
ID: model.NewId(),
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
CreateAt: 1,
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"Value 2"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
value1.Value = json.RawMessage(`"Updated Value 1"`)
|
||||||
|
|
||||||
|
_, err = ss.PropertyValue().Update([]*model.PropertyValue{value1, value2})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, "failed to update, some property values were not found")
|
||||||
|
|
||||||
|
// Check that the valid value was not updated
|
||||||
|
updated1, err := ss.PropertyValue().Get(value1.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, json.RawMessage(`"Value 1"`), updated1.Value)
|
||||||
|
require.Equal(t, originalUpdateAt, updated1.UpdateAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpsertPropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
|
t.Run("should fail if the property value is not valid", func(t *testing.T) {
|
||||||
|
value := &model.PropertyValue{
|
||||||
|
TargetID: "",
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"test value"`),
|
||||||
|
}
|
||||||
|
updatedValue, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
||||||
|
require.Zero(t, updatedValue)
|
||||||
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
||||||
|
|
||||||
|
value.TargetID = model.NewId()
|
||||||
|
value.GroupID = ""
|
||||||
|
updatedValue, err = ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
||||||
|
require.Zero(t, updatedValue)
|
||||||
|
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be able to insert new property values", func(t *testing.T) {
|
||||||
|
value1 := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"value 1"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
value2 := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"value 2"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value1, value2})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, values, 2)
|
||||||
|
require.NotEmpty(t, values[0].ID)
|
||||||
|
require.NotEmpty(t, values[1].ID)
|
||||||
|
require.NotZero(t, values[0].CreateAt)
|
||||||
|
require.NotZero(t, values[1].CreateAt)
|
||||||
|
|
||||||
|
valuesFromStore, err := ss.PropertyValue().GetMany([]string{values[0].ID, values[1].ID})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, valuesFromStore, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be able to update existing property values", func(t *testing.T) {
|
||||||
|
// Create initial value
|
||||||
|
value := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"initial value"`),
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyValue().Create(value)
|
||||||
|
require.NoError(t, err)
|
||||||
|
valueID := value.ID
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Update via upsert
|
||||||
|
value.ID = ""
|
||||||
|
value.Value = json.RawMessage(`"updated value"`)
|
||||||
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{value})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, values, 1)
|
||||||
|
require.Equal(t, valueID, values[0].ID)
|
||||||
|
require.Equal(t, json.RawMessage(`"updated value"`), values[0].Value)
|
||||||
|
require.Greater(t, values[0].UpdateAt, values[0].CreateAt)
|
||||||
|
|
||||||
|
// Verify in database
|
||||||
|
updated, err := ss.PropertyValue().Get(valueID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, json.RawMessage(`"updated value"`), updated.Value)
|
||||||
|
require.Greater(t, updated.UpdateAt, updated.CreateAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should handle mixed insert and update operations", func(t *testing.T) {
|
||||||
|
// Create first value
|
||||||
|
existingValue := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"existing value"`),
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyValue().Create(existingValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Prepare new value
|
||||||
|
newValue := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"new value"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing and insert new via upsert
|
||||||
|
existingValue.Value = json.RawMessage(`"updated existing"`)
|
||||||
|
values, err := ss.PropertyValue().Upsert([]*model.PropertyValue{existingValue, newValue})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, values, 2)
|
||||||
|
|
||||||
|
// Verify both values
|
||||||
|
newValueUpserted, err := ss.PropertyValue().Get(newValue.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, json.RawMessage(`"new value"`), newValueUpserted.Value)
|
||||||
|
existingValueUpserted, err := ss.PropertyValue().Get(existingValue.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, json.RawMessage(`"updated existing"`), existingValueUpserted.Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not perform any operation if one of the fields is invalid", func(t *testing.T) {
|
||||||
|
// Create initial valid value
|
||||||
|
existingValue := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: model.NewId(),
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"existing value"`),
|
||||||
|
}
|
||||||
|
_, err := ss.PropertyValue().Create(existingValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
originalValue := *existingValue
|
||||||
|
|
||||||
|
// Prepare an invalid value
|
||||||
|
invalidValue := &model.PropertyValue{
|
||||||
|
TargetID: model.NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: "", // Invalid: empty group ID
|
||||||
|
FieldID: model.NewId(),
|
||||||
|
Value: json.RawMessage(`"new value"`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to update existing and insert invalid via upsert
|
||||||
|
existingValue.Value = json.RawMessage(`"should not update"`)
|
||||||
|
_, err = ss.PropertyValue().Upsert([]*model.PropertyValue{existingValue, invalidValue})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "model.property_value.is_valid.app_error")
|
||||||
|
|
||||||
|
// Verify the existing value was not changed
|
||||||
|
retrieved, err := ss.PropertyValue().Get(existingValue.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, originalValue.Value, retrieved.Value)
|
||||||
|
require.Equal(t, originalValue.UpdateAt, retrieved.UpdateAt)
|
||||||
|
|
||||||
|
// Verify the invalid value was not inserted
|
||||||
|
results, err := ss.PropertyValue().SearchPropertyValues(model.PropertyValueSearchOpts{
|
||||||
|
TargetID: invalidValue.TargetID,
|
||||||
|
PerPage: 10,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, results)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeletePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
func testDeletePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
|
||||||
@ -364,18 +566,9 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
expectedError bool
|
expectedError bool
|
||||||
expectedIDs []string
|
expectedIDs []string
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "negative page",
|
|
||||||
opts: model.PropertyValueSearchOpts{
|
|
||||||
Page: -1,
|
|
||||||
PerPage: 10,
|
|
||||||
},
|
|
||||||
expectedError: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "negative per_page",
|
name: "negative per_page",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
Page: 0,
|
|
||||||
PerPage: -1,
|
PerPage: -1,
|
||||||
},
|
},
|
||||||
expectedError: true,
|
expectedError: true,
|
||||||
@ -384,7 +577,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by group_id",
|
name: "filter by group_id",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID, value2.ID},
|
expectedIDs: []string{value1.ID, value2.ID},
|
||||||
@ -394,7 +586,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
TargetType: "test_type",
|
TargetType: "test_type",
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID},
|
expectedIDs: []string{value1.ID},
|
||||||
@ -405,7 +596,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
TargetType: "test_type",
|
TargetType: "test_type",
|
||||||
IncludeDeleted: true,
|
IncludeDeleted: true,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID, value4.ID},
|
expectedIDs: []string{value1.ID, value4.ID},
|
||||||
@ -414,7 +604,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by target_id",
|
name: "filter by target_id",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID, value2.ID},
|
expectedIDs: []string{value1.ID, value2.ID},
|
||||||
@ -424,7 +613,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID, value2.ID},
|
expectedIDs: []string{value1.ID, value2.ID},
|
||||||
@ -433,7 +621,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "filter by field_id",
|
name: "filter by field_id",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
FieldID: fieldID,
|
FieldID: fieldID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID},
|
expectedIDs: []string{value1.ID},
|
||||||
@ -443,7 +630,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
FieldID: fieldID,
|
FieldID: fieldID,
|
||||||
IncludeDeleted: true,
|
IncludeDeleted: true,
|
||||||
Page: 0,
|
|
||||||
PerPage: 10,
|
PerPage: 10,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID, value4.ID},
|
expectedIDs: []string{value1.ID, value4.ID},
|
||||||
@ -452,7 +638,6 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "pagination page 0",
|
name: "pagination page 0",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 0,
|
|
||||||
PerPage: 1,
|
PerPage: 1,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value1.ID},
|
expectedIDs: []string{value1.ID},
|
||||||
@ -461,7 +646,10 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
name: "pagination page 1",
|
name: "pagination page 1",
|
||||||
opts: model.PropertyValueSearchOpts{
|
opts: model.PropertyValueSearchOpts{
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
Page: 1,
|
Cursor: model.PropertyValueSearchCursor{
|
||||||
|
CreateAt: value1.CreateAt,
|
||||||
|
PropertyValueID: value1.ID,
|
||||||
|
},
|
||||||
PerPage: 1,
|
PerPage: 1,
|
||||||
},
|
},
|
||||||
expectedIDs: []string{value2.ID},
|
expectedIDs: []string{value2.ID},
|
||||||
@ -477,7 +665,7 @@ func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var ids = make([]string, len(results))
|
ids := make([]string, len(results))
|
||||||
for i, value := range results {
|
for i, value := range results {
|
||||||
ids[i] = value.ID
|
ids[i] = value.ID
|
||||||
}
|
}
|
||||||
|
@ -2614,10 +2614,10 @@ func (s *TimerLayerChannelBookmarkStore) Delete(bookmarkID string, deleteFile bo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TimerLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string) error {
|
func (s *TimerLayerChannelBookmarkStore) ErrorIfBookmarkFileInfoAlreadyAttached(fileID string, channelID string) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileID)
|
err := s.ChannelBookmarkStore.ErrorIfBookmarkFileInfoAlreadyAttached(fileID, channelID)
|
||||||
|
|
||||||
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 {
|
||||||
@ -7105,6 +7105,22 @@ func (s *TimerLayerProductNoticesStore) View(userID string, notices []string) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerPropertyFieldStore) CountForGroup(groupID string, includeDeleted bool) (int64, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.PropertyFieldStore.CountForGroup(groupID, includeDeleted)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.CountForGroup", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
func (s *TimerLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@ -7185,10 +7201,10 @@ func (s *TimerLayerPropertyFieldStore) SearchPropertyFields(opts model.PropertyF
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TimerLayerPropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
|
func (s *TimerLayerPropertyFieldStore) Update(fields []*model.PropertyField) ([]*model.PropertyField, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
result, err := s.PropertyFieldStore.Update(field)
|
result, err := s.PropertyFieldStore.Update(fields)
|
||||||
|
|
||||||
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 {
|
||||||
@ -7329,10 +7345,10 @@ func (s *TimerLayerPropertyValueStore) SearchPropertyValues(opts model.PropertyV
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TimerLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
func (s *TimerLayerPropertyValueStore) Update(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
result, err := s.PropertyValueStore.Update(field)
|
result, err := s.PropertyValueStore.Update(values)
|
||||||
|
|
||||||
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 {
|
||||||
@ -7345,6 +7361,22 @@ func (s *TimerLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerPropertyValueStore) Upsert(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.PropertyValueStore.Upsert(values)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.Upsert", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
|
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
@ -108,16 +108,16 @@ type MetricsInterface interface {
|
|||||||
ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64)
|
ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64)
|
||||||
ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64)
|
ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64)
|
||||||
ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64)
|
ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64)
|
||||||
ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64)
|
ObserveClientFirstContentfulPaint(platform, agent, userID string, elapsed float64)
|
||||||
ObserveClientLargestContentfulPaint(platform, agent, region string, elapsed float64)
|
ObserveClientLargestContentfulPaint(platform, agent, region, userID string, elapsed float64)
|
||||||
ObserveClientInteractionToNextPaint(platform, agent, interaction string, elapsed float64)
|
ObserveClientInteractionToNextPaint(platform, agent, interaction, userID string, elapsed float64)
|
||||||
ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64)
|
ObserveClientCumulativeLayoutShift(platform, agent, userID string, elapsed float64)
|
||||||
IncrementClientLongTasks(platform, agent string, inc float64)
|
IncrementClientLongTasks(platform, agent, userID string, inc float64)
|
||||||
ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64)
|
ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64)
|
||||||
ObserveClientChannelSwitchDuration(platform, agent, fresh string, elapsed float64)
|
ObserveClientChannelSwitchDuration(platform, agent, fresh, userID string, elapsed float64)
|
||||||
ObserveClientTeamSwitchDuration(platform, agent, fresh string, elapsed float64)
|
ObserveClientTeamSwitchDuration(platform, agent, fresh, userID string, elapsed float64)
|
||||||
ObserveClientRHSLoadDuration(platform, agent string, elapsed float64)
|
ObserveClientRHSLoadDuration(platform, agent, userID string, elapsed float64)
|
||||||
ObserveGlobalThreadsLoadDuration(platform, agent string, elapsed float64)
|
ObserveGlobalThreadsLoadDuration(platform, agent, userID string, elapsed float64)
|
||||||
ObserveMobileClientLoadDuration(platform string, elapsed float64)
|
ObserveMobileClientLoadDuration(platform string, elapsed float64)
|
||||||
ObserveMobileClientChannelSwitchDuration(platform string, elapsed float64)
|
ObserveMobileClientChannelSwitchDuration(platform string, elapsed float64)
|
||||||
ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64)
|
ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64)
|
||||||
|
@ -78,9 +78,9 @@ func (_m *MetricsInterface) IncrementChannelIndexCounter() {
|
|||||||
_m.Called()
|
_m.Called()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncrementClientLongTasks provides a mock function with given fields: platform, agent, inc
|
// IncrementClientLongTasks provides a mock function with given fields: platform, agent, userID, inc
|
||||||
func (_m *MetricsInterface) IncrementClientLongTasks(platform string, agent string, inc float64) {
|
func (_m *MetricsInterface) IncrementClientLongTasks(platform string, agent string, userID string, inc float64) {
|
||||||
_m.Called(platform, agent, inc)
|
_m.Called(platform, agent, userID, inc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncrementClusterEventType provides a mock function with given fields: eventType
|
// IncrementClusterEventType provides a mock function with given fields: eventType
|
||||||
@ -303,29 +303,29 @@ func (_m *MetricsInterface) ObserveAPIEndpointDuration(endpoint string, method s
|
|||||||
_m.Called(endpoint, method, statusCode, originClient, pageLoadContext, elapsed)
|
_m.Called(endpoint, method, statusCode, originClient, pageLoadContext, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, fresh, elapsed
|
// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, fresh, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, fresh string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, fresh string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, fresh, elapsed)
|
_m.Called(platform, agent, fresh, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientCumulativeLayoutShift provides a mock function with given fields: platform, agent, elapsed
|
// ObserveClientCumulativeLayoutShift provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientCumulativeLayoutShift(platform string, agent string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientCumulativeLayoutShift(platform string, agent string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, elapsed)
|
_m.Called(platform, agent, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientFirstContentfulPaint provides a mock function with given fields: platform, agent, elapsed
|
// ObserveClientFirstContentfulPaint provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientFirstContentfulPaint(platform string, agent string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientFirstContentfulPaint(platform string, agent string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, elapsed)
|
_m.Called(platform, agent, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientInteractionToNextPaint provides a mock function with given fields: platform, agent, interaction, elapsed
|
// ObserveClientInteractionToNextPaint provides a mock function with given fields: platform, agent, interaction, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientInteractionToNextPaint(platform string, agent string, interaction string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientInteractionToNextPaint(platform string, agent string, interaction string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, interaction, elapsed)
|
_m.Called(platform, agent, interaction, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientLargestContentfulPaint provides a mock function with given fields: platform, agent, region, elapsed
|
// ObserveClientLargestContentfulPaint provides a mock function with given fields: platform, agent, region, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, agent string, region string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, agent string, region string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, region, elapsed)
|
_m.Called(platform, agent, region, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientPageLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed
|
// ObserveClientPageLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
@ -333,9 +333,9 @@ func (_m *MetricsInterface) ObserveClientPageLoadDuration(platform string, agent
|
|||||||
_m.Called(platform, agent, userID, elapsed)
|
_m.Called(platform, agent, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientRHSLoadDuration provides a mock function with given fields: platform, agent, elapsed
|
// ObserveClientRHSLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, elapsed)
|
_m.Called(platform, agent, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientSplashScreenEnd provides a mock function with given fields: platform, agent, pageType, userID, elapsed
|
// ObserveClientSplashScreenEnd provides a mock function with given fields: platform, agent, pageType, userID, elapsed
|
||||||
@ -343,9 +343,9 @@ func (_m *MetricsInterface) ObserveClientSplashScreenEnd(platform string, agent
|
|||||||
_m.Called(platform, agent, pageType, userID, elapsed)
|
_m.Called(platform, agent, pageType, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientTeamSwitchDuration provides a mock function with given fields: platform, agent, fresh, elapsed
|
// ObserveClientTeamSwitchDuration provides a mock function with given fields: platform, agent, fresh, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveClientTeamSwitchDuration(platform string, agent string, fresh string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveClientTeamSwitchDuration(platform string, agent string, fresh string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, fresh, elapsed)
|
_m.Called(platform, agent, fresh, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveClientTimeToDomInteractive provides a mock function with given fields: platform, agent, userID, elapsed
|
// ObserveClientTimeToDomInteractive provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
@ -388,9 +388,9 @@ func (_m *MetricsInterface) ObserveFilesSearchDuration(elapsed float64) {
|
|||||||
_m.Called(elapsed)
|
_m.Called(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveGlobalThreadsLoadDuration provides a mock function with given fields: platform, agent, elapsed
|
// ObserveGlobalThreadsLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed
|
||||||
func (_m *MetricsInterface) ObserveGlobalThreadsLoadDuration(platform string, agent string, elapsed float64) {
|
func (_m *MetricsInterface) ObserveGlobalThreadsLoadDuration(platform string, agent string, userID string, elapsed float64) {
|
||||||
_m.Called(platform, agent, elapsed)
|
_m.Called(platform, agent, userID, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObserveMobileClientChannelSwitchDuration provides a mock function with given fields: platform, elapsed
|
// ObserveMobileClientChannelSwitchDuration provides a mock function with given fields: platform, elapsed
|
||||||
|
@ -55,6 +55,8 @@ type MetricsInterfaceImpl struct {
|
|||||||
|
|
||||||
Registry *prometheus.Registry
|
Registry *prometheus.Registry
|
||||||
|
|
||||||
|
ClientSideUserIds map[string]bool
|
||||||
|
|
||||||
DbMasterConnectionsGauge prometheus.GaugeFunc
|
DbMasterConnectionsGauge prometheus.GaugeFunc
|
||||||
DbReadConnectionsGauge prometheus.GaugeFunc
|
DbReadConnectionsGauge prometheus.GaugeFunc
|
||||||
DbSearchConnectionsGauge prometheus.GaugeFunc
|
DbSearchConnectionsGauge prometheus.GaugeFunc
|
||||||
@ -240,7 +242,7 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new MetricsInterface. The driver and datasoruce parameters are added during
|
// New creates a new MetricsInterface. The driver and datasource parameters are added during
|
||||||
// migrating configuration store to the new platform service. Once the store and license are migrated,
|
// migrating configuration store to the new platform service. Once the store and license are migrated,
|
||||||
// we will be able to remove server dependency and lean on platform service during initialization.
|
// we will be able to remove server dependency and lean on platform service during initialization.
|
||||||
func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterfaceImpl {
|
func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterfaceImpl {
|
||||||
@ -248,6 +250,12 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Platform: ps,
|
Platform: ps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize ClientSideUserIds map
|
||||||
|
m.ClientSideUserIds = make(map[string]bool)
|
||||||
|
for _, userId := range ps.Config().MetricsSettings.ClientSideUserIds {
|
||||||
|
m.ClientSideUserIds[userId] = true
|
||||||
|
}
|
||||||
|
|
||||||
m.Registry = prometheus.NewRegistry()
|
m.Registry = prometheus.NewRegistry()
|
||||||
options := collectors.ProcessCollectorOpts{
|
options := collectors.ProcessCollectorOpts{
|
||||||
Namespace: MetricsNamespace,
|
Namespace: MetricsNamespace,
|
||||||
@ -1196,7 +1204,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration from when a browser starts to request a page from a server until when it starts to receive data in response (seconds)",
|
Help: "Duration from when a browser starts to request a page from a server until when it starts to receive data in response (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
m.Platform.Log(),
|
m.Platform.Log(),
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientTimeToFirstByte)
|
m.Registry.MustRegister(m.ClientTimeToFirstByte)
|
||||||
@ -1209,7 +1217,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration from when a browser starts to request a page from a server until when it receives the last byte of the resource or immediately before the transport connection is closed, whichever comes first. (seconds)",
|
Help: "Duration from when a browser starts to request a page from a server until when it receives the last byte of the resource or immediately before the transport connection is closed, whichever comes first. (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
m.Platform.Log(),
|
m.Platform.Log(),
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientTimeToLastByte)
|
m.Registry.MustRegister(m.ClientTimeToLastByte)
|
||||||
@ -1223,7 +1231,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15},
|
Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15},
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
m.Platform.Log(),
|
m.Platform.Log(),
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientTimeToDOMInteractive)
|
m.Registry.MustRegister(m.ClientTimeToDOMInteractive)
|
||||||
@ -1237,7 +1245,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15},
|
Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15},
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent", "page_type"},
|
[]string{"platform", "agent", "page_type", "user_id"},
|
||||||
m.Platform.Log(),
|
m.Platform.Log(),
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientSplashScreenEnd)
|
m.Registry.MustRegister(m.ClientSplashScreenEnd)
|
||||||
@ -1253,7 +1261,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20},
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20},
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientFirstContentfulPaint)
|
m.Registry.MustRegister(m.ClientFirstContentfulPaint)
|
||||||
|
|
||||||
@ -1268,7 +1276,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20},
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20},
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent", "region"},
|
[]string{"platform", "agent", "region", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientLargestContentfulPaint)
|
m.Registry.MustRegister(m.ClientLargestContentfulPaint)
|
||||||
|
|
||||||
@ -1280,7 +1288,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Measure of how long it takes for a user to see the effects of clicking with a mouse, tapping with a touchscreen, or pressing a key on the keyboard (seconds)",
|
Help: "Measure of how long it takes for a user to see the effects of clicking with a mouse, tapping with a touchscreen, or pressing a key on the keyboard (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent", "interaction"},
|
[]string{"platform", "agent", "interaction", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientInteractionToNextPaint)
|
m.Registry.MustRegister(m.ClientInteractionToNextPaint)
|
||||||
|
|
||||||
@ -1292,7 +1300,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Measure of how much a page's content shifts unexpectedly",
|
Help: "Measure of how much a page's content shifts unexpectedly",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientCumulativeLayoutShift)
|
m.Registry.MustRegister(m.ClientCumulativeLayoutShift)
|
||||||
|
|
||||||
@ -1304,7 +1312,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Counter of the number of times that the browser's main UI thread is blocked for more than 50ms by a single task",
|
Help: "Counter of the number of times that the browser's main UI thread is blocked for more than 50ms by a single task",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientLongTasks)
|
m.Registry.MustRegister(m.ClientLongTasks)
|
||||||
|
|
||||||
@ -1317,7 +1325,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 20, 40},
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 20, 40},
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
m.Platform.Log(),
|
m.Platform.Log(),
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientPageLoadDuration)
|
m.Registry.MustRegister(m.ClientPageLoadDuration)
|
||||||
@ -1330,7 +1338,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration of the time taken from when a user clicks on a channel in the LHS to when posts in that channel become visible (seconds)",
|
Help: "Duration of the time taken from when a user clicks on a channel in the LHS to when posts in that channel become visible (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent", "fresh"},
|
[]string{"platform", "agent", "fresh", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientChannelSwitchDuration)
|
m.Registry.MustRegister(m.ClientChannelSwitchDuration)
|
||||||
|
|
||||||
@ -1342,7 +1350,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration of the time taken from when a user clicks on a team in the LHS to when posts in that team become visible (seconds)",
|
Help: "Duration of the time taken from when a user clicks on a team in the LHS to when posts in that team become visible (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent", "fresh"},
|
[]string{"platform", "agent", "fresh", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientTeamSwitchDuration)
|
m.Registry.MustRegister(m.ClientTeamSwitchDuration)
|
||||||
|
|
||||||
@ -1354,7 +1362,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration of the time taken from when a user clicks to open a thread in the RHS until when posts in that thread become visible (seconds)",
|
Help: "Duration of the time taken from when a user clicks to open a thread in the RHS until when posts in that thread become visible (seconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientRHSLoadDuration)
|
m.Registry.MustRegister(m.ClientRHSLoadDuration)
|
||||||
|
|
||||||
@ -1366,7 +1374,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
|||||||
Help: "Duration of the time taken from when a user clicks to open Threads in the LHS until when the global threads view becomes visible (milliseconds)",
|
Help: "Duration of the time taken from when a user clicks to open Threads in the LHS until when the global threads view becomes visible (milliseconds)",
|
||||||
ConstLabels: additionalLabels,
|
ConstLabels: additionalLabels,
|
||||||
},
|
},
|
||||||
[]string{"platform", "agent"},
|
[]string{"platform", "agent", "user_id"},
|
||||||
)
|
)
|
||||||
m.Registry.MustRegister(m.ClientGlobalThreadsLoadDuration)
|
m.Registry.MustRegister(m.ClientGlobalThreadsLoadDuration)
|
||||||
|
|
||||||
@ -2024,63 +2032,81 @@ func (mi *MetricsInterfaceImpl) DecrementHTTPWebSockets(originClient string) {
|
|||||||
mi.HTTPWebsocketsGauge.With(prometheus.Labels{"origin_client": originClient}).Dec()
|
mi.HTTPWebsocketsGauge.With(prometheus.Labels{"origin_client": originClient}).Dec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mi *MetricsInterfaceImpl) getEffectiveUserID(userID string) string {
|
||||||
|
if mi.ClientSideUserIds[userID] {
|
||||||
|
return userID
|
||||||
|
}
|
||||||
|
return "<placeholder>"
|
||||||
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientTimeToFirstByte(platform, agent, userID string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientTimeToFirstByte(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientTimeToLastByte.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientTimeToLastByte.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientTimeToDOMInteractive.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientTimeToDOMInteractive.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64) {
|
||||||
mi.ClientSplashScreenEnd.With(prometheus.Labels{"platform": platform, "agent": agent, "page_type": pageType}, userID).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientSplashScreenEnd.With(prometheus.Labels{"platform": platform, "agent": agent, "page_type": pageType, "user_id": effectiveUserID}, userID).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientLargestContentfulPaint(platform, agent, region string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientLargestContentfulPaint(platform, agent, region, userID string, elapsed float64) {
|
||||||
mi.ClientLargestContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "region": region}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientLargestContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "region": region, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientInteractionToNextPaint(platform, agent, interaction string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientInteractionToNextPaint(platform, agent, interaction, userID string, elapsed float64) {
|
||||||
mi.ClientInteractionToNextPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "interaction": interaction}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientInteractionToNextPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "interaction": interaction, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientCumulativeLayoutShift(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientCumulativeLayoutShift.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientCumulativeLayoutShift.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent string, inc float64) {
|
func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent, userID string, inc float64) {
|
||||||
mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent}).Add(inc)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Add(inc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientPageLoadDuration.With(prometheus.Labels{
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
"platform": platform,
|
mi.ClientPageLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed)
|
||||||
"agent": agent,
|
|
||||||
}, userID).Observe(elapsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent, fresh string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent, fresh, userID string, elapsed float64) {
|
||||||
mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientTeamSwitchDuration(platform, agent, fresh string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientTeamSwitchDuration(platform, agent, fresh, userID string, elapsed float64) {
|
||||||
mi.ClientTeamSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientTeamSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveClientRHSLoadDuration(platform, agent string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveClientRHSLoadDuration(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientRHSLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientRHSLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent string, elapsed float64) {
|
func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent, userID string, elapsed float64) {
|
||||||
mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
effectiveUserID := mi.getEffectiveUserID(userID)
|
||||||
|
mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *MetricsInterfaceImpl) ObserveDesktopCpuUsage(platform, version, process string, usage float64) {
|
func (mi *MetricsInterfaceImpl) ObserveDesktopCpuUsage(platform, version, process string, usage float64) {
|
||||||
|
@ -7807,10 +7807,6 @@
|
|||||||
"id": "app.channel.add_member.deleted_user.app_error",
|
"id": "app.channel.add_member.deleted_user.app_error",
|
||||||
"translation": "Nelze přidat uživatele jako člena kanálu."
|
"translation": "Nelze přidat uživatele jako člena kanálu."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Schéma indexu kanálů pro vyhledávání je zastaralé. Doporučujeme znovu vygenerovat index kanálů.\nKlikněte na tlačítko `Rebuild Channels Index` [na stránce Elasticsearch prostřednictvím systémové konzole]({{.ElasticsearchSection}}), abyste tento problém vyřešili.\nDalší informace naleznete v changelogu Mattermost."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.channel.bookmark.save.app_error",
|
"id": "app.channel.bookmark.save.app_error",
|
||||||
"translation": "Nepodařilo se uložit záložku."
|
"translation": "Nepodařilo se uložit záložku."
|
||||||
@ -10394,5 +10390,17 @@
|
|||||||
{
|
{
|
||||||
"id": "model.property_value.is_valid.app_error",
|
"id": "model.property_value.is_valid.app_error",
|
||||||
"translation": "Neplatná hodnota vlastnosti: {{.FieldName}} ({{.Reason}})."
|
"translation": "Neplatná hodnota vlastnosti: {{.FieldName}} ({{.Reason}})."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.custom_profile_attributes.license_error",
|
||||||
|
"translation": "Vaše licence nepodporuje vlastní atributy profilu."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
|
"translation": "Nelze získat čtečku souborů ZIP."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Nelze spustit příkaz v odstraněném kanálu."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9737,10 +9737,6 @@
|
|||||||
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
||||||
"translation": "Die automatische Vervollständigung von Kanälen kann nicht aktiviert werden, da das Schema des Kanal-Index veraltet ist. Es wird empfohlen, den Kanal-Index neu zu erstellen. Weitere Informationen findest du im Mattermost Changelog"
|
"translation": "Die automatische Vervollständigung von Kanälen kann nicht aktiviert werden, da das Schema des Kanal-Index veraltet ist. Es wird empfohlen, den Kanal-Index neu zu erstellen. Weitere Informationen findest du im Mattermost Changelog"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Das Indexschema deines Suchkanals ist nicht mehr aktuell. Wir empfehlen dir, deinen Kanalindex neu zu erstellen.\nKlicke auf die Schaltfläche \"Index der Kanäle neu erstellen\" auf der [Elasticsearch-Seite über die Systemkonsole] ({{.ElasticsearchSection}}), um dieses Problem zu beheben.\nWeitere Informationen findest du im Mattermost Changelog."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "bleveengine.purge_list.not_implemented",
|
"id": "bleveengine.purge_list.not_implemented",
|
||||||
"translation": "Die Funktion Bereinigungsliste ist für Bleve nicht verfügbar."
|
"translation": "Die Funktion Bereinigungsliste ist für Bleve nicht verfügbar."
|
||||||
@ -10398,5 +10394,9 @@
|
|||||||
{
|
{
|
||||||
"id": "api.file.zip_file_reader.app_error",
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
"translation": "Es ist nicht möglich, einen Zip-Datei-Leser zu bekommen."
|
"translation": "Es ist nicht möglich, einen Zip-Datei-Leser zu bekommen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Kommando kann nicht in einem gelöschten Kanal ausgeführt werden."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1137,11 +1137,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.config.is_valid.elastic_search.enable_searching.app_error",
|
"id": "model.config.is_valid.elastic_search.enable_searching.app_error",
|
||||||
"translation": "Search EnableIndexing setting must be set to true when Elasticsearch EnableSearching is set to true"
|
"translation": "{{.EnableIndexing}} setting must be set to true when {{.Searching}} is set to true"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error",
|
"id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error",
|
||||||
"translation": "Search EnableIndexing setting must be set to true when Elasticsearch EnableAutocomplete is set to true"
|
"translation": "{{.EnableIndexing}} setting must be set to true when {{.Autocomplete}} is set to true"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.config.is_valid.elastic_search.connection_url.app_error",
|
"id": "model.config.is_valid.elastic_search.connection_url.app_error",
|
||||||
@ -3125,11 +3125,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.test_config.reenter_password",
|
"id": "ent.elasticsearch.test_config.reenter_password",
|
||||||
"translation": "The Elasticsearch Server URL or Username has changed. Please re-enter the Elasticsearch password to test connection."
|
"translation": "The Search Server URL or Username has changed. Please re-enter the password to test connection."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.test_config.license.error",
|
"id": "ent.elasticsearch.test_config.license.error",
|
||||||
"translation": "Your licence does not support Elasticsearch."
|
"translation": "Your Mattermost licence doesn't support indexed search."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.test_config.indexing_disabled.error",
|
"id": "ent.elasticsearch.test_config.indexing_disabled.error",
|
||||||
@ -3197,7 +3197,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.refresh_indexes.refresh_failed",
|
"id": "ent.elasticsearch.refresh_indexes.refresh_failed",
|
||||||
"translation": "Failed to refresh Elasticsearch indexes"
|
"translation": "Failed to refresh search indexes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.post.get_posts_batch_for_indexing.error",
|
"id": "ent.elasticsearch.post.get_posts_batch_for_indexing.error",
|
||||||
@ -9574,10 +9574,6 @@
|
|||||||
"id": "ent.elasticsearch.purge_indexes.unknown_index",
|
"id": "ent.elasticsearch.purge_indexes.unknown_index",
|
||||||
"translation": "Failed to delete an unknown index specified"
|
"translation": "Failed to delete an unknown index specified"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Your Elasticsearch channel index schema is out of date. It is recommended to regenerate your channel index.\nClick the `Rebuild Channels Index` button in [Elasticsearch section in System Console]({{.ElasticsearchSection}}) to fix the issue.\nSee Mattermost changelog for more information."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "model.channel_bookmark.is_valid.link_file.app_error",
|
"id": "model.channel_bookmark.is_valid.link_file.app_error",
|
||||||
"translation": "Cannot set a link and a file in the same bookmark."
|
"translation": "Cannot set a link and a file in the same bookmark."
|
||||||
@ -9728,7 +9724,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.purge_index.delete_failed",
|
"id": "ent.elasticsearch.purge_index.delete_failed",
|
||||||
"translation": "Failed to delete an Elasticsearch index"
|
"translation": "Failed to delete a search index"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.channel_bookmark.is_valid.image_url.app_error",
|
"id": "model.channel_bookmark.is_valid.image_url.app_error",
|
||||||
@ -10267,5 +10263,137 @@
|
|||||||
{
|
{
|
||||||
"id": "model.post.is_valid.message_length.app_error",
|
"id": "model.post.is_valid.message_length.app_error",
|
||||||
"translation": "Post Message property is longer than the maximum permitted length."
|
"translation": "Post Message property is longer than the maximum permitted length."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Command can't be executed in a deleted channel."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.filter_config_error",
|
||||||
|
"translation": "Unable to filter the configuration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.export.export_custom_emoji.mkdir.error",
|
||||||
|
"translation": "Unable to create a directory for custom emoji images"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.cpa_group_id.app_error",
|
||||||
|
"translation": "Cannot register Custom Profile Attributes property group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.get_property_field.app_error",
|
||||||
|
"translation": "Unable to get Custom Profile Attribute field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.create_property_field.app_error",
|
||||||
|
"translation": "Unable to create Custom Profile Attribute field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.limit_reached.app_error",
|
||||||
|
"translation": "Custom Profile Attributes field limit reached"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.list_property_values.app_error",
|
||||||
|
"translation": "Unable to get custom profile attribute values"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_value_creation.app_error",
|
||||||
|
"translation": "Cannot create property value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_value_list.app_error",
|
||||||
|
"translation": "Unable to retrieve property values"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_field_delete.app_error",
|
||||||
|
"translation": "Unable to delete Custom Profile Attribute field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_field_not_found.app_error",
|
||||||
|
"translation": "Custom Profile Attribute field not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_field_update.app_error",
|
||||||
|
"translation": "Unable to update Custom Profile Attribute field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.search_property_fields.app_error",
|
||||||
|
"translation": "Unable to search Custom Profile Attribute fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.file_info.delete_for_post_ids.app_error",
|
||||||
|
"translation": "Failed to remove the requested files from database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_value_update.app_error",
|
||||||
|
"translation": "Cannot update property value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.file_info.undelete_for_post_ids.app_error",
|
||||||
|
"translation": "Failed to restore post file attachments."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.import.validate_user_import_data.guest_roles_conflict.error",
|
||||||
|
"translation": "User roles are not consistent with guest status."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.post.restore_post_version.not_allowed.app_error",
|
||||||
|
"translation": "You do not have the appropriate permissions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.post.restore_post_version.get_single.app_error",
|
||||||
|
"translation": "Failed to get the old post version."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.post.restore_post_version.not_an_history_item.app_error",
|
||||||
|
"translation": "The provided post history ID does not correspond to any history item for the specified post."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.post.restore_post_version.not_valid_post_history_item.app_error",
|
||||||
|
"translation": "The provided post history ID does not correspond to a post history item."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.role.delete.app_error",
|
||||||
|
"translation": "Unable to delete role."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.message_export.actiance_export.calculate_channel_exports.channel_message",
|
||||||
|
"translation": "Exporting channel information for {{.NumChannels}} channels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.message_export.calculate_channel_exports.app_error",
|
||||||
|
"translation": "Failed to calculate channel export data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.message_export.job_data_conversion.app_error",
|
||||||
|
"translation": "Failed to convert a value from the job's data field."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.message_export.actiance_export.calculate_channel_exports.activity_message",
|
||||||
|
"translation": "Calculating channel activity: {{.NumCompleted}}/{{.NumChannels}} channels completed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.property_field.is_valid.app_error",
|
||||||
|
"translation": "Invalid property field: {{.FieldName}} ({{.Reason}})."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.property_value.is_valid.app_error",
|
||||||
|
"translation": "Invalid property value: {{.FieldName}} ({{.Reason}})."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.custom_profile_attributes.license_error",
|
||||||
|
"translation": "Your licence does not support Custom Profile Attributes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
|
"translation": "Unable to get a zip file reader."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.file_info.get_by_ids.app_error",
|
||||||
|
"translation": "Unable to get the file infos by ids for post edit history."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.link_metadata.is_valid.url_length.app_error",
|
||||||
|
"translation": "Length of link metadata URL is {{ .Length }} characters long, which exceeds the maximum limit of {{ .MaxLength }} characters."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -5006,6 +5006,10 @@
|
|||||||
"id": "app.custom_group.unique_name",
|
"id": "app.custom_group.unique_name",
|
||||||
"translation": "group name is not unique"
|
"translation": "group name is not unique"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.count_property_fields.app_error",
|
||||||
|
"translation": "Unable to count the number of fields for the custom profile attribute group"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.custom_profile_attributes.cpa_group_id.app_error",
|
"id": "app.custom_profile_attributes.cpa_group_id.app_error",
|
||||||
"translation": "Cannot register Custom Profile Attributes property group"
|
"translation": "Cannot register Custom Profile Attributes property group"
|
||||||
@ -5039,16 +5043,8 @@
|
|||||||
"translation": "Unable to update Custom Profile Attribute field"
|
"translation": "Unable to update Custom Profile Attribute field"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "app.custom_profile_attributes.property_value_creation.app_error",
|
"id": "app.custom_profile_attributes.property_value_upsert.app_error",
|
||||||
"translation": "Cannot create property value"
|
"translation": "Unable to upsert Custom Profile Attribute fields"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.custom_profile_attributes.property_value_list.app_error",
|
|
||||||
"translation": "Unable to retrieve property values"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "app.custom_profile_attributes.property_value_update.app_error",
|
|
||||||
"translation": "Cannot update property value"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "app.custom_profile_attributes.search_property_fields.app_error",
|
"id": "app.custom_profile_attributes.search_property_fields.app_error",
|
||||||
@ -9080,6 +9076,14 @@
|
|||||||
"id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error",
|
"id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error",
|
||||||
"translation": "Message export job GlobalRelaySettings.SmtpUsername must be set."
|
"translation": "Message export job GlobalRelaySettings.SmtpUsername must be set."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.metrics_client_side_user_id.app_error",
|
||||||
|
"translation": "Invalid client side user id: {{.Id}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.metrics_client_side_user_ids.app_error",
|
||||||
|
"translation": "Number of elements in ClientSideUserIds {{.CurrentLength}} is higher than maximum limit of {{.MaxLength}}."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "model.config.is_valid.move_thread.domain_invalid.app_error",
|
"id": "model.config.is_valid.move_thread.domain_invalid.app_error",
|
||||||
"translation": "Invalid domain for move thread settings"
|
"translation": "Invalid domain for move thread settings"
|
||||||
@ -9430,7 +9434,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.incoming_hook.id.app_error",
|
"id": "model.incoming_hook.id.app_error",
|
||||||
"translation": "Invalid Id."
|
"translation": "Invalid Id: {{.Id}}."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "model.incoming_hook.parse_data.app_error",
|
"id": "model.incoming_hook.parse_data.app_error",
|
||||||
|
@ -8851,10 +8851,6 @@
|
|||||||
"id": "api.oauth.get_access_token.bad_request.app_error",
|
"id": "api.oauth.get_access_token.bad_request.app_error",
|
||||||
"translation": "invalid_request : Bad request."
|
"translation": "invalid_request : Bad request."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Votre schéma d'index de canal n'est pas à jour sur Elasticsearch. Il est recommandé de régénérer votre index de canal.\nCliquez sur le bouton `Reconstruire l'index des canaux` de la [section Elasticsearch dans la console système]({{.ElasticsearchSection}}) pour régler ce problème.\nConsultez le journal des modifications pour plus de détails."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "model.outgoing_oauth_connection.is_valid.oauth_token_url.error",
|
"id": "model.outgoing_oauth_connection.is_valid.oauth_token_url.error",
|
||||||
"translation": "URL du jeton OAuth (OAuth Token URL) invalide."
|
"translation": "URL du jeton OAuth (OAuth Token URL) invalide."
|
||||||
|
@ -9569,10 +9569,6 @@
|
|||||||
"id": "app.channel.bookmark.update_sort.app_error",
|
"id": "app.channel.bookmark.update_sort.app_error",
|
||||||
"translation": "ブックマークをソートできませんでした。"
|
"translation": "ブックマークをソートできませんでした。"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "検索チャネルインデックススキーマが古くなっています。チャンネルインデックスの再生成を推奨します。\nこの問題を解決するには、[システムコンソールの Elasticsearch ページ]({{.ElasticsearchSection}}) にある `チャンネルインデックスの再構築` ボタンをクリックしてください。\n詳しくは Mattermostの変更履歴を参照してください。"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.import.import_line.null_role.error",
|
"id": "app.import.import_line.null_role.error",
|
||||||
"translation": "インポートデータ行に \"role\" タイプがありますが、roleオブジェクトがnullです。"
|
"translation": "インポートデータ行に \"role\" タイプがありますが、roleオブジェクトがnullです。"
|
||||||
@ -10380,5 +10376,17 @@
|
|||||||
{
|
{
|
||||||
"id": "model.property_value.is_valid.app_error",
|
"id": "model.property_value.is_valid.app_error",
|
||||||
"translation": "不正なプロパティ値: {{.FieldName}} {{.Reason}}。"
|
"translation": "不正なプロパティ値: {{.FieldName}} {{.Reason}}。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "削除されたチャンネルではコマンドを実行できません。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.custom_profile_attributes.license_error",
|
||||||
|
"translation": "あなたのライセンスはカスタムプロフィール属性をサポートしていません。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
|
"translation": "ZIPファイルリーダーを取得できませんでした。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9733,10 +9733,6 @@
|
|||||||
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
||||||
"translation": "Kanalen automatisch aanvullen kan niet worden ingeschakeld omdat het schema van de kanaalindex verouderd is. Het wordt aanbevolen om je kanaalindex te regenereren. Zie de Mattermost changelog voor meer informatie"
|
"translation": "Kanalen automatisch aanvullen kan niet worden ingeschakeld omdat het schema van de kanaalindex verouderd is. Het wordt aanbevolen om je kanaalindex te regenereren. Zie de Mattermost changelog voor meer informatie"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Het schema van de index van je zoekkanalen is verouderd. We raden aan om je kanaalindex te regenereren.\nKlik op de `Rebuild Channels Index` knop in de [Elasticsearch pagina via de System Console]({{.ElasticsearchSection}}) om dit probleem op te lossen.\nZie de Mattermost changelog voor meer informatie."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "bleveengine.purge_list.not_implemented",
|
"id": "bleveengine.purge_list.not_implemented",
|
||||||
"translation": "De functie lijst leegmaken is niet beschikbaar voor Bleve."
|
"translation": "De functie lijst leegmaken is niet beschikbaar voor Bleve."
|
||||||
@ -10402,5 +10398,17 @@
|
|||||||
{
|
{
|
||||||
"id": "app.custom_profile_attributes.property_value_update.app_error",
|
"id": "app.custom_profile_attributes.property_value_update.app_error",
|
||||||
"translation": "Kan de waarde van de eigenschap niet bijwerken"
|
"translation": "Kan de waarde van de eigenschap niet bijwerken"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Kan geen opdracht uitvoeren in verwijderde chatroom."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
|
"translation": "Kon geen zip-bestandslezer vinden."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.custom_profile_attributes.license_error",
|
||||||
|
"translation": "Jouw licentie ondersteunt geen Aangepaste Profielattributen."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9739,10 +9739,6 @@
|
|||||||
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
||||||
"translation": "Nie można włączyć autouzupełniania kanałów, ponieważ schemat indeksu kanałów jest nieaktualny. Zaleca się regenerację indeksu kanałów. Zobacz dziennik zmian Mattermost, aby uzyskać więcej informacji"
|
"translation": "Nie można włączyć autouzupełniania kanałów, ponieważ schemat indeksu kanałów jest nieaktualny. Zaleca się regenerację indeksu kanałów. Zobacz dziennik zmian Mattermost, aby uzyskać więcej informacji"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Twój schemat indeksu kanałów wyszukiwania jest nieaktualny. Zalecamy regenerację indeksu kanałów.\nKliknij przycisk `Przebuduj Indeks Kanałów` na stronie [Elasticsearch via the System Console]({{.ElasticsearchSection}}), aby naprawić ten błąd.\nZobacz logi Mattermost, aby uzyskać więcej informacji."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "bleveengine.purge_list.not_implemented",
|
"id": "bleveengine.purge_list.not_implemented",
|
||||||
"translation": "Funkcja listy czyszczenia nie jest dostępna dla Bleve."
|
"translation": "Funkcja listy czyszczenia nie jest dostępna dla Bleve."
|
||||||
|
@ -9303,10 +9303,6 @@
|
|||||||
"id": "app.acknowledgement.getforpost.get.app_error",
|
"id": "app.acknowledgement.getforpost.get.app_error",
|
||||||
"translation": "Não foi possível obter confirmação de recebimento da publicação."
|
"translation": "Não foi possível obter confirmação de recebimento da publicação."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "O esquema do índice de canais do Elasticsearch está desatualizado. É recomendável que você gere novamente o índice de canais.\nClique no botão `Reconstruir índices de Canais` na [seção Elasticsearch no System Console]({{.ElasticsearchSection}}) para corrigir o problema.\nConsulte o registro de alterações do Mattermost para obter mais informações."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.compile_report_chunks.unsupported_format",
|
"id": "app.compile_report_chunks.unsupported_format",
|
||||||
"translation": "Formato de relatório não suportado."
|
"translation": "Formato de relatório não suportado."
|
||||||
|
@ -9803,10 +9803,6 @@
|
|||||||
"id": "api.user.auth_switch.not_available.login_disabled.app_error",
|
"id": "api.user.auth_switch.not_available.login_disabled.app_error",
|
||||||
"translation": "Передача аутентификации недоступна, так как не включен ни вход по электронной почте, ни вход по имени пользователя."
|
"translation": "Передача аутентификации недоступна, так как не включен ни вход по электронной почте, ни вход по имени пользователя."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Схема поискового индекса ваших каналов устарела. Мы рекомендуем регенерировать индекс ваших каналов.\nНажмите кнопку `Ребилд индексов каналов` на [странице Elasticsearch через Системную консоль]({{.ElasticsearchSection}}), чтобы исправить эту проблему.\nБолее подробную информацию смотри в журнале изменений Mattermost."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "api.post.check_for_out_of_team_mentions.message.multiple",
|
"id": "api.post.check_for_out_of_team_mentions.message.multiple",
|
||||||
"translation": "@{{.Usernames}} и @{{.LastUsername}} не получили уведомлений об этом упоминании, потому что они не являются участниками этой команды."
|
"translation": "@{{.Usernames}} и @{{.LastUsername}} не получили уведомлений об этом упоминании, потому что они не являются участниками этой команды."
|
||||||
|
@ -9733,10 +9733,6 @@
|
|||||||
"id": "model.channel_bookmark.is_valid.update_at.app_error",
|
"id": "model.channel_bookmark.is_valid.update_at.app_error",
|
||||||
"translation": "\"Update at\" måste vara en giltig tid."
|
"translation": "\"Update at\" måste vara en giltig tid."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Ditt Elasticsearch-indexschema för kanaler är föråldrat. En omindexering av kanal-index rekommenderas.\nKlicka på knappen `Rebuild Channels Index` i [Elasticsearch-avsnittet i systemkonsolen] ({{.ElasticsearchSection}}) för att åtgärda problemet.\nSe Mattermost changelog för mer information."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
"id": "api.config.update.elasticsearch.autocomplete_cannot_be_enabled_error",
|
||||||
"translation": "Autokomplettering av kanaler kan inte aktiveras eftersom indexschemat för kanaler är föråldrat. Vi rekommenderar att du omindexerar ditt kanalindex. Se Mattermost changelog för mer information"
|
"translation": "Autokomplettering av kanaler kan inte aktiveras eftersom indexschemat för kanaler är föråldrat. Vi rekommenderar att du omindexerar ditt kanalindex. Se Mattermost changelog för mer information"
|
||||||
|
@ -9705,10 +9705,6 @@
|
|||||||
"id": "api.templates.ip_filters_changed_footer.title",
|
"id": "api.templates.ip_filters_changed_footer.title",
|
||||||
"translation": "Çalışma alanınıza erişmekte sorun mu yaşıyorsunuz?"
|
"translation": "Çalışma alanınıza erişmekte sorun mu yaşıyorsunuz?"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Arama kanal dizini şemanız güncel değil. Kanal dizininizi yeniden oluşturmanız önerilir.\nSorunu çözmek için [Sistem panosundaki Elasticsearch bölümünden] ({{.ElasticsearchSection}}) `Kanal dizinini yeniden oluştur` düğmesine tıklayın.\nAyrıntılı bilgi almak için Mattermost değişiklik günlüğüne bakın."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "app.user.update_active.user_limit.exceeded",
|
"id": "app.user.update_active.user_limit.exceeded",
|
||||||
"translation": "Kullanıcı etkinleştirilemedi. Sunucu güvenli kullanıcı sayısı sınırını aşıyor. Yöneticiniz ile görüşün: ERROR_SAFETY_LIMITS_EXCEEDED."
|
"translation": "Kullanıcı etkinleştirilemedi. Sunucu güvenli kullanıcı sayısı sınırını aşıyor. Yöneticiniz ile görüşün: ERROR_SAFETY_LIMITS_EXCEEDED."
|
||||||
@ -10398,5 +10394,9 @@
|
|||||||
{
|
{
|
||||||
"id": "model.property_value.is_valid.app_error",
|
"id": "model.property_value.is_valid.app_error",
|
||||||
"translation": "Özellik değeri geçersiz: {{.FieldName}} ({{.Reason}})."
|
"translation": "Özellik değeri geçersiz: {{.FieldName}} ({{.Reason}})."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Silinmiş bir kanalda komut yürütülemez."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -8279,10 +8279,6 @@
|
|||||||
"id": "app.channel.autofollow.app_error",
|
"id": "app.channel.autofollow.app_error",
|
||||||
"translation": "Не вдалося оновити участь у обговореннях для згаданого користувача"
|
"translation": "Не вдалося оновити участь у обговореннях для згаданого користувача"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "Схема індексу каналу пошуку застаріла. Ми рекомендуємо регенерувати індекс вашого каналу.\nНатисніть кнопку `Перебудувати індекс каналів` на [сторінці Elasticsearch через Системну консоль]({{.ElasticsearchSection}}), щоб вирішити цю проблему.\nДетальнішу інформацію ви можете переглянути у журналі змін Mattermost."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "api.user.create_user.user_limits.exceeded",
|
"id": "api.user.create_user.user_limits.exceeded",
|
||||||
"translation": "Неможливо створити користувача. Сервер перевищує ліміт безпечних користувачів. Зверніться до свого адміністратора з повідомленням: ERROR_SAFETY_LIMITS_EXCEEDED."
|
"translation": "Неможливо створити користувача. Сервер перевищує ліміт безпечних користувачів. Зверніться до свого адміністратора з повідомленням: ERROR_SAFETY_LIMITS_EXCEEDED."
|
||||||
@ -10342,5 +10338,17 @@
|
|||||||
{
|
{
|
||||||
"id": "app.custom_profile_attributes.limit_reached.app_error",
|
"id": "app.custom_profile_attributes.limit_reached.app_error",
|
||||||
"translation": "Досягнуто ліміт поля \"Атрибути профілю користувача\""
|
"translation": "Досягнуто ліміт поля \"Атрибути профілю користувача\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "Неможливо виконати команду у видаленому каналі."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_field_not_found.app_error",
|
||||||
|
"translation": "Користувацьке поле атрибуту профілю не знайдено"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.custom_profile_attributes.property_field_update.app_error",
|
||||||
|
"translation": "Не вдається оновити поле атрибуту користувацького профілю"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9709,10 +9709,6 @@
|
|||||||
"id": "app.channel.bookmark.update_sort.app_error",
|
"id": "app.channel.bookmark.update_sort.app_error",
|
||||||
"translation": "无法排序书签。"
|
"translation": "无法排序书签。"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "您的 Elasticsearch 频道索引表已过期。推荐您重新生成频道索引。\n在[系统控制台中的 Elasticsearch 部分]({{.ElasticsearchSection}})点击`重建频道索引`以解决问题。\n查看 Mattermost 更新日志以了解更多信息。"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.purge_index.delete_failed",
|
"id": "ent.elasticsearch.purge_index.delete_failed",
|
||||||
"translation": "删除一条 Elasticsearch 索引失败"
|
"translation": "删除一条 Elasticsearch 索引失败"
|
||||||
@ -10380,5 +10376,17 @@
|
|||||||
{
|
{
|
||||||
"id": "ent.message_export.job_data_conversion.app_error",
|
"id": "ent.message_export.job_data_conversion.app_error",
|
||||||
"translation": "无法转换任务数据字段中的值。"
|
"translation": "无法转换任务数据字段中的值。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.command.execute_command.deleted.error",
|
||||||
|
"translation": "不能在已删除的频道中执行命令。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.custom_profile_attributes.license_error",
|
||||||
|
"translation": "您的授权不支持自定义个人资料属性。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.file.zip_file_reader.app_error",
|
||||||
|
"translation": "无法获取 zip 文件读取器。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9683,10 +9683,6 @@
|
|||||||
"id": "api.user.create_user.user_limits.exceeded",
|
"id": "api.user.create_user.user_limits.exceeded",
|
||||||
"translation": "無法建立使用者。使用者數量超過伺服器安全限制。請聯絡您的管理員,並附上錯誤代碼:ERROR_SAFETY_LIMITS_EXCEEDED。"
|
"translation": "無法建立使用者。使用者數量超過伺服器安全限制。請聯絡您的管理員,並附上錯誤代碼:ERROR_SAFETY_LIMITS_EXCEEDED。"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "app.channel.elasticsearch_channel_index.notify_admin.message",
|
|
||||||
"translation": "您的 Elasticsearch 頻道索引架構已過期,建議重新生成您的頻道索引。\n請點ㄧ下 [系統管理後台的 Elasticsearch]{{.ElasticsearchSection}} 中的「重建頻道索引」按鈕來修復此問題。 \n有關更多資訊,請參閱 Mattermost 變更記錄。"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "ent.elasticsearch.purge_index.delete_failed",
|
"id": "ent.elasticsearch.purge_index.delete_failed",
|
||||||
"translation": "刪除一條 Elasticsearch 索引失敗"
|
"translation": "刪除一條 Elasticsearch 索引失敗"
|
||||||
|
@ -439,6 +439,7 @@ type ServiceSettings struct {
|
|||||||
MaximumPayloadSizeBytes *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
MaximumPayloadSizeBytes *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
||||||
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
||||||
ScheduledPosts *bool `access:"site_posts"`
|
ScheduledPosts *bool `access:"site_posts"`
|
||||||
|
EnableWebHubChannelIteration *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
}
|
}
|
||||||
|
|
||||||
var MattermostGiphySdkKey string
|
var MattermostGiphySdkKey string
|
||||||
@ -962,6 +963,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
|||||||
if s.ScheduledPosts == nil {
|
if s.ScheduledPosts == nil {
|
||||||
s.ScheduledPosts = NewPointer(true)
|
s.ScheduledPosts = NewPointer(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.EnableWebHubChannelIteration == nil {
|
||||||
|
s.EnableWebHubChannelIteration = NewPointer(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheSettings struct {
|
type CacheSettings struct {
|
||||||
@ -1071,11 +1076,12 @@ func (s *ClusterSettings) SetDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MetricsSettings struct {
|
type MetricsSettings struct {
|
||||||
Enable *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
Enable *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
||||||
BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
||||||
ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none
|
ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
EnableClientMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
EnableClientMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
||||||
EnableNotificationMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
EnableNotificationMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"`
|
||||||
|
ClientSideUserIds []string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MetricsSettings) SetDefaults() {
|
func (s *MetricsSettings) SetDefaults() {
|
||||||
@ -1098,6 +1104,23 @@ func (s *MetricsSettings) SetDefaults() {
|
|||||||
if s.EnableNotificationMetrics == nil {
|
if s.EnableNotificationMetrics == nil {
|
||||||
s.EnableNotificationMetrics = NewPointer(true)
|
s.EnableNotificationMetrics = NewPointer(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.ClientSideUserIds == nil {
|
||||||
|
s.ClientSideUserIds = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MetricsSettings) isValid() *AppError {
|
||||||
|
const maxLength = 5
|
||||||
|
if len(s.ClientSideUserIds) > maxLength {
|
||||||
|
return NewAppError("MetricsSettings.IsValid", "model.config.is_valid.metrics_client_side_user_ids.app_error", map[string]any{"MaxLength": maxLength, "CurrentLength": len(s.ClientSideUserIds)}, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
for _, id := range s.ClientSideUserIds {
|
||||||
|
if !IsValidId(id) {
|
||||||
|
return NewAppError("MetricsSettings.IsValid", "model.config.is_valid.metrics_client_side_user_id.app_error", map[string]any{"Id": id}, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExperimentalSettings struct {
|
type ExperimentalSettings struct {
|
||||||
@ -3806,6 +3829,10 @@ func (o *Config) IsValid() *AppError {
|
|||||||
return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest)
|
return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appErr := o.MetricsSettings.isValid(); appErr != nil {
|
||||||
|
return appErr
|
||||||
|
}
|
||||||
|
|
||||||
if appErr := o.CacheSettings.isValid(); appErr != nil {
|
if appErr := o.CacheSettings.isValid(); appErr != nil {
|
||||||
return appErr
|
return appErr
|
||||||
}
|
}
|
||||||
|
@ -4,3 +4,19 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
const CustomProfileAttributesPropertyGroupName = "custom_profile_attributes"
|
const CustomProfileAttributesPropertyGroupName = "custom_profile_attributes"
|
||||||
|
|
||||||
|
const CustomProfileAttributesPropertyAttrsSortOrder = "sort_order"
|
||||||
|
|
||||||
|
func CustomProfileAttributesPropertySortOrder(p *PropertyField) int {
|
||||||
|
value, ok := p.Attrs[CustomProfileAttributesPropertyAttrsSortOrder]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
order, ok := value.(float64)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(order)
|
||||||
|
}
|
||||||
|
@ -66,7 +66,7 @@ type IncomingWebhooksWithCount struct {
|
|||||||
|
|
||||||
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", map[string]any{"Id": o.Id}, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.CreateAt == 0 {
|
if o.CreateAt == 0 {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -98,7 +99,7 @@ func (pf *PropertyField) SanitizeInput() {
|
|||||||
type PropertyFieldPatch struct {
|
type PropertyFieldPatch struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Type *PropertyFieldType `json:"type"`
|
Type *PropertyFieldType `json:"type"`
|
||||||
Attrs *map[string]any `json:"attrs"`
|
Attrs *StringInterface `json:"attrs"`
|
||||||
TargetID *string `json:"target_id"`
|
TargetID *string `json:"target_id"`
|
||||||
TargetType *string `json:"target_type"`
|
TargetType *string `json:"target_type"`
|
||||||
}
|
}
|
||||||
@ -141,11 +142,39 @@ func (pf *PropertyField) Patch(patch *PropertyFieldPatch) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PropertyFieldSearchCursor struct {
|
||||||
|
PropertyFieldID string
|
||||||
|
CreateAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PropertyFieldSearchCursor) IsEmpty() bool {
|
||||||
|
return p.PropertyFieldID == "" && p.CreateAt == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PropertyFieldSearchCursor) IsValid() error {
|
||||||
|
if p.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CreateAt <= 0 {
|
||||||
|
return errors.New("create at cannot be negative or zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidId(p.PropertyFieldID) {
|
||||||
|
return errors.New("property field id is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type PropertyFieldSearchOpts struct {
|
type PropertyFieldSearchOpts struct {
|
||||||
GroupID string
|
GroupID string
|
||||||
TargetType string
|
TargetType string
|
||||||
TargetID string
|
TargetID string
|
||||||
IncludeDeleted bool
|
IncludeDeleted bool
|
||||||
Page int
|
Cursor PropertyFieldSearchCursor
|
||||||
PerPage int
|
PerPage int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pf *PropertyField) GetAttr(key string) any {
|
||||||
|
return pf.Attrs[key]
|
||||||
|
}
|
||||||
|
218
server/public/model/property_field_test.go
Normal file
218
server/public/model/property_field_test.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPropertyField_PreSave(t *testing.T) {
|
||||||
|
t.Run("sets ID if empty", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{}
|
||||||
|
pf.PreSave()
|
||||||
|
assert.NotEmpty(t, pf.ID)
|
||||||
|
assert.Len(t, pf.ID, 26) // Length of NewId()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps existing ID", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{ID: "existing_id"}
|
||||||
|
pf.PreSave()
|
||||||
|
assert.Equal(t, "existing_id", pf.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets CreateAt if zero", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{}
|
||||||
|
pf.PreSave()
|
||||||
|
assert.NotZero(t, pf.CreateAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets UpdateAt equal to CreateAt", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{}
|
||||||
|
pf.PreSave()
|
||||||
|
assert.Equal(t, pf.CreateAt, pf.UpdateAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyField_IsValid(t *testing.T) {
|
||||||
|
t.Run("valid field", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "test field",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.NoError(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid ID", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: "invalid",
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "test field",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid GroupID", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: "invalid",
|
||||||
|
Name: "test field",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty name", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid type", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "test field",
|
||||||
|
Type: "invalid",
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero CreateAt", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "test field",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: 0,
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero UpdateAt", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
ID: NewId(),
|
||||||
|
GroupID: NewId(),
|
||||||
|
Name: "test field",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: 0,
|
||||||
|
}
|
||||||
|
require.Error(t, pf.IsValid())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyField_SanitizeInput(t *testing.T) {
|
||||||
|
t.Run("trims spaces from name", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{Name: " test field "}
|
||||||
|
pf.SanitizeInput()
|
||||||
|
assert.Equal(t, "test field", pf.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyField_Patch(t *testing.T) {
|
||||||
|
t.Run("patches all fields", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
Name: "original name",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
TargetID: "original_target",
|
||||||
|
TargetType: "original_type",
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &PropertyFieldPatch{
|
||||||
|
Name: NewPointer("new name"),
|
||||||
|
Type: NewPointer(PropertyFieldTypeSelect),
|
||||||
|
TargetID: NewPointer("new_target"),
|
||||||
|
TargetType: NewPointer("new_type"),
|
||||||
|
Attrs: &StringInterface{"key": "value"},
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.Patch(patch)
|
||||||
|
|
||||||
|
assert.Equal(t, "new name", pf.Name)
|
||||||
|
assert.Equal(t, PropertyFieldTypeSelect, pf.Type)
|
||||||
|
assert.Equal(t, "new_target", pf.TargetID)
|
||||||
|
assert.Equal(t, "new_type", pf.TargetType)
|
||||||
|
assert.EqualValues(t, StringInterface{"key": "value"}, pf.Attrs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("patches only specified fields", func(t *testing.T) {
|
||||||
|
pf := &PropertyField{
|
||||||
|
Name: "original name",
|
||||||
|
Type: PropertyFieldTypeText,
|
||||||
|
TargetID: "original_target",
|
||||||
|
TargetType: "original_type",
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &PropertyFieldPatch{
|
||||||
|
Name: NewPointer("new name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.Patch(patch)
|
||||||
|
|
||||||
|
assert.Equal(t, "new name", pf.Name)
|
||||||
|
assert.Equal(t, PropertyFieldTypeText, pf.Type)
|
||||||
|
assert.Equal(t, "original_target", pf.TargetID)
|
||||||
|
assert.Equal(t, "original_type", pf.TargetType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyFieldSearchCursor_IsValid(t *testing.T) {
|
||||||
|
t.Run("empty cursor is valid", func(t *testing.T) {
|
||||||
|
cursor := PropertyFieldSearchCursor{}
|
||||||
|
assert.NoError(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid cursor", func(t *testing.T) {
|
||||||
|
cursor := PropertyFieldSearchCursor{
|
||||||
|
PropertyFieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
assert.NoError(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid PropertyFieldID", func(t *testing.T) {
|
||||||
|
cursor := PropertyFieldSearchCursor{
|
||||||
|
PropertyFieldID: "invalid",
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero CreateAt", func(t *testing.T) {
|
||||||
|
cursor := PropertyFieldSearchCursor{
|
||||||
|
PropertyFieldID: NewId(),
|
||||||
|
CreateAt: 0,
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative CreateAt", func(t *testing.T) {
|
||||||
|
cursor := PropertyFieldSearchCursor{
|
||||||
|
PropertyFieldID: NewId(),
|
||||||
|
CreateAt: -1,
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
}
|
@ -6,6 +6,8 @@ package model
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PropertyValue struct {
|
type PropertyValue struct {
|
||||||
@ -63,12 +65,36 @@ func (pv *PropertyValue) IsValid() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PropertyValueSearchCursor struct {
|
||||||
|
PropertyValueID string
|
||||||
|
CreateAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PropertyValueSearchCursor) IsEmpty() bool {
|
||||||
|
return p.PropertyValueID == "" && p.CreateAt == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PropertyValueSearchCursor) IsValid() error {
|
||||||
|
if p.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CreateAt <= 0 {
|
||||||
|
return errors.New("create at cannot be negative or zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidId(p.PropertyValueID) {
|
||||||
|
return errors.New("property field id is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type PropertyValueSearchOpts struct {
|
type PropertyValueSearchOpts struct {
|
||||||
GroupID string
|
GroupID string
|
||||||
TargetType string
|
TargetType string
|
||||||
TargetID string
|
TargetID string
|
||||||
FieldID string
|
FieldID string
|
||||||
IncludeDeleted bool
|
IncludeDeleted bool
|
||||||
Page int
|
Cursor PropertyValueSearchCursor
|
||||||
PerPage int
|
PerPage int
|
||||||
}
|
}
|
||||||
|
186
server/public/model/property_value_test.go
Normal file
186
server/public/model/property_value_test.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPropertyValue_PreSave(t *testing.T) {
|
||||||
|
t.Run("sets ID if empty", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{}
|
||||||
|
pv.PreSave()
|
||||||
|
assert.NotEmpty(t, pv.ID)
|
||||||
|
assert.Len(t, pv.ID, 26) // Length of NewId()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps existing ID", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{ID: "existing_id"}
|
||||||
|
pv.PreSave()
|
||||||
|
assert.Equal(t, "existing_id", pv.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets CreateAt if zero", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{}
|
||||||
|
pv.PreSave()
|
||||||
|
assert.NotZero(t, pv.CreateAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets UpdateAt equal to CreateAt", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{}
|
||||||
|
pv.PreSave()
|
||||||
|
assert.Equal(t, pv.CreateAt, pv.UpdateAt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyValue_IsValid(t *testing.T) {
|
||||||
|
t.Run("valid value", func(t *testing.T) {
|
||||||
|
value := json.RawMessage(`{"test": "value"}`)
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
Value: value,
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.NoError(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid ID", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: "invalid",
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid TargetID", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: "invalid",
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty TargetType", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid GroupID", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: "invalid",
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid FieldID", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: "invalid",
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero CreateAt", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: 0,
|
||||||
|
UpdateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero UpdateAt", func(t *testing.T) {
|
||||||
|
pv := &PropertyValue{
|
||||||
|
ID: NewId(),
|
||||||
|
TargetID: NewId(),
|
||||||
|
TargetType: "test_type",
|
||||||
|
GroupID: NewId(),
|
||||||
|
FieldID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
UpdateAt: 0,
|
||||||
|
}
|
||||||
|
require.Error(t, pv.IsValid())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyValueSearchCursor_IsValid(t *testing.T) {
|
||||||
|
t.Run("empty cursor is valid", func(t *testing.T) {
|
||||||
|
cursor := PropertyValueSearchCursor{}
|
||||||
|
assert.NoError(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid cursor", func(t *testing.T) {
|
||||||
|
cursor := PropertyValueSearchCursor{
|
||||||
|
PropertyValueID: NewId(),
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
assert.NoError(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid PropertyValueID", func(t *testing.T) {
|
||||||
|
cursor := PropertyValueSearchCursor{
|
||||||
|
PropertyValueID: "invalid",
|
||||||
|
CreateAt: GetMillis(),
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero CreateAt", func(t *testing.T) {
|
||||||
|
cursor := PropertyValueSearchCursor{
|
||||||
|
PropertyValueID: NewId(),
|
||||||
|
CreateAt: 0,
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative CreateAt", func(t *testing.T) {
|
||||||
|
cursor := PropertyValueSearchCursor{
|
||||||
|
PropertyValueID: NewId(),
|
||||||
|
CreateAt: -1,
|
||||||
|
}
|
||||||
|
assert.Error(t, cursor.IsValid())
|
||||||
|
})
|
||||||
|
}
|
@ -94,6 +94,10 @@ const (
|
|||||||
WebsocketScheduledPostCreated WebsocketEventType = "scheduled_post_created"
|
WebsocketScheduledPostCreated WebsocketEventType = "scheduled_post_created"
|
||||||
WebsocketScheduledPostUpdated WebsocketEventType = "scheduled_post_updated"
|
WebsocketScheduledPostUpdated WebsocketEventType = "scheduled_post_updated"
|
||||||
WebsocketScheduledPostDeleted WebsocketEventType = "scheduled_post_deleted"
|
WebsocketScheduledPostDeleted WebsocketEventType = "scheduled_post_deleted"
|
||||||
|
WebsocketEventCPAFieldCreated WebsocketEventType = "custom_profile_attributes_field_created"
|
||||||
|
WebsocketEventCPAFieldUpdated WebsocketEventType = "custom_profile_attributes_field_updated"
|
||||||
|
WebsocketEventCPAFieldDeleted WebsocketEventType = "custom_profile_attributes_field_deleted"
|
||||||
|
WebsocketEventCPAValuesUpdated WebsocketEventType = "custom_profile_attributes_values_updated"
|
||||||
|
|
||||||
WebSocketMsgTypeResponse = "response"
|
WebSocketMsgTypeResponse = "response"
|
||||||
WebSocketMsgTypeEvent = "event"
|
WebSocketMsgTypeEvent = "event"
|
||||||
|
@ -108,11 +108,19 @@ func SanitizeDataSource(driverName, dataSource string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
u.User = url.UserPassword("****", "****")
|
u.User = url.UserPassword("****", "****")
|
||||||
|
|
||||||
|
// Remove username and password from query string
|
||||||
params := u.Query()
|
params := u.Query()
|
||||||
params.Del("user")
|
params.Del("user")
|
||||||
params.Del("password")
|
params.Del("password")
|
||||||
u.RawQuery = params.Encode()
|
u.RawQuery = params.Encode()
|
||||||
return u.String(), nil
|
|
||||||
|
// Unescape the URL to make it human-readable
|
||||||
|
out, err := url.QueryUnescape(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
case model.DatabaseDriverMysql:
|
case model.DatabaseDriverMysql:
|
||||||
cfg, err := mysql.ParseDSN(dataSource)
|
cfg, err := mysql.ParseDSN(dataSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,13 +72,21 @@ func TestSanitizeDataSource(t *testing.T) {
|
|||||||
Original string
|
Original string
|
||||||
Sanitized string
|
Sanitized string
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"//****:****@",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"postgres://mmuser:mostest@localhost",
|
||||||
|
"postgres://****:****@localhost",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"postgres://mmuser:mostest@localhost/dummy?sslmode=disable",
|
"postgres://mmuser:mostest@localhost/dummy?sslmode=disable",
|
||||||
"postgres://%2A%2A%2A%2A:%2A%2A%2A%2A@localhost/dummy?sslmode=disable",
|
"postgres://****:****@localhost/dummy?sslmode=disable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest",
|
"postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest",
|
||||||
"postgres://%2A%2A%2A%2A:%2A%2A%2A%2A@localhost/dummy?sslmode=disable",
|
"postgres://****:****@localhost/dummy?sslmode=disable",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
driver := model.DatabaseDriverPostgres
|
driver := model.DatabaseDriverPostgres
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
"html-to-react": "1.6.0",
|
"html-to-react": "1.6.0",
|
||||||
"inobounce": "0.2.1",
|
"inobounce": "0.2.1",
|
||||||
"ipaddr.js": "2.1.0",
|
"ipaddr.js": "2.1.0",
|
||||||
"katex": "0.16.10",
|
"katex": "0.16.21",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"localforage-observable": "2.1.1",
|
"localforage-observable": "2.1.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
@ -0,0 +1,470 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with multiple items 1`] = `
|
||||||
|
<ClientSideUserIdsSetting
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
setByEnv={false}
|
||||||
|
value={
|
||||||
|
Array [
|
||||||
|
"userid1",
|
||||||
|
"userid2",
|
||||||
|
"id3",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Memo(Settings)
|
||||||
|
helpText={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId="MySetting"
|
||||||
|
label={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
setByEnv={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="form-group"
|
||||||
|
data-testid="MySetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label col-sm-4"
|
||||||
|
htmlFor="MySetting"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Client side user ids:
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="col-sm-8"
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
|
||||||
|
"id": "admin.customization.clientSideUserIdsPlaceholder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2,id3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="E.g.: \\"userid1,userid2\\""
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2,id3"
|
||||||
|
/>
|
||||||
|
</LocalizedPlaceholderInput>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
data-testid="MySettinghelp-text"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Set the user ids you want to track for client side metrics. Separate values with a comma.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Memo(Settings)>
|
||||||
|
</ClientSideUserIdsSetting>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with no items 1`] = `
|
||||||
|
<ClientSideUserIdsSetting
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
setByEnv={false}
|
||||||
|
value={Array []}
|
||||||
|
>
|
||||||
|
<Memo(Settings)
|
||||||
|
helpText={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId="MySetting"
|
||||||
|
label={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
setByEnv={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="form-group"
|
||||||
|
data-testid="MySetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label col-sm-4"
|
||||||
|
htmlFor="MySetting"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Client side user ids:
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="col-sm-8"
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
|
||||||
|
"id": "admin.customization.clientSideUserIdsPlaceholder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="E.g.: \\"userid1,userid2\\""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</LocalizedPlaceholderInput>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
data-testid="MySettinghelp-text"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Set the user ids you want to track for client side metrics. Separate values with a comma.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Memo(Settings)>
|
||||||
|
</ClientSideUserIdsSetting>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with one item 1`] = `
|
||||||
|
<ClientSideUserIdsSetting
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
setByEnv={false}
|
||||||
|
value={
|
||||||
|
Array [
|
||||||
|
"userid1",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Memo(Settings)
|
||||||
|
helpText={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId="MySetting"
|
||||||
|
label={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
setByEnv={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="form-group"
|
||||||
|
data-testid="MySetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label col-sm-4"
|
||||||
|
htmlFor="MySetting"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Client side user ids:
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="col-sm-8"
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
|
||||||
|
"id": "admin.customization.clientSideUserIdsPlaceholder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value="userid1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="E.g.: \\"userid1,userid2\\""
|
||||||
|
type="text"
|
||||||
|
value="userid1"
|
||||||
|
/>
|
||||||
|
</LocalizedPlaceholderInput>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
data-testid="MySettinghelp-text"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Set the user ids you want to track for client side metrics. Separate values with a comma.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Memo(Settings)>
|
||||||
|
</ClientSideUserIdsSetting>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when disabled 1`] = `
|
||||||
|
<ClientSideUserIdsSetting
|
||||||
|
disabled={true}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
setByEnv={false}
|
||||||
|
value={
|
||||||
|
Array [
|
||||||
|
"userid1",
|
||||||
|
"userid2",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Memo(Settings)
|
||||||
|
helpText={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId="MySetting"
|
||||||
|
label={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
setByEnv={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="form-group"
|
||||||
|
data-testid="MySetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label col-sm-4"
|
||||||
|
htmlFor="MySetting"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Client side user ids:
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="col-sm-8"
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
className="form-control"
|
||||||
|
disabled={true}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
|
||||||
|
"id": "admin.customization.clientSideUserIdsPlaceholder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
disabled={true}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="E.g.: \\"userid1,userid2\\""
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2"
|
||||||
|
/>
|
||||||
|
</LocalizedPlaceholderInput>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
data-testid="MySettinghelp-text"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Set the user ids you want to track for client side metrics. Separate values with a comma.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Memo(Settings)>
|
||||||
|
</ClientSideUserIdsSetting>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when set by environment variable 1`] = `
|
||||||
|
<ClientSideUserIdsSetting
|
||||||
|
disabled={false}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
setByEnv={true}
|
||||||
|
value={
|
||||||
|
Array [
|
||||||
|
"userid1",
|
||||||
|
"userid2",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Memo(Settings)
|
||||||
|
helpText={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId="MySetting"
|
||||||
|
label={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
setByEnv={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="form-group"
|
||||||
|
data-testid="MySetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label col-sm-4"
|
||||||
|
htmlFor="MySetting"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Client side user ids:"
|
||||||
|
id="admin.customization.clientSideUserIds"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Client side user ids:
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="col-sm-8"
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
className="form-control"
|
||||||
|
disabled={true}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "E.g.: \\"userid1,userid2\\"",
|
||||||
|
"id": "admin.customization.clientSideUserIdsPlaceholder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
disabled={true}
|
||||||
|
id="MySetting"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="E.g.: \\"userid1,userid2\\""
|
||||||
|
type="text"
|
||||||
|
value="userid1,userid2"
|
||||||
|
/>
|
||||||
|
</LocalizedPlaceholderInput>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
data-testid="MySettinghelp-text"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Set the user ids you want to track for client side metrics. Separate values with a comma."
|
||||||
|
id="admin.customization.clientSideUserIdsDesc"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Set the user ids you want to track for client side metrics. Separate values with a comma.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
<SetByEnv>
|
||||||
|
<div
|
||||||
|
className="alert alert-warning"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="This setting has been set through an environment variable. It cannot be changed through the System Console."
|
||||||
|
id="admin.set_by_env"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
This setting has been set through an environment variable. It cannot be changed through the System Console.
|
||||||
|
</span>
|
||||||
|
</FormattedMessage>
|
||||||
|
</div>
|
||||||
|
</SetByEnv>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Memo(Settings)>
|
||||||
|
</ClientSideUserIdsSetting>
|
||||||
|
`;
|
@ -49,6 +49,7 @@ import CompanyInfo, {searchableStrings as billingCompanyInfoSearchableStrings} f
|
|||||||
import CompanyInfoEdit from './billing/company_info_edit';
|
import CompanyInfoEdit from './billing/company_info_edit';
|
||||||
import BleveSettings, {searchableStrings as bleveSearchableStrings} from './bleve_settings';
|
import BleveSettings, {searchableStrings as bleveSearchableStrings} from './bleve_settings';
|
||||||
import BrandImageSetting from './brand_image_setting/brand_image_setting';
|
import BrandImageSetting from './brand_image_setting/brand_image_setting';
|
||||||
|
import ClientSideUserIdsSetting from './client_side_userids_setting';
|
||||||
import ClusterSettings, {searchableStrings as clusterSearchableStrings} from './cluster_settings';
|
import ClusterSettings, {searchableStrings as clusterSearchableStrings} from './cluster_settings';
|
||||||
import CustomEnableDisableGuestAccountsSetting from './custom_enable_disable_guest_accounts_setting';
|
import CustomEnableDisableGuestAccountsSetting from './custom_enable_disable_guest_accounts_setting';
|
||||||
import CustomTermsOfServiceSettings from './custom_terms_of_service_settings';
|
import CustomTermsOfServiceSettings from './custom_terms_of_service_settings';
|
||||||
@ -1963,6 +1964,15 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
it.configIsFalse('MetricsSettings', 'Enable'),
|
it.configIsFalse('MetricsSettings', 'Enable'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
key: 'MetricsSettings.ClientSideUserIds',
|
||||||
|
component: ClientSideUserIdsSetting,
|
||||||
|
isDisabled: it.any(
|
||||||
|
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.PERFORMANCE_MONITORING)),
|
||||||
|
it.configIsFalse('MetricsSettings', 'EnableClientMetrics'),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
key: 'MetricsSettings.ListenAddress',
|
key: 'MetricsSettings.ListenAddress',
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
|
|
||||||
|
import ClientSideUserIdsSetting from './client_side_userids_setting';
|
||||||
|
|
||||||
|
describe('components/AdminConsole/ClientSideUserIdsSetting', () => {
|
||||||
|
const baseProps = {
|
||||||
|
id: 'MySetting',
|
||||||
|
value: ['userid1', 'userid2'],
|
||||||
|
onChange: jest.fn(),
|
||||||
|
disabled: false,
|
||||||
|
setByEnv: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('initial state', () => {
|
||||||
|
test('with no items', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
value: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.state('value')).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with one item', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
value: ['userid1'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.state('value')).toEqual('userid1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with multiple items', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
value: ['userid1', 'userid2', 'id3'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.state('value')).toEqual('userid1,userid2,id3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onChange', () => {
|
||||||
|
test('called on change to empty', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('input').simulate('change', {target: {value: ''}});
|
||||||
|
|
||||||
|
expect(props.onChange).toBeCalledWith(baseProps.id, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('called on change to one item', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('input').simulate('change', {target: {value: ' id2 '}});
|
||||||
|
|
||||||
|
expect(props.onChange).toBeCalledWith(baseProps.id, ['id2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('called on change to two items', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('input').simulate('change', {target: {value: 'id1, id99'}});
|
||||||
|
|
||||||
|
expect(props.onChange).toBeCalledWith(baseProps.id, ['id1', 'id99']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders properly when disabled', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders properly when set by environment variable', () => {
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
setByEnv: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ClientSideUserIdsSetting {...props}/>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import type {ChangeEvent} from 'react';
|
||||||
|
import {defineMessage, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import LocalizedPlaceholderInput from 'components/localized_placeholder_input';
|
||||||
|
|
||||||
|
import Setting from './setting';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
value: string[];
|
||||||
|
onChange: (id: string, valueAsArray: string[]) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
setByEnv: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ClientSideUserIdsSetting extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
value: this.arrayToString(props.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
stringToArray = (str: string): string[] => {
|
||||||
|
return str.split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
arrayToString = (arr: string[]): string => {
|
||||||
|
return arr.join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const valueAsArray = this.stringToArray(e.target.value);
|
||||||
|
|
||||||
|
this.props.onChange(this.props.id, valueAsArray);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
value: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Setting
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.customization.clientSideUserIds'
|
||||||
|
defaultMessage='Client side user ids:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.customization.clientSideUserIdsDesc'
|
||||||
|
defaultMessage='Set the user ids you want to track for client side metrics. Separate values with a comma.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
inputId={this.props.id}
|
||||||
|
setByEnv={this.props.setByEnv}
|
||||||
|
>
|
||||||
|
<LocalizedPlaceholderInput
|
||||||
|
id={this.props.id}
|
||||||
|
className='form-control'
|
||||||
|
type='text'
|
||||||
|
placeholder={defineMessage({id: 'admin.customization.clientSideUserIdsPlaceholder', defaultMessage: 'E.g.: "userid1,userid2"'})}
|
||||||
|
value={this.state.value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={this.props.disabled || this.props.setByEnv}
|
||||||
|
/>
|
||||||
|
</Setting>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,10 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChooseClick = () => {
|
||||||
|
this.fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
handleChange = () => {
|
handleChange = () => {
|
||||||
const files = this.fileInputRef.current?.files;
|
const files = this.fileInputRef.current?.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@ -92,6 +96,7 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
|||||||
type='button'
|
type='button'
|
||||||
className='btn btn-tertiary'
|
className='btn btn-tertiary'
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
onClick={this.handleChooseClick}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.file_upload.chooseFile'
|
id='admin.file_upload.chooseFile'
|
||||||
|
@ -123,7 +123,9 @@ table.adminConsoleListTable {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
|
|
||||||
|
&.clickable:hover {
|
||||||
background-color: $sysCenterChannelColorWith8Alpha;
|
background-color: $sysCenterChannelColorWith8Alpha;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@ -207,6 +209,20 @@ table.adminConsoleListTable {
|
|||||||
tfoot {
|
tfoot {
|
||||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 24px;
|
||||||
|
height: calc(100% - 1px);
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.adminConsoleListTabletOptionalFoot,
|
.adminConsoleListTabletOptionalFoot,
|
||||||
|
@ -6,10 +6,14 @@ import {flexRender} from '@tanstack/react-table';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {useMemo} from 'react';
|
import React, {useMemo} from 'react';
|
||||||
import type {AriaAttributes, MouseEvent, ReactNode} from 'react';
|
import type {AriaAttributes, MouseEvent, ReactNode} from 'react';
|
||||||
|
import type {DropResult} from 'react-beautiful-dnd';
|
||||||
|
import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd';
|
||||||
import {FormattedMessage, defineMessages, useIntl} from 'react-intl';
|
import {FormattedMessage, defineMessages, useIntl} from 'react-intl';
|
||||||
import ReactSelect, {components} from 'react-select';
|
import ReactSelect, {components} from 'react-select';
|
||||||
import type {IndicatorContainerProps, ValueType} from 'react-select';
|
import type {IndicatorContainerProps, ValueType} from 'react-select';
|
||||||
|
|
||||||
|
import {DragVerticalIcon} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||||
|
|
||||||
import {Pagination} from './pagination';
|
import {Pagination} from './pagination';
|
||||||
@ -56,6 +60,7 @@ export type TableMeta = {
|
|||||||
loadingState?: LoadingStates;
|
loadingState?: LoadingStates;
|
||||||
emptyDataMessage?: ReactNode;
|
emptyDataMessage?: ReactNode;
|
||||||
onRowClick?: (row: string) => void;
|
onRowClick?: (row: string) => void;
|
||||||
|
onReorder?: (prev: number, next: number) => void;
|
||||||
disablePrevPage?: boolean;
|
disablePrevPage?: boolean;
|
||||||
disableNextPage?: boolean;
|
disableNextPage?: boolean;
|
||||||
disablePaginationControls?: boolean;
|
disablePaginationControls?: boolean;
|
||||||
@ -117,6 +122,14 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
|
const {source, destination} = result;
|
||||||
|
if (!destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tableMeta.onReorder?.(source.index, destination.index);
|
||||||
|
};
|
||||||
|
|
||||||
const colCount = props.table.getAllColumns().length;
|
const colCount = props.table.getAllColumns().length;
|
||||||
const rowCount = props.table.getRowModel().rows.length;
|
const rowCount = props.table.getRowModel().rows.length;
|
||||||
|
|
||||||
@ -164,6 +177,7 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
|||||||
})}
|
})}
|
||||||
disabled={header.column.getCanSort() && tableMeta.loadingState === LoadingStates.Loading}
|
disabled={header.column.getCanSort() && tableMeta.loadingState === LoadingStates.Loading}
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
style={{width: header.column.getSize()}}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
|
||||||
@ -193,69 +207,104 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
{props.table.getRowModel().rows.map((row) => (
|
<Droppable droppableId='table-body'>
|
||||||
<tr
|
{(provided, snap) => (
|
||||||
id={`${rowIdPrefix}${row.original.id}`}
|
<tbody
|
||||||
key={row.id}
|
ref={provided.innerRef}
|
||||||
onClick={handleRowClick}
|
{...provided.droppableProps}
|
||||||
>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td
|
|
||||||
key={cell.id}
|
|
||||||
id={`${cellIdPrefix}${cell.id}`}
|
|
||||||
headers={`${headerIdPrefix}${cell.column.id}`}
|
|
||||||
className={classNames(`${cell.column.id}`, {
|
|
||||||
[PINNED_CLASS]: cell.column.getCanPin(),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{cell.getIsPlaceholder() ? null : flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* State where it is initially loading the data */}
|
|
||||||
{(tableMeta.loadingState === LoadingStates.Loading && rowCount === 0) && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={colCount}
|
|
||||||
className='noRows'
|
|
||||||
disabled={true}
|
|
||||||
>
|
>
|
||||||
<LoadingSpinner
|
{props.table.getRowModel().rows.map((row) => (
|
||||||
text={formatMessage({id: 'adminConsole.list.table.genericLoading', defaultMessage: 'Loading'})}
|
<Draggable
|
||||||
/>
|
draggableId={row.original.id}
|
||||||
</td>
|
key={row.original.id}
|
||||||
</tr>
|
index={row.index}
|
||||||
)}
|
isDragDisabled={!tableMeta.onReorder}
|
||||||
|
>
|
||||||
|
{(provided) => {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
id={`${rowIdPrefix}${row.original.id}`}
|
||||||
|
key={row.id}
|
||||||
|
onClick={handleRowClick}
|
||||||
|
className={classNames({clickable: Boolean(tableMeta.onRowClick) && !snap.isDraggingOver})}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell, i) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
id={`${cellIdPrefix}${cell.id}`}
|
||||||
|
headers={`${headerIdPrefix}${cell.column.id}`}
|
||||||
|
className={classNames(`${cell.column.id}`, {
|
||||||
|
[PINNED_CLASS]: cell.column.getCanPin(),
|
||||||
|
})}
|
||||||
|
style={{width: cell.column.getSize()}}
|
||||||
|
>
|
||||||
|
{tableMeta.onReorder && i === 0 && (
|
||||||
|
<span
|
||||||
|
className='dragHandle'
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
<DragVerticalIcon size={18}/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cell.getIsPlaceholder() ? null : flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
{/* State where there is no data */}
|
))}
|
||||||
{(tableMeta.loadingState === LoadingStates.Loaded && rowCount === 0) && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={colCount}
|
|
||||||
className='noRows'
|
|
||||||
disabled={true}
|
|
||||||
>
|
|
||||||
{tableMeta.emptyDataMessage || formatMessage({id: 'adminConsole.list.table.genericNoData', defaultMessage: 'No data'})}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* State where there is an error loading the data */}
|
{provided.placeholder}
|
||||||
{tableMeta.loadingState === LoadingStates.Failed && (
|
|
||||||
<tr>
|
{/* State where it is initially loading the data */}
|
||||||
<td
|
{(tableMeta.loadingState === LoadingStates.Loading && rowCount === 0) && (
|
||||||
colSpan={colCount}
|
<tr>
|
||||||
className='noRows'
|
<td
|
||||||
disabled={true}
|
colSpan={colCount}
|
||||||
>
|
className='noRows'
|
||||||
{formatMessage({id: 'adminConsole.list.table.genericError', defaultMessage: 'There was an error loading the data, please try again'})}
|
disabled={true}
|
||||||
</td>
|
>
|
||||||
</tr>
|
<LoadingSpinner
|
||||||
)}
|
text={formatMessage({id: 'adminConsole.list.table.genericLoading', defaultMessage: 'Loading'})}
|
||||||
</tbody>
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* State where there is no data */}
|
||||||
|
{(tableMeta.loadingState === LoadingStates.Loaded && rowCount === 0) && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={colCount}
|
||||||
|
className='noRows'
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
{tableMeta.emptyDataMessage || formatMessage({id: 'adminConsole.list.table.genericNoData', defaultMessage: 'No data'})}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* State where there is an error loading the data */}
|
||||||
|
{tableMeta.loadingState === LoadingStates.Failed && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={colCount}
|
||||||
|
className='noRows'
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
{formatMessage({id: 'adminConsole.list.table.genericError', defaultMessage: 'There was an error loading the data, please try again'})}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
{props.table.getFooterGroups().map((footerGroup) => (
|
{props.table.getFooterGroups().map((footerGroup) => (
|
||||||
<tr key={footerGroup.id}>
|
<tr key={footerGroup.id}>
|
||||||
|
@ -7,7 +7,7 @@ import React, {useEffect, useMemo, useState} from 'react';
|
|||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import styled, {css} from 'styled-components';
|
import styled, {css} from 'styled-components';
|
||||||
|
|
||||||
import {PlusIcon, TextBoxOutlineIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
|
import {MenuVariantIcon, PlusIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {UserPropertyField} from '@mattermost/types/properties';
|
import type {UserPropertyField} from '@mattermost/types/properties';
|
||||||
import {collectionToArray} from '@mattermost/types/utilities';
|
import {collectionToArray} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ type Props = {
|
|||||||
type FieldActions = {
|
type FieldActions = {
|
||||||
updateField: (field: UserPropertyField) => void;
|
updateField: (field: UserPropertyField) => void;
|
||||||
deleteField: (id: string) => void;
|
deleteField: (id: string) => void;
|
||||||
|
reorderField: (field: UserPropertyField, nextOrder: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserPropertiesTable = (): SectionHook => {
|
export const useUserPropertiesTable = (): SectionHook => {
|
||||||
@ -53,6 +54,7 @@ export const useUserPropertiesTable = (): SectionHook => {
|
|||||||
data={userPropertyFields}
|
data={userPropertyFields}
|
||||||
updateField={itemOps.update}
|
updateField={itemOps.update}
|
||||||
deleteField={itemOps.delete}
|
deleteField={itemOps.delete}
|
||||||
|
reorderField={itemOps.reorder}
|
||||||
/>
|
/>
|
||||||
{nonDeletedCount < Constants.MAX_CUSTOM_ATTRIBUTES && (
|
{nonDeletedCount < Constants.MAX_CUSTOM_ATTRIBUTES && (
|
||||||
<LinkButton onClick={itemOps.create}>
|
<LinkButton onClick={itemOps.create}>
|
||||||
@ -78,13 +80,14 @@ export const useUserPropertiesTable = (): SectionHook => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UserPropertiesTable({data: collection, updateField, deleteField}: Props & FieldActions) {
|
export function UserPropertiesTable({data: collection, updateField, deleteField, reorderField}: Props & FieldActions) {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const data = collectionToArray(collection);
|
const data = collectionToArray(collection);
|
||||||
const col = createColumnHelper<UserPropertyField>();
|
const col = createColumnHelper<UserPropertyField>();
|
||||||
const columns = useMemo<Array<ColumnDef<UserPropertyField, any>>>(() => {
|
const columns = useMemo<Array<ColumnDef<UserPropertyField, any>>>(() => {
|
||||||
return [
|
return [
|
||||||
col.accessor('name', {
|
col.accessor('name', {
|
||||||
|
size: 180,
|
||||||
header: () => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<ColHeaderLeft>
|
<ColHeaderLeft>
|
||||||
@ -150,6 +153,7 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
}),
|
}),
|
||||||
col.accessor('type', {
|
col.accessor('type', {
|
||||||
|
size: 100,
|
||||||
header: () => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<ColHeaderLeft>
|
<ColHeaderLeft>
|
||||||
@ -166,7 +170,7 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
if (type === 'text') {
|
if (type === 'text') {
|
||||||
type = (
|
type = (
|
||||||
<>
|
<>
|
||||||
<TextBoxOutlineIcon
|
<MenuVariantIcon
|
||||||
size={18}
|
size={18}
|
||||||
color={'rgba(var(--center-channel-color-rgb), 0.64)'}
|
color={'rgba(var(--center-channel-color-rgb), 0.64)'}
|
||||||
/>
|
/>
|
||||||
@ -187,8 +191,17 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
}),
|
}),
|
||||||
|
col.display({
|
||||||
|
id: 'options',
|
||||||
|
size: 300,
|
||||||
|
header: () => <></>,
|
||||||
|
cell: () => <></>,
|
||||||
|
enableHiding: false,
|
||||||
|
enableSorting: false,
|
||||||
|
}),
|
||||||
col.display({
|
col.display({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
|
size: 100,
|
||||||
header: () => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<ColHeaderRight>
|
<ColHeaderRight>
|
||||||
@ -202,7 +215,6 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
cell: ({row}) => (
|
cell: ({row}) => (
|
||||||
<Actions
|
<Actions
|
||||||
field={row.original}
|
field={row.original}
|
||||||
updateField={updateField}
|
|
||||||
deleteField={deleteField}
|
deleteField={deleteField}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -215,9 +227,6 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
initialState: {
|
|
||||||
sorting: [],
|
|
||||||
},
|
|
||||||
getCoreRowModel: getCoreRowModel<UserPropertyField>(),
|
getCoreRowModel: getCoreRowModel<UserPropertyField>(),
|
||||||
getSortedRowModel: getSortedRowModel<UserPropertyField>(),
|
getSortedRowModel: getSortedRowModel<UserPropertyField>(),
|
||||||
enableSortingRemoval: false,
|
enableSortingRemoval: false,
|
||||||
@ -226,6 +235,9 @@ export function UserPropertiesTable({data: collection, updateField, deleteField}
|
|||||||
meta: {
|
meta: {
|
||||||
tableId: 'userProperties',
|
tableId: 'userProperties',
|
||||||
disablePaginationControls: true,
|
disablePaginationControls: true,
|
||||||
|
onReorder: (prev: number, next: number) => {
|
||||||
|
reorderField(collection.data[collection.order[prev]], next);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
});
|
});
|
||||||
@ -283,7 +295,7 @@ const TableWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Actions = ({field, deleteField}: {field: UserPropertyField} & FieldActions) => {
|
const Actions = ({field, deleteField}: {field: UserPropertyField} & Pick<FieldActions, 'deleteField'>) => {
|
||||||
const {promptDelete} = useUserPropertyFieldDelete();
|
const {promptDelete} = useUserPropertyFieldDelete();
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import {act} from '@testing-library/react-hooks';
|
import {act} from '@testing-library/react-hooks';
|
||||||
|
|
||||||
import type {UserPropertyField} from '@mattermost/types/properties';
|
import type {UserPropertyField, UserPropertyFieldPatch} from '@mattermost/types/properties';
|
||||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@ -48,11 +48,11 @@ describe('useUserPropertyFields', () => {
|
|||||||
const deleteField = jest.spyOn(Client4, 'deleteCustomProfileAttributeField');
|
const deleteField = jest.spyOn(Client4, 'deleteCustomProfileAttributeField');
|
||||||
const createField = jest.spyOn(Client4, 'createCustomProfileAttributeField');
|
const createField = jest.spyOn(Client4, 'createCustomProfileAttributeField');
|
||||||
|
|
||||||
const baseField = {type: 'text' as const, group_id: 'custom_profile_attributes' as const, create_at: 1736541716295, delete_at: 0, update_at: 0};
|
const baseField = {type: 'text', group_id: 'custom_profile_attributes', create_at: 1736541716295, delete_at: 0, update_at: 0} as const;
|
||||||
const field0: UserPropertyField = {id: 'f0', name: 'test attribute 0', ...baseField};
|
const field0: UserPropertyField = {...baseField, id: 'f0', name: 'test attribute 0'};
|
||||||
const field1: UserPropertyField = {id: 'f1', name: 'test attribute 1', ...baseField};
|
const field1: UserPropertyField = {...baseField, id: 'f1', name: 'test attribute 1'};
|
||||||
const field2: UserPropertyField = {id: 'f2', name: 'test attribute 2', ...baseField};
|
const field2: UserPropertyField = {...baseField, id: 'f2', name: 'test attribute 2'};
|
||||||
const field3: UserPropertyField = {id: 'f3', name: 'test attribute 3', ...baseField};
|
const field3: UserPropertyField = {...baseField, id: 'f3', name: 'test attribute 3'};
|
||||||
|
|
||||||
getFields.mockResolvedValue([field0, field1, field2, field3]);
|
getFields.mockResolvedValue([field0, field1, field2, field3]);
|
||||||
|
|
||||||
@ -134,6 +134,59 @@ describe('useUserPropertyFields', () => {
|
|||||||
expect(fields4.data[field1.id].name).toBe('changed attribute value');
|
expect(fields4.data[field1.id].name).toBe('changed attribute value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should successfully handle reordering', async () => {
|
||||||
|
patchField.mockImplementation((id: string, patch: UserPropertyFieldPatch) => Promise.resolve({...baseField, ...patch, id, update_at: Date.now()} as UserPropertyField));
|
||||||
|
|
||||||
|
const {result, rerender, waitFor} = renderHookWithContext(() => {
|
||||||
|
return useUserPropertyFields();
|
||||||
|
}, getBaseState());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const [, read] = result.current;
|
||||||
|
expect(read.loading).toBe(false);
|
||||||
|
});
|
||||||
|
const [fields2,,, ops2] = result.current;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ops2.reorder(fields2.data[field1.id], 0);
|
||||||
|
});
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
const [fields3, readIO3, pendingIO3] = result.current;
|
||||||
|
|
||||||
|
// expect 2 changed fields to be in the correct order
|
||||||
|
expect(fields3.data[field1.id]?.attrs?.sort_order).toBe(0);
|
||||||
|
expect(fields3.data[field0.id]?.attrs?.sort_order).toBe(1);
|
||||||
|
expect(pendingIO3.hasChanges).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const data = await pendingIO3.commit();
|
||||||
|
if (data) {
|
||||||
|
readIO3.setData(data);
|
||||||
|
}
|
||||||
|
jest.runAllTimers();
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const [,, pending] = result.current;
|
||||||
|
expect(pending.saving).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(patchField).toHaveBeenCalledWith(field1.id, {type: 'text', name: 'test attribute 1', attrs: {sort_order: 0}});
|
||||||
|
expect(patchField).toHaveBeenCalledWith(field0.id, {type: 'text', name: 'test attribute 0', attrs: {sort_order: 1}});
|
||||||
|
|
||||||
|
const [fields4,, pendingIO4] = result.current;
|
||||||
|
expect(pendingIO4.hasChanges).toBe(false);
|
||||||
|
expect(pendingIO4.error).toBe(undefined);
|
||||||
|
expect(fields4.order).toEqual(['f1', 'f0', 'f2', 'f3']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should successfully handle deletes', async () => {
|
it('should successfully handle deletes', async () => {
|
||||||
const {result, rerender, waitFor} = renderHookWithContext(() => {
|
const {result, rerender, waitFor} = renderHookWithContext(() => {
|
||||||
return useUserPropertyFields();
|
return useUserPropertyFields();
|
||||||
@ -224,8 +277,8 @@ describe('useUserPropertyFields', () => {
|
|||||||
expect(pendingIO4.saving).toBe(false);
|
expect(pendingIO4.saving).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createField).toHaveBeenCalledWith({type: 'text', name: 'Text'});
|
expect(createField).toHaveBeenCalledWith({type: 'text', name: 'Text', attrs: {sort_order: 4}});
|
||||||
expect(createField).toHaveBeenCalledWith({type: 'text', name: 'Text 2'});
|
expect(createField).toHaveBeenCalledWith({type: 'text', name: 'Text 2', attrs: {sort_order: 5}});
|
||||||
|
|
||||||
const [fields4,,,] = result.current;
|
const [fields4,,,] = result.current;
|
||||||
expect(Object.values(fields4.data)).toEqual(expect.arrayContaining([
|
expect(Object.values(fields4.data)).toEqual(expect.arrayContaining([
|
||||||
|
@ -11,6 +11,7 @@ import {collectionAddItem, collectionFromArray, collectionRemoveItem, collection
|
|||||||
import type {PartialExcept, IDMappedCollection, IDMappedObjects} from '@mattermost/types/utilities';
|
import type {PartialExcept, IDMappedCollection, IDMappedObjects} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
import {insertWithoutDuplicates} from 'mattermost-redux/utils/array_utils';
|
||||||
|
|
||||||
import {generateId} from 'utils/utils';
|
import {generateId} from 'utils/utils';
|
||||||
|
|
||||||
@ -26,7 +27,8 @@ export const useUserPropertyFields = () => {
|
|||||||
const [fieldCollection, readIO] = useThing<UserPropertyFields>(useMemo(() => ({
|
const [fieldCollection, readIO] = useThing<UserPropertyFields>(useMemo(() => ({
|
||||||
get: async () => {
|
get: async () => {
|
||||||
const data = await Client4.getCustomProfileAttributeFields();
|
const data = await Client4.getCustomProfileAttributeFields();
|
||||||
return collectionFromArray(data);
|
const sorted = data.sort((a, b) => (a.attrs?.sort_order ?? 0) - (b.attrs?.sort_order ?? 0));
|
||||||
|
return collectionFromArray(sorted);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
select: (state) => {
|
select: (state) => {
|
||||||
@ -65,7 +67,7 @@ export const useUserPropertyFields = () => {
|
|||||||
errors: {}, // start with errors cleared; don't keep stale errors
|
errors: {}, // start with errors cleared; don't keep stale errors
|
||||||
};
|
};
|
||||||
|
|
||||||
// delete - all
|
// delete
|
||||||
await Promise.all(process.delete.map(async ({id}) => {
|
await Promise.all(process.delete.map(async ({id}) => {
|
||||||
return Client4.deleteCustomProfileAttributeField(id).
|
return Client4.deleteCustomProfileAttributeField(id).
|
||||||
then(() => {
|
then(() => {
|
||||||
@ -80,11 +82,11 @@ export const useUserPropertyFields = () => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// update - all
|
// update
|
||||||
await Promise.all(process.edit.map(async (pendingItem) => {
|
await Promise.all(process.edit.map(async (pendingItem) => {
|
||||||
const {id, name, type} = pendingItem;
|
const {id, name, type, attrs} = pendingItem;
|
||||||
|
|
||||||
return Client4.patchCustomProfileAttributeField(id, {name, type}).
|
return Client4.patchCustomProfileAttributeField(id, {name, type, attrs}).
|
||||||
then((nextItem) => {
|
then((nextItem) => {
|
||||||
// data:updated
|
// data:updated
|
||||||
next.data[id] = nextItem;
|
next.data[id] = nextItem;
|
||||||
@ -94,12 +96,11 @@ export const useUserPropertyFields = () => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// create - each, to preserve created/sort ordering
|
// create
|
||||||
for (const pendingItem of process.create) {
|
await Promise.all(process.create.map(async (pendingItem) => {
|
||||||
const {id, name, type} = pendingItem;
|
const {id, name, type, attrs} = pendingItem;
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
return Client4.createCustomProfileAttributeField({name, type, attrs}).
|
||||||
await Client4.createCustomProfileAttributeField({name, type}).
|
|
||||||
then((newItem) => {
|
then((newItem) => {
|
||||||
// data:created (delete pending data)
|
// data:created (delete pending data)
|
||||||
Reflect.deleteProperty(next.data, id);
|
Reflect.deleteProperty(next.data, id);
|
||||||
@ -111,7 +112,7 @@ export const useUserPropertyFields = () => {
|
|||||||
catch((reason: ClientError) => {
|
catch((reason: ClientError) => {
|
||||||
next.errors = {...next.errors, [id]: reason};
|
next.errors = {...next.errors, [id]: reason};
|
||||||
});
|
});
|
||||||
}
|
}));
|
||||||
|
|
||||||
if (isEmpty(next.errors)) {
|
if (isEmpty(next.errors)) {
|
||||||
Reflect.deleteProperty(next, 'errors');
|
Reflect.deleteProperty(next, 'errors');
|
||||||
@ -175,11 +176,34 @@ export const useUserPropertyFields = () => {
|
|||||||
},
|
},
|
||||||
create: () => {
|
create: () => {
|
||||||
pendingIO.apply((pending) => {
|
pendingIO.apply((pending) => {
|
||||||
|
const nextOrder = Object.values(pending.data).filter((x) => !isDeletePending(x)).length;
|
||||||
const name = getIncrementedName('Text', pending);
|
const name = getIncrementedName('Text', pending);
|
||||||
const field = newPendingField({name, type: 'text'});
|
const field = newPendingField({name, type: 'text', attrs: {sort_order: nextOrder}});
|
||||||
return collectionAddItem(pending, field);
|
return collectionAddItem(pending, field);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
reorder: ({id}, nextItemOrder) => {
|
||||||
|
pendingIO.apply((pending) => {
|
||||||
|
const nextOrder = insertWithoutDuplicates(pending.order, id, nextItemOrder);
|
||||||
|
|
||||||
|
if (nextOrder === pending.order) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItems = Object.values(pending.data).reduce<UserPropertyField[]>((changedItems, item) => {
|
||||||
|
const itemCurrentOrder = item.attrs?.sort_order;
|
||||||
|
const itemNextOrder = nextOrder.indexOf(item.id);
|
||||||
|
|
||||||
|
if (itemNextOrder !== itemCurrentOrder) {
|
||||||
|
changedItems.push({...item, attrs: {sort_order: itemNextOrder}});
|
||||||
|
}
|
||||||
|
|
||||||
|
return changedItems;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return collectionReplaceItem({...pending, order: nextOrder}, ...nextItems);
|
||||||
|
});
|
||||||
|
},
|
||||||
delete: (id: string) => {
|
delete: (id: string) => {
|
||||||
pendingIO.apply((pending) => {
|
pendingIO.apply((pending) => {
|
||||||
const field = pending.data[id];
|
const field = pending.data[id];
|
||||||
|
@ -201,6 +201,11 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
@ -208,7 +213,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="desktopNotification-all"
|
data-testid="desktopNotification-all"
|
||||||
id="desktopNotification-all"
|
id="desktopNotification-all"
|
||||||
name="desktopNotification-all"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -221,7 +226,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-mention"
|
data-testid="desktopNotification-mention"
|
||||||
id="desktopNotification-mention"
|
id="desktopNotification-mention"
|
||||||
name="desktopNotification-mention"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -233,7 +238,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-none"
|
data-testid="desktopNotification-none"
|
||||||
id="desktopNotification-none"
|
id="desktopNotification-none"
|
||||||
name="desktopNotification-none"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
@ -637,6 +642,11 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
@ -644,7 +654,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="desktopNotification-all"
|
data-testid="desktopNotification-all"
|
||||||
id="desktopNotification-all"
|
id="desktopNotification-all"
|
||||||
name="desktopNotification-all"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -657,7 +667,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-mention"
|
data-testid="desktopNotification-mention"
|
||||||
id="desktopNotification-mention"
|
id="desktopNotification-mention"
|
||||||
name="desktopNotification-mention"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -669,7 +679,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-none"
|
data-testid="desktopNotification-none"
|
||||||
id="desktopNotification-none"
|
id="desktopNotification-none"
|
||||||
name="desktopNotification-none"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
@ -836,13 +846,18 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
data-testid="MobileNotification-all"
|
data-testid="MobileNotification-all"
|
||||||
id="MobileNotification-all"
|
id="MobileNotification-all"
|
||||||
name="MobileNotification-all"
|
name="push"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -855,7 +870,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="MobileNotification-mention"
|
data-testid="MobileNotification-mention"
|
||||||
id="MobileNotification-mention"
|
id="MobileNotification-mention"
|
||||||
name="MobileNotification-mention"
|
name="push"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -868,7 +883,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="MobileNotification-none"
|
data-testid="MobileNotification-none"
|
||||||
id="MobileNotification-none"
|
id="MobileNotification-none"
|
||||||
name="MobileNotification-none"
|
name="push"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
@ -1530,6 +1545,11 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
@ -1537,7 +1557,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="desktopNotification-all"
|
data-testid="desktopNotification-all"
|
||||||
id="desktopNotification-all"
|
id="desktopNotification-all"
|
||||||
name="desktopNotification-all"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -1550,7 +1570,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-mention"
|
data-testid="desktopNotification-mention"
|
||||||
id="desktopNotification-mention"
|
id="desktopNotification-mention"
|
||||||
name="desktopNotification-mention"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -1562,7 +1582,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-none"
|
data-testid="desktopNotification-none"
|
||||||
id="desktopNotification-none"
|
id="desktopNotification-none"
|
||||||
name="desktopNotification-none"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
@ -2047,6 +2067,11 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
@ -2054,7 +2079,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="desktopNotification-all"
|
data-testid="desktopNotification-all"
|
||||||
id="desktopNotification-all"
|
id="desktopNotification-all"
|
||||||
name="desktopNotification-all"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -2067,7 +2092,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-mention"
|
data-testid="desktopNotification-mention"
|
||||||
id="desktopNotification-mention"
|
id="desktopNotification-mention"
|
||||||
name="desktopNotification-mention"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -2079,7 +2104,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-none"
|
data-testid="desktopNotification-none"
|
||||||
id="desktopNotification-none"
|
id="desktopNotification-none"
|
||||||
name="desktopNotification-none"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
@ -2564,6 +2589,11 @@ Object {
|
|||||||
<fieldset
|
<fieldset
|
||||||
class="mm-modal-generic-section-item__fieldset-radio"
|
class="mm-modal-generic-section-item__fieldset-radio"
|
||||||
>
|
>
|
||||||
|
<legend
|
||||||
|
class="hidden-label"
|
||||||
|
>
|
||||||
|
Notify me about…
|
||||||
|
</legend>
|
||||||
<label
|
<label
|
||||||
class="mm-modal-generic-section-item__label-radio"
|
class="mm-modal-generic-section-item__label-radio"
|
||||||
>
|
>
|
||||||
@ -2571,7 +2601,7 @@ Object {
|
|||||||
checked=""
|
checked=""
|
||||||
data-testid="desktopNotification-all"
|
data-testid="desktopNotification-all"
|
||||||
id="desktopNotification-all"
|
id="desktopNotification-all"
|
||||||
name="desktopNotification-all"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
/>
|
/>
|
||||||
@ -2584,7 +2614,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-mention"
|
data-testid="desktopNotification-mention"
|
||||||
id="desktopNotification-mention"
|
id="desktopNotification-mention"
|
||||||
name="desktopNotification-mention"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="mention"
|
value="mention"
|
||||||
/>
|
/>
|
||||||
@ -2596,7 +2626,7 @@ Object {
|
|||||||
<input
|
<input
|
||||||
data-testid="desktopNotification-none"
|
data-testid="desktopNotification-none"
|
||||||
id="desktopNotification-none"
|
id="desktopNotification-none"
|
||||||
name="desktopNotification-none"
|
name="desktop"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="none"
|
value="none"
|
||||||
/>
|
/>
|
||||||
|
@ -240,22 +240,22 @@ export default function ChannelNotificationsModal(props: Props) {
|
|||||||
handleChange={(e) => handleChange({push: e.target.value})}
|
handleChange={(e) => handleChange({push: e.target.value})}
|
||||||
/>
|
/>
|
||||||
{props.collapsedReplyThreads && settings.push === 'mention' &&
|
{props.collapsedReplyThreads && settings.push === 'mention' &&
|
||||||
<CheckboxSettingItem
|
<CheckboxSettingItem
|
||||||
dataTestId='mobile-reply-threads-checkbox-section'
|
dataTestId='mobile-reply-threads-checkbox-section'
|
||||||
title={formatMessage({
|
title={formatMessage({
|
||||||
id: 'channel_notifications.ThreadsReplyTitle',
|
id: 'channel_notifications.ThreadsReplyTitle',
|
||||||
defaultMessage: 'Thread reply notifications',
|
defaultMessage: 'Thread reply notifications',
|
||||||
})}
|
})}
|
||||||
inputFieldTitle={
|
inputFieldTitle={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_notifications.checkbox.threadsReplyTitle'
|
id='channel_notifications.checkbox.threadsReplyTitle'
|
||||||
defaultMessage="Notify me about replies to threads I\'m following"
|
defaultMessage="Notify me about replies to threads I\'m following"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
inputFieldValue={settings.push_threads === 'all'}
|
inputFieldValue={settings.push_threads === 'all'}
|
||||||
inputFieldData={utils.MobileReplyThreadsInputFieldData}
|
inputFieldData={utils.MobileReplyThreadsInputFieldData}
|
||||||
handleChange={(e) => handleChange({push_threads: e ? 'all' : 'mention'})}
|
handleChange={(e) => handleChange({push_threads: e ? 'all' : 'mention'})}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {defineMessage, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import type {FieldsetCheckbox} from 'components/widgets/modals/components/checkbox_setting_item';
|
import type {FieldsetCheckbox} from 'components/widgets/modals/components/checkbox_setting_item';
|
||||||
import type {FieldsetRadio} from 'components/widgets/modals/components/radio_setting_item';
|
import type {FieldsetRadio} from 'components/widgets/modals/components/radio_setting_item';
|
||||||
@ -41,69 +41,70 @@ export const AutoFollowThreadsInputFieldData: FieldsetCheckbox = {
|
|||||||
dataTestId: 'autoFollowThreads',
|
dataTestId: 'autoFollowThreads',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const desktopNotificationInputFieldData = (defaultOption: string): FieldsetRadio => {
|
const defaultMessage = defineMessage({
|
||||||
return {
|
id: 'channel_notifications.default',
|
||||||
options: [
|
defaultMessage: '(default)',
|
||||||
{
|
});
|
||||||
dataTestId: `desktopNotification-${NotificationLevels.ALL}`,
|
|
||||||
title: (
|
export const desktopNotificationInputFieldData = (defaultOption: string): FieldsetRadio => ({
|
||||||
<FormattedMessage
|
options: [
|
||||||
id='channelNotifications.desktopNotification.allMessages'
|
{
|
||||||
defaultMessage='All new messages {optionalDefault}'
|
dataTestId: `desktopNotification-${NotificationLevels.ALL}`,
|
||||||
values={{
|
title: (
|
||||||
optionalDefault: defaultOption === NotificationLevels.ALL ? (
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='channelNotifications.desktopNotification.allMessages'
|
||||||
id='channel_notifications.default'
|
defaultMessage='All new messages {optionalDefault}'
|
||||||
defaultMessage='(default)'
|
values={{
|
||||||
/>) : undefined,
|
optionalDefault: defaultOption === NotificationLevels.ALL ? (
|
||||||
}}
|
<FormattedMessage
|
||||||
/>
|
{...defaultMessage}
|
||||||
),
|
/>) : undefined,
|
||||||
name: `desktopNotification-${NotificationLevels.ALL}`,
|
}}
|
||||||
key: `desktopNotification-${NotificationLevels.ALL}`,
|
/>
|
||||||
value: NotificationLevels.ALL,
|
),
|
||||||
},
|
name: 'desktop',
|
||||||
{
|
key: `desktopNotification-${NotificationLevels.ALL}`,
|
||||||
dataTestId: `desktopNotification-${NotificationLevels.MENTION}`,
|
value: NotificationLevels.ALL,
|
||||||
title: (
|
},
|
||||||
<FormattedMessage
|
{
|
||||||
id='channelNotifications.desktopNotification.mention'
|
dataTestId: `desktopNotification-${NotificationLevels.MENTION}`,
|
||||||
defaultMessage='Mentions, direct messages, and keywords only {optionalDefault}'
|
title: (
|
||||||
values={{
|
<FormattedMessage
|
||||||
optionalDefault: defaultOption === NotificationLevels.MENTION ? (
|
id='channelNotifications.desktopNotification.mention'
|
||||||
<FormattedMessage
|
defaultMessage='Mentions, direct messages, and keywords only {optionalDefault}'
|
||||||
id='channel_notifications.default'
|
values={{
|
||||||
defaultMessage='(default)'
|
optionalDefault: defaultOption === NotificationLevels.MENTION ? (
|
||||||
/>) : undefined,
|
<FormattedMessage
|
||||||
}}
|
{...defaultMessage}
|
||||||
/>
|
/>) : undefined,
|
||||||
),
|
}}
|
||||||
name: `desktopNotification-${NotificationLevels.MENTION}`,
|
/>
|
||||||
key: `desktopNotification-${NotificationLevels.MENTION}`,
|
),
|
||||||
value: NotificationLevels.MENTION,
|
name: 'desktop',
|
||||||
},
|
key: `desktopNotification-${NotificationLevels.MENTION}`,
|
||||||
{
|
value: NotificationLevels.MENTION,
|
||||||
dataTestId: `desktopNotification-${NotificationLevels.NONE}`,
|
},
|
||||||
title: (
|
{
|
||||||
<FormattedMessage
|
dataTestId: `desktopNotification-${NotificationLevels.NONE}`,
|
||||||
id='channelNotifications.desktopNotification.nothing'
|
title: (
|
||||||
defaultMessage='Nothing {optionalDefault}'
|
<FormattedMessage
|
||||||
values={{
|
id='channelNotifications.desktopNotification.nothing'
|
||||||
optionalDefault: defaultOption === NotificationLevels.NONE ? (
|
defaultMessage='Nothing {optionalDefault}'
|
||||||
<FormattedMessage
|
values={{
|
||||||
id='channel_notifications.default'
|
optionalDefault: defaultOption === NotificationLevels.NONE ? (
|
||||||
defaultMessage='(default)'
|
<FormattedMessage
|
||||||
/>) : undefined,
|
{...defaultMessage}
|
||||||
}}
|
/>) : undefined,
|
||||||
/>
|
}}
|
||||||
),
|
/>
|
||||||
name: `desktopNotification-${NotificationLevels.NONE}`,
|
),
|
||||||
key: `desktopNotification-${NotificationLevels.NONE}`,
|
name: 'desktop',
|
||||||
value: NotificationLevels.NONE,
|
key: `desktopNotification-${NotificationLevels.NONE}`,
|
||||||
},
|
value: NotificationLevels.NONE,
|
||||||
],
|
},
|
||||||
};
|
],
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const desktopNotificationSoundsCheckboxFieldData: FieldsetCheckbox = {
|
export const desktopNotificationSoundsCheckboxFieldData: FieldsetCheckbox = {
|
||||||
name: 'desktopNotificationSoundsCheckbox',
|
name: 'desktopNotificationSoundsCheckbox',
|
||||||
@ -116,69 +117,66 @@ export const desktopNotificationSoundsSelectFieldData: FieldsetReactSelect = {
|
|||||||
options: optionsOfMessageNotificationSoundsSelect,
|
options: optionsOfMessageNotificationSoundsSelect,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mobileNotificationInputFieldData = (defaultOption: string): FieldsetRadio => {
|
export const mobileNotificationInputFieldData = (defaultOption: string): FieldsetRadio => ({
|
||||||
return {
|
options: [
|
||||||
options: [
|
{
|
||||||
{
|
dataTestId: `MobileNotification-${NotificationLevels.ALL}`,
|
||||||
dataTestId: `MobileNotification-${NotificationLevels.ALL}`,
|
title: (
|
||||||
title: (
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='channelNotifications.mobileNotification.newMessages'
|
||||||
id='channelNotifications.mobileNotification.newMessages'
|
defaultMessage='All new messages {optionalDefault}'
|
||||||
defaultMessage='All new messages {optionalDefault}'
|
values={{
|
||||||
values={{
|
optionalDefault: defaultOption === NotificationLevels.ALL ? (
|
||||||
optionalDefault: defaultOption === NotificationLevels.ALL ? (
|
<FormattedMessage
|
||||||
<FormattedMessage
|
{...defaultMessage}
|
||||||
id='channel_notifications.default'
|
/>) : undefined,
|
||||||
defaultMessage='(default)'
|
}}
|
||||||
/>) : undefined,
|
/>
|
||||||
}}
|
),
|
||||||
/>
|
name: 'push',
|
||||||
),
|
key: `MobileNotification-${NotificationLevels.ALL}`,
|
||||||
name: `MobileNotification-${NotificationLevels.ALL}`,
|
value: NotificationLevels.ALL,
|
||||||
key: `MobileNotification-${NotificationLevels.ALL}`,
|
},
|
||||||
value: NotificationLevels.ALL,
|
{
|
||||||
},
|
dataTestId: `MobileNotification-${NotificationLevels.MENTION}`,
|
||||||
{
|
title: (
|
||||||
dataTestId: `MobileNotification-${NotificationLevels.MENTION}`,
|
<FormattedMessage
|
||||||
title: (
|
|
||||||
<FormattedMessage
|
id='channelNotifications.mobileNotification.mention'
|
||||||
id='channelNotifications.mobileNotification.mention'
|
defaultMessage='Mentions, direct messages, and keywords only {optionalDefault}'
|
||||||
defaultMessage='Mentions, direct messages, and keywords only {optionalDefault}'
|
values={{
|
||||||
values={{
|
optionalDefault: defaultOption === NotificationLevels.MENTION ? (
|
||||||
optionalDefault: defaultOption === NotificationLevels.MENTION ? (
|
<FormattedMessage
|
||||||
<FormattedMessage
|
{...defaultMessage}
|
||||||
id='channel_notifications.default'
|
/>) : undefined,
|
||||||
defaultMessage='(default)'
|
}}
|
||||||
/>) : undefined,
|
/>
|
||||||
}}
|
),
|
||||||
/>
|
name: 'push',
|
||||||
),
|
key: `MobileNotification-${NotificationLevels.MENTION}`,
|
||||||
name: `MobileNotification-${NotificationLevels.MENTION}`,
|
value: NotificationLevels.MENTION,
|
||||||
key: `MobileNotification-${NotificationLevels.MENTION}`,
|
},
|
||||||
value: NotificationLevels.MENTION,
|
{
|
||||||
},
|
dataTestId: `MobileNotification-${NotificationLevels.NONE}`,
|
||||||
{
|
title: (
|
||||||
dataTestId: `MobileNotification-${NotificationLevels.NONE}`,
|
<FormattedMessage
|
||||||
title: (
|
id='channelNotifications.mobileNotification.nothing'
|
||||||
<FormattedMessage
|
defaultMessage='Nothing {optionalDefault}'
|
||||||
id='channelNotifications.mobileNotification.nothing'
|
values={{
|
||||||
defaultMessage='Nothing {optionalDefault}'
|
optionalDefault: defaultOption === NotificationLevels.NONE ? (
|
||||||
values={{
|
<FormattedMessage
|
||||||
optionalDefault: defaultOption === NotificationLevels.NONE ? (
|
{...defaultMessage}
|
||||||
<FormattedMessage
|
/>) : undefined,
|
||||||
id='channel_notifications.default'
|
}}
|
||||||
defaultMessage='(default)'
|
/>
|
||||||
/>) : undefined,
|
),
|
||||||
}}
|
name: 'push',
|
||||||
/>
|
key: `MobileNotification-${NotificationLevels.NONE}`,
|
||||||
),
|
value: NotificationLevels.NONE,
|
||||||
name: `MobileNotification-${NotificationLevels.NONE}`,
|
},
|
||||||
key: `MobileNotification-${NotificationLevels.NONE}`,
|
],
|
||||||
value: NotificationLevels.NONE,
|
}
|
||||||
},
|
);
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
desktopNotificationInputFieldData,
|
desktopNotificationInputFieldData,
|
||||||
|
@ -159,13 +159,16 @@ function PostReminderSubmenu(props: Props) {
|
|||||||
leadingElement={<ClockOutlineIcon size={18}/>}
|
leadingElement={<ClockOutlineIcon size={18}/>}
|
||||||
trailingElements={<span className={'dot-menu__item-trailing-icon'}><ChevronRightIcon size={16}/></span>}
|
trailingElements={<span className={'dot-menu__item-trailing-icon'}><ChevronRightIcon size={16}/></span>}
|
||||||
menuId={`remind_post_${props.post.id}-menu`}
|
menuId={`remind_post_${props.post.id}-menu`}
|
||||||
|
subMenuHeader={
|
||||||
|
<h5 className={'dot-menu__post-reminder-menu-header'}>
|
||||||
|
{formatMessage(
|
||||||
|
{
|
||||||
|
id: 'post_info.post_reminder.sub_menu.header',
|
||||||
|
defaultMessage: 'Set a reminder for:',
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</h5>}
|
||||||
>
|
>
|
||||||
<h5 className={'dot-menu__post-reminder-menu-header'}>
|
|
||||||
{formatMessage(
|
|
||||||
{id: 'post_info.post_reminder.sub_menu.header',
|
|
||||||
defaultMessage: 'Set a reminder for:'},
|
|
||||||
)}
|
|
||||||
</h5>
|
|
||||||
{postReminderSubMenuItems}
|
{postReminderSubMenuItems}
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 56px;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -68,6 +69,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DraftList.Drafts__main {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,12 +7,13 @@
|
|||||||
min-width: 114px;
|
min-width: 114px;
|
||||||
max-width: 496px;
|
max-width: 496px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
|
padding: 4px 0;
|
||||||
background-color: var(--center-channel-bg);
|
background-color: var(--center-channel-bg);
|
||||||
box-shadow: var(--elevation-4);
|
box-shadow: var(--elevation-4), 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset;
|
||||||
}
|
}
|
||||||
&.AsSubMenu {
|
&.AsSubMenu {
|
||||||
& .MuiPaper-root {
|
& .MuiPaper-root {
|
||||||
box-shadow: var(--elevation-5);
|
box-shadow: var(--elevation-5), 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ describe('menu click handlers', () => {
|
|||||||
expect(screen.getByText('Open modal from submenu')).toBeInTheDocument();
|
expect(screen.getByText('Open modal from submenu')).toBeInTheDocument();
|
||||||
|
|
||||||
// Press the down arrow once to focus first submenu item and then twice more to select the one we want
|
// Press the down arrow once to focus first submenu item and then twice more to select the one we want
|
||||||
userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}');
|
userEvent.keyboard('{arrowdown}{arrowdown}');
|
||||||
|
|
||||||
expect(screen.getByText('Open modal from submenu').closest('li')).toHaveFocus();
|
expect(screen.getByText('Open modal from submenu').closest('li')).toHaveFocus();
|
||||||
|
|
||||||
|
@ -309,6 +309,7 @@ export const MenuItemStyled = styled(MuiMenuItem, {
|
|||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
color: isRegular ? 'rgba(var(--center-channel-color-rgb), 0.75)' : 'var(--error-text)',
|
color: isRegular ? 'rgba(var(--center-channel-color-rgb), 0.75)' : 'var(--error-text)',
|
||||||
|
marginInlineStart: '24px',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
lineHeight: '16px',
|
lineHeight: '16px',
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// 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 MuiMenu from '@mui/material/Menu';
|
|
||||||
import MuiMenuList from '@mui/material/MenuList';
|
import MuiMenuList from '@mui/material/MenuList';
|
||||||
|
import MuiPopover from '@mui/material/Popover';
|
||||||
import type {PopoverOrigin} from '@mui/material/Popover';
|
import type {PopoverOrigin} from '@mui/material/Popover';
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
@ -33,6 +33,8 @@ import {SubMenuContext, useMenuContextValue} from './menu_context';
|
|||||||
import {MenuItem} from './menu_item';
|
import {MenuItem} from './menu_item';
|
||||||
import type {Props as MenuItemProps} from './menu_item';
|
import type {Props as MenuItemProps} from './menu_item';
|
||||||
|
|
||||||
|
import './menu.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: MenuItemProps['id'];
|
id: MenuItemProps['id'];
|
||||||
leadingElement?: MenuItemProps['leadingElement'];
|
leadingElement?: MenuItemProps['leadingElement'];
|
||||||
@ -47,6 +49,7 @@ interface Props {
|
|||||||
menuAriaDescribedBy?: string;
|
menuAriaDescribedBy?: string;
|
||||||
forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed
|
forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed
|
||||||
|
|
||||||
|
subMenuHeader?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +66,7 @@ export function SubMenu(props: Props) {
|
|||||||
menuAriaDescribedBy,
|
menuAriaDescribedBy,
|
||||||
forceOpenOnLeft,
|
forceOpenOnLeft,
|
||||||
children,
|
children,
|
||||||
|
subMenuHeader,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@ -136,6 +140,7 @@ export function SubMenu(props: Props) {
|
|||||||
menuId,
|
menuId,
|
||||||
menuAriaLabel,
|
menuAriaLabel,
|
||||||
children,
|
children,
|
||||||
|
subMenuHeader,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -166,35 +171,30 @@ export function SubMenu(props: Props) {
|
|||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<MuiMenu
|
<SubMenuContext.Provider value={providerValue}>
|
||||||
anchorEl={anchorElement}
|
<MuiPopover
|
||||||
open={isSubMenuOpen}
|
anchorEl={anchorElement}
|
||||||
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
open={isSubMenuOpen}
|
||||||
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
||||||
sx={{pointerEvents: 'none'}}
|
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
||||||
className='menu_menuStyled AsSubMenu'
|
className='menu_menuStyled AsSubMenu'
|
||||||
>
|
|
||||||
{/* This component is needed here to re enable pointer events for the submenu items which we had to disable above as */}
|
|
||||||
{/* pointer turns to default as soon as it leaves the parent menu */}
|
|
||||||
{/* Notice we dont use the below component in menu.tsx */}
|
|
||||||
<MuiMenuList
|
|
||||||
id={menuId}
|
|
||||||
component='ul'
|
|
||||||
aria-label={menuAriaLabel}
|
|
||||||
aria-describedby={menuAriaDescribedBy}
|
|
||||||
className={A11yClassNames.POPUP}
|
|
||||||
onKeyDown={handleSubMenuKeyDown}
|
|
||||||
sx={{
|
|
||||||
pointerEvents: 'auto', // reset pointer events to default from here on
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SubMenuContext.Provider value={providerValue}>
|
{subMenuHeader}
|
||||||
|
<MuiMenuList
|
||||||
|
id={menuId}
|
||||||
|
aria-label={menuAriaLabel}
|
||||||
|
aria-describedby={menuAriaDescribedBy}
|
||||||
|
className={A11yClassNames.POPUP}
|
||||||
|
onKeyDown={handleSubMenuKeyDown}
|
||||||
|
autoFocusItem={isSubMenuOpen}
|
||||||
|
sx={{
|
||||||
|
py: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</SubMenuContext.Provider>
|
</MuiMenuList>
|
||||||
</MuiMenuList>
|
</MuiPopover>
|
||||||
</MuiMenu>
|
</SubMenuContext.Provider>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -203,6 +203,7 @@ interface SubMenuModalProps {
|
|||||||
menuId: Props['menuId'];
|
menuId: Props['menuId'];
|
||||||
menuAriaLabel?: Props['menuAriaLabel'];
|
menuAriaLabel?: Props['menuAriaLabel'];
|
||||||
children: Props['children'];
|
children: Props['children'];
|
||||||
|
subMenuHeader?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubMenuModal(props: SubMenuModalProps) {
|
function SubMenuModal(props: SubMenuModalProps) {
|
||||||
@ -224,9 +225,11 @@ function SubMenuModal(props: SubMenuModalProps) {
|
|||||||
className='menuModal'
|
className='menuModal'
|
||||||
>
|
>
|
||||||
<MuiMenuList
|
<MuiMenuList
|
||||||
|
component={'div'}
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
onClick={handleModalClose}
|
onClick={handleModalClose}
|
||||||
>
|
>
|
||||||
|
{props.subMenuHeader}
|
||||||
{props.children}
|
{props.children}
|
||||||
</MuiMenuList>
|
</MuiMenuList>
|
||||||
</GenericModal>
|
</GenericModal>
|
||||||
|
@ -58,7 +58,7 @@ const AllowedDomainsSelect = ({allowedDomains, setAllowedDomains, setHasChanges,
|
|||||||
defaultMessage: 'When enabled, users can only join the team if their email matches a specific domain (e.g. "mattermost.org")',
|
defaultMessage: 'When enabled, users can only join the team if their email matches a specific domain (e.g. "mattermost.org")',
|
||||||
})}
|
})}
|
||||||
descriptionAboveContent={true}
|
descriptionAboveContent={true}
|
||||||
inputFieldData={{name: 'name'}}
|
inputFieldData={{name: 'showAllowedDomains'}}
|
||||||
inputFieldValue={showAllowedDomains}
|
inputFieldValue={showAllowedDomains}
|
||||||
handleChange={handleEnableAllowedDomains}
|
handleChange={handleEnableAllowedDomains}
|
||||||
/>
|
/>
|
||||||
|
@ -57,10 +57,10 @@ const OpenInvite = ({isGroupConstrained, allowOpenInvite, setAllowOpenInvite}: P
|
|||||||
inputFieldTitle={
|
inputFieldTitle={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='general_tab.openInviteTitle'
|
id='general_tab.openInviteTitle'
|
||||||
defaultMessage='Allow only users with a specific email domain to join this team'
|
defaultMessage='Allow any user with an account on this server to join this team'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
inputFieldData={{name: 'name'}}
|
inputFieldData={{name: 'allowOpenInvite'}}
|
||||||
inputFieldValue={allowOpenInvite}
|
inputFieldValue={allowOpenInvite}
|
||||||
handleChange={setAllowOpenInvite}
|
handleChange={setAllowOpenInvite}
|
||||||
title={formatMessage({
|
title={formatMessage({
|
||||||
|
@ -120,4 +120,31 @@ describe('components/TeamSettings', () => {
|
|||||||
id: defaultProps.team?.id,
|
id: defaultProps.team?.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MM-62891 should toggle the right checkboxes when their labels are clicked on', () => {
|
||||||
|
renderWithContext(<AccessTab {...defaultProps}/>);
|
||||||
|
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow only users with a specific email domain to join this team'})).not.toBeChecked();
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow any user with an account on this server to join this team'})).not.toBeChecked();
|
||||||
|
|
||||||
|
userEvent.click(screen.getByText('Allow only users with a specific email domain to join this team'));
|
||||||
|
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow only users with a specific email domain to join this team'})).toBeChecked();
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow any user with an account on this server to join this team'})).not.toBeChecked();
|
||||||
|
|
||||||
|
userEvent.click(screen.getByText('Allow only users with a specific email domain to join this team'));
|
||||||
|
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow only users with a specific email domain to join this team'})).not.toBeChecked();
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow any user with an account on this server to join this team'})).not.toBeChecked();
|
||||||
|
|
||||||
|
userEvent.click(screen.getByText('Allow any user with an account on this server to join this team'));
|
||||||
|
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow only users with a specific email domain to join this team'})).not.toBeChecked();
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow any user with an account on this server to join this team'})).toBeChecked();
|
||||||
|
|
||||||
|
userEvent.click(screen.getByText('Allow any user with an account on this server to join this team'));
|
||||||
|
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow only users with a specific email domain to join this team'})).not.toBeChecked();
|
||||||
|
expect(screen.getByRole('checkbox', {name: 'Allow any user with an account on this server to join this team'})).not.toBeChecked();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,7 @@ function mapStateToProps(state: GlobalState, {location: {pathname}}: Props) {
|
|||||||
unreadStatus: getUnreadStatus(state),
|
unreadStatus: getUnreadStatus(state),
|
||||||
inGlobalThreads: matchPath(pathname, {path: '/:team/threads/:threadIdentifier?'}) != null,
|
inGlobalThreads: matchPath(pathname, {path: '/:team/threads/:threadIdentifier?'}) != null,
|
||||||
inDrafts: matchPath(pathname, {path: '/:team/drafts'}) != null,
|
inDrafts: matchPath(pathname, {path: '/:team/drafts'}) != null,
|
||||||
|
inScheduledPosts: matchPath(pathname, {path: '/:team/scheduled_posts'}) != null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ describe('components/UnreadsStatusHandler', () => {
|
|||||||
currentTeammate: null,
|
currentTeammate: null,
|
||||||
inGlobalThreads: false,
|
inGlobalThreads: false,
|
||||||
inDrafts: false,
|
inDrafts: false,
|
||||||
|
inScheduledPosts: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('set correctly the title when needed', () => {
|
test('set correctly the title when needed', () => {
|
||||||
@ -83,6 +84,38 @@ describe('components/UnreadsStatusHandler', () => {
|
|||||||
currentTeammate: {} as Props['currentTeammate']});
|
currentTeammate: {} as Props['currentTeammate']});
|
||||||
instance.updateTitle();
|
instance.updateTitle();
|
||||||
expect(document.title).toBe('Mattermost - Join a team');
|
expect(document.title).toBe('Mattermost - Join a team');
|
||||||
|
|
||||||
|
wrapper.setProps({
|
||||||
|
inDrafts: false,
|
||||||
|
inScheduledPosts: true,
|
||||||
|
unreadStatus: 0,
|
||||||
|
});
|
||||||
|
instance.updateTitle();
|
||||||
|
expect(document.title).toBe('Scheduled - Test team display name');
|
||||||
|
|
||||||
|
wrapper.setProps({
|
||||||
|
inDrafts: false,
|
||||||
|
inScheduledPosts: true,
|
||||||
|
unreadStatus: 10,
|
||||||
|
});
|
||||||
|
instance.updateTitle();
|
||||||
|
expect(document.title).toBe('(10) Scheduled - Test team display name');
|
||||||
|
|
||||||
|
wrapper.setProps({
|
||||||
|
inDrafts: true,
|
||||||
|
inScheduledPosts: false,
|
||||||
|
unreadStatus: 0,
|
||||||
|
});
|
||||||
|
instance.updateTitle();
|
||||||
|
expect(document.title).toBe('Drafts - Test team display name');
|
||||||
|
|
||||||
|
wrapper.setProps({
|
||||||
|
inDrafts: true,
|
||||||
|
inScheduledPosts: false,
|
||||||
|
unreadStatus: 10,
|
||||||
|
});
|
||||||
|
instance.updateTitle();
|
||||||
|
expect(document.title).toBe('(10) Drafts - Test team display name');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should set correct title on mentions on safari', () => {
|
test('should set correct title on mentions on safari', () => {
|
||||||
@ -149,4 +182,18 @@ describe('components/UnreadsStatusHandler', () => {
|
|||||||
|
|
||||||
expect(document.title).toBe('Drafts - Test team display name');
|
expect(document.title).toBe('Drafts - Test team display name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should display correct title when in scheduled posts tab', () => {
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<UnreadsStatusHandler
|
||||||
|
{...defaultProps}
|
||||||
|
inScheduledPosts={true}
|
||||||
|
currentChannel={undefined}
|
||||||
|
siteName={undefined}
|
||||||
|
/>,
|
||||||
|
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
|
||||||
|
wrapper.instance().updateTitle();
|
||||||
|
|
||||||
|
expect(document.title).toBe('Scheduled - Test team display name');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,7 @@ type Props = {
|
|||||||
currentTeammate: Channel | null;
|
currentTeammate: Channel | null;
|
||||||
inGlobalThreads: boolean;
|
inGlobalThreads: boolean;
|
||||||
inDrafts: boolean;
|
inDrafts: boolean;
|
||||||
|
inScheduledPosts: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UnreadsStatusHandlerClass extends React.PureComponent<Props> {
|
export class UnreadsStatusHandlerClass extends React.PureComponent<Props> {
|
||||||
@ -90,6 +91,7 @@ export class UnreadsStatusHandlerClass extends React.PureComponent<Props> {
|
|||||||
unreadStatus,
|
unreadStatus,
|
||||||
inGlobalThreads,
|
inGlobalThreads,
|
||||||
inDrafts,
|
inDrafts,
|
||||||
|
inScheduledPosts,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
@ -126,6 +128,15 @@ export class UnreadsStatusHandlerClass extends React.PureComponent<Props> {
|
|||||||
displayName: currentTeam.display_name,
|
displayName: currentTeam.display_name,
|
||||||
siteName: currentSiteName,
|
siteName: currentSiteName,
|
||||||
});
|
});
|
||||||
|
} else if (currentTeam && inScheduledPosts) {
|
||||||
|
document.title = formatMessage({
|
||||||
|
id: 'scheduledPosts.title',
|
||||||
|
defaultMessage: '{prefix}Scheduled - {displayName} {siteName}',
|
||||||
|
}, {
|
||||||
|
prefix: `${mentionTitle}${unreadTitle}`,
|
||||||
|
displayName: currentTeam.display_name,
|
||||||
|
siteName: currentSiteName,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
document.title = formatMessage({id: 'sidebar.team_select', defaultMessage: '{siteName} - Join a team'}, {siteName: currentSiteName || 'Mattermost'});
|
document.title = formatMessage({id: 'sidebar.team_select', defaultMessage: '{siteName} - Join a team'}, {siteName: currentSiteName || 'Mattermost'});
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ exports[`UserAccountNameMenuItem should not break if no props are passed 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<li
|
<li
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR cUFZeQ userAccountMenu_nameMenuItem"
|
class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR jjvBbU userAccountMenu_nameMenuItem"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
|
@ -196,20 +196,21 @@ export default function UserAccountDndMenuItem(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
role='menuitemradio' // Prevents menu item from closing, not a recommended solution
|
|
||||||
aria-checked={props.isStatusDnd}
|
aria-checked={props.isStatusDnd}
|
||||||
trailingElements={trailingElement}
|
trailingElements={trailingElement}
|
||||||
|
subMenuHeader={
|
||||||
|
<h5
|
||||||
|
id='userAccountMenu_dndSubMenuTitle'
|
||||||
|
className='userAccountMenu_dndMenuItem_subMenuTitle'
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'userAccountMenu.dndSubMenu.title',
|
||||||
|
defaultMessage: 'Clear after:',
|
||||||
|
})}
|
||||||
|
</h5>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h5
|
|
||||||
id='userAccountMenu_dndSubMenuTitle'
|
|
||||||
className='userAccountMenu_dndMenuItem_subMenuTitle'
|
|
||||||
aria-hidden={true}
|
|
||||||
>
|
|
||||||
{formatMessage({
|
|
||||||
id: 'userAccountMenu.dndSubMenu.title',
|
|
||||||
defaultMessage: 'Clear after:',
|
|
||||||
})}
|
|
||||||
</h5>
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={DND_SUB_MENU_ITEMS_IDS.DO_NOT_CLEAR}
|
id={DND_SUB_MENU_ITEMS_IDS.DO_NOT_CLEAR}
|
||||||
labels={
|
labels={
|
||||||
|
@ -54,7 +54,12 @@ function RadioSettingItem({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<fieldset className='mm-modal-generic-section-item__fieldset-radio'>
|
<fieldset
|
||||||
|
className='mm-modal-generic-section-item__fieldset-radio'
|
||||||
|
>
|
||||||
|
<legend className='hidden-label'>
|
||||||
|
{title}
|
||||||
|
</legend>
|
||||||
{[...fields]}
|
{[...fields]}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
|
@ -2251,13 +2251,10 @@
|
|||||||
"analytics.system.privateGroups": "Прыватныя каналы",
|
"analytics.system.privateGroups": "Прыватныя каналы",
|
||||||
"analytics.system.publicChannels": "Публічныя каналы",
|
"analytics.system.publicChannels": "Публічныя каналы",
|
||||||
"analytics.system.skippedIntensiveQueries": "Для забеспячэння максімальнай прадукцыйнасці некаторыя статыстычныя даныя адключаны. Вы можаце [паўторна ўключыць іх у config.json](!https://docs.mattermost.com/administration/statistics.html).",
|
"analytics.system.skippedIntensiveQueries": "Для забеспячэння максімальнай прадукцыйнасці некаторыя статыстычныя даныя адключаны. Вы можаце [паўторна ўключыць іх у config.json](!https://docs.mattermost.com/administration/statistics.html).",
|
||||||
"analytics.system.textPosts": "Толькі тэкставыя паведамленні",
|
|
||||||
"analytics.system.title": "Статыстыка сістэмы",
|
"analytics.system.title": "Статыстыка сістэмы",
|
||||||
"analytics.system.totalBotPosts": "Усяго паведамленняў ад ботаў",
|
"analytics.system.totalBotPosts": "Усяго паведамленняў ад ботаў",
|
||||||
"analytics.system.totalChannels": "Усяго каналаў",
|
"analytics.system.totalChannels": "Усяго каналаў",
|
||||||
"analytics.system.totalCommands": "Усяго каманд",
|
"analytics.system.totalCommands": "Усяго каманд",
|
||||||
"analytics.system.totalFilePosts": "Паведамленні з файламі",
|
|
||||||
"analytics.system.totalHashtagPosts": "Паведамленні з хэштэгамі",
|
|
||||||
"analytics.system.totalIncomingWebhooks": "Уваходныя вэбхукі",
|
"analytics.system.totalIncomingWebhooks": "Уваходныя вэбхукі",
|
||||||
"analytics.system.totalMasterDbConnections": "Падлучэнняў да галоўнай БД",
|
"analytics.system.totalMasterDbConnections": "Падлучэнняў да галоўнай БД",
|
||||||
"analytics.system.totalOutgoingWebhooks": "Выходныя вэбхукі",
|
"analytics.system.totalOutgoingWebhooks": "Выходныя вэбхукі",
|
||||||
|
@ -2833,13 +2833,10 @@
|
|||||||
"analytics.system.privateGroups": "Частни канали",
|
"analytics.system.privateGroups": "Частни канали",
|
||||||
"analytics.system.publicChannels": "Публични канали",
|
"analytics.system.publicChannels": "Публични канали",
|
||||||
"analytics.system.skippedIntensiveQueries": "За да се увеличи ефективността, някои статистически данни са деактивирани. Можете да <link>ги активирате отново в config.json</link>.",
|
"analytics.system.skippedIntensiveQueries": "За да се увеличи ефективността, някои статистически данни са деактивирани. Можете да <link>ги активирате отново в config.json</link>.",
|
||||||
"analytics.system.textPosts": "Публикации само с текст",
|
|
||||||
"analytics.system.title": "Статистика за системата",
|
"analytics.system.title": "Статистика за системата",
|
||||||
"analytics.system.totalBotPosts": "Общо публикации на ботове",
|
"analytics.system.totalBotPosts": "Общо публикации на ботове",
|
||||||
"analytics.system.totalChannels": "Общо канали",
|
"analytics.system.totalChannels": "Общо канали",
|
||||||
"analytics.system.totalCommands": "Общо команди",
|
"analytics.system.totalCommands": "Общо команди",
|
||||||
"analytics.system.totalFilePosts": "Публикации с файлове",
|
|
||||||
"analytics.system.totalHashtagPosts": "Публикации с хаштагове",
|
|
||||||
"analytics.system.totalIncomingWebhooks": "Входящи webhook",
|
"analytics.system.totalIncomingWebhooks": "Входящи webhook",
|
||||||
"analytics.system.totalMasterDbConnections": "Връзки с главна БД",
|
"analytics.system.totalMasterDbConnections": "Връзки с главна БД",
|
||||||
"analytics.system.totalOutgoingWebhooks": "Изходящи webhook",
|
"analytics.system.totalOutgoingWebhooks": "Изходящи webhook",
|
||||||
|
@ -246,6 +246,7 @@
|
|||||||
"add_teams_to_scheme.confirmation.message": "Tento tým je již vybrán v jiném týmovém schématu, jste si jisti, že ho chcete přesunout do tohoto týmového schématu?",
|
"add_teams_to_scheme.confirmation.message": "Tento tým je již vybrán v jiném týmovém schématu, jste si jisti, že ho chcete přesunout do tohoto týmového schématu?",
|
||||||
"add_teams_to_scheme.confirmation.title": "Změna schématu přepsání pro tým?",
|
"add_teams_to_scheme.confirmation.title": "Změna schématu přepsání pro tým?",
|
||||||
"add_teams_to_scheme.modalTitle": "Přidat týmy do seznamu pro výběr týmu",
|
"add_teams_to_scheme.modalTitle": "Přidat týmy do seznamu pro výběr týmu",
|
||||||
|
"add_teams_to_scheme.select_team.label": "Zvolte tým {label}",
|
||||||
"add_user_to_channel_modal.add": "Přidat",
|
"add_user_to_channel_modal.add": "Přidat",
|
||||||
"add_user_to_channel_modal.cancel": "Zrušit",
|
"add_user_to_channel_modal.cancel": "Zrušit",
|
||||||
"add_user_to_channel_modal.help": "Zadejte pro vyhledání kanálu. Použijte ↑↓ pro procházení, ↵ pro výběr, ESC pro zavření.",
|
"add_user_to_channel_modal.help": "Zadejte pro vyhledání kanálu. Použijte ↑↓ pro procházení, ↵ pro výběr, ESC pro zavření.",
|
||||||
@ -580,17 +581,17 @@
|
|||||||
"admin.complianceExport.createJob.title": "Spustit Export souladu s pravidly",
|
"admin.complianceExport.createJob.title": "Spustit Export souladu s pravidly",
|
||||||
"admin.complianceExport.exportFormat.actiance": "Actiance XML",
|
"admin.complianceExport.exportFormat.actiance": "Actiance XML",
|
||||||
"admin.complianceExport.exportFormat.csv": "CSV",
|
"admin.complianceExport.exportFormat.csv": "CSV",
|
||||||
"admin.complianceExport.exportFormat.globalrelay": "EML globálního přenositelného zákaznického účtu",
|
"admin.complianceExport.exportFormat.globalrelay": "Global Relay EML",
|
||||||
"admin.complianceExport.exportFormat.title": "Exportní formát:",
|
"admin.complianceExport.exportFormat.title": "Exportní formát:",
|
||||||
"admin.complianceExport.exportFormatDetail.details": "Pro Actiance XML jsou soubory exportu pro dodržování předpisů ukládány do podadresáře exports v nastaveném <a>Místním úložišti</a>. Pro Global Relay EML jsou soubory zasílány na nastavenou e-mailovou adresu.",
|
"admin.complianceExport.exportFormatDetail.details": "Pro Actiance XML jsou soubory exportu pro dodržování předpisů ukládány do podadresáře exports v nastaveném <a>Místním úložišti</a>. Pro Global Relay EML jsou soubory zasílány na nastavenou e-mailovou adresu.",
|
||||||
"admin.complianceExport.exportFormatDetail.intro": "„Formát exportu pro dodržování předpisů. Odpovídá systému, do kterého chcete data importovat.",
|
"admin.complianceExport.exportFormatDetail.intro": "„Formát exportu pro dodržování předpisů. Odpovídá systému, do kterého chcete data importovat.",
|
||||||
"admin.complianceExport.exportJobStartTime.description": "Nastavte počáteční čas pravidelného denního exportu. Zvolte dobu, kdy bude váš systém používat nejméně lidí. Formát musí být 24 hodinový ve tvaru HH: MM.",
|
"admin.complianceExport.exportJobStartTime.description": "Nastavte počáteční čas pravidelného denního exportu. Zvolte dobu, kdy bude váš systém používat nejméně lidí. Formát musí být 24 hodinový ve tvaru HH: MM.",
|
||||||
"admin.complianceExport.exportJobStartTime.example": "Např.: \"02:00\"",
|
"admin.complianceExport.exportJobStartTime.example": "Např.: \"02:00\"",
|
||||||
"admin.complianceExport.exportJobStartTime.title": "Čas exportu zpráv o shodě:",
|
"admin.complianceExport.exportJobStartTime.title": "Čas exportu zpráv o shodě:",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPPort.description": "Port SMTP serveru, který obdrží vaše EML globálního přenositelného zákaznického účtu.",
|
"admin.complianceExport.globalRelayCustomSMTPPort.description": "SMTP serverový port, který přijme váš Global Relay EML.",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPPort.example": "Např.: \"25\"",
|
"admin.complianceExport.globalRelayCustomSMTPPort.example": "Např.: \"25\"",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPPort.title": "Port SMTP serveru:",
|
"admin.complianceExport.globalRelayCustomSMTPPort.title": "Port SMTP serveru:",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPServerName.description": "Název SMTP serveru, který obdrží vaše EML globálního přenositelného zákaznického účtu.",
|
"admin.complianceExport.globalRelayCustomSMTPServerName.description": "Název SMTP serveru, který přijme váš Global Relay EML.",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPServerName.example": "Např.: \"feeds.globalrelay.com\"",
|
"admin.complianceExport.globalRelayCustomSMTPServerName.example": "Např.: \"feeds.globalrelay.com\"",
|
||||||
"admin.complianceExport.globalRelayCustomSMTPServerName.title": "Název SMTP serveru:",
|
"admin.complianceExport.globalRelayCustomSMTPServerName.title": "Název SMTP serveru:",
|
||||||
"admin.complianceExport.globalRelayCustomerType.a10.description": "A10/Typ 10",
|
"admin.complianceExport.globalRelayCustomerType.a10.description": "A10/Typ 10",
|
||||||
@ -598,7 +599,7 @@
|
|||||||
"admin.complianceExport.globalRelayCustomerType.custom.description": "Vlastní",
|
"admin.complianceExport.globalRelayCustomerType.custom.description": "Vlastní",
|
||||||
"admin.complianceExport.globalRelayCustomerType.description": "Typ globálního přenositelného zákaznického účtu, který má vaše organizace k dispozici.",
|
"admin.complianceExport.globalRelayCustomerType.description": "Typ globálního přenositelného zákaznického účtu, který má vaše organizace k dispozici.",
|
||||||
"admin.complianceExport.globalRelayCustomerType.title": "Globálně přenositelný zákaznický účet:",
|
"admin.complianceExport.globalRelayCustomerType.title": "Globálně přenositelný zákaznický účet:",
|
||||||
"admin.complianceExport.globalRelayEmailAddress.description": "E-mailová adresa, kterou váš globální přenosový server sleduje pro příchozí exporty souladu.",
|
"admin.complianceExport.globalRelayEmailAddress.description": "E-mailová adresa, kterou váš server Global Relay sleduje pro příchozí exporty souladu.",
|
||||||
"admin.complianceExport.globalRelayEmailAddress.example": "Např.: \"globalrelay@mattermost.com\"",
|
"admin.complianceExport.globalRelayEmailAddress.example": "Např.: \"globalrelay@mattermost.com\"",
|
||||||
"admin.complianceExport.globalRelayEmailAddress.title": "Email globálně přenositelného zákaznického účtu:",
|
"admin.complianceExport.globalRelayEmailAddress.title": "Email globálně přenositelného zákaznického účtu:",
|
||||||
"admin.complianceExport.globalRelaySMTPPassword.description": "Heslo, které se používá k ověření proti serveru SMTP GlobalRelay.",
|
"admin.complianceExport.globalRelaySMTPPassword.description": "Heslo, které se používá k ověření proti serveru SMTP GlobalRelay.",
|
||||||
@ -609,8 +610,8 @@
|
|||||||
"admin.complianceExport.globalRelaySMTPUsername.title": "Uživatelské jméno SMTP:",
|
"admin.complianceExport.globalRelaySMTPUsername.title": "Uživatelské jméno SMTP:",
|
||||||
"admin.complianceExport.messagesExportedCount": "{count} zpráv vyexportováno.",
|
"admin.complianceExport.messagesExportedCount": "{count} zpráv vyexportováno.",
|
||||||
"admin.complianceExport.title": "Export zprávy o shodě",
|
"admin.complianceExport.title": "Export zprávy o shodě",
|
||||||
"admin.complianceExport.warningCount": "{count} narazil(y) na varování, podrobnosti v souboru warning.txt",
|
"admin.complianceExport.warningCount": "{count} varování zjištěno, podrobnosti naleznete v souboru warning.txt",
|
||||||
"admin.complianceExport.warningCount.globalrelay": "{count} narazil(y) na varování, podrobnosti v logu",
|
"admin.complianceExport.warningCount.globalrelay": "{count} varování zjištěno, podrobnosti naleznete v protokolu",
|
||||||
"admin.complianceMonitoring.userActivityLogsTitle": "Záznamy aktivit uživatelů",
|
"admin.complianceMonitoring.userActivityLogsTitle": "Záznamy aktivit uživatelů",
|
||||||
"admin.compliance_export_feature_discovery.copy": "Spouštějte každodenní zprávy o compliance a exportujte je do různých formátů, které mohou být použity nástroji pro integraci třetích stran, jako je Smarsh (Actiance).",
|
"admin.compliance_export_feature_discovery.copy": "Spouštějte každodenní zprávy o compliance a exportujte je do různých formátů, které mohou být použity nástroji pro integraci třetích stran, jako je Smarsh (Actiance).",
|
||||||
"admin.compliance_export_feature_discovery.title": "Provádění exportů v souladu s předpisy pomocí Mattermost Enterprise",
|
"admin.compliance_export_feature_discovery.title": "Provádění exportů v souladu s předpisy pomocí Mattermost Enterprise",
|
||||||
@ -640,9 +641,9 @@
|
|||||||
"admin.compliance_table.type": "Typ",
|
"admin.compliance_table.type": "Typ",
|
||||||
"admin.compliance_table.userId": "Požadováno kým",
|
"admin.compliance_table.userId": "Požadováno kým",
|
||||||
"admin.connectionSecurityNone": "Žádný",
|
"admin.connectionSecurityNone": "Žádný",
|
||||||
"admin.connectionSecurityNoneDescription": "Mattermost se bude připojovat přes nezabezpečené připojení.",
|
"admin.connectionSecurityNoneDescription": "Mattermost se připojí přes nezabezpečené připojení.",
|
||||||
"admin.connectionSecurityStart": "STARTTLS",
|
"admin.connectionSecurityStart": "STARTTLS",
|
||||||
"admin.connectionSecurityStartDescription": "Vezme stávající nezabezpečené připojení a pokusí se ho povýšit na zabezpečené připojení pomocí TLS.",
|
"admin.connectionSecurityStartDescription": "Převede stávající nezabezpečené připojení a pokusí se jej upgradovat na zabezpečené připojení pomocí TLS.",
|
||||||
"admin.connectionSecurityTitle": "Zabezpečení připojení:",
|
"admin.connectionSecurityTitle": "Zabezpečení připojení:",
|
||||||
"admin.connectionSecurityTls": "TLS",
|
"admin.connectionSecurityTls": "TLS",
|
||||||
"admin.connectionSecurityTlsDescription": "Šifruje komunikaci mezi Mattermost a vaším serverem.",
|
"admin.connectionSecurityTlsDescription": "Šifruje komunikaci mezi Mattermost a vaším serverem.",
|
||||||
@ -652,8 +653,8 @@
|
|||||||
"admin.customization.allowSyncedDraftsDesc": "Pokud je tato funkce povolena, budou se návrhy zpráv uživatelů synchronizovat se serverem, aby k nim bylo možné přistupovat z jakéhokoli zařízení. Uživatelé mohou toto chování zrušit v nastavení účtu.",
|
"admin.customization.allowSyncedDraftsDesc": "Pokud je tato funkce povolena, budou se návrhy zpráv uživatelů synchronizovat se serverem, aby k nim bylo možné přistupovat z jakéhokoli zařízení. Uživatelé mohou toto chování zrušit v nastavení účtu.",
|
||||||
"admin.customization.androidAppDownloadLinkDesc": "Přidání odkazu ke stažení aplikace pro Android. Uživatelům, kteří přistupují k webu v mobilním webovém prohlížeči, bude zobrazena stránka, která jim umožní stáhnout aplikaci. Ponechte toto pole prázdné, abyste zabránili zobrazování stránky.",
|
"admin.customization.androidAppDownloadLinkDesc": "Přidání odkazu ke stažení aplikace pro Android. Uživatelům, kteří přistupují k webu v mobilním webovém prohlížeči, bude zobrazena stránka, která jim umožní stáhnout aplikaci. Ponechte toto pole prázdné, abyste zabránili zobrazování stránky.",
|
||||||
"admin.customization.androidAppDownloadLinkTitle": "Odkaz na Android aplikaci:",
|
"admin.customization.androidAppDownloadLinkTitle": "Odkaz na Android aplikaci:",
|
||||||
"admin.customization.announcement.allowBannerDismissalDesc": "Pokud je povoleno, mohou uživatelé odmítnout banner až do příští aktualizace. Pokud je zakázáno, banner je trvale viditelný, dokud ho systémový administrátor nevypne.",
|
"admin.customization.announcement.allowBannerDismissalDesc": "Pokud je nastavena hodnota „true“, uživatelé mohou banner zavřít až do jeho další aktualizace. Pokud je nastavena hodnota „false“, banner zůstane trvale viditelný, dokud jej nevypne správce systému.",
|
||||||
"admin.customization.announcement.allowBannerDismissalTitle": "Povolit schování banneru:",
|
"admin.customization.announcement.allowBannerDismissalTitle": "Povolit zavření banneru:",
|
||||||
"admin.customization.announcement.bannerColorTitle": "Barva banneru:",
|
"admin.customization.announcement.bannerColorTitle": "Barva banneru:",
|
||||||
"admin.customization.announcement.bannerTextColorTitle": "Barva textu banneru:",
|
"admin.customization.announcement.bannerTextColorTitle": "Barva textu banneru:",
|
||||||
"admin.customization.announcement.bannerTextDesc": "Text, který se objeví v oznamovacím banneru.",
|
"admin.customization.announcement.bannerTextDesc": "Text, který se objeví v oznamovacím banneru.",
|
||||||
@ -663,20 +664,20 @@
|
|||||||
"admin.customization.appDownloadLinkDesc": "Přidat odkaz na stránku stažení Mattermost aplikací. Pokud odkaz existuje, možnost \"Stáhnout Mattermost aplikace\" bude přidána do nabídky produktů tak, že uživatelé budou moci nalézt stránku stažení. Nechte toto pole prázdné, aby možnost byla schována z nabídky produktů.",
|
"admin.customization.appDownloadLinkDesc": "Přidat odkaz na stránku stažení Mattermost aplikací. Pokud odkaz existuje, možnost \"Stáhnout Mattermost aplikace\" bude přidána do nabídky produktů tak, že uživatelé budou moci nalézt stránku stažení. Nechte toto pole prázdné, aby možnost byla schována z nabídky produktů.",
|
||||||
"admin.customization.appDownloadLinkTitle": "Odkaz na stránku s aplikacemi Mattermost:",
|
"admin.customization.appDownloadLinkTitle": "Odkaz na stránku s aplikacemi Mattermost:",
|
||||||
"admin.customization.customUrlSchemes": "Vlastní schéma URL adresy:",
|
"admin.customization.customUrlSchemes": "Vlastní schéma URL adresy:",
|
||||||
"admin.customization.customUrlSchemesDesc": "Umožňuje z textu udělat odkaz, pokud začíná některým z uvedených schémat URL oddělených čárkou. Ve výchozím nastavení budou odkazy vytvářeny na následující schémata: \"http\", \"https\", \"ftp\", \"tel\" a \"mailto\".",
|
"admin.customization.customUrlSchemesDesc": "Povolí textu zprávy vytvořit odkaz, pokud začíná některým z uvedených URL schémat oddělených čárkami. Ve výchozím nastavení budou následující schémata vytvářet odkazy: \"http\", \"https\", \"ftp\", \"tel\" a \"mailto\".",
|
||||||
"admin.customization.customUrlSchemesPlaceholder": "Např.: \"git,smtp\"",
|
"admin.customization.customUrlSchemesPlaceholder": "Např.: \"git,smtp\"",
|
||||||
"admin.customization.enableCustomEmojiDesc": "Umožnit uživatelům vytvářet vlastní emodži pro použití ve zprávách. Pokud je tato funkce povolena, lze k nastavení vlastních emodži přistupovat v kanálech prostřednictvím nástroje pro výběr emodži.",
|
"admin.customization.enableCustomEmojiDesc": "Povolit uživatelům vytvářet vlastní emoji pro použití v zprávách. Po povolení budou nastavení vlastních emoji přístupná v kanálech prostřednictvím výběru emoji.",
|
||||||
"admin.customization.enableCustomEmojiTitle": "Povolit vlastní emoji:",
|
"admin.customization.enableCustomEmojiTitle": "Povolit vlastní emoji:",
|
||||||
"admin.customization.enableDesktopLandingPageDesc": "Jestli chcete nebo nechcete nabízet uživateli použití Desktopové aplikace při prvním užití Mattermostu.",
|
"admin.customization.enableDesktopLandingPageDesc": "Zda vyzvat uživatele k použití desktopové aplikace, když poprvé použije Mattermost.",
|
||||||
"admin.customization.enableDesktopLandingPageTitle": "Povolit úvodní stránku desktopové aplikace:",
|
"admin.customization.enableDesktopLandingPageTitle": "Povolit úvodní stránku desktopové aplikace:",
|
||||||
"admin.customization.enableEmojiPickerDesc": "Výběr emodži umožňuje uživatelům vybrat emodži, které chtějí přidat jako reakci nebo použít ve zprávách. Povolení výběru emodži s velkým počtem vlastních emodži může zpomalit výkon.",
|
"admin.customization.enableEmojiPickerDesc": "Výběr emodži umožňuje uživatelům vybrat emodži, které chtějí přidat jako reakci nebo použít ve zprávách. Povolení výběru emodži s velkým počtem vlastních emodži může zpomalit výkon.",
|
||||||
"admin.customization.enableEmojiPickerTitle": "Povolit nabídku emotikonů:",
|
"admin.customization.enableEmojiPickerTitle": "Povolit nabídku emotikonů:",
|
||||||
"admin.customization.enableGifPickerDesc": "Povolit uživatelům volit GIFy z emodži pickeru.",
|
"admin.customization.enableGifPickerDesc": "Povolit uživatelům volit GIFy z emodži pickeru.",
|
||||||
"admin.customization.enableGifPickerTitle": "Povolit nabídku GIFů:",
|
"admin.customization.enableGifPickerTitle": "Povolit nabídku GIFů:",
|
||||||
"admin.customization.enableInlineLatexDesc": "Povolení vykreslování inline kódu Latexu. Pokud je hodnota false, lze Latex vykreslovat pouze v bloku kódu pomocí zvýraznění syntaxe. Podrobnosti o formátování textu naleznete v naší <link>dokumentaci k webu </link> .",
|
"admin.customization.enableInlineLatexDesc": "Povolení vykreslování inline kódu Latexu. Pokud je hodnota false, lze Latex vykreslovat pouze v bloku kódu pomocí zvýraznění syntaxe. Podrobnosti o formátování textu naleznete v naší <link>dokumentaci k webu </link> .",
|
||||||
"admin.customization.enableInlineLatexTitle": "Povolte vykreslování Latex inline:",
|
"admin.customization.enableInlineLatexTitle": "Povolit zobrazení inline LaTeXu:",
|
||||||
"admin.customization.enableLatexDesc": "Povolení vykreslování Latexu v blocích kódu. Pokud je false, bude kód Latex pouze zvýrazněn.\n\nPovolení vykreslování Latexu není doporučeno v prostředích, kde nejsou všichni uživatelé věrohodní.",
|
"admin.customization.enableLatexDesc": "Povolení vykreslování Latexu v blocích kódu. Pokud je false, bude kód Latex pouze zvýrazněn.\n\nPovolení vykreslování Latexu není doporučeno v prostředích, kde nejsou všichni uživatelé věrohodní.",
|
||||||
"admin.customization.enableLatexTitle": "Povolit vykreslování Latex:",
|
"admin.customization.enableLatexTitle": "Povolit zobrazení LaTeXu:",
|
||||||
"admin.customization.enableLinkPreviewsDesc": "Zobrazení náhledu obsahu webové stránky, odkazů na obrázky a odkazů na YouTube pod zprávou, pokud jsou k dispozici. Server musí být připojen k internetu a mít přístup přes bránu firewall (pokud je to možné) k webovým stránkám, od kterých se očekávají náhledy. Uživatelé mohou tyto náhledy zakázat v nabídce Nastavení > Zobrazení > Náhledy odkazů na webové stránky.",
|
"admin.customization.enableLinkPreviewsDesc": "Zobrazení náhledu obsahu webové stránky, odkazů na obrázky a odkazů na YouTube pod zprávou, pokud jsou k dispozici. Server musí být připojen k internetu a mít přístup přes bránu firewall (pokud je to možné) k webovým stránkám, od kterých se očekávají náhledy. Uživatelé mohou tyto náhledy zakázat v nabídce Nastavení > Zobrazení > Náhledy odkazů na webové stránky.",
|
||||||
"admin.customization.enableLinkPreviewsTitle": "Povolení náhledů odkazů na webové stránky:",
|
"admin.customization.enableLinkPreviewsTitle": "Povolení náhledů odkazů na webové stránky:",
|
||||||
"admin.customization.enablePermalinkPreviewsDesc": "Pokud je tato funkce povolena, odkazy na zprávy Mattermostu vygenerují náhled pro všechny uživatele, kteří mají přístup k původní zprávě. Podrobnosti naleznete v naší dokumentaci na adrese <link></link> .",
|
"admin.customization.enablePermalinkPreviewsDesc": "Pokud je tato funkce povolena, odkazy na zprávy Mattermostu vygenerují náhled pro všechny uživatele, kteří mají přístup k původní zprávě. Podrobnosti naleznete v naší dokumentaci na adrese <link></link> .",
|
||||||
@ -685,47 +686,47 @@
|
|||||||
"admin.customization.enableSVGsTitle": "Povolte SVG:",
|
"admin.customization.enableSVGsTitle": "Povolte SVG:",
|
||||||
"admin.customization.iosAppDownloadLinkDesc": "Přidání odkazu ke stažení iOS aplikace. Uživatelé, kteří navštíví stránku v mobilním prohlížeči budou odkázáni na stránku, která jim umožní stáhnout aplikaci. Ponechte toto pole prázdné, abyste zabránili zobrazování stránky.",
|
"admin.customization.iosAppDownloadLinkDesc": "Přidání odkazu ke stažení iOS aplikace. Uživatelé, kteří navštíví stránku v mobilním prohlížeči budou odkázáni na stránku, která jim umožní stáhnout aplikaci. Ponechte toto pole prázdné, abyste zabránili zobrazování stránky.",
|
||||||
"admin.customization.iosAppDownloadLinkTitle": "Odkaz ke stažení iOS aplikace:",
|
"admin.customization.iosAppDownloadLinkTitle": "Odkaz ke stažení iOS aplikace:",
|
||||||
"admin.customization.maxMarkdownNodesDesc": "Při renderování Markdown textu v mobilní aplikaci, kontrolovat maximální počet Markdown elementů (např. emodži, odkazy, tabulky, apod.) které mohou být v jednom kusu textu. Pokud je nastaveno 0, nebude limit použit.",
|
"admin.customization.maxMarkdownNodesDesc": "Při renderování textu v Markdown formátu v mobilní aplikaci určuje maximální počet prvků Markdownu (např. emoji, odkazy, buňky tabulek atd.), které mohou být v jednom textu. Pokud je nastaveno na 0, použije se výchozí limit.",
|
||||||
"admin.customization.maxMarkdownNodesTitle": "Maximální počet Markdown nodů:",
|
"admin.customization.maxMarkdownNodesTitle": "Maximální počet uzlů Markdownu:",
|
||||||
"admin.customization.restrictLinkPreviewsDesc": "Pro výše uvedený seznam domén oddělených čárkou se nezobrazí náhledy odkazů a náhledy obrázků.",
|
"admin.customization.restrictLinkPreviewsDesc": "Pro výše uvedený seznam domén oddělených čárkou se nezobrazí náhledy odkazů a náhledy obrázků.",
|
||||||
"admin.customization.restrictLinkPreviewsExample": "Např.: \"internal.mycompany.com, images.example.com\"",
|
"admin.customization.restrictLinkPreviewsExample": "Např.: \"internal.mycompany.com, images.example.com\"",
|
||||||
"admin.customization.restrictLinkPreviewsTitle": "Zakázat náhledy odkazů na webové stránky z těchto domén:",
|
"admin.customization.restrictLinkPreviewsTitle": "Zakázat náhledy odkazů na webové stránky z těchto domén:",
|
||||||
"admin.customization.uniqueEmojiReactionLimitPerPost": "Počet reakčních unikátních emodži:",
|
"admin.customization.uniqueEmojiReactionLimitPerPost": "Limit pro jedinečné reakce emoji:",
|
||||||
"admin.customization.uniqueEmojiReactionLimitPerPost.maxValue": "Hodnotu nelze zvýšit nad 500.",
|
"admin.customization.uniqueEmojiReactionLimitPerPost.maxValue": "Nelze zvýšit limit na hodnotu vyšší než 500.",
|
||||||
"admin.customization.uniqueEmojiReactionLimitPerPost.minValue": "Hodnotu nelze nastavit níž, než 0.",
|
"admin.customization.uniqueEmojiReactionLimitPerPost.minValue": "Nelze snížit limit pod 0.",
|
||||||
"admin.customization.uniqueEmojiReactionLimitPerPostDesc": "Počet jedinečných reakcí emodži, které lze přidat k příspěvku. Zvýšení tohoto limitu by mohlo vést ke špatnému výkonu klienta. Maximum je 500.",
|
"admin.customization.uniqueEmojiReactionLimitPerPostDesc": "Počet unikátních emoji reakcí, které mohou být přidány k příspěvku. Zvýšení tohoto limitu může vést k horší výkonnosti klienta. Maximální hodnota je 500.",
|
||||||
"admin.customization.uniqueEmojiReactionLimitPerPostPlaceholder": "Např.: 25",
|
"admin.customization.uniqueEmojiReactionLimitPerPostPlaceholder": "Např.: 25",
|
||||||
"admin.data_grid.empty": "Nebyly nalezeny žádné položky",
|
"admin.data_grid.empty": "Nebyly nalezeny žádné položky",
|
||||||
"admin.data_grid.loading": "Nahrávám",
|
"admin.data_grid.loading": "Nahrávám",
|
||||||
"admin.data_grid.paginatorCount": "{startCount, number} - {endCount, number} z {total, number}",
|
"admin.data_grid.paginatorCount": "{startCount, number} - {endCount, number} z {total, number}",
|
||||||
"admin.data_retention.channel_team_counts": "{team_count} {team_count, plural, one {tým} other {týmy}}, {channel_count} {channel_count, plural, one {kanál} other {kanály}}",
|
"admin.data_retention.channel_team_counts": "{team_count} {team_count, plural, one {tým} other {týmy}}, {channel_count} {channel_count, plural, one {kanál} other {kanály}}",
|
||||||
"admin.data_retention.channel_team_counts_empty": "NEUPLATŇUJE SE",
|
"admin.data_retention.channel_team_counts_empty": "N/A",
|
||||||
"admin.data_retention.createJob.instructions": "Denní čas pro kontrolu zásad a spuštění úlohy mazání:",
|
"admin.data_retention.createJob.instructions": "Denní čas pro kontrolu zásad a spuštění úkolu pro smazání:",
|
||||||
"admin.data_retention.createJob.title": "Spustit úlohu odstranění",
|
"admin.data_retention.createJob.title": "Spustit úkol pro smazání nyní",
|
||||||
"admin.data_retention.customPolicies.addPolicy": "Přidat zásady",
|
"admin.data_retention.customPolicies.addPolicy": "Přidat zásady",
|
||||||
"admin.data_retention.customPolicies.subTitle": "Přizpůsobte si, jak dlouho budou konkrétní týmy a kanály uchovávat zprávy.",
|
"admin.data_retention.customPolicies.subTitle": "Přizpůsobte si, jak dlouho budou konkrétní týmy a kanály uchovávat zprávy.",
|
||||||
"admin.data_retention.customPolicies.title": "Vlastní zásady uchovávání dat",
|
"admin.data_retention.customPolicies.title": "Vlastní zásady uchovávání",
|
||||||
"admin.data_retention.customPoliciesTable.appliedTo": "Aplikováno na",
|
"admin.data_retention.customPoliciesTable.appliedTo": "Použito na",
|
||||||
"admin.data_retention.customPoliciesTable.channelMessages": "Zprávy kanálu",
|
"admin.data_retention.customPoliciesTable.channelMessages": "Zprávy kanálu",
|
||||||
"admin.data_retention.customPoliciesTable.description": "Popis",
|
"admin.data_retention.customPoliciesTable.description": "Popis",
|
||||||
"admin.data_retention.customTitle": "Vlastní zásady uchovávání dat",
|
"admin.data_retention.customTitle": "Vlastní Zásady Uchovávání",
|
||||||
"admin.data_retention.custom_policy.cancel": "Zrušit",
|
"admin.data_retention.custom_policy.cancel": "Zrušit",
|
||||||
"admin.data_retention.custom_policy.channel_selector.addChannels": "Přidání kanálů",
|
"admin.data_retention.custom_policy.channel_selector.addChannels": "Přidat kanály",
|
||||||
"admin.data_retention.custom_policy.channel_selector.subTitle": "Přidejte kanály, které se budou řídit touto zásadou uchovávání.",
|
"admin.data_retention.custom_policy.channel_selector.subTitle": "Přidat kanály, které budou následovat tuto politiku uchovávání dat.",
|
||||||
"admin.data_retention.custom_policy.channel_selector.title": "Přiřazené kanály",
|
"admin.data_retention.custom_policy.channel_selector.title": "Přiřazené kanály",
|
||||||
"admin.data_retention.custom_policy.form.durationInput.error": "Chybné zpracování zprávy o uchování.",
|
"admin.data_retention.custom_policy.form.durationInput.error": "Chyba při analýze uchovávání zpráv.",
|
||||||
"admin.data_retention.custom_policy.form.input": "Název politiky",
|
"admin.data_retention.custom_policy.form.input": "Název politiky",
|
||||||
"admin.data_retention.custom_policy.form.input.error": "Název zásady nemůže být prázdný.",
|
"admin.data_retention.custom_policy.form.input.error": "Název politiky nemůže být prázdný.",
|
||||||
"admin.data_retention.custom_policy.form.subTitle": "Pojmenujte zásady a nakonfigurujte nastavení uchovávání.",
|
"admin.data_retention.custom_policy.form.subTitle": "Dejte své politice název a nastavte pravidla uchovávání dat.",
|
||||||
"admin.data_retention.custom_policy.form.teamsError": "Do zásady je třeba přidat tým nebo kanál.",
|
"admin.data_retention.custom_policy.form.teamsError": "Musíte přidat tým nebo kanál do této politiky.",
|
||||||
"admin.data_retention.custom_policy.form.title": "Jméno a uchovávání",
|
"admin.data_retention.custom_policy.form.title": "Jméno a uchovávání",
|
||||||
"admin.data_retention.custom_policy.save": "Uložit",
|
"admin.data_retention.custom_policy.save": "Uložit",
|
||||||
"admin.data_retention.custom_policy.serverError": "Ve formuláři výše se vyskytly chyby",
|
"admin.data_retention.custom_policy.serverError": "Ve formuláři výše se vyskytly chyby",
|
||||||
"admin.data_retention.custom_policy.team_selector.addTeams": "Přidat týmy",
|
"admin.data_retention.custom_policy.team_selector.addTeams": "Přidat týmy",
|
||||||
"admin.data_retention.custom_policy.team_selector.subTitle": "Přidejte kanály, které se budou řídit touto zásadou uchovávání.",
|
"admin.data_retention.custom_policy.team_selector.subTitle": "Přidejte kanály, které se budou řídit touto zásadou uchovávání.",
|
||||||
"admin.data_retention.custom_policy.team_selector.title": "Přidělené týmy",
|
"admin.data_retention.custom_policy.team_selector.title": "Přiřazené týmy",
|
||||||
"admin.data_retention.custom_policy.teams.remove": "Odstranit",
|
"admin.data_retention.custom_policy.teams.remove": "Odstranit",
|
||||||
"admin.data_retention.form.channelAndDirectMessageRetention": "Udržování kanálů a přímých zpráv",
|
"admin.data_retention.form.channelAndDirectMessageRetention": "Retence kanálů a přímých zpráv",
|
||||||
"admin.data_retention.form.days": "Dny",
|
"admin.data_retention.form.days": "Dny",
|
||||||
"admin.data_retention.form.fileRetention": "Uchovávání souborů",
|
"admin.data_retention.form.fileRetention": "Uchovávání souborů",
|
||||||
"admin.data_retention.form.hours": "Hodiny",
|
"admin.data_retention.form.hours": "Hodiny",
|
||||||
@ -750,7 +751,7 @@
|
|||||||
"admin.data_retention.retention_years": "{count} {count, plural, one {rok} other {roky}}",
|
"admin.data_retention.retention_years": "{count} {count, plural, one {rok} other {roky}}",
|
||||||
"admin.data_retention.settings.title": "Zásady uchovávání údajů",
|
"admin.data_retention.settings.title": "Zásady uchovávání údajů",
|
||||||
"admin.data_retention.title": "Zásady uchovávání údajů",
|
"admin.data_retention.title": "Zásady uchovávání údajů",
|
||||||
"admin.data_retention_feature_discovery.copy": "Data uchovávejte jen tak dlouho, jak potřebujete. Vytvořte úlohy pro uchovávání dat pro vybrané kanály a týmy, abyste automaticky odstranili jednorázová data.",
|
"admin.data_retention_feature_discovery.copy": "Uchovávejte svá data pouze tak dlouho, jak je potřebujete. Vytvořte úkoly pro uchovávání dat pro vybrané kanály a týmy, které automaticky odstraní nepotřebná data.",
|
||||||
"admin.data_retention_feature_discovery.title": "Vytváření plánů uchovávání dat pomocí Mattermost Enterprise",
|
"admin.data_retention_feature_discovery.title": "Vytváření plánů uchovávání dat pomocí Mattermost Enterprise",
|
||||||
"admin.database.migrations_table.help_text": "Seznam migrací schémat použitých na váš datový sklad.",
|
"admin.database.migrations_table.help_text": "Seznam migrací schémat použitých na váš datový sklad.",
|
||||||
"admin.database.migrations_table.name": "Jméno",
|
"admin.database.migrations_table.name": "Jméno",
|
||||||
@ -1494,6 +1495,7 @@
|
|||||||
"admin.logs.showErrors": "Zobrazit posledních {n} chyb",
|
"admin.logs.showErrors": "Zobrazit posledních {n} chyb",
|
||||||
"admin.logs.title": "Serverové záznamy",
|
"admin.logs.title": "Serverové záznamy",
|
||||||
"admin.manage_roles.additionalRoles": "Vyberte další oprávnění pro účet. <link>Dozvědět se více o rolích a oprávněních</link>.",
|
"admin.manage_roles.additionalRoles": "Vyberte další oprávnění pro účet. <link>Dozvědět se více o rolích a oprávněních</link>.",
|
||||||
|
"admin.manage_roles.additionalRoles_warning": "<strong>Poznámka: </strong><span>Výše udělená oprávnění platí pro celý účet, bez ohledu na to, zda je ověřen pomocí relace cookie nebo osobního přístupového tokenu. Například výběr post:all umožní účtu přispívat do kanálů, jejichž není členem, i bez použití osobního přístupového tokenu.</span>",
|
||||||
"admin.manage_roles.allowUserAccessTokens": "Povolit tomuto účtu generovat <link>osobní přístupové tokeny</link>.",
|
"admin.manage_roles.allowUserAccessTokens": "Povolit tomuto účtu generovat <link>osobní přístupové tokeny</link>.",
|
||||||
"admin.manage_roles.allowUserAccessTokensDesc": "Zrušení tohoto oprávnění neodstraní existující tokeny. K jejich odstranění musíte přejít na stránku Správa tokenů uživatele.",
|
"admin.manage_roles.allowUserAccessTokensDesc": "Zrušení tohoto oprávnění neodstraní existující tokeny. K jejich odstranění musíte přejít na stránku Správa tokenů uživatele.",
|
||||||
"admin.manage_roles.botAdditionalRoles": "Vyberte další oprávnění pro účet. <link>Dozvědět se více o rolích a oprávněních</link>.",
|
"admin.manage_roles.botAdditionalRoles": "Vyberte další oprávnění pro účet. <link>Dozvědět se více o rolích a oprávněních</link>.",
|
||||||
@ -1594,8 +1596,10 @@
|
|||||||
"admin.password.preview": "Náhled chybové zprávy",
|
"admin.password.preview": "Náhled chybové zprávy",
|
||||||
"admin.password.symbol": "Alespoň jeden symbol (např. \"~!@#$%^&*()\")",
|
"admin.password.symbol": "Alespoň jeden symbol (např. \"~!@#$%^&*()\")",
|
||||||
"admin.password.uppercase": "Alespoň jedno velké písmeno",
|
"admin.password.uppercase": "Alespoň jedno velké písmeno",
|
||||||
|
"admin.permissions.group.convert_private_channel_to_public.description": "Převést soukromé kanály na veřejné",
|
||||||
|
"admin.permissions.group.convert_private_channel_to_public.name": "Převést na veřejné",
|
||||||
"admin.permissions.group.convert_public_channel_to_private.description": "Převést veřejné kanály na privátní",
|
"admin.permissions.group.convert_public_channel_to_private.description": "Převést veřejné kanály na privátní",
|
||||||
"admin.permissions.group.convert_public_channel_to_private.name": "Převést kanály",
|
"admin.permissions.group.convert_public_channel_to_private.name": "Převést na soukromé",
|
||||||
"admin.permissions.group.custom_groups.description": "Vytvoření, úprava, odstranění a správa členů vlastních skupin.",
|
"admin.permissions.group.custom_groups.description": "Vytvoření, úprava, odstranění a správa členů vlastních skupin.",
|
||||||
"admin.permissions.group.custom_groups.name": "Vlastní skupiny",
|
"admin.permissions.group.custom_groups.name": "Vlastní skupiny",
|
||||||
"admin.permissions.group.delete_posts.description": "Smazat příspěvky vlastní a ostatních.",
|
"admin.permissions.group.delete_posts.description": "Smazat příspěvky vlastní a ostatních.",
|
||||||
@ -1655,9 +1659,9 @@
|
|||||||
"admin.permissions.permission.assign_system_admin_role.description": "Přiřadit roli správce systému",
|
"admin.permissions.permission.assign_system_admin_role.description": "Přiřadit roli správce systému",
|
||||||
"admin.permissions.permission.assign_system_admin_role.name": "Přiřadit roli správce systému",
|
"admin.permissions.permission.assign_system_admin_role.name": "Přiřadit roli správce systému",
|
||||||
"admin.permissions.permission.convert_private_channel_to_public.description": "Převést soukromé kanály na veřejné",
|
"admin.permissions.permission.convert_private_channel_to_public.description": "Převést soukromé kanály na veřejné",
|
||||||
"admin.permissions.permission.convert_private_channel_to_public.name": "Převést kanály",
|
"admin.permissions.permission.convert_private_channel_to_public.name": "Převést na veřejné",
|
||||||
"admin.permissions.permission.convert_public_channel_to_private.description": "Převést veřejné kanály na soukromé",
|
"admin.permissions.permission.convert_public_channel_to_private.description": "Převést veřejné kanály na soukromé",
|
||||||
"admin.permissions.permission.convert_public_channel_to_private.name": "Převést kanály",
|
"admin.permissions.permission.convert_public_channel_to_private.name": "Převést na soukromé",
|
||||||
"admin.permissions.permission.create_custom_group.description": "Vytvořit vlastní skupiny.",
|
"admin.permissions.permission.create_custom_group.description": "Vytvořit vlastní skupiny.",
|
||||||
"admin.permissions.permission.create_custom_group.name": "Vytvořit",
|
"admin.permissions.permission.create_custom_group.name": "Vytvořit",
|
||||||
"admin.permissions.permission.create_direct_channel.description": "Vytvořit přímou zprávu",
|
"admin.permissions.permission.create_direct_channel.description": "Vytvořit přímou zprávu",
|
||||||
@ -1883,7 +1887,8 @@
|
|||||||
"admin.permissions.sysconsole_section_user_management_channels.name": "Kanály",
|
"admin.permissions.sysconsole_section_user_management_channels.name": "Kanály",
|
||||||
"admin.permissions.sysconsole_section_user_management_groups.name": "Skupiny",
|
"admin.permissions.sysconsole_section_user_management_groups.name": "Skupiny",
|
||||||
"admin.permissions.sysconsole_section_user_management_permissions.name": "Oprávnění",
|
"admin.permissions.sysconsole_section_user_management_permissions.name": "Oprávnění",
|
||||||
"admin.permissions.sysconsole_section_user_management_system_roles.name": "Systémové role",
|
"admin.permissions.sysconsole_section_user_management_system_roles.description": "Nastavení „Žádný přístup“ omezuje pouze rozhraní Systémové konzole. Základní API koncové body zůstávají přístupné všem uživatelům v režimu pouze pro čtení pro základní funkčnost produktu.",
|
||||||
|
"admin.permissions.sysconsole_section_user_management_system_roles.name": "Delegovaná granulární správa",
|
||||||
"admin.permissions.sysconsole_section_user_management_teams.name": "Týmy",
|
"admin.permissions.sysconsole_section_user_management_teams.name": "Týmy",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.description": "Nemohu resetovat hesla adminům",
|
"admin.permissions.sysconsole_section_user_management_users.description": "Nemohu resetovat hesla adminům",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.name": "Uživatelé",
|
"admin.permissions.sysconsole_section_user_management_users.name": "Uživatelé",
|
||||||
@ -2486,6 +2491,7 @@
|
|||||||
"admin.sidebar.smtp": "SMTP",
|
"admin.sidebar.smtp": "SMTP",
|
||||||
"admin.sidebar.subscription": "Předplatné",
|
"admin.sidebar.subscription": "Předplatné",
|
||||||
"admin.sidebar.systemRoles": "Delegovaná podrobná administrace",
|
"admin.sidebar.systemRoles": "Delegovaná podrobná administrace",
|
||||||
|
"admin.sidebar.system_properties": "Systémové vlastnosti",
|
||||||
"admin.sidebar.teamStatistics": "Statistiky týmu",
|
"admin.sidebar.teamStatistics": "Statistiky týmu",
|
||||||
"admin.sidebar.teams": "Týmy",
|
"admin.sidebar.teams": "Týmy",
|
||||||
"admin.sidebar.userManagement": "Správa uživatelů",
|
"admin.sidebar.userManagement": "Správa uživatelů",
|
||||||
@ -2562,6 +2568,23 @@
|
|||||||
"admin.systemUserDetail.teamList.teamType.groupSync": "Synchronizace skupiny",
|
"admin.systemUserDetail.teamList.teamType.groupSync": "Synchronizace skupiny",
|
||||||
"admin.systemUserDetail.teamList.teamType.inviteOnly": "Pouze na pozvání",
|
"admin.systemUserDetail.teamList.teamType.inviteOnly": "Pouze na pozvání",
|
||||||
"admin.systemUserDetail.title": "Nastavení uživatele",
|
"admin.systemUserDetail.title": "Nastavení uživatele",
|
||||||
|
"admin.system_properties.confirm.delete.button": "Smazat",
|
||||||
|
"admin.system_properties.confirm.delete.text": "Smazání této vlastnosti odstraní všechny hodnoty definované uživatelem, které jsou s ní spojeny.",
|
||||||
|
"admin.system_properties.confirm.delete.title": "Smazat vlastnost {name}",
|
||||||
|
"admin.system_properties.details.saving_changes": "Ukládám nastavení…",
|
||||||
|
"admin.system_properties.details.saving_changes_error": "Při ukládání nastavení se vyskytla chyba",
|
||||||
|
"admin.system_properties.user_properties.add_property": "Přidat vlastnost",
|
||||||
|
"admin.system_properties.user_properties.subtitle": "Přizpůsobte vlastnosti, které se zobrazí v uživatelských profilech",
|
||||||
|
"admin.system_properties.user_properties.table.actions": "Úkony",
|
||||||
|
"admin.system_properties.user_properties.table.actions.delete": "Smazat",
|
||||||
|
"admin.system_properties.user_properties.table.property": "Vlastnost",
|
||||||
|
"admin.system_properties.user_properties.table.property_name.input.name": "Název vlastnosti",
|
||||||
|
"admin.system_properties.user_properties.table.type": "Typ",
|
||||||
|
"admin.system_properties.user_properties.table.type.text": "Text",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_required": "Prosím, zadejte název vlastnosti.",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_taken": "Název vlastnosti již existuje.",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_unique": "Název vlastnosti musí být jedinečný.",
|
||||||
|
"admin.system_properties.user_properties.title": "Vlastnosti uživatele",
|
||||||
"admin.system_roles_feature_discovery.copy": "Přiřaďte přizpůsobitelné administrátorské role, abyste vybraným uživatelům poskytli přístup pro čtení/ zápis k vybraným sekcím systémové konzole.",
|
"admin.system_roles_feature_discovery.copy": "Přiřaďte přizpůsobitelné administrátorské role, abyste vybraným uživatelům poskytli přístup pro čtení/ zápis k vybraným sekcím systémové konzole.",
|
||||||
"admin.system_roles_feature_discovery.title": "Poskytněte kontrolovaný přístup do konzoly systému s Mattermost Enterprise",
|
"admin.system_roles_feature_discovery.title": "Poskytněte kontrolovaný přístup do konzoly systému s Mattermost Enterprise",
|
||||||
"admin.system_users.column_toggler.dropdownAriaLabel": "Nabídka viditelnosti sloupců",
|
"admin.system_users.column_toggler.dropdownAriaLabel": "Nabídka viditelnosti sloupců",
|
||||||
@ -2885,13 +2908,10 @@
|
|||||||
"analytics.system.publicChannels": "Veřejné kanály",
|
"analytics.system.publicChannels": "Veřejné kanály",
|
||||||
"analytics.system.seatsPurchased": "Licencovaná místa",
|
"analytics.system.seatsPurchased": "Licencovaná místa",
|
||||||
"analytics.system.skippedIntensiveQueries": "Pro maximalizaci výkonu jsou některé statistiky vypnuty. Můžete <link>znovu povolit v konfiguračním souboru config.json</link>.",
|
"analytics.system.skippedIntensiveQueries": "Pro maximalizaci výkonu jsou některé statistiky vypnuty. Můžete <link>znovu povolit v konfiguračním souboru config.json</link>.",
|
||||||
"analytics.system.textPosts": "Pouze textové příspěvky",
|
|
||||||
"analytics.system.title": "Statistiky systemu",
|
"analytics.system.title": "Statistiky systemu",
|
||||||
"analytics.system.totalBotPosts": "Množstvý zpráv od botů",
|
"analytics.system.totalBotPosts": "Množstvý zpráv od botů",
|
||||||
"analytics.system.totalChannels": "Celkový počet kanálů",
|
"analytics.system.totalChannels": "Celkový počet kanálů",
|
||||||
"analytics.system.totalCommands": "Celkový počet příkazů",
|
"analytics.system.totalCommands": "Celkový počet příkazů",
|
||||||
"analytics.system.totalFilePosts": "Příspěvky se soubory",
|
|
||||||
"analytics.system.totalHashtagPosts": "Zprávy s hashtagamy",
|
|
||||||
"analytics.system.totalIncomingWebhooks": "Příchozí webhooky",
|
"analytics.system.totalIncomingWebhooks": "Příchozí webhooky",
|
||||||
"analytics.system.totalMasterDbConnections": "Celkový počet připojení k hlavní databázi",
|
"analytics.system.totalMasterDbConnections": "Celkový počet připojení k hlavní databázi",
|
||||||
"analytics.system.totalOutgoingWebhooks": "Odchozí webové háčky",
|
"analytics.system.totalOutgoingWebhooks": "Odchozí webové háčky",
|
||||||
@ -3211,9 +3231,11 @@
|
|||||||
"channel_header.closeChannelInfo": "Skrýt informace",
|
"channel_header.closeChannelInfo": "Skrýt informace",
|
||||||
"channel_header.convert": "Převést na soukromý kanál",
|
"channel_header.convert": "Převést na soukromý kanál",
|
||||||
"channel_header.delete": "Archivovat kanál",
|
"channel_header.delete": "Archivovat kanál",
|
||||||
|
"channel_header.directchannel": "{displayName} (vy) Menu kanálu",
|
||||||
"channel_header.directchannel.you": "{displayname} (já) ",
|
"channel_header.directchannel.you": "{displayname} (já) ",
|
||||||
"channel_header.flagged": "Uložené zprávy",
|
"channel_header.flagged": "Uložené zprávy",
|
||||||
"channel_header.groupMessageHasGuests": "Tato skupinová zpráva má návštěvníky",
|
"channel_header.groupMessageHasGuests": "Tato skupinová zpráva má návštěvníky",
|
||||||
|
"channel_header.headerText.addNewButton": "Přidat záhlaví kanálu",
|
||||||
"channel_header.lastActive": "Aktivní {timestamp}",
|
"channel_header.lastActive": "Aktivní {timestamp}",
|
||||||
"channel_header.lastOnline": "Naposledy online {timestamp}",
|
"channel_header.lastOnline": "Naposledy online {timestamp}",
|
||||||
"channel_header.leave": "Opustit kanál",
|
"channel_header.leave": "Opustit kanál",
|
||||||
@ -3222,6 +3244,7 @@
|
|||||||
"channel_header.mute": "Ztlumit kanál",
|
"channel_header.mute": "Ztlumit kanál",
|
||||||
"channel_header.muteConversation": "Ztlumit konverzaci",
|
"channel_header.muteConversation": "Ztlumit konverzaci",
|
||||||
"channel_header.openChannelInfo": "Zobrazit info",
|
"channel_header.openChannelInfo": "Zobrazit info",
|
||||||
|
"channel_header.otherchannel": "{displayName} Menu kanálu",
|
||||||
"channel_header.pinnedPosts": "Připnuté zprávy",
|
"channel_header.pinnedPosts": "Připnuté zprávy",
|
||||||
"channel_header.recentMentions": "Nedávné zmínky",
|
"channel_header.recentMentions": "Nedávné zmínky",
|
||||||
"channel_header.rename": "Přejmenovat kanál",
|
"channel_header.rename": "Přejmenovat kanál",
|
||||||
@ -3259,6 +3282,7 @@
|
|||||||
"channel_info_rhs.menu.members": "Členové",
|
"channel_info_rhs.menu.members": "Členové",
|
||||||
"channel_info_rhs.menu.notification_preferences": "Předvolby oznámení",
|
"channel_info_rhs.menu.notification_preferences": "Předvolby oznámení",
|
||||||
"channel_info_rhs.menu.pinned": "Připnuté zprávy",
|
"channel_info_rhs.menu.pinned": "Připnuté zprávy",
|
||||||
|
"channel_info_rhs.menu.title": "Akce informací o kanálu",
|
||||||
"channel_info_rhs.top_buttons.add_people": "Přidat lidi",
|
"channel_info_rhs.top_buttons.add_people": "Přidat lidi",
|
||||||
"channel_info_rhs.top_buttons.add_people.tooltip": "Přidat člena týmu do tohoto kanálu",
|
"channel_info_rhs.top_buttons.add_people.tooltip": "Přidat člena týmu do tohoto kanálu",
|
||||||
"channel_info_rhs.top_buttons.copied": "Zkopírováno",
|
"channel_info_rhs.top_buttons.copied": "Zkopírováno",
|
||||||
@ -3683,6 +3707,8 @@
|
|||||||
"email_verify.return": "Vraťte se k přihlášení",
|
"email_verify.return": "Vraťte se k přihlášení",
|
||||||
"email_verify.sending": "Odesílání e-mailu…",
|
"email_verify.sending": "Odesílání e-mailu…",
|
||||||
"email_verify.sent": "Ověřovací e-mail odeslán",
|
"email_verify.sent": "Ověřovací e-mail odeslán",
|
||||||
|
"emoji_gif_picker.dialog.emojis": "Výběr emoji",
|
||||||
|
"emoji_gif_picker.dialog.gifs": "Výběr GIF",
|
||||||
"emoji_gif_picker.tabs.emojis": "Emodži",
|
"emoji_gif_picker.tabs.emojis": "Emodži",
|
||||||
"emoji_gif_picker.tabs.gifs": "GIF obrázky",
|
"emoji_gif_picker.tabs.gifs": "GIF obrázky",
|
||||||
"emoji_list.actions": "Akce",
|
"emoji_list.actions": "Akce",
|
||||||
@ -4329,6 +4355,7 @@
|
|||||||
"mobile.set_status.dnd.icon": "Ikona nerušit",
|
"mobile.set_status.dnd.icon": "Ikona nerušit",
|
||||||
"mobile.set_status.offline.icon": "Offline ikona",
|
"mobile.set_status.offline.icon": "Offline ikona",
|
||||||
"mobile.set_status.online.icon": "Ikona připojen",
|
"mobile.set_status.online.icon": "Ikona připojen",
|
||||||
|
"modal.header_close": "Zavřít",
|
||||||
"modal.manual_status.ask": "Příště se neptat",
|
"modal.manual_status.ask": "Příště se neptat",
|
||||||
"modal.manual_status.auto_responder.message_away": "Chceš nastavit svůj status na \"Pryč\" a zrušit automatické odpovědi?",
|
"modal.manual_status.auto_responder.message_away": "Chceš nastavit svůj status na \"Pryč\" a zrušit automatické odpovědi?",
|
||||||
"modal.manual_status.auto_responder.message_dnd": "Chceš nastavit svůj status na \"Nerušit\" a vypnout automatické odpovědi?",
|
"modal.manual_status.auto_responder.message_dnd": "Chceš nastavit svůj status na \"Nerušit\" a vypnout automatické odpovědi?",
|
||||||
@ -4377,6 +4404,7 @@
|
|||||||
"more_channels.view": "Zobrazit",
|
"more_channels.view": "Zobrazit",
|
||||||
"more_direct_channels.directchannel.deactivated": "{displayname} – Deaktivováno",
|
"more_direct_channels.directchannel.deactivated": "{displayname} – Deaktivováno",
|
||||||
"more_direct_channels.directchannel.you": "{displayname} (Vy)",
|
"more_direct_channels.directchannel.you": "{displayname} (Vy)",
|
||||||
|
"more_direct_channels.new_convo_add.label": "Přidat možnost {label}",
|
||||||
"more_direct_channels.new_convo_note": "Tím zahájíte novou konverzaci. Pokud přidáváte hodně lidí, zvažte vytvoření soukromého kanálu.",
|
"more_direct_channels.new_convo_note": "Tím zahájíte novou konverzaci. Pokud přidáváte hodně lidí, zvažte vytvoření soukromého kanálu.",
|
||||||
"more_direct_channels.new_convo_note.full": "Dosáhli jste maximálního počtu lidí pro tuto konverzaci. Zvažte místo toho vytvoření soukromého kanálu.",
|
"more_direct_channels.new_convo_note.full": "Dosáhli jste maximálního počtu lidí pro tuto konverzaci. Zvažte místo toho vytvoření soukromého kanálu.",
|
||||||
"more_direct_channels.title": "Přímé zprávy",
|
"more_direct_channels.title": "Přímé zprávy",
|
||||||
@ -4516,6 +4544,7 @@
|
|||||||
"onboardingTask.checklist.main_subtitle": "Pořádně se pusťme do toho.",
|
"onboardingTask.checklist.main_subtitle": "Pořádně se pusťme do toho.",
|
||||||
"onboardingTask.checklist.no_thanks": "Ne, děkuji",
|
"onboardingTask.checklist.no_thanks": "Ne, děkuji",
|
||||||
"onboardingTask.checklist.start_enterprise_now": "Vyzkoušejte si Enterprise edici zdarma!",
|
"onboardingTask.checklist.start_enterprise_now": "Vyzkoušejte si Enterprise edici zdarma!",
|
||||||
|
"onboardingTask.checklist.start_onboarding_process": "Zahájit proces onboardingu.",
|
||||||
"onboardingTask.checklist.task_complete_your_profile": "Vyplňte svůj profil.",
|
"onboardingTask.checklist.task_complete_your_profile": "Vyplňte svůj profil.",
|
||||||
"onboardingTask.checklist.task_download_mm_apps": "Stáhněte si desktopovou a mobilní aplikaci.",
|
"onboardingTask.checklist.task_download_mm_apps": "Stáhněte si desktopovou a mobilní aplikaci.",
|
||||||
"onboardingTask.checklist.task_invite_team_members": "Pozvěte členy týmu do pracovního prostoru.",
|
"onboardingTask.checklist.task_invite_team_members": "Pozvěte členy týmu do pracovního prostoru.",
|
||||||
@ -4628,6 +4657,9 @@
|
|||||||
"post.ariaLabel.replyMessage": "{authorName} odpověděl(a), {message}, dne {date} v {time}",
|
"post.ariaLabel.replyMessage": "{authorName} odpověděl(a), {message}, dne {date} v {time}",
|
||||||
"post.reminder.acknowledgement": "V {reminderTime}, {reminderDate} vám bude připomenuta tato zpráva od uživatele {username}: {permaLink}",
|
"post.reminder.acknowledgement": "V {reminderTime}, {reminderDate} vám bude připomenuta tato zpráva od uživatele {username}: {permaLink}",
|
||||||
"post.reminder.systemBot": "Dobrý den, zde je vaše připomenutí ohledně této zprávy od uživatele {username}: {permaLink}",
|
"post.reminder.systemBot": "Dobrý den, zde je vaše připomenutí ohledně této zprávy od uživatele {username}: {permaLink}",
|
||||||
|
"post.renderError.message": "Došlo k chybě při vykreslování tohoto příspěvku.",
|
||||||
|
"post.renderError.retry": "Opakovat",
|
||||||
|
"post.renderError.retryLabel": "Opakovat vykreslení tohoto příspěvku",
|
||||||
"post_body.check_for_out_of_channel_groups_mentions.message": "nebyli notifikováni tímto zmiňováním, protože nejsou v tomto kanálu. Nemohou být přidáni do kanálu, protože nejsou členy propojených skupin. Pro přidání jich do tohoto kanálu musí být přidáni do jedné z propojených skupin.",
|
"post_body.check_for_out_of_channel_groups_mentions.message": "nebyli notifikováni tímto zmiňováním, protože nejsou v tomto kanálu. Nemohou být přidáni do kanálu, protože nejsou členy propojených skupin. Pro přidání jich do tohoto kanálu musí být přidáni do jedné z propojených skupin.",
|
||||||
"post_body.check_for_out_of_channel_mentions.link.and": " a ",
|
"post_body.check_for_out_of_channel_mentions.link.and": " a ",
|
||||||
"post_body.check_for_out_of_channel_mentions.link.private": "přidejte je k tomuto soukromému kanálu",
|
"post_body.check_for_out_of_channel_mentions.link.private": "přidejte je k tomuto soukromému kanálu",
|
||||||
@ -4784,6 +4816,8 @@
|
|||||||
"pricing_modal.title": "Vybrat plán",
|
"pricing_modal.title": "Vybrat plán",
|
||||||
"pricing_modal.wantToTry": "Chcete zkusit? ",
|
"pricing_modal.wantToTry": "Chcete zkusit? ",
|
||||||
"pricing_modal.wantToUpgrade": "Chcete upgradovat? ",
|
"pricing_modal.wantToUpgrade": "Chcete upgradovat? ",
|
||||||
|
"profile_popover.aria_label.with_username": "Vyskakovací okno profilu {userName}",
|
||||||
|
"profile_popover.aria_label.without_username": "vyskakovací okno profilu",
|
||||||
"promote_to_user_modal.desc": "Tato akce povýší hosta {username} na člena. To umožní uživateli připojit se k veřejným kanálům a komunikovat s uživateli mimo kanály, kterých je aktuálně členem. Jste si jisti, že chcete povýšit hosta {username} na člena?",
|
"promote_to_user_modal.desc": "Tato akce povýší hosta {username} na člena. To umožní uživateli připojit se k veřejným kanálům a komunikovat s uživateli mimo kanály, kterých je aktuálně členem. Jste si jisti, že chcete povýšit hosta {username} na člena?",
|
||||||
"promote_to_user_modal.promote": "Povýšit",
|
"promote_to_user_modal.promote": "Povýšit",
|
||||||
"promote_to_user_modal.title": "Povýšit hosta {username} na člena",
|
"promote_to_user_modal.title": "Povýšit hosta {username} na člena",
|
||||||
@ -4937,6 +4971,7 @@
|
|||||||
"search_hint.enter_to_search": "Stiskněte Enter pro vyhledávání",
|
"search_hint.enter_to_search": "Stiskněte Enter pro vyhledávání",
|
||||||
"search_hint.enter_to_select": "Stiskněte Enter pro výběr",
|
"search_hint.enter_to_select": "Stiskněte Enter pro výběr",
|
||||||
"search_hint.filter": "Filtrovat hledání pomocí:",
|
"search_hint.filter": "Filtrovat hledání pomocí:",
|
||||||
|
"search_hint.reset_filters": "Filtry byly nastaveny na výchozí hodnoty, protože jste vybrali jiný tým",
|
||||||
"search_item.channelArchived": "Archivováno",
|
"search_item.channelArchived": "Archivováno",
|
||||||
"search_item.direct": "Přímá zpráva (od {username})",
|
"search_item.direct": "Přímá zpráva (od {username})",
|
||||||
"search_item.file_tag.direct_message": "Přímá zpráva",
|
"search_item.file_tag.direct_message": "Přímá zpráva",
|
||||||
@ -4952,6 +4987,9 @@
|
|||||||
"search_list_option.on": "Příspěvky v datu",
|
"search_list_option.on": "Příspěvky v datu",
|
||||||
"search_list_option.phrases": "Příspěvky s frázemi",
|
"search_list_option.phrases": "Příspěvky s frázemi",
|
||||||
"search_results.channel-files-header": "Nedávné soubory",
|
"search_results.channel-files-header": "Nedávné soubory",
|
||||||
|
"search_teams_selector.all_teams": "Všechny Týmy",
|
||||||
|
"search_teams_selector.search_teams": "Vyhledat týmy",
|
||||||
|
"search_teams_selector.your_teams": "Vaše týmy",
|
||||||
"sectionNotice.dismiss": "Skrýt oznámení",
|
"sectionNotice.dismiss": "Skrýt oznámení",
|
||||||
"select_team.icon": "Ikona Výběr týmu",
|
"select_team.icon": "Ikona Výběr týmu",
|
||||||
"select_team.join.icon": "Ikona Připojit se k týmu",
|
"select_team.join.icon": "Ikona Připojit se k týmu",
|
||||||
@ -5120,6 +5158,14 @@
|
|||||||
"sidebar.types.favorites": "OBLÍBENÉ",
|
"sidebar.types.favorites": "OBLÍBENÉ",
|
||||||
"sidebar.types.unreads": "NEPŘEČTENÉ",
|
"sidebar.types.unreads": "NEPŘEČTENÉ",
|
||||||
"sidebar.unreads": "Více nepřečtených",
|
"sidebar.unreads": "Více nepřečtených",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.browseChannelsMenuItem.primaryLabel": "Procházet Kanály",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createCategoryMenuItem.primaryLabel": "Vytvořit Novou Kategorii",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createNewChannelMenuItem.primaryLabel": "Vytvořit Nový Kanál",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createUserGroupMenuItem.primaryLabel": "Vytvořit novou skupinu uživatelů",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.invitePeopleMenuItem.primaryLabel": "Pozvat Lidi",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.invitePeopleMenuItem.secondaryLabel": "Přidat lidi do týmu",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.openDirectMessageMenuItem.primaryLabel": "Otevřít přímou zprávu",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenuButton.label": "Procházet nebo vytvořit kanály",
|
||||||
"sidebar_left.addChannelsCta": "Přidat kanály",
|
"sidebar_left.addChannelsCta": "Přidat kanály",
|
||||||
"sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Rozbalovací nabídka Přidat kanál",
|
"sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Rozbalovací nabídka Přidat kanál",
|
||||||
"sidebar_left.add_channel_dropdown.browseChannels": "Prohlížet kanály",
|
"sidebar_left.add_channel_dropdown.browseChannels": "Prohlížet kanály",
|
||||||
@ -5535,6 +5581,7 @@
|
|||||||
"user.settings.display.theme.title": "Téma",
|
"user.settings.display.theme.title": "Téma",
|
||||||
"user.settings.display.timezone": "Časové pásmo",
|
"user.settings.display.timezone": "Časové pásmo",
|
||||||
"user.settings.display.title": "Nastavení zobrazení",
|
"user.settings.display.title": "Nastavení zobrazení",
|
||||||
|
"user.settings.general.attributeExtra": "Toto se zobrazí v náhledu vašeho profilu.",
|
||||||
"user.settings.general.close": "Zavřít",
|
"user.settings.general.close": "Zavřít",
|
||||||
"user.settings.general.confirmEmail": "Potvrdit e-mail",
|
"user.settings.general.confirmEmail": "Potvrdit e-mail",
|
||||||
"user.settings.general.currentEmail": "Aktuální e-mail",
|
"user.settings.general.currentEmail": "Aktuální e-mail",
|
||||||
@ -5549,6 +5596,7 @@
|
|||||||
"user.settings.general.emailOffice365CantUpdate": "Přihlášení probíhá prostřednictvím Entra ID. E-mail nelze změnit. E-mailová adresa používaná pro upozornění je {email}.",
|
"user.settings.general.emailOffice365CantUpdate": "Přihlášení probíhá prostřednictvím Entra ID. E-mail nelze změnit. E-mailová adresa používaná pro upozornění je {email}.",
|
||||||
"user.settings.general.emailOpenIdCantUpdate": "Přihlášení se děje přes OpenID Connect. Email nelze změnit. Emailová adresa použita pro notifikace je {email}.",
|
"user.settings.general.emailOpenIdCantUpdate": "Přihlášení se děje přes OpenID Connect. Email nelze změnit. Emailová adresa použita pro notifikace je {email}.",
|
||||||
"user.settings.general.emailSamlCantUpdate": "Přihlášení probíhá prostřednictvím služby GitLab. E-mail nelze zmněnit. E-mailová adresa používaná pro upozornění je {email}.",
|
"user.settings.general.emailSamlCantUpdate": "Přihlášení probíhá prostřednictvím služby GitLab. E-mail nelze zmněnit. E-mailová adresa používaná pro upozornění je {email}.",
|
||||||
|
"user.settings.general.emptyAttribute": "Klikněte na 'Upravit' a přidejte svůj vlastní atribut",
|
||||||
"user.settings.general.emptyName": "Klikněte pro přidání svého celého jména",
|
"user.settings.general.emptyName": "Klikněte pro přidání svého celého jména",
|
||||||
"user.settings.general.emptyNickname": "Klepněte na tlačítko \"Upravit\" pro přidání přezdívky",
|
"user.settings.general.emptyNickname": "Klepněte na tlačítko \"Upravit\" pro přidání přezdívky",
|
||||||
"user.settings.general.emptyPassword": "Prosím zadejte své aktuální heslo.",
|
"user.settings.general.emptyPassword": "Prosím zadejte své aktuální heslo.",
|
||||||
@ -5565,6 +5613,7 @@
|
|||||||
"user.settings.general.loginLdap": "Přihlášení se provádí prostřednictvím AD/LDAP ({email})",
|
"user.settings.general.loginLdap": "Přihlášení se provádí prostřednictvím AD/LDAP ({email})",
|
||||||
"user.settings.general.loginOffice365": "Přihlášeno přes Entra ID ({email})",
|
"user.settings.general.loginOffice365": "Přihlášeno přes Entra ID ({email})",
|
||||||
"user.settings.general.loginSaml": "Přihlášení se provádí prostřednictvím Office 365 ({email})",
|
"user.settings.general.loginSaml": "Přihlášení se provádí prostřednictvím Office 365 ({email})",
|
||||||
|
"user.settings.general.mobile.emptyAttribute": "Klikněte pro přidání vlastního atributu",
|
||||||
"user.settings.general.mobile.emptyName": "Klikněte pro přidání svého celého jména",
|
"user.settings.general.mobile.emptyName": "Klikněte pro přidání svého celého jména",
|
||||||
"user.settings.general.mobile.emptyNickname": "Klepněte pro přidání přezdívky",
|
"user.settings.general.mobile.emptyNickname": "Klepněte pro přidání přezdívky",
|
||||||
"user.settings.general.mobile.emptyPosition": "Klikněte pro přidání svého pracovního titulu / pozice",
|
"user.settings.general.mobile.emptyPosition": "Klikněte pro přidání svého pracovního titulu / pozice",
|
||||||
@ -5824,6 +5873,39 @@
|
|||||||
"user.settings.tokens.tokenId": "ID tokenu: ",
|
"user.settings.tokens.tokenId": "ID tokenu: ",
|
||||||
"user.settings.tokens.tokenLoading": "Nahrávám...",
|
"user.settings.tokens.tokenLoading": "Nahrávám...",
|
||||||
"user.settings.tokens.userAccessTokensNone": "Žádné osobní přístupové tokeny.",
|
"user.settings.tokens.userAccessTokensNone": "Žádné osobní přístupové tokeny.",
|
||||||
|
"userAccountMenu.awayMenuItem.label": "Pryč",
|
||||||
|
"userAccountMenu.dndMenuItem.primaryLabel": "Režim nerušit",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel": "Vypnout všechna upozornění",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.doNotClear": "Do nekonečna",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilLaterSomeTime": "Do {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTodaySomeTime": "Do {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTomorrowSomeTime": "Do zítra {time}",
|
||||||
|
"userAccountMenu.dndSubMenu.title": "Smazat po:",
|
||||||
|
"userAccountMenu.dndSubMenuItem.1Hour": "1 hodina",
|
||||||
|
"userAccountMenu.dndSubMenuItem.2Hours": "2 hodiny",
|
||||||
|
"userAccountMenu.dndSubMenuItem.30Minutes": "30 min",
|
||||||
|
"userAccountMenu.dndSubMenuItem.custom": "Zvolte datum a čas",
|
||||||
|
"userAccountMenu.dndSubMenuItem.doNotClear": "Nemazat",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrow": "Zítra",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrowsDateTime": "{shortDay}, {shortTime}",
|
||||||
|
"userAccountMenu.logoutMenuItem.label": "Odhlásit Se",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.away": "Stav je \"Pryč\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.dnd": "Stav je \"Nerušit\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.offline": "Stav je \"Offline\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.online": "Stav je \"Online\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.ooo": "Stav je \"Mimo kancelář\".",
|
||||||
|
"userAccountMenu.menuButton.ariaLabel": "Menu účtu uživatele",
|
||||||
|
"userAccountMenu.offlineMenuItem.label": "Offline",
|
||||||
|
"userAccountMenu.onlineMenuItem.label": "Online",
|
||||||
|
"userAccountMenu.oooMenuItem.primaryLabel": "Mimo kancelář",
|
||||||
|
"userAccountMenu.oooMenuItem.secondaryLabel": "Automatické odpovědi jsou zapnuty",
|
||||||
|
"userAccountMenu.profileMenuItem.label": "Profil",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.clearTooltip": "Smazat vlastní stav",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithExpiryAndNoText.ariaDescription": "Stav exspiruje v {time}. Nastavit vlastní stav.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndExpiry.ariaDescription": "Stav je \"{text}\" a vyprší v {time}. Nastavte vlastní status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndNoExpiry.ariaDescription": "Stav je \"{text}\". Nastavte vlastní status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusSet": "Nastavte vlastní stav",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusTextSet": "Nastavte text vlastního stavu",
|
||||||
"userGuideHelp.askTheCommunity": "Zeptejte se komunity",
|
"userGuideHelp.askTheCommunity": "Zeptejte se komunity",
|
||||||
"userGuideHelp.helpResources": "Zdroje nápovědy",
|
"userGuideHelp.helpResources": "Zdroje nápovědy",
|
||||||
"userGuideHelp.keyboardShortcuts": "Klávesové zkratky",
|
"userGuideHelp.keyboardShortcuts": "Klávesové zkratky",
|
||||||
|
@ -1887,7 +1887,8 @@
|
|||||||
"admin.permissions.sysconsole_section_user_management_channels.name": "Kanäle",
|
"admin.permissions.sysconsole_section_user_management_channels.name": "Kanäle",
|
||||||
"admin.permissions.sysconsole_section_user_management_groups.name": "Gruppen",
|
"admin.permissions.sysconsole_section_user_management_groups.name": "Gruppen",
|
||||||
"admin.permissions.sysconsole_section_user_management_permissions.name": "Berechtigungen",
|
"admin.permissions.sysconsole_section_user_management_permissions.name": "Berechtigungen",
|
||||||
"admin.permissions.sysconsole_section_user_management_system_roles.name": "Systemrollen",
|
"admin.permissions.sysconsole_section_user_management_system_roles.description": "Die Einstellung \"Kein Zugriff\" schränkt nur die Oberfläche der Systemkonsole ein. Die zugrundeliegenden API-Endpunkte sind für alle Benutzer in einem Nur-Lese-Status für grundlegende Produktfunktionen zugänglich.",
|
||||||
|
"admin.permissions.sysconsole_section_user_management_system_roles.name": "Delegierte differenzierte Verwaltung",
|
||||||
"admin.permissions.sysconsole_section_user_management_teams.name": "Teams",
|
"admin.permissions.sysconsole_section_user_management_teams.name": "Teams",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.description": "Kann Admin Passwörter nicht zurücksetzen",
|
"admin.permissions.sysconsole_section_user_management_users.description": "Kann Admin Passwörter nicht zurücksetzen",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.name": "Benutzer",
|
"admin.permissions.sysconsole_section_user_management_users.name": "Benutzer",
|
||||||
@ -2577,9 +2578,11 @@
|
|||||||
"admin.system_properties.user_properties.table.actions": "Aktionen",
|
"admin.system_properties.user_properties.table.actions": "Aktionen",
|
||||||
"admin.system_properties.user_properties.table.actions.delete": "Löschen",
|
"admin.system_properties.user_properties.table.actions.delete": "Löschen",
|
||||||
"admin.system_properties.user_properties.table.property": "Eigenschaft",
|
"admin.system_properties.user_properties.table.property": "Eigenschaft",
|
||||||
|
"admin.system_properties.user_properties.table.property_name.input.name": "Eigenschaftsname",
|
||||||
"admin.system_properties.user_properties.table.type": "Typ",
|
"admin.system_properties.user_properties.table.type": "Typ",
|
||||||
"admin.system_properties.user_properties.table.type.text": "Text",
|
"admin.system_properties.user_properties.table.type.text": "Text",
|
||||||
"admin.system_properties.user_properties.table.validation.name_required": "Bitte gib einen Eigenschaftsnamen ein.",
|
"admin.system_properties.user_properties.table.validation.name_required": "Bitte gib einen Eigenschaftsnamen ein.",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_taken": "Eigenschaftsname ist bereits vergeben.",
|
||||||
"admin.system_properties.user_properties.table.validation.name_unique": "Eigenschaftsnamen müssen eindeutig sein.",
|
"admin.system_properties.user_properties.table.validation.name_unique": "Eigenschaftsnamen müssen eindeutig sein.",
|
||||||
"admin.system_properties.user_properties.title": "Benutzer-Eigenschaften",
|
"admin.system_properties.user_properties.title": "Benutzer-Eigenschaften",
|
||||||
"admin.system_roles_feature_discovery.copy": "Verwende anpassbare Verwaltungsrollen um ausgesuchten Benutzern Lese- und/oder Schreibzugriff auf ausgewählte Sektionen der Systemkonsole zu geben.",
|
"admin.system_roles_feature_discovery.copy": "Verwende anpassbare Verwaltungsrollen um ausgesuchten Benutzern Lese- und/oder Schreibzugriff auf ausgewählte Sektionen der Systemkonsole zu geben.",
|
||||||
@ -2905,13 +2908,10 @@
|
|||||||
"analytics.system.publicChannels": "Öffentliche Kanäle",
|
"analytics.system.publicChannels": "Öffentliche Kanäle",
|
||||||
"analytics.system.seatsPurchased": "Lizensierte Sitze",
|
"analytics.system.seatsPurchased": "Lizensierte Sitze",
|
||||||
"analytics.system.skippedIntensiveQueries": "Um die Performance zu maximieren, sind einige Statistiken deaktiviert. Du kannst sie in der <link>config.json reaktivieren</link>.",
|
"analytics.system.skippedIntensiveQueries": "Um die Performance zu maximieren, sind einige Statistiken deaktiviert. Du kannst sie in der <link>config.json reaktivieren</link>.",
|
||||||
"analytics.system.textPosts": "Nur-Text Beiträge",
|
|
||||||
"analytics.system.title": "Systemstatistiken",
|
"analytics.system.title": "Systemstatistiken",
|
||||||
"analytics.system.totalBotPosts": "Anzahl der Nachrichten von Bots",
|
"analytics.system.totalBotPosts": "Anzahl der Nachrichten von Bots",
|
||||||
"analytics.system.totalChannels": "Kanäle Gesamt",
|
"analytics.system.totalChannels": "Kanäle Gesamt",
|
||||||
"analytics.system.totalCommands": "Befehle Gesamt",
|
"analytics.system.totalCommands": "Befehle Gesamt",
|
||||||
"analytics.system.totalFilePosts": "Beiträge mit Dateien",
|
|
||||||
"analytics.system.totalHashtagPosts": "Beiträge mit Hashtags",
|
|
||||||
"analytics.system.totalIncomingWebhooks": "Eingehende Webhooks",
|
"analytics.system.totalIncomingWebhooks": "Eingehende Webhooks",
|
||||||
"analytics.system.totalMasterDbConnections": "Master DB Verbindungen",
|
"analytics.system.totalMasterDbConnections": "Master DB Verbindungen",
|
||||||
"analytics.system.totalOutgoingWebhooks": "Ausgehende Webhooks",
|
"analytics.system.totalOutgoingWebhooks": "Ausgehende Webhooks",
|
||||||
@ -3282,6 +3282,7 @@
|
|||||||
"channel_info_rhs.menu.members": "Mitglieder",
|
"channel_info_rhs.menu.members": "Mitglieder",
|
||||||
"channel_info_rhs.menu.notification_preferences": "Benachrichtigungseinstellungen",
|
"channel_info_rhs.menu.notification_preferences": "Benachrichtigungseinstellungen",
|
||||||
"channel_info_rhs.menu.pinned": "Angeheftete Nachrichten",
|
"channel_info_rhs.menu.pinned": "Angeheftete Nachrichten",
|
||||||
|
"channel_info_rhs.menu.title": "Aktionen Kanalinfo",
|
||||||
"channel_info_rhs.top_buttons.add_people": "Benutzer hinzufügen",
|
"channel_info_rhs.top_buttons.add_people": "Benutzer hinzufügen",
|
||||||
"channel_info_rhs.top_buttons.add_people.tooltip": "Teammitglieder zu diesem Kanal hinzufügen",
|
"channel_info_rhs.top_buttons.add_people.tooltip": "Teammitglieder zu diesem Kanal hinzufügen",
|
||||||
"channel_info_rhs.top_buttons.copied": "Kopiert",
|
"channel_info_rhs.top_buttons.copied": "Kopiert",
|
||||||
@ -4970,6 +4971,7 @@
|
|||||||
"search_hint.enter_to_search": "Drücke Enter zum Suchen",
|
"search_hint.enter_to_search": "Drücke Enter zum Suchen",
|
||||||
"search_hint.enter_to_select": "Drücke Enter, um auszuwählen",
|
"search_hint.enter_to_select": "Drücke Enter, um auszuwählen",
|
||||||
"search_hint.filter": "Filtere deine Suche mit:",
|
"search_hint.filter": "Filtere deine Suche mit:",
|
||||||
|
"search_hint.reset_filters": "Deine Filter wurden zurückgesetzt, weil du ein anderes Team gewählt hast",
|
||||||
"search_item.channelArchived": "Archiviert",
|
"search_item.channelArchived": "Archiviert",
|
||||||
"search_item.direct": "Direktnachricht (mit {username})",
|
"search_item.direct": "Direktnachricht (mit {username})",
|
||||||
"search_item.file_tag.direct_message": "Direktnachricht",
|
"search_item.file_tag.direct_message": "Direktnachricht",
|
||||||
@ -4985,6 +4987,9 @@
|
|||||||
"search_list_option.on": "Nachrichten am",
|
"search_list_option.on": "Nachrichten am",
|
||||||
"search_list_option.phrases": "Nachrichten mit Phrasen",
|
"search_list_option.phrases": "Nachrichten mit Phrasen",
|
||||||
"search_results.channel-files-header": "Aktuellste Dateien",
|
"search_results.channel-files-header": "Aktuellste Dateien",
|
||||||
|
"search_teams_selector.all_teams": "Alle Teams",
|
||||||
|
"search_teams_selector.search_teams": "Teams suchen",
|
||||||
|
"search_teams_selector.your_teams": "Deine Teams",
|
||||||
"sectionNotice.dismiss": "Mitteilung entfernen",
|
"sectionNotice.dismiss": "Mitteilung entfernen",
|
||||||
"select_team.icon": "\"Team auswählen\"-Symbol",
|
"select_team.icon": "\"Team auswählen\"-Symbol",
|
||||||
"select_team.join.icon": "\"Team beitreten\"-Symbol",
|
"select_team.join.icon": "\"Team beitreten\"-Symbol",
|
||||||
|
@ -246,6 +246,7 @@
|
|||||||
"add_teams_to_scheme.confirmation.message": "This team is already selected in another team scheme, are you sure you want to move it to this team scheme?",
|
"add_teams_to_scheme.confirmation.message": "This team is already selected in another team scheme, are you sure you want to move it to this team scheme?",
|
||||||
"add_teams_to_scheme.confirmation.title": "Team Override Scheme Change?",
|
"add_teams_to_scheme.confirmation.title": "Team Override Scheme Change?",
|
||||||
"add_teams_to_scheme.modalTitle": "Add Teams to Team Selection List",
|
"add_teams_to_scheme.modalTitle": "Add Teams to Team Selection List",
|
||||||
|
"add_teams_to_scheme.select_team.label": "Select team {label}",
|
||||||
"add_user_to_channel_modal.add": "Add",
|
"add_user_to_channel_modal.add": "Add",
|
||||||
"add_user_to_channel_modal.cancel": "Cancel",
|
"add_user_to_channel_modal.cancel": "Cancel",
|
||||||
"add_user_to_channel_modal.help": "Type to find a channel. Use ↑↓ to browse, ↵ to select, ESC to dismiss.",
|
"add_user_to_channel_modal.help": "Type to find a channel. Use ↑↓ to browse, ↵ to select, ESC to dismiss.",
|
||||||
@ -482,7 +483,7 @@
|
|||||||
"admin.channel_settings.channel_details.archiveChannel": "Archive Channel",
|
"admin.channel_settings.channel_details.archiveChannel": "Archive Channel",
|
||||||
"admin.channel_settings.channel_details.isDefaultDescr": "This default channel cannot be converted into a private channel.",
|
"admin.channel_settings.channel_details.isDefaultDescr": "This default channel cannot be converted into a private channel.",
|
||||||
"admin.channel_settings.channel_details.isPublic": "Public channel or private channel",
|
"admin.channel_settings.channel_details.isPublic": "Public channel or private channel",
|
||||||
"admin.channel_settings.channel_details.isPublicDescr": "If `public` the channel is discoverable and any user can join, or if `private` invitations are required. Toggle to convert public channels to private. When Group Sync is enabled, private channels cannot be converted to public.",
|
"admin.channel_settings.channel_details.isPublicDescr": "Select Public for a channel any user can find and join. {br}Select Private to require channel invitations to join. {br}Use this switch to change this channel from public to private or from private to public.",
|
||||||
"admin.channel_settings.channel_details.syncGroupMembers": "Sync Group Members",
|
"admin.channel_settings.channel_details.syncGroupMembers": "Sync Group Members",
|
||||||
"admin.channel_settings.channel_details.syncGroupMembersDescr": "When enabled, adding and removing users from groups will add or remove them from this channel. The only way of inviting members to this channel is by adding the groups they belong to. <link>Learn More</link>",
|
"admin.channel_settings.channel_details.syncGroupMembersDescr": "When enabled, adding and removing users from groups will add or remove them from this channel. The only way of inviting members to this channel is by adding the groups they belong to. <link>Learn More</link>",
|
||||||
"admin.channel_settings.channel_details.unarchiveChannel": "Unarchive Channel",
|
"admin.channel_settings.channel_details.unarchiveChannel": "Unarchive Channel",
|
||||||
@ -1494,6 +1495,7 @@
|
|||||||
"admin.logs.showErrors": "Show last {n} errors",
|
"admin.logs.showErrors": "Show last {n} errors",
|
||||||
"admin.logs.title": "Server Logs",
|
"admin.logs.title": "Server Logs",
|
||||||
"admin.manage_roles.additionalRoles": "Select additional permissions for the account. <link>Read more about roles and permissions</link>.",
|
"admin.manage_roles.additionalRoles": "Select additional permissions for the account. <link>Read more about roles and permissions</link>.",
|
||||||
|
"admin.manage_roles.additionalRoles_warning": "<strong>Note:</strong><span>The permissions granted above apply to the account as a whole, regardless of whether it is authenticated using a session cookie or a personal access token. For example, selecting post:all will allow the account to post to channels it is not a member of, even without using a personal access token.</span>",
|
||||||
"admin.manage_roles.allowUserAccessTokens": "Allow this account to generate <link>personal access tokens</link>.",
|
"admin.manage_roles.allowUserAccessTokens": "Allow this account to generate <link>personal access tokens</link>.",
|
||||||
"admin.manage_roles.allowUserAccessTokensDesc": "Removing this permission doesn't delete existing tokens. To delete them, go to the user's Manage Tokens menu.",
|
"admin.manage_roles.allowUserAccessTokensDesc": "Removing this permission doesn't delete existing tokens. To delete them, go to the user's Manage Tokens menu.",
|
||||||
"admin.manage_roles.botAdditionalRoles": "Select additional permissions for the account. <link>Read more about roles and permissions</link>.",
|
"admin.manage_roles.botAdditionalRoles": "Select additional permissions for the account. <link>Read more about roles and permissions</link>.",
|
||||||
@ -1594,8 +1596,10 @@
|
|||||||
"admin.password.preview": "Error message preview",
|
"admin.password.preview": "Error message preview",
|
||||||
"admin.password.symbol": "At least one symbol (e.g. '~!@#$%^&*()')",
|
"admin.password.symbol": "At least one symbol (e.g. '~!@#$%^&*()')",
|
||||||
"admin.password.uppercase": "At least one uppercase letter",
|
"admin.password.uppercase": "At least one uppercase letter",
|
||||||
|
"admin.permissions.group.convert_private_channel_to_public.description": "Convert private channels to public",
|
||||||
|
"admin.permissions.group.convert_private_channel_to_public.name": "Convert to public",
|
||||||
"admin.permissions.group.convert_public_channel_to_private.description": "Convert public channels to private",
|
"admin.permissions.group.convert_public_channel_to_private.description": "Convert public channels to private",
|
||||||
"admin.permissions.group.convert_public_channel_to_private.name": "Convert Channels",
|
"admin.permissions.group.convert_public_channel_to_private.name": "Convert to private",
|
||||||
"admin.permissions.group.custom_groups.description": "Create, edit, delete and manage the members of custom groups.",
|
"admin.permissions.group.custom_groups.description": "Create, edit, delete and manage the members of custom groups.",
|
||||||
"admin.permissions.group.custom_groups.name": "Custom Groups",
|
"admin.permissions.group.custom_groups.name": "Custom Groups",
|
||||||
"admin.permissions.group.delete_posts.description": "Delete own and other user's posts.",
|
"admin.permissions.group.delete_posts.description": "Delete own and other user's posts.",
|
||||||
@ -1655,9 +1659,9 @@
|
|||||||
"admin.permissions.permission.assign_system_admin_role.description": "Assign system admin role",
|
"admin.permissions.permission.assign_system_admin_role.description": "Assign system admin role",
|
||||||
"admin.permissions.permission.assign_system_admin_role.name": "Assign system admin role",
|
"admin.permissions.permission.assign_system_admin_role.name": "Assign system admin role",
|
||||||
"admin.permissions.permission.convert_private_channel_to_public.description": "Convert private channels to public",
|
"admin.permissions.permission.convert_private_channel_to_public.description": "Convert private channels to public",
|
||||||
"admin.permissions.permission.convert_private_channel_to_public.name": "Convert Channels",
|
"admin.permissions.permission.convert_private_channel_to_public.name": "Convert to public",
|
||||||
"admin.permissions.permission.convert_public_channel_to_private.description": "Convert public channels to private",
|
"admin.permissions.permission.convert_public_channel_to_private.description": "Convert public channels to private",
|
||||||
"admin.permissions.permission.convert_public_channel_to_private.name": "Convert Channels",
|
"admin.permissions.permission.convert_public_channel_to_private.name": "Convert to private",
|
||||||
"admin.permissions.permission.create_custom_group.description": "Create custom groups.",
|
"admin.permissions.permission.create_custom_group.description": "Create custom groups.",
|
||||||
"admin.permissions.permission.create_custom_group.name": "Create",
|
"admin.permissions.permission.create_custom_group.name": "Create",
|
||||||
"admin.permissions.permission.create_direct_channel.description": "Create direct channel",
|
"admin.permissions.permission.create_direct_channel.description": "Create direct channel",
|
||||||
@ -1883,7 +1887,8 @@
|
|||||||
"admin.permissions.sysconsole_section_user_management_channels.name": "Channels",
|
"admin.permissions.sysconsole_section_user_management_channels.name": "Channels",
|
||||||
"admin.permissions.sysconsole_section_user_management_groups.name": "Groups",
|
"admin.permissions.sysconsole_section_user_management_groups.name": "Groups",
|
||||||
"admin.permissions.sysconsole_section_user_management_permissions.name": "Permissions",
|
"admin.permissions.sysconsole_section_user_management_permissions.name": "Permissions",
|
||||||
"admin.permissions.sysconsole_section_user_management_system_roles.name": "System Roles",
|
"admin.permissions.sysconsole_section_user_management_system_roles.description": "Setting 'No Access' restricts the System Console interface only. The underlying API endpoints are accessible to all users in a read-only state for basic product functionality.",
|
||||||
|
"admin.permissions.sysconsole_section_user_management_system_roles.name": "Delegated Granular Administration",
|
||||||
"admin.permissions.sysconsole_section_user_management_teams.name": "Teams",
|
"admin.permissions.sysconsole_section_user_management_teams.name": "Teams",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.description": "Cannot reset admin passwords",
|
"admin.permissions.sysconsole_section_user_management_users.description": "Cannot reset admin passwords",
|
||||||
"admin.permissions.sysconsole_section_user_management_users.name": "Users",
|
"admin.permissions.sysconsole_section_user_management_users.name": "Users",
|
||||||
@ -2486,6 +2491,7 @@
|
|||||||
"admin.sidebar.smtp": "SMTP",
|
"admin.sidebar.smtp": "SMTP",
|
||||||
"admin.sidebar.subscription": "Subscription",
|
"admin.sidebar.subscription": "Subscription",
|
||||||
"admin.sidebar.systemRoles": "Delegated Granular Administration",
|
"admin.sidebar.systemRoles": "Delegated Granular Administration",
|
||||||
|
"admin.sidebar.system_properties": "System Properties",
|
||||||
"admin.sidebar.teamStatistics": "Team Statistics",
|
"admin.sidebar.teamStatistics": "Team Statistics",
|
||||||
"admin.sidebar.teams": "Teams",
|
"admin.sidebar.teams": "Teams",
|
||||||
"admin.sidebar.userManagement": "User Management",
|
"admin.sidebar.userManagement": "User Management",
|
||||||
@ -2562,6 +2568,23 @@
|
|||||||
"admin.systemUserDetail.teamList.teamType.groupSync": "Group Sync",
|
"admin.systemUserDetail.teamList.teamType.groupSync": "Group Sync",
|
||||||
"admin.systemUserDetail.teamList.teamType.inviteOnly": "Invite Only",
|
"admin.systemUserDetail.teamList.teamType.inviteOnly": "Invite Only",
|
||||||
"admin.systemUserDetail.title": "User Configuration",
|
"admin.systemUserDetail.title": "User Configuration",
|
||||||
|
"admin.system_properties.confirm.delete.button": "Delete",
|
||||||
|
"admin.system_properties.confirm.delete.text": "Deleting this property will remove all associated user-defined values.",
|
||||||
|
"admin.system_properties.confirm.delete.title": "Delete {name} property",
|
||||||
|
"admin.system_properties.details.saving_changes": "Saving configuration…",
|
||||||
|
"admin.system_properties.details.saving_changes_error": "An error occurred while saving the configuration",
|
||||||
|
"admin.system_properties.user_properties.add_property": "Add property",
|
||||||
|
"admin.system_properties.user_properties.subtitle": "Customise the properties to show in user profiles",
|
||||||
|
"admin.system_properties.user_properties.table.actions": "Actions",
|
||||||
|
"admin.system_properties.user_properties.table.actions.delete": "Delete",
|
||||||
|
"admin.system_properties.user_properties.table.property": "Property",
|
||||||
|
"admin.system_properties.user_properties.table.property_name.input.name": "Property Name",
|
||||||
|
"admin.system_properties.user_properties.table.type": "Type",
|
||||||
|
"admin.system_properties.user_properties.table.type.text": "Text",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_required": "Please enter a property name.",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_taken": "Property name already taken.",
|
||||||
|
"admin.system_properties.user_properties.table.validation.name_unique": "Property names must be unique.",
|
||||||
|
"admin.system_properties.user_properties.title": "User Properties",
|
||||||
"admin.system_roles_feature_discovery.copy": "Assign customisable admin roles to give designated users read and/or write access to select sections of System Console.",
|
"admin.system_roles_feature_discovery.copy": "Assign customisable admin roles to give designated users read and/or write access to select sections of System Console.",
|
||||||
"admin.system_roles_feature_discovery.title": "Provide controlled access to the System Console with Mattermost Enterprise",
|
"admin.system_roles_feature_discovery.title": "Provide controlled access to the System Console with Mattermost Enterprise",
|
||||||
"admin.system_users.column_toggler.dropdownAriaLabel": "Columns visibility menu",
|
"admin.system_users.column_toggler.dropdownAriaLabel": "Columns visibility menu",
|
||||||
@ -2885,13 +2908,10 @@
|
|||||||
"analytics.system.publicChannels": "Public Channels",
|
"analytics.system.publicChannels": "Public Channels",
|
||||||
"analytics.system.seatsPurchased": "Licensed Seats",
|
"analytics.system.seatsPurchased": "Licensed Seats",
|
||||||
"analytics.system.skippedIntensiveQueries": "To maximise performance, some statistics are disabled. You can <link>re-enable them in config.json</link>.",
|
"analytics.system.skippedIntensiveQueries": "To maximise performance, some statistics are disabled. You can <link>re-enable them in config.json</link>.",
|
||||||
"analytics.system.textPosts": "Posts with Text-only",
|
|
||||||
"analytics.system.title": "System Statistics",
|
"analytics.system.title": "System Statistics",
|
||||||
"analytics.system.totalBotPosts": "Total Posts from Bots",
|
"analytics.system.totalBotPosts": "Total Posts from Bots",
|
||||||
"analytics.system.totalChannels": "Total Channels",
|
"analytics.system.totalChannels": "Total Channels",
|
||||||
"analytics.system.totalCommands": "Total Commands",
|
"analytics.system.totalCommands": "Total Commands",
|
||||||
"analytics.system.totalFilePosts": "Posts with Files",
|
|
||||||
"analytics.system.totalHashtagPosts": "Posts with Hashtags",
|
|
||||||
"analytics.system.totalIncomingWebhooks": "Incoming Webhooks",
|
"analytics.system.totalIncomingWebhooks": "Incoming Webhooks",
|
||||||
"analytics.system.totalMasterDbConnections": "Master DB Conns",
|
"analytics.system.totalMasterDbConnections": "Master DB Conns",
|
||||||
"analytics.system.totalOutgoingWebhooks": "Outgoing Webhooks",
|
"analytics.system.totalOutgoingWebhooks": "Outgoing Webhooks",
|
||||||
@ -3211,9 +3231,11 @@
|
|||||||
"channel_header.closeChannelInfo": "Close Info",
|
"channel_header.closeChannelInfo": "Close Info",
|
||||||
"channel_header.convert": "Convert to Private Channel",
|
"channel_header.convert": "Convert to Private Channel",
|
||||||
"channel_header.delete": "Archive Channel",
|
"channel_header.delete": "Archive Channel",
|
||||||
|
"channel_header.directchannel": "{displayName} (you) Channel Menu",
|
||||||
"channel_header.directchannel.you": "{displayname} (you) ",
|
"channel_header.directchannel.you": "{displayname} (you) ",
|
||||||
"channel_header.flagged": "Saved messages",
|
"channel_header.flagged": "Saved messages",
|
||||||
"channel_header.groupMessageHasGuests": "This group message has guests",
|
"channel_header.groupMessageHasGuests": "This group message has guests",
|
||||||
|
"channel_header.headerText.addNewButton": "Add a channel header",
|
||||||
"channel_header.lastActive": "Last online {timestamp}",
|
"channel_header.lastActive": "Last online {timestamp}",
|
||||||
"channel_header.lastOnline": "Last online {timestamp}",
|
"channel_header.lastOnline": "Last online {timestamp}",
|
||||||
"channel_header.leave": "Leave Channel",
|
"channel_header.leave": "Leave Channel",
|
||||||
@ -3222,6 +3244,7 @@
|
|||||||
"channel_header.mute": "Mute Channel",
|
"channel_header.mute": "Mute Channel",
|
||||||
"channel_header.muteConversation": "Mute Conversation",
|
"channel_header.muteConversation": "Mute Conversation",
|
||||||
"channel_header.openChannelInfo": "View Info",
|
"channel_header.openChannelInfo": "View Info",
|
||||||
|
"channel_header.otherchannel": "{displayName} Channel Menu",
|
||||||
"channel_header.pinnedPosts": "Pinned messages",
|
"channel_header.pinnedPosts": "Pinned messages",
|
||||||
"channel_header.recentMentions": "Recent mentions",
|
"channel_header.recentMentions": "Recent mentions",
|
||||||
"channel_header.rename": "Rename Channel",
|
"channel_header.rename": "Rename Channel",
|
||||||
@ -3259,6 +3282,7 @@
|
|||||||
"channel_info_rhs.menu.members": "Members",
|
"channel_info_rhs.menu.members": "Members",
|
||||||
"channel_info_rhs.menu.notification_preferences": "Notification Preferences",
|
"channel_info_rhs.menu.notification_preferences": "Notification Preferences",
|
||||||
"channel_info_rhs.menu.pinned": "Pinned messages",
|
"channel_info_rhs.menu.pinned": "Pinned messages",
|
||||||
|
"channel_info_rhs.menu.title": "Channel Info Actions",
|
||||||
"channel_info_rhs.top_buttons.add_people": "Add People",
|
"channel_info_rhs.top_buttons.add_people": "Add People",
|
||||||
"channel_info_rhs.top_buttons.add_people.tooltip": "Add team members to this channel",
|
"channel_info_rhs.top_buttons.add_people.tooltip": "Add team members to this channel",
|
||||||
"channel_info_rhs.top_buttons.copied": "Copied",
|
"channel_info_rhs.top_buttons.copied": "Copied",
|
||||||
@ -3683,6 +3707,8 @@
|
|||||||
"email_verify.return": "Return to log in",
|
"email_verify.return": "Return to log in",
|
||||||
"email_verify.sending": "Sending email…",
|
"email_verify.sending": "Sending email…",
|
||||||
"email_verify.sent": "Verification email sent",
|
"email_verify.sent": "Verification email sent",
|
||||||
|
"emoji_gif_picker.dialog.emojis": "Emoji Picker",
|
||||||
|
"emoji_gif_picker.dialog.gifs": "GIF Picker",
|
||||||
"emoji_gif_picker.tabs.emojis": "Emojis",
|
"emoji_gif_picker.tabs.emojis": "Emojis",
|
||||||
"emoji_gif_picker.tabs.gifs": "GIFs",
|
"emoji_gif_picker.tabs.gifs": "GIFs",
|
||||||
"emoji_list.actions": "Actions",
|
"emoji_list.actions": "Actions",
|
||||||
@ -3965,6 +3991,7 @@
|
|||||||
"globalThreads.searchGuidance.title": "That’s the end of the list.",
|
"globalThreads.searchGuidance.title": "That’s the end of the list.",
|
||||||
"globalThreads.sidebarLink": "Threads",
|
"globalThreads.sidebarLink": "Threads",
|
||||||
"globalThreads.threadList.noUnreadThreads": "No unread threads",
|
"globalThreads.threadList.noUnreadThreads": "No unread threads",
|
||||||
|
"globalThreads.threadList.noUnreadThreads.subtitle": "You're all caught up",
|
||||||
"globalThreads.threadPane.unreadMessageLink": "You have {numUnread, plural, =0 {no unread threads} =1 {<link>{numUnread} thread</link>} other {<link>{numUnread} threads</link>}} {numUnread, plural, =0 {} other {with unread messages}}",
|
"globalThreads.threadPane.unreadMessageLink": "You have {numUnread, plural, =0 {no unread threads} =1 {<link>{numUnread} thread</link>} other {<link>{numUnread} threads</link>}} {numUnread, plural, =0 {} other {with unread messages}}",
|
||||||
"globalThreads.threadPane.unselectedTitle": "{numUnread, plural, =0 {Looks like you’re all caught up} other {Catch up on your threads}}",
|
"globalThreads.threadPane.unselectedTitle": "{numUnread, plural, =0 {Looks like you’re all caught up} other {Catch up on your threads}}",
|
||||||
"globalThreads.title": "{prefix}Threads - {displayName} {siteName}",
|
"globalThreads.title": "{prefix}Threads - {displayName} {siteName}",
|
||||||
@ -4328,6 +4355,7 @@
|
|||||||
"mobile.set_status.dnd.icon": "Do Not Disturb Icon",
|
"mobile.set_status.dnd.icon": "Do Not Disturb Icon",
|
||||||
"mobile.set_status.offline.icon": "Offline Icon",
|
"mobile.set_status.offline.icon": "Offline Icon",
|
||||||
"mobile.set_status.online.icon": "Online Icon",
|
"mobile.set_status.online.icon": "Online Icon",
|
||||||
|
"modal.header_close": "Close",
|
||||||
"modal.manual_status.ask": "Do not ask me again",
|
"modal.manual_status.ask": "Do not ask me again",
|
||||||
"modal.manual_status.auto_responder.message_away": "Would you like to switch your status to 'Away' and disable automatic replies?",
|
"modal.manual_status.auto_responder.message_away": "Would you like to switch your status to 'Away' and disable automatic replies?",
|
||||||
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to 'Do Not Disturb' and disable automatic replies?",
|
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to 'Do Not Disturb' and disable automatic replies?",
|
||||||
@ -4376,6 +4404,7 @@
|
|||||||
"more_channels.view": "View",
|
"more_channels.view": "View",
|
||||||
"more_direct_channels.directchannel.deactivated": "{displayname} - Deactivated",
|
"more_direct_channels.directchannel.deactivated": "{displayname} - Deactivated",
|
||||||
"more_direct_channels.directchannel.you": "{displayname} (you)",
|
"more_direct_channels.directchannel.you": "{displayname} (you)",
|
||||||
|
"more_direct_channels.new_convo_add.label": "Add option {label}",
|
||||||
"more_direct_channels.new_convo_note": "This will start a new conversation. If you're adding a lot of people, consider creating a private channel instead.",
|
"more_direct_channels.new_convo_note": "This will start a new conversation. If you're adding a lot of people, consider creating a private channel instead.",
|
||||||
"more_direct_channels.new_convo_note.full": "You've reached the maximum number of people for this conversation. Consider creating a private channel instead.",
|
"more_direct_channels.new_convo_note.full": "You've reached the maximum number of people for this conversation. Consider creating a private channel instead.",
|
||||||
"more_direct_channels.title": "Direct Messages",
|
"more_direct_channels.title": "Direct Messages",
|
||||||
@ -4515,6 +4544,7 @@
|
|||||||
"onboardingTask.checklist.main_subtitle": "Let's get up and running.",
|
"onboardingTask.checklist.main_subtitle": "Let's get up and running.",
|
||||||
"onboardingTask.checklist.no_thanks": "No thanks",
|
"onboardingTask.checklist.no_thanks": "No thanks",
|
||||||
"onboardingTask.checklist.start_enterprise_now": "Start your free Enterprise trial now!",
|
"onboardingTask.checklist.start_enterprise_now": "Start your free Enterprise trial now!",
|
||||||
|
"onboardingTask.checklist.start_onboarding_process": "Start the onboarding process.",
|
||||||
"onboardingTask.checklist.task_complete_your_profile": "Complete your profile",
|
"onboardingTask.checklist.task_complete_your_profile": "Complete your profile",
|
||||||
"onboardingTask.checklist.task_download_mm_apps": "Download the Desktop and Mobile Apps",
|
"onboardingTask.checklist.task_download_mm_apps": "Download the Desktop and Mobile Apps",
|
||||||
"onboardingTask.checklist.task_invite_team_members": "Invite team members to the workspace",
|
"onboardingTask.checklist.task_invite_team_members": "Invite team members to the workspace",
|
||||||
@ -4627,6 +4657,9 @@
|
|||||||
"post.ariaLabel.replyMessage": "At {time} {date}, {authorName} replied, {message}",
|
"post.ariaLabel.replyMessage": "At {time} {date}, {authorName} replied, {message}",
|
||||||
"post.reminder.acknowledgement": "You will be reminded at {reminderTime}, {reminderDate} about this message from {username}: {permaLink}",
|
"post.reminder.acknowledgement": "You will be reminded at {reminderTime}, {reminderDate} about this message from {username}: {permaLink}",
|
||||||
"post.reminder.systemBot": "Hi there, here's your reminder about this message from {username}: {permaLink}",
|
"post.reminder.systemBot": "Hi there, here's your reminder about this message from {username}: {permaLink}",
|
||||||
|
"post.renderError.message": "An error occurred while rendering this post.",
|
||||||
|
"post.renderError.retry": "Retry",
|
||||||
|
"post.renderError.retryLabel": "Retry rendering this post",
|
||||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
|
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
|
||||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||||
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
||||||
@ -4783,6 +4816,8 @@
|
|||||||
"pricing_modal.title": "Select a plan",
|
"pricing_modal.title": "Select a plan",
|
||||||
"pricing_modal.wantToTry": "Want to try? ",
|
"pricing_modal.wantToTry": "Want to try? ",
|
||||||
"pricing_modal.wantToUpgrade": "Want to upgrade? ",
|
"pricing_modal.wantToUpgrade": "Want to upgrade? ",
|
||||||
|
"profile_popover.aria_label.with_username": "{userName}'s profile popover",
|
||||||
|
"profile_popover.aria_label.without_username": "profile popover",
|
||||||
"promote_to_user_modal.desc": "This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?",
|
"promote_to_user_modal.desc": "This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?",
|
||||||
"promote_to_user_modal.promote": "Promote",
|
"promote_to_user_modal.promote": "Promote",
|
||||||
"promote_to_user_modal.title": "Promote guest {username} to member",
|
"promote_to_user_modal.title": "Promote guest {username} to member",
|
||||||
@ -4936,6 +4971,7 @@
|
|||||||
"search_hint.enter_to_search": "Press Enter to search",
|
"search_hint.enter_to_search": "Press Enter to search",
|
||||||
"search_hint.enter_to_select": "Press Enter to select",
|
"search_hint.enter_to_select": "Press Enter to select",
|
||||||
"search_hint.filter": "Filter your search with:",
|
"search_hint.filter": "Filter your search with:",
|
||||||
|
"search_hint.reset_filters": "Your filters were reset because you chose a different team",
|
||||||
"search_item.channelArchived": "Archived",
|
"search_item.channelArchived": "Archived",
|
||||||
"search_item.direct": "Direct Message (with {username})",
|
"search_item.direct": "Direct Message (with {username})",
|
||||||
"search_item.file_tag.direct_message": "Direct Message",
|
"search_item.file_tag.direct_message": "Direct Message",
|
||||||
@ -4951,6 +4987,9 @@
|
|||||||
"search_list_option.on": "Messages on a date",
|
"search_list_option.on": "Messages on a date",
|
||||||
"search_list_option.phrases": "Messages with phrases",
|
"search_list_option.phrases": "Messages with phrases",
|
||||||
"search_results.channel-files-header": "Recent files",
|
"search_results.channel-files-header": "Recent files",
|
||||||
|
"search_teams_selector.all_teams": "All Teams",
|
||||||
|
"search_teams_selector.search_teams": "Search teams",
|
||||||
|
"search_teams_selector.your_teams": "Your teams",
|
||||||
"sectionNotice.dismiss": "Dismiss notice",
|
"sectionNotice.dismiss": "Dismiss notice",
|
||||||
"select_team.icon": "Select Team Icon",
|
"select_team.icon": "Select Team Icon",
|
||||||
"select_team.join.icon": "Join Team Icon",
|
"select_team.join.icon": "Join Team Icon",
|
||||||
@ -5119,6 +5158,14 @@
|
|||||||
"sidebar.types.favorites": "FAVOURITES",
|
"sidebar.types.favorites": "FAVOURITES",
|
||||||
"sidebar.types.unreads": "UNREADS",
|
"sidebar.types.unreads": "UNREADS",
|
||||||
"sidebar.unreads": "More unreads",
|
"sidebar.unreads": "More unreads",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.browseChannelsMenuItem.primaryLabel": "Browse channels",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createCategoryMenuItem.primaryLabel": "Create new category",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createNewChannelMenuItem.primaryLabel": "Create new channel",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.createUserGroupMenuItem.primaryLabel": "Create new user group",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.invitePeopleMenuItem.primaryLabel": "Invite people",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.invitePeopleMenuItem.secondaryLabel": "Add people to the team",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenu.openDirectMessageMenuItem.primaryLabel": "Open a direct message",
|
||||||
|
"sidebarLeft.browserOrCreateChannelMenuButton.label": "Browse or create channels",
|
||||||
"sidebar_left.addChannelsCta": "Add channels",
|
"sidebar_left.addChannelsCta": "Add channels",
|
||||||
"sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Add Channel Dropdown",
|
"sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Add Channel Dropdown",
|
||||||
"sidebar_left.add_channel_dropdown.browseChannels": "Browse channels",
|
"sidebar_left.add_channel_dropdown.browseChannels": "Browse channels",
|
||||||
@ -5145,6 +5192,7 @@
|
|||||||
"sidebar_left.sidebar_category_menu.viewCategory": "Mark category as read",
|
"sidebar_left.sidebar_category_menu.viewCategory": "Mark category as read",
|
||||||
"sidebar_left.sidebar_channel.selectedCount": "{count} selected",
|
"sidebar_left.sidebar_channel.selectedCount": "{count} selected",
|
||||||
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
|
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
|
||||||
|
"sidebar_left.sidebar_channel_menu.bookmarks": "Bookmarks Bar",
|
||||||
"sidebar_left.sidebar_channel_menu.channels": "Channels",
|
"sidebar_left.sidebar_channel_menu.channels": "Channels",
|
||||||
"sidebar_left.sidebar_channel_menu.copyLink": "Copy Link",
|
"sidebar_left.sidebar_channel_menu.copyLink": "Copy Link",
|
||||||
"sidebar_left.sidebar_channel_menu.dropdownAriaLabel": "Edit channel menu",
|
"sidebar_left.sidebar_channel_menu.dropdownAriaLabel": "Edit channel menu",
|
||||||
@ -5533,6 +5581,7 @@
|
|||||||
"user.settings.display.theme.title": "Theme",
|
"user.settings.display.theme.title": "Theme",
|
||||||
"user.settings.display.timezone": "Timezone",
|
"user.settings.display.timezone": "Timezone",
|
||||||
"user.settings.display.title": "Display Settings",
|
"user.settings.display.title": "Display Settings",
|
||||||
|
"user.settings.general.attributeExtra": "This will be shown in your profile popover.",
|
||||||
"user.settings.general.close": "Close",
|
"user.settings.general.close": "Close",
|
||||||
"user.settings.general.confirmEmail": "Confirm Email",
|
"user.settings.general.confirmEmail": "Confirm Email",
|
||||||
"user.settings.general.currentEmail": "Current Email",
|
"user.settings.general.currentEmail": "Current Email",
|
||||||
@ -5547,6 +5596,7 @@
|
|||||||
"user.settings.general.emailOffice365CantUpdate": "Login is handled through Entra ID and can't be updated. The email address used for notifications is {email}.",
|
"user.settings.general.emailOffice365CantUpdate": "Login is handled through Entra ID and can't be updated. The email address used for notifications is {email}.",
|
||||||
"user.settings.general.emailOpenIdCantUpdate": "Login occurs through OpenID Connect. Email cannot be updated. Email address used for notifications is {email}.",
|
"user.settings.general.emailOpenIdCantUpdate": "Login occurs through OpenID Connect. Email cannot be updated. Email address used for notifications is {email}.",
|
||||||
"user.settings.general.emailSamlCantUpdate": "Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.",
|
"user.settings.general.emailSamlCantUpdate": "Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.",
|
||||||
|
"user.settings.general.emptyAttribute": "Click 'Edit' to add your custom attribute",
|
||||||
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
|
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
|
||||||
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
||||||
"user.settings.general.emptyPassword": "Please enter your current password.",
|
"user.settings.general.emptyPassword": "Please enter your current password.",
|
||||||
@ -5563,6 +5613,7 @@
|
|||||||
"user.settings.general.loginLdap": "Login done through AD/LDAP ({email})",
|
"user.settings.general.loginLdap": "Login done through AD/LDAP ({email})",
|
||||||
"user.settings.general.loginOffice365": "Login handled by Entra ID ({email})",
|
"user.settings.general.loginOffice365": "Login handled by Entra ID ({email})",
|
||||||
"user.settings.general.loginSaml": "Login done through SAML ({email})",
|
"user.settings.general.loginSaml": "Login done through SAML ({email})",
|
||||||
|
"user.settings.general.mobile.emptyAttribute": "Click to add your custom attribute",
|
||||||
"user.settings.general.mobile.emptyName": "Click to add your full name",
|
"user.settings.general.mobile.emptyName": "Click to add your full name",
|
||||||
"user.settings.general.mobile.emptyNickname": "Click to add a nickname",
|
"user.settings.general.mobile.emptyNickname": "Click to add a nickname",
|
||||||
"user.settings.general.mobile.emptyPosition": "Click to add your job title / position",
|
"user.settings.general.mobile.emptyPosition": "Click to add your job title / position",
|
||||||
@ -5822,6 +5873,38 @@
|
|||||||
"user.settings.tokens.tokenId": "Token ID: ",
|
"user.settings.tokens.tokenId": "Token ID: ",
|
||||||
"user.settings.tokens.tokenLoading": "Loading...",
|
"user.settings.tokens.tokenLoading": "Loading...",
|
||||||
"user.settings.tokens.userAccessTokensNone": "No personal access tokens.",
|
"user.settings.tokens.userAccessTokensNone": "No personal access tokens.",
|
||||||
|
"userAccountMenu.awayMenuItem.label": "Away",
|
||||||
|
"userAccountMenu.dndMenuItem.primaryLabel": "Do not disturb",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel": "Disables all notifications",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilLaterSomeTime": "Until {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTodaySomeTime": "Until {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTomorrowSomeTime": "Until tomorrow {time}",
|
||||||
|
"userAccountMenu.dndSubMenu.title": "Clear after:",
|
||||||
|
"userAccountMenu.dndSubMenuItem.1Hour": "1 hour",
|
||||||
|
"userAccountMenu.dndSubMenuItem.2Hours": "2 hours",
|
||||||
|
"userAccountMenu.dndSubMenuItem.30Minutes": "30 minutes",
|
||||||
|
"userAccountMenu.dndSubMenuItem.custom": "Choose date and time",
|
||||||
|
"userAccountMenu.dndSubMenuItem.doNotClear": "Don't clear",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrow": "Tomorrow",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrowsDateTime": "{shortDay}, {shortTime}",
|
||||||
|
"userAccountMenu.logoutMenuItem.label": "Log out",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.away": "Status is 'Away'.",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.dnd": "Status is 'Do not disturb'.",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.offline": "Status is 'Offline'.",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.online": "Status is 'Online'.",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.ooo": "Status is 'Out of office'.",
|
||||||
|
"userAccountMenu.menuButton.ariaLabel": "User's account menu",
|
||||||
|
"userAccountMenu.offlineMenuItem.label": "Offline",
|
||||||
|
"userAccountMenu.onlineMenuItem.label": "Online",
|
||||||
|
"userAccountMenu.oooMenuItem.primaryLabel": "Out of office",
|
||||||
|
"userAccountMenu.oooMenuItem.secondaryLabel": "Automatic replies are enabled",
|
||||||
|
"userAccountMenu.profileMenuItem.label": "Profile",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.clearTooltip": "Clear custom status",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithExpiryAndNoText.ariaDescription": "Status expires at {time}. Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndExpiry.ariaDescription": "Status is '\\{text}'\\ and expires at {time}. Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndNoExpiry.ariaDescription": "Status is '\\{text}'\\. Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusSet": "Set custom status",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusTextSet": "Set custom status text",
|
||||||
"userGuideHelp.askTheCommunity": "Ask the community",
|
"userGuideHelp.askTheCommunity": "Ask the community",
|
||||||
"userGuideHelp.helpResources": "Help resources",
|
"userGuideHelp.helpResources": "Help resources",
|
||||||
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user