PLT-2992 Added the ability to use different themes for each team (#3411)

* Cleaned up user_settings_theme.jsx and import_theme_modal.jsx

* Made ImportThemeModal use a callback to return the theme to the user settings modal instead of saving it directly

* Moved user theme from model to preferences

* Added serverside API to delete preferences TODO update package with client stuff

* Changed constants.jsx so that Preferences and ActionTypes can be imported on their own

* Updated ThemeProps migration code to properly rename solarized code themes

* Fixed warnings thrown by AppDispatcher

* Added clientside UI to support team-specific themes

* Removed debugging code from test

* Fixed setting a user's theme when they haven't set their theme before
Этот коммит содержится в:
Harrison Healey
2016-07-14 10:08:36 -04:00
коммит произвёл Christopher Speller
родитель 8e810bc2eb
Коммит caabfbcdd5
23 изменённых файлов: 671 добавлений и 273 удалений

Просмотреть файл

@@ -16,6 +16,7 @@ func InitPreference() {
BaseRoutes.Preferences.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET") BaseRoutes.Preferences.Handle("/", ApiUserRequired(getAllPreferences)).Methods("GET")
BaseRoutes.Preferences.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST") BaseRoutes.Preferences.Handle("/save", ApiUserRequired(savePreferences)).Methods("POST")
BaseRoutes.Preferences.Handle("/delete", ApiUserRequired(deletePreferences)).Methods("POST")
BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET") BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", ApiUserRequired(getPreferenceCategory)).Methods("GET")
BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET") BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getPreference)).Methods("GET")
} }
@@ -81,3 +82,30 @@ func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(data.ToJson())) w.Write([]byte(data.ToJson()))
} }
} }
func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
preferences, err := model.PreferencesFromJson(r.Body)
if err != nil {
c.Err = model.NewLocAppError("savePreferences", "api.preference.delete_preferences.decode.app_error", nil, err.Error())
c.Err.StatusCode = http.StatusBadRequest
return
}
for _, preference := range preferences {
if c.Session.UserId != preference.UserId {
c.Err = model.NewLocAppError("deletePreferences", "api.preference.delete_preferences.user_id.app_error",
nil, "session.user_id="+c.Session.UserId+",preference.user_id="+preference.UserId)
c.Err.StatusCode = http.StatusForbidden
return
}
}
for _, preference := range preferences {
if result := <-Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil {
c.Err = result.Err
return
}
}
ReturnStatusOK(w)
}

Просмотреть файл

@@ -161,3 +161,49 @@ func TestGetPreference(t *testing.T) {
t.Fatal("preference updated incorrectly") t.Fatal("preference updated incorrectly")
} }
} }
func TestDeletePreferences(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
user1 := th.BasicUser
var originalCount int
if result, err := Client.GetAllPreferences(); err != nil {
t.Fatal(err)
} else {
originalCount = len(result.Data.(model.Preferences))
}
// save 10 preferences
var preferences model.Preferences
for i := 0; i < 10; i++ {
preference := model.Preference{
UserId: user1.Id,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: model.NewId(),
}
preferences = append(preferences, preference)
}
if _, err := Client.SetPreferences(&preferences); err != nil {
t.Fatal(err)
}
// delete 10 preferences
th.LoginBasic2()
if _, err := Client.DeletePreferences(&preferences); err == nil {
t.Fatal("shouldn't have been able to delete another user's preferences")
}
th.LoginBasic()
if _, err := Client.DeletePreferences(&preferences); err != nil {
t.Fatal(err)
}
if result, err := Client.GetAllPreferences(); err != nil {
t.Fatal(err)
} else if data := result.Data.(model.Preferences); len(data) != originalCount {
t.Fatal("should've deleted preferences")
}
}

Просмотреть файл

@@ -1139,6 +1139,14 @@
"id": "api.post_get_post_by_id.get.app_error", "id": "api.post_get_post_by_id.get.app_error",
"translation": "Unable to get post" "translation": "Unable to get post"
}, },
{
"id": "api.preference.delete_preferences.decode.app_error",
"translation": "Unable to decode preferences from request"
},
{
"id": "api.preference.delete_preferences.user_id.app_error",
"translation": "Unable to delete preferences for other user"
},
{ {
"id": "api.preference.init.debug", "id": "api.preference.init.debug",
"translation": "Initializing preference api routes" "translation": "Initializing preference api routes"
@@ -3011,6 +3019,10 @@
"id": "model.preference.is_valid.name.app_error", "id": "model.preference.is_valid.name.app_error",
"translation": "Invalid name" "translation": "Invalid name"
}, },
{
"id": "model.preference.is_valid.theme.app_error",
"translation": "Invalid theme"
},
{ {
"id": "model.preference.is_valid.value.app_error", "id": "model.preference.is_valid.value.app_error",
"translation": "Value is too long" "translation": "Value is too long"
@@ -3175,10 +3187,6 @@
"id": "model.user.is_valid.team_id.app_error", "id": "model.user.is_valid.team_id.app_error",
"translation": "Invalid team id" "translation": "Invalid team id"
}, },
{
"id": "model.user.is_valid.theme.app_error",
"translation": "Invalid theme"
},
{ {
"id": "model.user.is_valid.update_at.app_error", "id": "model.user.is_valid.update_at.app_error",
"translation": "Update at must be a valid time" "translation": "Update at must be a valid time"
@@ -3775,6 +3783,10 @@
"id": "store.sql_post.update.app_error", "id": "store.sql_post.update.app_error",
"translation": "We couldn't update the Post" "translation": "We couldn't update the Post"
}, },
{
"id": "store.sql_preference.delete.app_error",
"translation": "We encountered an error while deleting preferences"
},
{ {
"id": "store.sql_preference.delete_unused_features.debug", "id": "store.sql_preference.delete_unused_features.debug",
"translation": "Deleting any unused pre-release features" "translation": "Deleting any unused pre-release features"
@@ -4055,6 +4067,10 @@
"id": "store.sql_user.get_unread_count.app_error", "id": "store.sql_user.get_unread_count.app_error",
"translation": "We could not get the unread message count for the user" "translation": "We could not get the unread message count for the user"
}, },
{
"id": "store.sql_user.migrate_theme.critical",
"translation": "Failed to migrate User.ThemeProps to Preferences table %v"
},
{ {
"id": "store.sql_user.missing_account.const", "id": "store.sql_user.missing_account.const",
"translation": "We couldn't find an existing account matching your email address for this team. This team may require an invite from the team owner to join." "translation": "We couldn't find an existing account matching your email address for this team. This team may require an invite from the team owner to join."

Просмотреть файл

@@ -1556,6 +1556,16 @@ func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
} }
} }
// DeletePreferences deletes a list of preferences owned by the current user. If successful,
// it will return status=ok. Otherwise, an error will be returned.
func (c *Client) DeletePreferences(preferences *Preferences) (bool, *AppError) {
if r, err := c.DoApiPost("/preferences/delete", preferences.ToJson()); err != nil {
return false, err
} else {
return c.CheckStatusOK(r), nil
}
}
func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) { func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil { if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil {
return nil, err return nil, err

Просмотреть файл

@@ -6,6 +6,8 @@ package model
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"regexp"
"strings"
"unicode/utf8" "unicode/utf8"
) )
@@ -17,6 +19,9 @@ const (
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings" PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews" PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
PREFERENCE_CATEGORY_THEME = "theme"
// the name for theme props is the team id
PREFERENCE_CATEGORY_LAST = "last" PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel" PREFERENCE_NAME_LAST_CHANNEL = "channel"
) )
@@ -57,13 +62,48 @@ func (o *Preference) IsValid() *AppError {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category) return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category)
} }
if len(o.Name) == 0 || len(o.Name) > 32 { if len(o.Name) > 32 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name) return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name)
} }
if utf8.RuneCountInString(o.Value) > 128 { if utf8.RuneCountInString(o.Value) > 2000 {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value) return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value)
} }
if o.Category == PREFERENCE_CATEGORY_THEME {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
return NewLocAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value)
}
}
return nil return nil
} }
func (o *Preference) PreUpdate() {
if o.Category == PREFERENCE_CATEGORY_THEME {
// decode the value of theme (a map of strings to string) and eliminate any invalid values
var props map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&props); err != nil {
// just continue, the invalid preference value should get caught by IsValid before saving
return
}
colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
// blank out any invalid theme values
for name, value := range props {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !colorPattern.MatchString(value) {
props[name] = "#ffffff"
}
}
if b, err := json.Marshal(props); err == nil {
o.Value = string(b)
}
}
}

