Merge pull request #1416 from florianorben/PLT-1125
PLT-1125: Add "Preview pre-release features" option
Этот коммит содержится в:
@@ -12,6 +12,7 @@ import (
|
||||
const (
|
||||
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
|
||||
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
|
||||
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
|
||||
)
|
||||
|
||||
type Preference struct {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
l4g "code.google.com/p/log4go"
|
||||
"github.com/go-gorp/gorp"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
@@ -13,6 +14,10 @@ type SqlPreferenceStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
const (
|
||||
FEATURE_TOGGLE_PREFIX = "feature_enabled_"
|
||||
)
|
||||
|
||||
func NewSqlPreferenceStore(sqlStore *SqlStore) PreferenceStore {
|
||||
s := &SqlPreferenceStore{sqlStore}
|
||||
|
||||
@@ -36,6 +41,23 @@ func (s SqlPreferenceStore) CreateIndexesIfNotExists() {
|
||||
s.CreateIndexIfNotExists("idx_preferences_name", "Preferences", "Name")
|
||||
}
|
||||
|
||||
func (s SqlPreferenceStore) DeleteUnusedFeatures() {
|
||||
l4g.Debug("Deleting any unused pre-release features")
|
||||
|
||||
sql := `DELETE
|
||||
FROM Preferences
|
||||
WHERE
|
||||
Category = :Category
|
||||
AND Value = :Value
|
||||
AND Name LIKE '` + FEATURE_TOGGLE_PREFIX + `%'`
|
||||
|
||||
queryParams := map[string]string{
|
||||
"Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
|
||||
"Value": "false",
|
||||
}
|
||||
s.GetMaster().Exec(sql, queryParams)
|
||||
}
|
||||
|
||||
func (s SqlPreferenceStore) Save(preferences *model.Preferences) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
@@ -257,3 +279,28 @@ func (s SqlPreferenceStore) PermanentDeleteByUser(userId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPreferenceStore) IsFeatureEnabled(feature, userId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
if value, err := s.GetReplica().SelectStr(`SELECT
|
||||
value
|
||||
FROM
|
||||
Preferences
|
||||
WHERE
|
||||
UserId = :UserId
|
||||
AND Category = :Category
|
||||
AND Name = :Name`, map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Name": FEATURE_TOGGLE_PREFIX + feature}); err != nil {
|
||||
result.Err = model.NewAppError("SqlPreferenceStore.IsFeatureEnabled", "We encountered an error while finding a pre release feature preference", err.Error())
|
||||
} else {
|
||||
result.Data = value == "true"
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -232,3 +232,138 @@ func TestPreferenceDelete(t *testing.T) {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFeatureEnabled(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
feature1 := "testFeat1"
|
||||
feature2 := "testFeat2"
|
||||
feature3 := "testFeat3"
|
||||
|
||||
userId := model.NewId()
|
||||
category := model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS
|
||||
|
||||
features := model.Preferences{
|
||||
{
|
||||
UserId: userId,
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature1,
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
UserId: userId,
|
||||
Category: category,
|
||||
Name: model.NewId(),
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
UserId: userId,
|
||||
Category: model.NewId(),
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature1,
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
UserId: model.NewId(),
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature2,
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
UserId: model.NewId(),
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature3,
|
||||
Value: "foobar",
|
||||
},
|
||||
}
|
||||
|
||||
Must(store.Preference().Save(&features))
|
||||
|
||||
if result := <-store.Preference().IsFeatureEnabled(feature1, userId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if data := result.Data.(bool); data != true {
|
||||
t.Fatalf("got incorrect setting for feature1, %v=%v", true, data)
|
||||
}
|
||||
|
||||
if result := <-store.Preference().IsFeatureEnabled(feature2, userId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if data := result.Data.(bool); data != false {
|
||||
t.Fatalf("got incorrect setting for feature2, %v=%v", false, data)
|
||||
}
|
||||
|
||||
// make sure we get false if something different than "true" or "false" has been saved to database
|
||||
if result := <-store.Preference().IsFeatureEnabled(feature3, userId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if data := result.Data.(bool); data != false {
|
||||
t.Fatalf("got incorrect setting for feature3, %v=%v", false, data)
|
||||
}
|
||||
|
||||
// make sure false is returned if a non-existent feature is queried
|
||||
if result := <-store.Preference().IsFeatureEnabled("someOtherFeature", userId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if data := result.Data.(bool); data != false {
|
||||
t.Fatalf("got incorrect setting for non-existent feature 'someOtherFeature', %v=%v", false, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUnusedFeatures(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
userId1 := model.NewId()
|
||||
userId2 := model.NewId()
|
||||
category := model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS
|
||||
feature1 := "feature1"
|
||||
feature2 := "feature2"
|
||||
|
||||
features := model.Preferences{
|
||||
{
|
||||
UserId: userId1,
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature1,
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
UserId: userId2,
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature1,
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
UserId: userId1,
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature2,
|
||||
Value: "false",
|
||||
},
|
||||
{
|
||||
UserId: userId2,
|
||||
Category: category,
|
||||
Name: FEATURE_TOGGLE_PREFIX + feature2,
|
||||
Value: "true",
|
||||
},
|
||||
}
|
||||
|
||||
Must(store.Preference().Save(&features))
|
||||
|
||||
store.(*SqlStore).preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
//make sure features with value "false" have actually been deleted from the database
|
||||
if val, err := store.(*SqlStore).preference.(*SqlPreferenceStore).GetReplica().SelectInt(`SELECT COUNT(*)
|
||||
FROM Preferences
|
||||
WHERE Category = :Category
|
||||
AND Value = :Val
|
||||
AND Name LIKE '`+FEATURE_TOGGLE_PREFIX+`%'`, map[string]interface{}{"Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Val": "false"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if val != 0 {
|
||||
t.Fatalf("Found %d features with value 'false', expected all to be deleted", val)
|
||||
}
|
||||
//
|
||||
// make sure features with value "true" remain saved
|
||||
if val, err := store.(*SqlStore).preference.(*SqlPreferenceStore).GetReplica().SelectInt(`SELECT COUNT(*)
|
||||
FROM Preferences
|
||||
WHERE Category = :Category
|
||||
AND Value = :Val
|
||||
AND Name LIKE '`+FEATURE_TOGGLE_PREFIX+`%'`, map[string]interface{}{"Category": model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, "Val": "true"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if val == 0 {
|
||||
t.Fatalf("Found %d features with value 'true', expected to find at least %d features", val, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,8 @@ func NewSqlStore() Store {
|
||||
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
|
||||
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
|
||||
|
||||
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
|
||||
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
|
||||
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
|
||||
|
||||
@@ -186,4 +186,5 @@ type PreferenceStore interface {
|
||||
GetCategory(userId string, category string) StoreChannel
|
||||
GetAll(userId string) StoreChannel
|
||||
PermanentDeleteByUser(userId string) StoreChannel
|
||||
IsFeatureEnabled(feature, userId string) StoreChannel
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as Utils from '../utils/utils.jsx';
|
||||
import Constants from '../utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
||||
|
||||
export default class Textbox extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -303,7 +304,19 @@ export default class Textbox extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const previewLinkVisible = this.props.messageText.length > 0;
|
||||
let previewLink = null;
|
||||
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
|
||||
const previewLinkVisible = this.props.messageText.length > 0;
|
||||
previewLink = (
|
||||
<a
|
||||
style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
|
||||
onClick={this.showPreview}
|
||||
className='textbox-preview-link'
|
||||
>
|
||||
{this.state.preview ? 'Edit message' : 'Preview'}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -342,19 +355,13 @@ export default class Textbox extends React.Component {
|
||||
dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.messageText) : ''}}
|
||||
>
|
||||
</div>
|
||||
{previewLink}
|
||||
<a
|
||||
onClick={this.showHelp}
|
||||
className='textbox-help-link'
|
||||
>
|
||||
{'Help'}
|
||||
</a>
|
||||
<a
|
||||
style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
|
||||
onClick={this.showPreview}
|
||||
className='textbox-preview-link'
|
||||
>
|
||||
{this.state.preview ? 'Edit' : 'Preview'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import SettingItemMin from '../setting_item_min.jsx';
|
||||
import SettingItemMax from '../setting_item_max.jsx';
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
import PreferenceStore from '../../stores/preference_store.jsx';
|
||||
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
||||
|
||||
export default class AdvancedSettingsDisplay extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,21 +14,33 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
|
||||
this.updateSection = this.updateSection.bind(this);
|
||||
this.updateSetting = this.updateSetting.bind(this);
|
||||
this.setupInitialState = this.setupInitialState.bind(this);
|
||||
this.toggleFeature = this.toggleFeature.bind(this);
|
||||
this.saveEnabledFeatures = this.saveEnabledFeatures.bind(this);
|
||||
|
||||
this.state = this.setupInitialState();
|
||||
}
|
||||
|
||||
setupInitialState() {
|
||||
const sendOnCtrlEnter = PreferenceStore.getPreference(
|
||||
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
'send_on_ctrl_enter',
|
||||
{value: 'false'}
|
||||
).value;
|
||||
|
||||
return {
|
||||
settings: {send_on_ctrl_enter: sendOnCtrlEnter}
|
||||
const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
|
||||
const advancedSettings = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
|
||||
const settings = {
|
||||
send_on_ctrl_enter: PreferenceStore.getPreference(
|
||||
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
'send_on_ctrl_enter',
|
||||
{value: 'false'}
|
||||
).value
|
||||
};
|
||||
|
||||
let enabledFeatures = 0;
|
||||
advancedSettings.forEach((setting) => {
|
||||
preReleaseFeaturesKeys.forEach((key) => {
|
||||
const feature = PreReleaseFeatures[key];
|
||||
if (setting.name === Constants.FeatureTogglePrefix + feature.label) {
|
||||
settings[setting.name] = setting.value;
|
||||
if (setting.value === 'true') {
|
||||
enabledFeatures++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures};
|
||||
}
|
||||
|
||||
updateSetting(setting, value) {
|
||||
@@ -36,14 +49,45 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
this.setState(settings);
|
||||
}
|
||||
|
||||
handleSubmit(setting) {
|
||||
const preference = PreferenceStore.setPreference(
|
||||
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
setting,
|
||||
this.state.settings[setting]
|
||||
);
|
||||
toggleFeature(feature, checked) {
|
||||
const settings = this.state.settings;
|
||||
settings[Constants.FeatureTogglePrefix + feature] = String(checked);
|
||||
|
||||
Client.savePreferences([preference],
|
||||
let enabledFeatures = 0;
|
||||
Object.keys(this.state.settings).forEach((setting) => {
|
||||
if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0 && this.state.settings[setting] === 'true') {
|
||||
enabledFeatures++;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({settings, enabledFeatures});
|
||||
}
|
||||
|
||||
saveEnabledFeatures() {
|
||||
const features = [];
|
||||
Object.keys(this.state.settings).forEach((setting) => {
|
||||
if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0) {
|
||||
features.push(setting);
|
||||
}
|
||||
});
|
||||
|
||||
this.handleSubmit(features);
|
||||
}
|
||||
|
||||
handleSubmit(settings) {
|
||||
const preferences = [];
|
||||
|
||||
(Array.isArray(settings) ? settings : [settings]).forEach((setting) => {
|
||||
preferences.push(
|
||||
PreferenceStore.setPreference(
|
||||
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
setting,
|
||||
String(this.state.settings[setting])
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Client.savePreferences(preferences,
|
||||
() => {
|
||||
PreferenceStore.emitChange();
|
||||
this.updateSection('');
|
||||
@@ -118,6 +162,66 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let previewFeaturesSection;
|
||||
let previewFeaturesSectionDivider;
|
||||
if (this.state.preReleaseFeaturesKeys.length > 0) {
|
||||
previewFeaturesSectionDivider = (
|
||||
<div className='divider-light'/>
|
||||
);
|
||||
|
||||
if (this.props.activeSection === 'advancedPreviewFeatures') {
|
||||
const inputs = [];
|
||||
|
||||
this.state.preReleaseFeaturesKeys.forEach((key) => {
|
||||
const feature = this.state.preReleaseFeatures[key];
|
||||
inputs.push(
|
||||
<div key={'advancedPreviewFeatures_' + feature.label}>
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={this.state.settings[Constants.FeatureTogglePrefix + feature.label] === 'true'}
|
||||
onChange={(e) => {
|
||||
this.toggleFeature(feature.label, e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{feature.description}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
inputs.push(
|
||||
<div key='advancedPreviewFeatures_helptext'>
|
||||
<br/>
|
||||
{'Check any pre-released features you\'d like to preview.'}
|
||||
</div>
|
||||
);
|
||||
|
||||
previewFeaturesSection = (
|
||||
<SettingItemMax
|
||||
title='Preview pre-release features'
|
||||
inputs={inputs}
|
||||
submit={this.saveEnabledFeatures}
|
||||
server_error={serverError}
|
||||
updateSection={(e) => {
|
||||
this.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
previewFeaturesSection = (
|
||||
<SettingItemMin
|
||||
title='Preview pre-release features'
|
||||
describe={this.state.enabledFeatures + (this.state.enabledFeatures === 1 ? ' Feature ' : ' Features ') + 'enabled'}
|
||||
updateSection={() => this.props.updateSection('advancedPreviewFeatures')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
@@ -145,6 +249,8 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
<h3 className='tab-header'>{'Advanced Settings'}</h3>
|
||||
<div className='divider-dark first'/>
|
||||
{ctrlSendSection}
|
||||
{previewFeaturesSectionDivider}
|
||||
{previewFeaturesSection}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -398,5 +398,12 @@ export default {
|
||||
},
|
||||
NotificationPrefs: {
|
||||
MENTION: 'mention'
|
||||
},
|
||||
FeatureTogglePrefix: 'feature_enabled_',
|
||||
PRE_RELEASE_FEATURES: {
|
||||
MARKDOWN_PREVIEW: {
|
||||
label: 'markdown_preview', // github issue: https://github.com/mattermost/platform/pull/1389
|
||||
description: 'Show markdown preview option in message input box'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1221,3 +1221,7 @@ export function getPostTerm(post) {
|
||||
|
||||
return postTerm;
|
||||
}
|
||||
|
||||
export function isFeatureEnabled(feature) {
|
||||
return PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, Constants.FeatureTogglePrefix + feature.label, {value: 'false'}).value === 'true';
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ body.ios {
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: none;
|
||||
white-space: normal;
|
||||
}
|
||||
.textbox-preview-link, .textbox-help-link {
|
||||
position: absolute;
|
||||
@@ -283,14 +284,14 @@ body.ios {
|
||||
.custom-textarea {
|
||||
padding-top: 8px;
|
||||
padding-right: 28px;
|
||||
max-height: 160px;
|
||||
max-height: 162px !important;
|
||||
overflow: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.textarea-div {
|
||||
padding-top: 8px;
|
||||
padding-right: 30px;
|
||||
max-height: 160px;
|
||||
max-height: 163px !important;
|
||||
overflow: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user