Merge pull request #1416 from florianorben/PLT-1125

PLT-1125: Add "Preview pre-release features" option
Этот коммит содержится в:
Corey Hulen
2015-11-27 19:58:05 -08:00
родитель 42a001c4e2 c50144de04
Коммит d4eb8743e3
10 изменённых файлов: 341 добавлений и 30 удалений

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

@@ -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;
}