Просмотреть файл

@@ -4,6 +4,7 @@
package model package model
import ( import (
"encoding/json"
"strings" "strings"
"testing" "testing"
) )
@@ -31,7 +32,7 @@ func TestPreferenceIsValid(t *testing.T) {
preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW
if err := preference.IsValid(); err != nil { if err := preference.IsValid(); err != nil {
t.Fatal() t.Fatal(err)
} }
preference.Name = strings.Repeat("01234567890", 20) preference.Name = strings.Repeat("01234567890", 20)
@@ -41,16 +42,48 @@ func TestPreferenceIsValid(t *testing.T) {
preference.Name = NewId() preference.Name = NewId()
if err := preference.IsValid(); err != nil { if err := preference.IsValid(); err != nil {
t.Fatal() t.Fatal(err)
} }
preference.Value = strings.Repeat("01234567890", 20) preference.Value = strings.Repeat("01234567890", 201)
if err := preference.IsValid(); err == nil { if err := preference.IsValid(); err == nil {
t.Fatal() t.Fatal()
} }
preference.Value = "1234garbage" preference.Value = "1234garbage"
if err := preference.IsValid(); err != nil { if err := preference.IsValid(); err != nil {
t.Fatal(err)
}
preference.Category = PREFERENCE_CATEGORY_THEME
if err := preference.IsValid(); err == nil {
t.Fatal() t.Fatal()
} }
preference.Value = `{"color": "#ff0000", "color2": "#faf"}`
if err := preference.IsValid(); err != nil {
t.Fatal(err)
}
}
func TestPreferencePreUpdate(t *testing.T) {
preference := Preference{
Category: PREFERENCE_CATEGORY_THEME,
Value: `{"color": "#ff0000", "color2": "#faf", "codeTheme": "github", "invalid": "invalid"}`,
}
preference.PreUpdate()
var props map[string]string
if err := json.NewDecoder(strings.NewReader(preference.Value)).Decode(&props); err != nil {
t.Fatal(err)
}
if props["color"] != "#ff0000" || props["color2"] != "#faf" || props["codeTheme"] != "github" {
t.Fatal("shouldn't have changed valid props")
}
if props["invalid"] == "invalid" {
t.Fatal("should have changed invalid prop")
}
} }

Просмотреть файл

