Automatic channel category sorting (#30866)
* Automatic channel category sorting * Fix types * AIed * Fix issue where categories are updated for all users * Move all logic to server, clean up * PR feedback * Fix lint --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c6a11763a8
Коммит
25a4839a9e
@@ -608,6 +608,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
DisableWakeUpReconnectHandler: false,
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds: 3000,
|
||||
YoutubeReferrerPolicy: false,
|
||||
ExperimentalChannelCategorySorting: false,
|
||||
},
|
||||
AnalyticsSettings: {
|
||||
MaxUsersForStatistics: 2500,
|
||||
|
||||
@@ -181,6 +181,8 @@ func (a *App) CreateChannelWithUser(c request.CTX, channel *model.Channel, userI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.addChannelToDefaultCategory(c, userID, channel)
|
||||
|
||||
var user *model.User
|
||||
if user, err = a.GetUser(userID); err != nil {
|
||||
return nil, err
|
||||
@@ -208,6 +210,10 @@ func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelNam
|
||||
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Clean up the channel name and display name
|
||||
newChannelName = strings.TrimSpace(newChannelName)
|
||||
newDisplayName = strings.TrimSpace(newDisplayName)
|
||||
|
||||
channel.Name = newChannelName
|
||||
if newDisplayName != "" {
|
||||
channel.DisplayName = newDisplayName
|
||||
@@ -222,6 +228,8 @@ func (a *App) RenameChannel(c request.CTX, channel *model.Channel, newChannelNam
|
||||
}
|
||||
|
||||
func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
|
||||
a.handleChannelCategoryName(channel)
|
||||
|
||||
channel.DisplayName = strings.TrimSpace(channel.DisplayName)
|
||||
sc, nErr := a.Srv().Store().Channel().Save(c, channel, *a.Config().TeamSettings.MaxChannelsPerTeam)
|
||||
if nErr != nil {
|
||||
@@ -864,11 +872,14 @@ func (a *App) PatchChannel(c request.CTX, channel *model.Channel, patch *model.C
|
||||
oldChannelPurpose := channel.Purpose
|
||||
|
||||
channel.Patch(patch)
|
||||
a.handleChannelCategoryName(channel)
|
||||
channel, err := a.UpdateChannel(c, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.addChannelToDefaultCategory(c, userID, channel)
|
||||
|
||||
if oldChannelDisplayName != channel.DisplayName {
|
||||
if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
|
||||
c.Logger().Warn(err.Error())
|
||||
@@ -1665,6 +1676,8 @@ func (a *App) AddUserToChannel(c request.CTX, user *model.User, channel *model.C
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.addChannelToDefaultCategory(c, user.Id, channel)
|
||||
|
||||
// We are sending separate websocket events to the user added and to the channel
|
||||
// This is to get around potential cluster syncing issues where other nodes may not receive the most up to date channel members
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", map[string]bool{user.Id: true}, "")
|
||||
@@ -3878,3 +3891,56 @@ func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *m
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *App) handleChannelCategoryName(channel *model.Channel) {
|
||||
if *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting && strings.Contains(channel.DisplayName, "/") {
|
||||
parts := strings.Split(channel.DisplayName, "/")
|
||||
channel.DisplayName = strings.TrimSpace(strings.Join(parts[1:], "/"))
|
||||
channel.DefaultCategoryName = strings.TrimSpace(parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) addChannelToDefaultCategory(c request.CTX, userID string, channel *model.Channel) {
|
||||
// Add channel to default category if specified
|
||||
if channel.DefaultCategoryName != "" && *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting {
|
||||
// Get user's categories for this team
|
||||
categories, err := a.GetSidebarCategoriesForTeamForUser(c, userID, channel.TeamId)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to get sidebar categories", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
// Find or create the category
|
||||
var targetCategory *model.SidebarCategoryWithChannels
|
||||
for _, category := range categories.Categories {
|
||||
if category.Type == model.SidebarCategoryCustom && strings.EqualFold(category.DisplayName, channel.DefaultCategoryName) {
|
||||
targetCategory = category
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetCategory == nil {
|
||||
// Create new category if it doesn't exist
|
||||
targetCategory = &model.SidebarCategoryWithChannels{
|
||||
SidebarCategory: model.SidebarCategory{
|
||||
UserId: userID,
|
||||
TeamId: channel.TeamId,
|
||||
Type: model.SidebarCategoryCustom,
|
||||
DisplayName: channel.DefaultCategoryName,
|
||||
Sorting: model.SidebarCategorySortDefault,
|
||||
},
|
||||
Channels: []string{channel.Id},
|
||||
}
|
||||
_, err = a.CreateSidebarCategory(c, userID, channel.TeamId, targetCategory)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to create default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
|
||||
}
|
||||
} else {
|
||||
// Add channel to existing category
|
||||
targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...)
|
||||
_, err = a.UpdateSidebarCategories(c, userID, channel.TeamId, []*model.SidebarCategoryWithChannels{targetCategory})
|
||||
if err != nil {
|
||||
mlog.Error("Failed to update default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3395,3 +3395,139 @@ func TestPatchChannel(t *testing.T) {
|
||||
require.Equal(t, "model.channel.is_valid.banner_info.channel_type.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateChannelWithCategorySorting(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Enable ExperimentalChannelCategorySorting
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = true
|
||||
})
|
||||
|
||||
t.Run("should set category when adding user to channel with category and trim white spaces", func(t *testing.T) {
|
||||
channel := &model.Channel{
|
||||
DisplayName: " Category / Channel Name ",
|
||||
Name: "name1",
|
||||
Type: model.ChannelTypeOpen,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
}
|
||||
|
||||
channel, appErr := th.App.CreateChannelWithUser(th.Context, channel, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "Channel Name", channel.DisplayName)
|
||||
require.Equal(t, "Category", channel.DefaultCategoryName)
|
||||
|
||||
// Verify channel is in default category
|
||||
categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
foundCategory := false
|
||||
for _, category := range categories.Categories {
|
||||
if category.DisplayName == "Category" {
|
||||
foundCategory = true
|
||||
assert.Contains(t, category.Channels, channel.Id)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundCategory, "Category 'Category' not found in sidebar categories")
|
||||
|
||||
// Add user to channel
|
||||
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify channel is in default category
|
||||
categories2, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser2.Id, th.BasicTeam.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
foundCategory2 := false
|
||||
for _, category := range categories2.Categories {
|
||||
if category.DisplayName == "Category" {
|
||||
foundCategory2 = true
|
||||
assert.Contains(t, category.Channels, channel.Id)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundCategory2, "Category 'Category' not found in sidebar categories")
|
||||
})
|
||||
|
||||
t.Run("should not set category when feature is disabled", func(t *testing.T) {
|
||||
channel := &model.Channel{
|
||||
DisplayName: "Category2/Channel Name",
|
||||
Name: "name2",
|
||||
Type: model.ChannelTypeOpen,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
}
|
||||
|
||||
channel, appErr := th.App.CreateChannel(th.Context, channel, false)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "Channel Name", channel.DisplayName)
|
||||
require.Equal(t, "Category2", channel.DefaultCategoryName)
|
||||
|
||||
// Disable ExperimentalChannelCategorySorting
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = false
|
||||
})
|
||||
|
||||
// Add user to channel
|
||||
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Verify channel is in default category
|
||||
categories, appErr := th.App.GetSidebarCategoriesForTeamForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
foundCategory := false
|
||||
for _, category := range categories.Categories {
|
||||
if category.DisplayName == "Category2" {
|
||||
foundCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.False(t, foundCategory, "Category 'Category2' not found in sidebar categories")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchChannelWithCategorySorting(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Enable ExperimentalChannelCategorySorting
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = true
|
||||
})
|
||||
|
||||
// Create initial channel
|
||||
channel := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
|
||||
channel.DisplayName = "Initial Name"
|
||||
channel, appErr := th.App.UpdateChannel(th.Context, channel)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Add user to channel
|
||||
_, appErr = th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Patch channel with new display name containing category
|
||||
patch := &model.ChannelPatch{
|
||||
DisplayName: model.NewPointer(" New Category / New Channel Name "),
|
||||
}
|
||||
|
||||
patchedChannel, appErr := th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "New Channel Name", patchedChannel.DisplayName)
|
||||
require.Equal(t, "New Category", patchedChannel.DefaultCategoryName)
|
||||
|
||||
// Test that category is not updated when feature is disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ExperimentalSettings.ExperimentalChannelCategorySorting = false
|
||||
})
|
||||
|
||||
patch = &model.ChannelPatch{
|
||||
DisplayName: model.NewPointer("Disabled Category/Channel Name"),
|
||||
}
|
||||
|
||||
patchedChannel, appErr = th.App.PatchChannel(th.Context, channel, patch, channel.CreatorId)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "Disabled Category/Channel Name", patchedChannel.DisplayName)
|
||||
require.Equal(t, "New Category", patchedChannel.DefaultCategoryName)
|
||||
}
|
||||
|
||||
@@ -271,6 +271,8 @@ channels/db/migrations/mysql/000136_create_attribute_view.down.sql
|
||||
channels/db/migrations/mysql/000136_create_attribute_view.up.sql
|
||||
channels/db/migrations/mysql/000137_update_attribute_view.down.sql
|
||||
channels/db/migrations/mysql/000137_update_attribute_view.up.sql
|
||||
channels/db/migrations/mysql/000138_add_default_category_name_to_channel.down.sql
|
||||
channels/db/migrations/mysql/000138_add_default_category_name_to_channel.up.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||
@@ -543,3 +545,5 @@ channels/db/migrations/postgres/000136_create_attribute_view.down.sql
|
||||
channels/db/migrations/postgres/000136_create_attribute_view.up.sql
|
||||
channels/db/migrations/postgres/000137_update_attribute_view.down.sql
|
||||
channels/db/migrations/postgres/000137_update_attribute_view.up.sql
|
||||
channels/db/migrations/postgres/000138_add_default_category_name_to_channel.down.sql
|
||||
channels/db/migrations/postgres/000138_add_default_category_name_to_channel.up.sql
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_name = 'Channels'
|
||||
AND table_schema = DATABASE()
|
||||
AND column_name = 'DefaultCategoryName'
|
||||
),
|
||||
'ALTER TABLE Channels DROP COLUMN DefaultCategoryName;',
|
||||
'SELECT 1;'
|
||||
));
|
||||
|
||||
PREPARE removeColumnIfExists FROM @preparedStatement;
|
||||
EXECUTE removeColumnIfExists;
|
||||
DEALLOCATE PREPARE removeColumnIfExists;
|
||||
@@ -0,0 +1,14 @@
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE table_name = 'Channels'
|
||||
AND table_schema = DATABASE()
|
||||
AND column_name = 'DefaultCategoryName'
|
||||
),
|
||||
'ALTER TABLE Channels ADD COLUMN DefaultCategoryName varchar(64) NOT NULL DEFAULT "";',
|
||||
'SELECT 1;'
|
||||
));
|
||||
|
||||
PREPARE addColumnIfNotExists FROM @preparedStatement;
|
||||
EXECUTE addColumnIfNotExists;
|
||||
DEALLOCATE PREPARE addColumnIfNotExists;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE channels DROP COLUMN IF EXISTS DefaultCategoryName;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS DefaultCategoryName varchar(64) NOT NULL DEFAULT '';
|
||||
@@ -137,6 +137,7 @@ func channelSliceColumns(isSelect bool, prefix ...string) []string {
|
||||
p + "TotalMsgCountRoot",
|
||||
p + "LastRootPostAt",
|
||||
p + "BannerInfo",
|
||||
p + "DefaultCategoryName",
|
||||
}
|
||||
|
||||
if isSelect {
|
||||
@@ -172,6 +173,7 @@ func channelToSlice(channel *model.Channel) []any {
|
||||
channel.TotalMsgCountRoot,
|
||||
channel.LastRootPostAt,
|
||||
channel.BannerInfo,
|
||||
channel.DefaultCategoryName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,7 +836,8 @@ func (s SqlChannelStore) updateChannelT(transaction *sqlxTxWrapper, channel *mod
|
||||
Shared=:Shared,
|
||||
TotalMsgCountRoot=:TotalMsgCountRoot,
|
||||
LastRootPostAt=:LastRootPostAt,
|
||||
BannerInfo=:BannerInfo
|
||||
BannerInfo=:BannerInfo,
|
||||
DefaultCategoryName=:DefaultCategoryName
|
||||
WHERE Id=:Id`, channel)
|
||||
if err != nil {
|
||||
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
|
||||
|
||||
@@ -100,6 +100,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
||||
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
||||
props["DisableWakeUpReconnectHandler"] = strconv.FormatBool(*c.ExperimentalSettings.DisableWakeUpReconnectHandler)
|
||||
props["UsersStatusAndProfileFetchingPollIntervalMilliseconds"] = strconv.FormatInt(*c.ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds, 10)
|
||||
props["ExperimentalChannelCategorySorting"] = strconv.FormatBool(*c.ExperimentalSettings.ExperimentalChannelCategorySorting)
|
||||
|
||||
// Here we set the new option, but we also send the old FeatureFlag property for backwards compatibility on mobile < 2.27
|
||||
props["EnableCrossTeamSearch"] = strconv.FormatBool(*c.ServiceSettings.EnableCrossTeamSearch)
|
||||
|
||||
@@ -77,29 +77,30 @@ func (c ChannelBannerInfo) Value() (driver.Value, error) {
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
TeamId string `json:"team_id"`
|
||||
Type ChannelType `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Name string `json:"name"`
|
||||
Header string `json:"header"`
|
||||
Purpose string `json:"purpose"`
|
||||
LastPostAt int64 `json:"last_post_at"`
|
||||
TotalMsgCount int64 `json:"total_msg_count"`
|
||||
ExtraUpdateAt int64 `json:"extra_update_at"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
SchemeId *string `json:"scheme_id"`
|
||||
Props map[string]any `json:"props"`
|
||||
GroupConstrained *bool `json:"group_constrained"`
|
||||
Shared *bool `json:"shared"`
|
||||
TotalMsgCountRoot int64 `json:"total_msg_count_root"`
|
||||
PolicyID *string `json:"policy_id"`
|
||||
LastRootPostAt int64 `json:"last_root_post_at"`
|
||||
BannerInfo *ChannelBannerInfo `json:"banner_info"`
|
||||
PolicyEnforced bool `json:"policy_enforced"`
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
TeamId string `json:"team_id"`
|
||||
Type ChannelType `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Name string `json:"name"`
|
||||
Header string `json:"header"`
|
||||
Purpose string `json:"purpose"`
|
||||
LastPostAt int64 `json:"last_post_at"`
|
||||
TotalMsgCount int64 `json:"total_msg_count"`
|
||||
ExtraUpdateAt int64 `json:"extra_update_at"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
SchemeId *string `json:"scheme_id"`
|
||||
Props map[string]any `json:"props"`
|
||||
GroupConstrained *bool `json:"group_constrained"`
|
||||
Shared *bool `json:"shared"`
|
||||
TotalMsgCountRoot int64 `json:"total_msg_count_root"`
|
||||
PolicyID *string `json:"policy_id"`
|
||||
LastRootPostAt int64 `json:"last_root_post_at"`
|
||||
BannerInfo *ChannelBannerInfo `json:"banner_info"`
|
||||
PolicyEnforced bool `json:"policy_enforced"`
|
||||
DefaultCategoryName string `json:"default_category_name"`
|
||||
}
|
||||
|
||||
func (o *Channel) Auditable() map[string]any {
|
||||
@@ -362,7 +363,7 @@ func (o *Channel) IsOpen() bool {
|
||||
|
||||
func (o *Channel) Patch(patch *ChannelPatch) {
|
||||
if patch.DisplayName != nil {
|
||||
o.DisplayName = *patch.DisplayName
|
||||
o.DisplayName = strings.TrimSpace(*patch.DisplayName)
|
||||
}
|
||||
|
||||
if patch.Name != nil {
|
||||
|
||||
@@ -1163,6 +1163,7 @@ type ExperimentalSettings struct {
|
||||
DisableWakeUpReconnectHandler *bool `access:"experimental_features"`
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds *int64 `access:"experimental_features"`
|
||||
YoutubeReferrerPolicy *bool `access:"experimental_features"`
|
||||
ExperimentalChannelCategorySorting *bool `access:"experimental_features"`
|
||||
}
|
||||
|
||||
func (s *ExperimentalSettings) SetDefaults() {
|
||||
@@ -1213,6 +1214,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
||||
if s.YoutubeReferrerPolicy == nil {
|
||||
s.YoutubeReferrerPolicy = NewPointer(false)
|
||||
}
|
||||
|
||||
if s.ExperimentalChannelCategorySorting == nil {
|
||||
s.ExperimentalChannelCategorySorting = NewPointer(false)
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyticsSettings struct {
|
||||
|
||||
@@ -6812,6 +6812,13 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
help_text: defineMessage({id: 'admin.experimental.youtubeReferrerPolicy.desc', defaultMessage: 'When true, the referrer policy for embedded YouTube videos will be set to "strict-origin-when-cross-origin" which resolves issues where YouTube video previews display as unavailable, while balancing the need to protect user privacy with some degree of referral data to support web functionalities, like analytics, logging, and third-party integrations. When false, the referrer policy will be set to "no-referrer" which enhances user privacy by not disclosing the source URL, but limits the ability to track user engagement and traffic sources in analytics tools.'}),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ExperimentalSettings.ExperimentalChannelCategorySorting',
|
||||
label: defineMessage({id: 'admin.experimental.channelCategorySorting.title', defaultMessage: 'Channel Category Sorting:'}),
|
||||
help_text: defineMessage({id: 'admin.experimental.channelCategorySorting.desc', defaultMessage: 'When true, channels will be automatically sorted into categories based on their names using a "/" delimiter.'}),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -237,7 +237,7 @@ function ChannelSettingsInfoTab({
|
||||
header: channelHeader.trim(),
|
||||
};
|
||||
|
||||
const {error} = await dispatch(patchChannel(channel.id, updated));
|
||||
const {data, error} = await dispatch(patchChannel(channel.id, updated));
|
||||
if (error) {
|
||||
handleServerError(error as ServerError);
|
||||
return false;
|
||||
@@ -245,10 +245,10 @@ function ChannelSettingsInfoTab({
|
||||
|
||||
// After every successful save, update local state to match the saved values
|
||||
// with this, we make sure that the unsavedChanges check will return false after saving
|
||||
setDisplayName(updated.display_name);
|
||||
setChannelURL(updated.name);
|
||||
setChannelPurpose(updated.purpose);
|
||||
setChannelHeader(updated.header);
|
||||
setDisplayName(data?.display_name ?? updated.display_name);
|
||||
setChannelURL(data?.name ?? updated.name);
|
||||
setChannelPurpose(data?.purpose ?? updated.purpose);
|
||||
setChannelHeader(data?.header ?? updated.header);
|
||||
return true;
|
||||
}, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setFormError, handleServerError]);
|
||||
|
||||
|
||||
@@ -1033,6 +1033,8 @@
|
||||
"admin.experimental.allowCustomThemes.title": "Allow Custom Themes:",
|
||||
"admin.experimental.allowedEmailDomain.desc": "(Optional) When set, users must have an email ending in this domain to move threads. Multiple domains can be specified by separating them with commas.",
|
||||
"admin.experimental.allowedEmailDomain.title": "Allowed Email Domain",
|
||||
"admin.experimental.channelCategorySorting.desc": "When true, channels will be automatically sorted into categories based on their names using a \"/\" delimiter.",
|
||||
"admin.experimental.channelCategorySorting.title": "Channel Category Sorting:",
|
||||
"admin.experimental.clientSideCertCheck.desc": "When **primary**, after the client side certificate is verified, user’s email is retrieved from the certificate and is used to log in without a password. When **secondary**, after the client side certificate is verified, user’s email is retrieved from the certificate and matched against the one supplied by the user. If they match, the user logs in with regular email/password credentials.",
|
||||
"admin.experimental.clientSideCertCheck.options.primary": "primary",
|
||||
"admin.experimental.clientSideCertCheck.options.secondary": "secondary",
|
||||
|
||||
@@ -69,6 +69,7 @@ export type Channel = {
|
||||
policy_id?: string | null;
|
||||
banner_info?: ChannelBanner;
|
||||
policy_enforced?: boolean;
|
||||
default_category_name?: string;
|
||||
};
|
||||
|
||||
export type ServerChannel = Channel & {
|
||||
|
||||
@@ -112,6 +112,7 @@ export type ClientConfig = {
|
||||
EnableUserDeactivation: string;
|
||||
EnableUserTypingMessages: string;
|
||||
EnforceMultifactorAuthentication: string;
|
||||
ExperimentalChannelCategorySorting: string;
|
||||
ExperimentalClientSideCertCheck: string;
|
||||
ExperimentalClientSideCertEnable: string;
|
||||
ExperimentalEnableAuthenticationTransfer: string;
|
||||
@@ -822,6 +823,7 @@ export type ExperimentalSettings = {
|
||||
DisableWakeUpReconnectHandler: boolean;
|
||||
UsersStatusAndProfileFetchingPollIntervalMilliseconds: number;
|
||||
YoutubeReferrerPolicy: boolean;
|
||||
ExperimentalChannelCategorySorting: boolean;
|
||||
};
|
||||
|
||||
export type AnalyticsSettings = {
|
||||
|
||||
Ссылка в новой задаче
Block a user