@@ -49,7 +49,6 @@ type User struct {
AllowMarketing bool `json:"allow_marketing,omitempty"` AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"` Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"` NotifyProps StringMap `json:"notify_props,omitempty"`
ThemeProps StringMap `json:"theme_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"` LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"` LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"` FailedAttempts int `json:"failed_attempts,omitempty"`
@@ -106,10 +105,6 @@ func (u *User) IsValid() *AppError {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id) return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
} }
if len(u.ThemeProps) > 2000 {
return NewLocAppError("User.IsValid", "model.user.is_valid.theme.app_error", nil, "user_id="+u.Id)
}
return nil return nil
} }
@@ -179,21 +174,6 @@ func (u *User) PreUpdate() {
} }
u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",") u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",")
} }
if u.ThemeProps != nil {
colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
// blank out any invalid theme values
for name, value := range u.ThemeProps {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !colorPattern.MatchString(value) {
u.ThemeProps[name] = "#ffffff"
}
}
}
} }
func (u *User) SetDefaultNotifications() { func (u *User) SetDefaultNotifications() {
@@ -282,7 +262,6 @@ func (u *User) ClearNonProfileFields() {
u.AllowMarketing = false u.AllowMarketing = false
u.Props = StringMap{} u.Props = StringMap{}
u.NotifyProps = StringMap{} u.NotifyProps = StringMap{}
u.ThemeProps = StringMap{}
u.LastPasswordUpdate = 0 u.LastPasswordUpdate = 0
u.LastPictureUpdate = 0 u.LastPictureUpdate = 0
u.FailedAttempts = 0 u.FailedAttempts = 0

Просмотреть файл

@@ -39,19 +39,6 @@ func TestUserPreSave(t *testing.T) {
func TestUserPreUpdate(t *testing.T) { func TestUserPreUpdate(t *testing.T) {
user := User{Password: "test"} user := User{Password: "test"}
user.PreUpdate() user.PreUpdate()
user.ThemeProps = StringMap{
"codeTheme": "github",
"awayIndicator": "#cdbd4e",
"buttonColor": "invalid",
}
user.PreUpdate()
if user.ThemeProps["codeTheme"] != "github" || user.ThemeProps["awayIndicator"] != "#cdbd4e" {
t.Fatal("shouldn't have changed valid theme props")
} else if user.ThemeProps["buttonColor"] != "#ffffff" {
t.Fatal("should've changed invalid theme prop")
}
} }
func TestUserUpdateMentionKeysFromUsername(t *testing.T) { func TestUserUpdateMentionKeysFromUsername(t *testing.T) {

Просмотреть файл

@@ -26,7 +26,7 @@ func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore {
table.ColMap("UserId").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26)
table.ColMap("Category").SetMaxSize(32) table.ColMap("Category").SetMaxSize(32)
table.ColMap("Name").SetMaxSize(32) table.ColMap("Name").SetMaxSize(32)
table.ColMap("Value").SetMaxSize(128) table.ColMap("Value").SetMaxSize(2000)
} }
return s return s
@@ -100,6 +100,8 @@ func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
func (s SqlPreferenceStore) save(transaction *gorp.Transaction, preference *model.Preference) StoreResult { func (s SqlPreferenceStore) save(transaction *gorp.Transaction, preference *model.Preference) StoreResult {
result := StoreResult{} result := StoreResult{}
preference.PreUpdate()
if result.Err = preference.IsValid(); result.Err != nil { if result.Err = preference.IsValid(); result.Err != nil {
return result return result
} }
@@ -304,3 +306,26 @@ func (s SqlPreferenceStore) IsFeatureEnabled(feature, userId string) StoreChanne
return storeChannel return storeChannel
} }
func (s SqlPreferenceStore) Delete(userId, category, name string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Exec(
`DELETE FROM
Preferences
WHERE
UserId = :UserId
AND Category = :Category
AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": category, "Name": name}); err != nil {
result.Err = model.NewLocAppError("SqlPreferenceStore.Delete", "store.sql_preference.delete.app_error", nil, err.Error())
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

Просмотреть файл

@@ -193,7 +193,7 @@ func TestPreferenceGetAll(t *testing.T) {
} }
} }
func TestPreferenceDelete(t *testing.T) { func TestPreferenceDeleteByUser(t *testing.T) {
Setup() Setup()
userId := model.NewId() userId := model.NewId()
@@ -367,3 +367,28 @@ func TestDeleteUnusedFeatures(t *testing.T) {
t.Fatalf("Found %d features with value 'true', expected to find at least %d features", val, 2) t.Fatalf("Found %d features with value 'true', expected to find at least %d features", val, 2)
} }
} }
func TestPreferenceDelete(t *testing.T) {
Setup()
preference := model.Preference{
UserId: model.NewId(),
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: model.NewId(),
Value: "value1a",
}
Must(store.Preference().Save(&model.Preferences{preference}))
if prefs := Must(store.Preference().GetAll(preference.UserId)).(model.Preferences); len([]model.Preference(prefs)) != 1 {
t.Fatal("should've returned 1 preference")
}
if result := <-store.Preference().Delete(preference.UserId, preference.Category, preference.Name); result.Err != nil {
t.Fatal(result.Err)
}
if prefs := Must(store.Preference().GetAll(preference.UserId)).(model.Preferences); len([]model.Preference(prefs)) != 0 {
t.Fatal("should've returned no preferences")
}
}

Просмотреть файл

@@ -9,7 +9,9 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/model" "github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
) )
@@ -40,7 +42,6 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("Roles").SetMaxSize(64) table.ColMap("Roles").SetMaxSize(64)
table.ColMap("Props").SetMaxSize(4000) table.ColMap("Props").SetMaxSize(4000)
table.ColMap("NotifyProps").SetMaxSize(2000) table.ColMap("NotifyProps").SetMaxSize(2000)
table.ColMap("ThemeProps").SetMaxSize(2000)
table.ColMap("Locale").SetMaxSize(5) table.ColMap("Locale").SetMaxSize(5)
table.ColMap("MfaSecret").SetMaxSize(128) table.ColMap("MfaSecret").SetMaxSize(128)
} }
@@ -53,27 +54,66 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE) us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE)
// ADDED for 3.2 REMOVE for 3.6 // ADDED for 3.2 REMOVE for 3.6
var data []*model.User if us.DoesColumnExist("Users", "ThemeProps") {
if _, err := us.GetReplica().Select(&data, "SELECT * FROM Users WHERE ThemeProps LIKE '%solarized%'"); err == nil { params := map[string]interface{}{
for _, user := range data { "Category": model.PREFERENCE_CATEGORY_THEME,
shouldUpdate := false "Name": "",
if user.ThemeProps["codeTheme"] == "solarized_dark" { }
user.ThemeProps["codeTheme"] = "solarized-dark"
shouldUpdate = true
} else if user.ThemeProps["codeTheme"] == "solarized_light" {
user.ThemeProps["codeTheme"] = "solarized-light"
shouldUpdate = true
}
if shouldUpdate { transaction, err := us.GetMaster().Begin()
if result := <-us.Update(user, true); result.Err != nil { if err != nil {
return themeMigrationFailed(err)
} }
// increase size of Value column of Preferences table to match the size of the ThemeProps column
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
if _, err := transaction.Exec("ALTER TABLE Preferences ALTER COLUMN Value TYPE varchar(2000)"); err != nil {
themeMigrationFailed(err)
}
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
if _, err := transaction.Exec("ALTER TABLE Preferences MODIFY Value text"); err != nil {
themeMigrationFailed(err)
} }
} }
// copy data across
if _, err := transaction.Exec(
`INSERT INTO
Preferences(UserId, Category, Name, Value)
SELECT
Id, '`+model.PREFERENCE_CATEGORY_THEME+`', '', ThemeProps
FROM
Users`, params); err != nil {
themeMigrationFailed(err)
}
// delete old data
if _, err := transaction.Exec("ALTER TABLE Users DROP COLUMN ThemeProps"); err != nil {
themeMigrationFailed(err)
}
if err := transaction.Commit(); err != nil {
themeMigrationFailed(err)
}
// rename solarized_* code themes to solarized-* to match client changes in 3.0
var data model.Preferences
if _, err := us.GetReplica().Select(&data, "SELECT * FROM Preferences WHERE Category = '"+model.PREFERENCE_CATEGORY_THEME+"' AND Value LIKE '%solarized_%'"); err == nil {
for i := range data {
data[i].Value = strings.Replace(data[i].Value, "solarized_", "solarized-", -1)
}
us.Preference().Save(&data)
}
} }
} }
func themeMigrationFailed(err error) {
l4g.Critical(utils.T("store.sql_user.migrate_theme.critical"), err)
time.Sleep(time.Second)
panic(fmt.Sprintf(utils.T("store.sql_user.migrate_theme.critical"), err.Error()))
}
func (us SqlUserStore) CreateIndexesIfNotExists() { func (us SqlUserStore) CreateIndexesIfNotExists() {
us.CreateIndexIfNotExists("idx_users_email", "Users", "Email") us.CreateIndexIfNotExists("idx_users_email", "Users", "Email")
} }

Просмотреть файл

@@ -243,6 +243,7 @@ type PreferenceStore interface {
Get(userId string, category string, name string) StoreChannel Get(userId string, category string, name string) StoreChannel
GetCategory(userId string, category string) StoreChannel GetCategory(userId string, category string) StoreChannel
GetAll(userId string) StoreChannel GetAll(userId string) StoreChannel
Delete(userId, category, name string) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel PermanentDeleteByUser(userId string) StoreChannel
IsFeatureEnabled(feature, userId string) StoreChannel IsFeatureEnabled(feature, userId string) StoreChannel
} }

Просмотреть файл

@@ -1,10 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import Client from 'utils/web_client.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'utils/web_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {ActionTypes, Preferences} from 'utils/constants.jsx';
export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) { export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) {
Client.ldapToEmail( Client.ldapToEmail(
@@ -28,3 +33,52 @@ export function getMoreDmList() {
AsyncClient.getProfilesForDirectMessageList(); AsyncClient.getProfilesForDirectMessageList();
AsyncClient.getTeamMembers(TeamStore.getCurrentId()); AsyncClient.getTeamMembers(TeamStore.getCurrentId());
} }
export function saveTheme(teamId, theme, onSuccess, onError) {
AsyncClient.savePreference(
Preferences.CATEGORY_THEME,
teamId,
JSON.stringify(theme),
() => {
onThemeSaved(teamId, theme, onSuccess);
},
(err) => {
onError(err);
}
);
}
function onThemeSaved(teamId, theme, onSuccess) {
const themePreferences = PreferenceStore.getCategory(Preferences.CATEGORY_THEME);
if (teamId !== '' && themePreferences.size > 1) {
// no extra handling to be done to delete team-specific themes
onSuccess();
return;
}
const toDelete = [];
for (const [name] of themePreferences) {
if (name === '') {
continue;
}
toDelete.push({
user_id: UserStore.getCurrentId(),
category: Preferences.CATEGORY_THEME,
name
});
}
// we're saving a new global theme so delete any team-specific ones
AsyncClient.deletePreferences(toDelete);
// delete them locally before we hear from the server so that the UI flow is smoother
AppDispatcher.handleServerAction({
type: ActionTypes.DELETED_PREFERENCES,
preferences: toDelete
});
onSuccess();
}

Просмотреть файл

@@ -92,15 +92,6 @@ export default class LoggedIn extends React.Component {
id: user.id id: user.id
}); });
} }
// Update CSS classes to match user theme
if (user) {
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
}
} }
onUserChanged() { onUserChanged() {

Просмотреть файл

@@ -41,19 +41,34 @@ export default class NeedsTeam extends React.Component {
constructor(params) { constructor(params) {
super(params); super(params);
this.onChanged = this.onChanged.bind(this); this.onTeamChanged = this.onTeamChanged.bind(this);
this.onPreferencesChanged = this.onPreferencesChanged.bind(this);
const team = TeamStore.getCurrent();
this.state = { this.state = {
team: TeamStore.getCurrent() team,
theme: PreferenceStore.getTheme(team.id)
}; };
} }
onChanged() { onTeamChanged() {
const team = TeamStore.getCurrent();
this.setState({ this.setState({
team: TeamStore.getCurrent() team,
theme: PreferenceStore.getTheme(team.id)
}); });
} }
onPreferencesChanged(category) {
if (!category || category === Preferences.CATEGORY_THEME) {
this.setState({
theme: PreferenceStore.getTheme(this.state.team.id)
});
}
}
componentWillMount() { componentWillMount() {
// Go to tutorial if we are first arriving // Go to tutorial if we are first arriving
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
@@ -63,7 +78,8 @@ export default class NeedsTeam extends React.Component {
} }
componentDidMount() { componentDidMount() {
TeamStore.addChangeListener(this.onChanged); TeamStore.addChangeListener(this.onTeamChanged);
PreferenceStore.addChangeListener(this.onPreferencesChanged);
// Emit view action // Emit view action
GlobalActions.viewLoggedIn(); GlobalActions.viewLoggedIn();
@@ -80,10 +96,19 @@ export default class NeedsTeam extends React.Component {
$(window).on('blur', () => { $(window).on('blur', () => {
window.isActive = false; window.isActive = false;
}); });
Utils.applyTheme(this.state.theme);
}
componentDidUpdate(prevProps, prevState) {
if (!Utils.areObjectsEqual(prevState.theme, this.state.theme)) {
Utils.applyTheme(this.state.theme);
}
} }
componentWillUnmount() { componentWillUnmount() {
TeamStore.removeChangeListener(this.onChanged); TeamStore.removeChangeListener(this.onTeamChanged);
PreferenceStore.removeChangeListener(this.onPreferencesChanged);
$(window).off('focus'); $(window).off('focus');
$(window).off('blur'); $(window).off('blur');
} }

Просмотреть файл

@@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {
</li> </li>
<li className='setting-list-item'> <li className='setting-list-item'>
<hr/> <hr/>
{this.props.submitExtra}
{serverError} {serverError}
{clientError} {clientError}
{submit} {submit}
@@ -113,5 +114,6 @@ SettingItemMax.propTypes = {
updateSection: React.PropTypes.func, updateSection: React.PropTypes.func,
submit: React.PropTypes.func, submit: React.PropTypes.func,
title: React.PropTypes.node, title: React.PropTypes.node,
width: React.PropTypes.string width: React.PropTypes.string,
submitExtra: React.PropTypes.node
}; };

Просмотреть файл

@@ -1,30 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ReactDOM from 'react-dom';
import ModalStore from 'stores/modal_store.jsx'; import ModalStore from 'stores/modal_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
import {Modal} from 'react-bootstrap'; import {Modal} from 'react-bootstrap';
import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
const holders = defineMessages({
submitError: {
id: 'user.settings.import_theme.submitError',
defaultMessage: 'Invalid format, please try copying and pasting in again.'
}
});
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
import React from 'react'; import React from 'react';
class ImportThemeModal extends React.Component { export default class ImportThemeModal extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -33,26 +21,42 @@ class ImportThemeModal extends React.Component {
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.state = { this.state = {
value: '',
inputError: '', inputError: '',
show: false show: false,
callback: null
}; };
} }
componentDidMount() { componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
} }
componentWillUnmount() { componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
} }
updateShow(show) {
this.setState({show}); updateShow(show, args) {
this.setState({
show,
callback: args.callback
});
} }
handleSubmit(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const text = ReactDOM.findDOMNode(this.refs.input).value; const text = this.state.value;
if (!this.isInputValid(text)) { if (!this.isInputValid(text)) {
this.setState({inputError: this.props.intl.formatMessage(holders.submitError)}); this.setState({
inputError: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
return; return;
} }
@@ -81,26 +85,13 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7'; theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github'; theme.codeTheme = 'github';
const user = UserStore.getCurrentUser(); this.state.callback(theme);
user.theme_props = theme; this.setState({
show: false,
Client.updateUser(user, Constants.UserUpdateEvents.THEME, callback: null
(data) => { });
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_ME,
me: data
});
this.setState({show: false});
Utils.applyTheme(theme);
},
(err) => {
var state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}
);
} }
isInputValid(text) { isInputValid(text) {
if (text.length === 0) { if (text.length === 0) {
return false; return false;
@@ -134,13 +125,25 @@ class ImportThemeModal extends React.Component {
return true; return true;
} }
handleChange(e) { handleChange(e) {
if (this.isInputValid(e.target.value)) { const value = e.target.value;
this.setState({value});
if (this.isInputValid(value)) {
this.setState({inputError: null}); this.setState({inputError: null});
} else { } else {
this.setState({inputError: this.props.intl.formatMessage(holders.submitError)}); this.setState({
inputError: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
} }
} }
render() { render() {
return ( return (
<span> <span>
@@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
<div className='form-group less'> <div className='form-group less'>
<div className='col-sm-9'> <div className='col-sm-9'>
<input <input
ref='input'
type='text' type='text'
className='form-control' className='form-control'
value={this.state.value}
onChange={this.handleChange} onChange={this.handleChange}
/> />
<div className='input__help'> <div className='input__help'>
@@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
); );
} }
} }
ImportThemeModal.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(ImportThemeModal);

Просмотреть файл

@@ -8,28 +8,18 @@ import PremadeThemeChooser from './premade_theme_chooser.jsx';
import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx'; import SettingItemMax from '../setting_item_max.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
import Client from 'utils/web_client.jsx'; import * as UserActions from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx'; import {FormattedMessage} from 'react-intl';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const holders = defineMessages({
themeTitle: {
id: 'user.settings.display.theme.title',
defaultMessage: 'Theme'
},
themeDescribe: {
id: 'user.settings.display.theme.describe',
defaultMessage: 'Open to manage your theme'
}
});
import React from 'react'; import React from 'react';
@@ -47,6 +37,7 @@ export default class ThemeSetting extends React.Component {
this.originalTheme = Object.assign({}, this.state.theme); this.originalTheme = Object.assign({}, this.state.theme);
} }
componentDidMount() { componentDidMount() {
UserStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange);
@@ -54,17 +45,20 @@ export default class ThemeSetting extends React.Component {
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
} }
} }
componentDidUpdate() { componentDidUpdate() {
if (this.props.selected) { if (this.props.selected) {
$('.color-btn').removeClass('active-border'); $('.color-btn').removeClass('active-border');
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.selected && !nextProps.selected) { if (this.props.selected && !nextProps.selected) {
this.resetFields(); this.resetFields();
} }
} }
componentWillUnmount() { componentWillUnmount() {
UserStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange);
@@ -73,27 +67,35 @@ export default class ThemeSetting extends React.Component {
Utils.applyTheme(state.theme); Utils.applyTheme(state.theme);
} }
} }
getStateFromStores() { getStateFromStores() {
const user = UserStore.getCurrentUser(); const teamId = TeamStore.getCurrentId();
let theme = null;
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
theme = Object.assign({}, user.theme_props);
} else {
theme = $.extend(true, {}, Constants.THEMES.default);
}
let type = 'premade';
if (theme.type === 'custom') {
type = 'custom';
}
const theme = PreferenceStore.getTheme(teamId);
if (!theme.codeTheme) { if (!theme.codeTheme) {
theme.codeTheme = Constants.DEFAULT_CODE_THEME; theme.codeTheme = Constants.DEFAULT_CODE_THEME;
} }
return {theme, type}; let showAllTeamsCheckbox = false;
let applyToAllTeams = true;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true') {
// show the "apply to all teams" checkbox if the user is on more than one team
showAllTeamsCheckbox = Object.keys(TeamStore.getAll()).length > 1;
// check the "apply to all teams" checkbox by default if the user has any team-specific themes
applyToAllTeams = PreferenceStore.getCategory(Preferences.CATEGORY_THEME).size <= 1;
}
return {
teamId: TeamStore.getCurrentId(),
theme,
type: theme.type || 'premade',
showAllTeamsCheckbox,
applyToAllTeams
};
} }
onChange() { onChange() {
const newState = this.getStateFromStores(); const newState = this.getStateFromStores();
@@ -103,21 +105,20 @@ export default class ThemeSetting extends React.Component {
this.props.setEnforceFocus(true); this.props.setEnforceFocus(true);
} }
scrollToTop() { scrollToTop() {
$('.ps-container.modal-body').scrollTop(0); $('.ps-container.modal-body').scrollTop(0);
} }
submitTheme(e) { submitTheme(e) {
e.preventDefault(); e.preventDefault();
var user = UserStore.getCurrentUser();
user.theme_props = this.state.theme;
Client.updateUser(user, Constants.UserUpdateEvents.THEME, const teamId = this.state.applyToAllTeams ? '' : this.state.teamId;
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_ME,
me: data
});
UserActions.saveTheme(
teamId,
this.state.theme,
() => {
this.props.setRequireConfirm(false); this.props.setRequireConfirm(false);
this.originalTheme = Object.assign({}, this.state.theme); this.originalTheme = Object.assign({}, this.state.theme);
this.scrollToTop(); this.scrollToTop();
@@ -130,6 +131,7 @@ export default class ThemeSetting extends React.Component {
} }
); );
} }
updateTheme(theme) { updateTheme(theme) {
let themeChanged = this.state.theme.length === theme.length; let themeChanged = this.state.theme.length === theme.length;
if (!themeChanged) { if (!themeChanged) {
@@ -148,9 +150,11 @@ export default class ThemeSetting extends React.Component {
this.setState({theme}); this.setState({theme});
Utils.applyTheme(theme); Utils.applyTheme(theme);
} }
updateType(type) { updateType(type) {
this.setState({type}); this.setState({type});
} }
resetFields() { resetFields() {
const state = this.getStateFromStores(); const state = this.getStateFromStores();
state.serverError = null; state.serverError = null;
@@ -161,17 +165,18 @@ export default class ThemeSetting extends React.Component {
this.props.setRequireConfirm(false); this.props.setRequireConfirm(false);
} }
handleImportModal() { handleImportModal() {
AppDispatcher.handleViewAction({ AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL, type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
value: true value: true,
callback: this.updateTheme
}); });
this.props.setEnforceFocus(false); this.props.setEnforceFocus(false);
} }
render() {
const {formatMessage} = this.props.intl;
render() {
var serverError; var serverError;
if (this.state.serverError) { if (this.state.serverError) {
serverError = this.state.serverError; serverError = this.state.serverError;
@@ -266,9 +271,29 @@ export default class ThemeSetting extends React.Component {
</div> </div>
); );
let allTeamsCheckbox = null;
if (this.state.showAllTeamsCheckbox) {
allTeamsCheckbox = (
<div className='checkbox user-settings__submit-checkbox'>
<label>
<input
type='checkbox'
checked={this.state.applyToAllTeams}
onChange={(e) => this.setState({applyToAllTeams: e.target.checked})}
/>
<FormattedMessage
id='user.settings.display.theme.applyToAllTeams'
defaultMessage='Apply New Theme to All Teams'
/>
</label>
</div>
);
}
themeUI = ( themeUI = (
<SettingItemMax <SettingItemMax
inputs={inputs} inputs={inputs}
submitExtra={allTeamsCheckbox}
submit={this.submitTheme} submit={this.submitTheme}
server_error={serverError} server_error={serverError}
width='full' width='full'
@@ -281,8 +306,18 @@ export default class ThemeSetting extends React.Component {
} else { } else {
themeUI = ( themeUI = (
<SettingItemMin <SettingItemMin
title={formatMessage(holders.themeTitle)} title={
describe={formatMessage(holders.themeDescribe)} <FormattedMessage
id='user.settings.display.theme.title'
defaultMessage='Theme'
/>
}
describe={
<FormattedMessage
id='user.settings.display.theme.describe'
defaultMessage='Open to manage your theme'
/>
}
updateSection={() => { updateSection={() => {
this.props.updateSection('theme'); this.props.updateSection('theme');
}} }}
@@ -295,11 +330,8 @@ export default class ThemeSetting extends React.Component {
} }
ThemeSetting.propTypes = { ThemeSetting.propTypes = {
intl: intlShape.isRequired,
selected: React.PropTypes.bool.isRequired, selected: React.PropTypes.bool.isRequired,
updateSection: React.PropTypes.func.isRequired, updateSection: React.PropTypes.func.isRequired,
setRequireConfirm: React.PropTypes.func.isRequired, setRequireConfirm: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired setEnforceFocus: React.PropTypes.func.isRequired
}; };
export default injectIntl(ThemeSetting);

Просмотреть файл

@@ -9,7 +9,7 @@ const PayloadSources = Constants.PayloadSources;
const AppDispatcher = Object.assign(new Flux.Dispatcher(), { const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleServerAction: function performServerAction(action) { handleServerAction: function performServerAction(action) {
if (!action.type) { if (!action.type) {
console.warning('handleServerAction called with undefined action type'); // eslint-disable-line no-console console.warn('handleServerAction called with undefined action type'); // eslint-disable-line no-console
} }
var payload = { var payload = {
@@ -21,7 +21,7 @@ const AppDispatcher = Object.assign(new Flux.Dispatcher(), {
handleViewAction: function performViewAction(action) { handleViewAction: function performViewAction(action) {
if (!action.type) { if (!action.type) {
console.warning('handleViewAction called with undefined action type'); // eslint-disable-line no-console console.warn('handleViewAction called with undefined action type'); // eslint-disable-line no-console
} }
var payload = { var payload = {

Просмотреть файл

@@ -475,3 +475,8 @@
.no-resize { .no-resize {
resize: none; resize: none;
} }
.user-settings__submit-checkbox {
padding-top: 0px;
padding-bottom: 20px;
}

Просмотреть файл

@@ -54,6 +54,16 @@ class PreferenceStoreClass extends EventEmitter {
return parseInt(this.preferences.get(key), 10); return parseInt(this.preferences.get(key), 10);
} }
getObject(category, name, defaultValue = null) {
const key = this.getKey(category, name);
if (!this.preferences.has(key)) {
return defaultValue;
}
return JSON.parse(this.preferences.get(key));
}
getCategory(category) { getCategory(category) {
const prefix = category + '--'; const prefix = category + '--';
@@ -78,6 +88,10 @@ class PreferenceStoreClass extends EventEmitter {
} }
} }
deletePreference(preference) {
this.preferences.delete(this.getKey(preference.category, preference.name));
}
clear() { clear() {
this.preferences.clear(); this.preferences.clear();
} }
@@ -94,6 +108,18 @@ class PreferenceStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback); this.removeListener(CHANGE_EVENT, callback);
} }
getTheme(teamId) {
if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, teamId))) {
return this.getObject(Constants.Preferences.CATEGORY_THEME, teamId);
}
if (this.preferences.has(this.getKey(Constants.Preferences.CATEGORY_THEME, ''))) {
return this.getObject(Constants.Preferences.CATEGORY_THEME, '');
}
return Constants.THEMES.default;
}
handleEventPayload(payload) { handleEventPayload(payload) {
const action = payload.action; const action = payload.action;
@@ -108,6 +134,12 @@ class PreferenceStoreClass extends EventEmitter {
this.setPreferencesFromServer(action.preferences); this.setPreferencesFromServer(action.preferences);
this.emitChange(); this.emitChange();
break; break;
case ActionTypes.DELETED_PREFERENCES:
for (const preference of action.preferences) {
this.deletePreference(preference);
}
this.emitChange();
break;
} }
} }
} }

Просмотреть файл

@@ -852,6 +852,29 @@ export function savePreferences(preferences, success, error) {
); );
} }
export function deletePreferences(preferences, success, error) {
Client.deletePreferences(
preferences,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.DELETED_PREFERENCES,
preferences
});
if (success) {
success(data);
}
},
(err) => {
dispatchError(err, 'deletePreferences');
if (error) {
error();
}
}
);
}
export function getSuggestedCommands(command, suggestionId, component) { export function getSuggestedCommands(command, suggestionId, component) {
Client.listCommands( Client.listCommands(
(data) => { (data) => {

Просмотреть файл

@@ -33,100 +33,125 @@ import mattermostDarkThemeImage from 'images/themes/mattermost_dark.png';
import mattermostThemeImage from 'images/themes/mattermost.png'; import mattermostThemeImage from 'images/themes/mattermost.png';
import windows10ThemeImage from 'images/themes/windows_dark.png'; import windows10ThemeImage from 'images/themes/windows_dark.png';
export default { export const Preferences = {
ActionTypes: keyMirror({ CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
RECEIVED_ERROR: null, CATEGORY_DISPLAY_SETTINGS: 'display_settings',
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
DISPLAY_PREFER_FULL_NAME: 'full_name',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
TUTORIAL_STEP: 'tutorial_step',
CHANNEL_DISPLAY_MODE: 'channel_display_mode',
CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
CHANNEL_DISPLAY_MODE_DEFAULT: 'centered',
MESSAGE_DISPLAY: 'message_display',
MESSAGE_DISPLAY_CLEAN: 'clean',
MESSAGE_DISPLAY_COMPACT: 'compact',
MESSAGE_DISPLAY_DEFAULT: 'clean',
COLLAPSE_DISPLAY: 'collapse_previews',
COLLAPSE_DISPLAY_DEFAULT: 'false',
USE_MILITARY_TIME: 'use_military_time',
CATEGORY_THEME: 'theme'
};
CLICK_CHANNEL: null, export const ActionTypes = keyMirror({
CREATE_CHANNEL: null, RECEIVED_ERROR: null,
LEAVE_CHANNEL: null,
CREATE_POST: null,
CREATE_COMMENT: null,
POST_DELETED: null,
REMOVE_POST: null,
RECEIVED_CHANNELS: null, CLICK_CHANNEL: null,
RECEIVED_CHANNEL: null, CREATE_CHANNEL: null,
RECEIVED_MORE_CHANNELS: null, LEAVE_CHANNEL: null,
RECEIVED_CHANNEL_EXTRA_INFO: null, CREATE_POST: null,
CREATE_COMMENT: null,
POST_DELETED: null,
REMOVE_POST: null,
FOCUS_POST: null, RECEIVED_CHANNELS: null,
RECEIVED_POSTS: null, RECEIVED_CHANNEL: null,
RECEIVED_FOCUSED_POST: null, RECEIVED_MORE_CHANNELS: null,
RECEIVED_POST: null, RECEIVED_CHANNEL_EXTRA_INFO: null,
RECEIVED_EDIT_POST: null,
RECEIVED_SEARCH: null,
RECEIVED_SEARCH_TERM: null,
RECEIVED_POST_SELECTED: null,
RECEIVED_MENTION_DATA: null,
RECEIVED_ADD_MENTION: null,
RECEIVED_PROFILES_FOR_DM_LIST: null, FOCUS_POST: null,
RECEIVED_PROFILES: null, RECEIVED_POSTS: null,
RECEIVED_DIRECT_PROFILES: null, RECEIVED_FOCUSED_POST: null,
RECEIVED_ME: null, RECEIVED_POST: null,
RECEIVED_SESSIONS: null, RECEIVED_EDIT_POST: null,
RECEIVED_AUDITS: null, RECEIVED_SEARCH: null,
RECEIVED_TEAMS: null, RECEIVED_SEARCH_TERM: null,
RECEIVED_STATUSES: null, RECEIVED_POST_SELECTED: null,
RECEIVED_PREFERENCE: null, RECEIVED_MENTION_DATA: null,
RECEIVED_PREFERENCES: null, RECEIVED_ADD_MENTION: null,
RECEIVED_FILE_INFO: null,
RECEIVED_ANALYTICS: null,
RECEIVED_INCOMING_WEBHOOKS: null, RECEIVED_PROFILES_FOR_DM_LIST: null,
RECEIVED_INCOMING_WEBHOOK: null, RECEIVED_PROFILES: null,
REMOVED_INCOMING_WEBHOOK: null, RECEIVED_DIRECT_PROFILES: null,
RECEIVED_OUTGOING_WEBHOOKS: null, RECEIVED_ME: null,
RECEIVED_OUTGOING_WEBHOOK: null, RECEIVED_SESSIONS: null,
UPDATED_OUTGOING_WEBHOOK: null, RECEIVED_AUDITS: null,
REMOVED_OUTGOING_WEBHOOK: null, RECEIVED_TEAMS: null,
RECEIVED_COMMANDS: null, RECEIVED_STATUSES: null,
RECEIVED_COMMAND: null, RECEIVED_PREFERENCE: null,
UPDATED_COMMAND: null, RECEIVED_PREFERENCES: null,
REMOVED_COMMAND: null, DELETED_PREFERENCES: null,
RECEIVED_FILE_INFO: null,
RECEIVED_ANALYTICS: null,
RECEIVED_CUSTOM_EMOJIS: null, RECEIVED_INCOMING_WEBHOOKS: null,
RECEIVED_CUSTOM_EMOJI: null, RECEIVED_INCOMING_WEBHOOK: null,
UPDATED_CUSTOM_EMOJI: null, REMOVED_INCOMING_WEBHOOK: null,
REMOVED_CUSTOM_EMOJI: null, RECEIVED_OUTGOING_WEBHOOKS: null,
RECEIVED_OUTGOING_WEBHOOK: null,
UPDATED_OUTGOING_WEBHOOK: null,
REMOVED_OUTGOING_WEBHOOK: null,
RECEIVED_COMMANDS: null,
RECEIVED_COMMAND: null,
UPDATED_COMMAND: null,
REMOVED_COMMAND: null,
RECEIVED_MSG: null, RECEIVED_CUSTOM_EMOJIS: null,
RECEIVED_CUSTOM_EMOJI: null,
UPDATED_CUSTOM_EMOJI: null,
REMOVED_CUSTOM_EMOJI: null,
RECEIVED_MY_TEAM: null, RECEIVED_MSG: null,
CREATED_TEAM: null,
RECEIVED_CONFIG: null, RECEIVED_MY_TEAM: null,
RECEIVED_LOGS: null, CREATED_TEAM: null,
RECEIVED_SERVER_AUDITS: null,
RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
RECEIVED_ALL_TEAMS: null,
RECEIVED_ALL_TEAM_LISTINGS: null,
RECEIVED_TEAM_MEMBERS: null,
RECEIVED_MEMBERS_FOR_TEAM: null,
RECEIVED_LOCALE: null, RECEIVED_CONFIG: null,
RECEIVED_LOGS: null,
RECEIVED_SERVER_AUDITS: null,
RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
RECEIVED_ALL_TEAMS: null,
RECEIVED_ALL_TEAM_LISTINGS: null,
RECEIVED_TEAM_MEMBERS: null,
RECEIVED_MEMBERS_FOR_TEAM: null,
SHOW_SEARCH: null, RECEIVED_LOCALE: null,
USER_TYPING: null, SHOW_SEARCH: null,
TOGGLE_IMPORT_THEME_MODAL: null, USER_TYPING: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_LEAVE_TEAM_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
TOGGLE_GET_POST_LINK_MODAL: null,
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
TOGGLE_REGISTER_APP_MODAL: null,
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
SUGGESTION_PRETEXT_CHANGED: null, TOGGLE_IMPORT_THEME_MODAL: null,
SUGGESTION_RECEIVED_SUGGESTIONS: null, TOGGLE_INVITE_MEMBER_MODAL: null,
SUGGESTION_CLEAR_SUGGESTIONS: null, TOGGLE_LEAVE_TEAM_MODAL: null,
SUGGESTION_COMPLETE_WORD: null, TOGGLE_DELETE_POST_MODAL: null,
SUGGESTION_SELECT_NEXT: null, TOGGLE_GET_POST_LINK_MODAL: null,
SUGGESTION_SELECT_PREVIOUS: null TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
}), TOGGLE_REGISTER_APP_MODAL: null,
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
SUGGESTION_PRETEXT_CHANGED: null,
SUGGESTION_RECEIVED_SUGGESTIONS: null,
SUGGESTION_CLEAR_SUGGESTIONS: null,
SUGGESTION_COMPLETE_WORD: null,
SUGGESTION_SELECT_NEXT: null,
SUGGESTION_SELECT_PREVIOUS: null
});
export const Constants = {
Preferences,
ActionTypes,
PayloadSources: keyMirror({ PayloadSources: keyMirror({
SERVER_ACTION: null, SERVER_ACTION: null,
@@ -174,7 +199,6 @@ export default {
FULLNAME: 'fullname', FULLNAME: 'fullname',
NICKNAME: 'nickname', NICKNAME: 'nickname',
EMAIL: 'email', EMAIL: 'email',
THEME: 'theme',
LANGUAGE: 'language' LANGUAGE: 'language'
}, },
@@ -551,25 +575,6 @@ export default {
Ubuntu: 'font--ubuntu' Ubuntu: 'font--ubuntu'
}, },
DEFAULT_FONT: 'Open Sans', DEFAULT_FONT: 'Open Sans',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
DISPLAY_PREFER_FULL_NAME: 'full_name',
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
TUTORIAL_STEP: 'tutorial_step',
CHANNEL_DISPLAY_MODE: 'channel_display_mode',
CHANNEL_DISPLAY_MODE_CENTERED: 'centered',
CHANNEL_DISPLAY_MODE_FULL_SCREEN: 'full',
CHANNEL_DISPLAY_MODE_DEFAULT: 'full',
MESSAGE_DISPLAY: 'message_display',
MESSAGE_DISPLAY_CLEAN: 'clean',
MESSAGE_DISPLAY_COMPACT: 'compact',
MESSAGE_DISPLAY_DEFAULT: 'clean',
COLLAPSE_DISPLAY: 'collapse_previews',
COLLAPSE_DISPLAY_DEFAULT: 'false',
USE_MILITARY_TIME: 'use_military_time'
},
TutorialSteps: { TutorialSteps: {
INTRO_SCREENS: 0, INTRO_SCREENS: 0,
POST_POPOVER: 1, POST_POPOVER: 1,
@@ -779,3 +784,5 @@ export default {
PERMISSIONS_TEAM_ADMIN: 'team_admin', PERMISSIONS_TEAM_ADMIN: 'team_admin',
PERMISSIONS_SYSTEM_ADMIN: 'system_admin' PERMISSIONS_SYSTEM_ADMIN: 'system_admin'
}; };
export default Constants;