Merge pull request #1444 from matrix-org/luke/refactor-editable-list-from-alias-settings

Factor out EditableItemList from AliasSettings
This commit is contained in:
Luke Barnard 2017-10-04 10:56:17 +01:00 committed by GitHub
commit 1938a767a2
2 changed files with 191 additions and 75 deletions

View file

@ -0,0 +1,149 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler.js';
const EditableItem = React.createClass({
displayName: 'EditableItem',
propTypes: {
initialValue: PropTypes.string,
index: PropTypes.number,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onRemove: PropTypes.func,
onAdd: PropTypes.func,
addOnChange: PropTypes.bool,
},
onChange: function(value) {
this.setState({ value });
if (this.props.onChange) this.props.onChange(value, this.props.index);
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
},
onRemove: function() {
if (this.props.onRemove) this.props.onRemove(this.props.index);
},
onAdd: function() {
if (this.props.onAdd) this.props.onAdd(this.state.value);
},
render: function() {
const EditableText = sdk.getComponent('elements.EditableText');
return <div className="mx_EditableItem">
<EditableText
className="mx_EditableItem_editable"
placeholderClassName="mx_EditableItem_editablePlaceholder"
placeholder={this.props.placeholder}
blurToCancel={false}
editable={true}
initialValue={this.props.initialValue}
onValueChanged={this.onChange} />
{ this.props.onAdd ?
<div className="mx_EditableItem_addButton">
<img className="mx_filterFlipColor"
src="img/plus.svg" width="14" height="14"
alt={_t("Add")} onClick={this.onAdd} />
</div>
:
<div className="mx_EditableItem_removeButton">
<img className="mx_filterFlipColor"
src="img/cancel-small.svg" width="14" height="14"
alt={_t("Delete")} onClick={this.onRemove} />
</div>
}
</div>;
},
});
module.exports = React.createClass({
displayName: 'EditableItemList',
propTypes: {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
onNewItemChanged: PropTypes.func,
onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func,
onItemRemoved: PropTypes. func,
},
getDefaultProps: function() {
return {
onItemAdded: () => {},
onItemEdited: () => {},
onItemRemoved: () => {},
onNewItemChanged: () => {},
};
},
onItemAdded: function(value) {
this.props.onItemAdded(value);
},
onItemEdited: function(value, index) {
if (value.length === 0) {
this.onItemRemoved(index);
} else {
this.onItemEdited(value, index);
}
},
onItemRemoved: function(index) {
this.props.onItemRemoved(index);
},
onNewItemChanged: function(value) {
this.props.onNewItemChanged(value);
},
render: function() {
const editableItems = this.props.items.map((item, index) => {
return <EditableItem
key={index}
index={index}
initialValue={item}
onChange={this.onItemEdited}
onRemove={this.onItemRemoved}
placeholder={this.props.placeholder}
/>;
});
const label = this.props.items.length > 0 ?
this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItems }
<EditableItem
key={-1}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}
addOnChange={true}
placeholder={this.props.placeholder}
/>
</div>);
},
});

View file

@ -136,24 +136,25 @@ module.exports = React.createClass({
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases); return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
}, },
onAliasAdded: function(alias) { onNewAliasChanged: function(value) {
this.setState({newAlias: value});
},
onLocalAliasAdded: function(alias) {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
if (this.isAliasValid(alias)) { const localDomain = MatrixClientPeg.get().getDomain();
// add this alias to the domain to aliases dict if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
var domain = alias.replace(/^.*?:/, ''); this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
// XXX: do we need to deep copy aliases before editing it? this.state.domainToAliases[localDomain].push(alias);
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
this.state.domainToAliases[domain].push(alias);
this.setState({
domainToAliases: this.state.domainToAliases
});
// reset the add field this.setState({
this.refs.add_alias.setValue(''); // FIXME domainToAliases: this.state.domainToAliases,
} // Reset the add field
else { newAlias: "",
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); });
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, { Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
title: _t('Invalid alias format'), title: _t('Invalid alias format'),
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }), description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
@ -161,15 +162,13 @@ module.exports = React.createClass({
} }
}, },
onAliasChanged: function(domain, index, alias) { onLocalAliasChanged: function(alias, index) {
if (alias === "") return; // hit the delete button to delete please if (alias === "") return; // hit the delete button to delete please
var oldAlias; const localDomain = MatrixClientPeg.get().getDomain();
if (this.isAliasValid(alias)) { if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
oldAlias = this.state.domainToAliases[domain][index]; this.state.domainToAliases[localDomain][index] = alias;
this.state.domainToAliases[domain][index] = alias; } else {
} const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, { Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
title: _t('Invalid address format'), title: _t('Invalid address format'),
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }), description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
@ -177,15 +176,16 @@ module.exports = React.createClass({
} }
}, },
onAliasDeleted: function(domain, index) { onLocalAliasDeleted: function(index) {
const localDomain = MatrixClientPeg.get().getDomain();
// It's a bit naughty to directly manipulate this.state, and React would // It's a bit naughty to directly manipulate this.state, and React would
// normally whine at you, but it can't see us doing the splice. Given we // normally whine at you, but it can't see us doing the splice. Given we
// promptly setState anyway, it's just about acceptable. The alternative // promptly setState anyway, it's just about acceptable. The alternative
// would be to arbitrarily deepcopy to a temp variable and then setState // would be to arbitrarily deepcopy to a temp variable and then setState
// that, but why bother when we can cut this corner. // that, but why bother when we can cut this corner.
var alias = this.state.domainToAliases[domain].splice(index, 1); this.state.domainToAliases[localDomain].splice(index, 1);
this.setState({ this.setState({
domainToAliases: this.state.domainToAliases domainToAliases: this.state.domainToAliases,
}); });
}, },
@ -198,6 +198,7 @@ module.exports = React.createClass({
render: function() { render: function() {
var self = this; var self = this;
var EditableText = sdk.getComponent("elements.EditableText"); var EditableText = sdk.getComponent("elements.EditableText");
var EditableItemList = sdk.getComponent("elements.EditableItemList");
var localDomain = MatrixClientPeg.get().getDomain(); var localDomain = MatrixClientPeg.get().getDomain();
var canonical_alias_section; var canonical_alias_section;
@ -257,58 +258,24 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_aliasLabel"> <div className="mx_RoomSettings_aliasLabel">
{ _t('The main address for this room is') }: { canonical_alias_section } { _t('The main address for this room is') }: { canonical_alias_section }
</div> </div>
<div className="mx_RoomSettings_aliasLabel"> <EditableItemList
{ (this.state.domainToAliases[localDomain] && className={"mx_RoomSettings_localAliases"}
this.state.domainToAliases[localDomain].length > 0) items={this.state.domainToAliases[localDomain] || []}
? _t('Local addresses for this room:') newItem={this.state.newAlias}
: _t('This room has no local addresses') } onNewItemChanged={this.onNewAliasChanged}
</div> onItemAdded={this.onLocalAliasAdded}
<div className="mx_RoomSettings_aliasesTable"> onItemEdited={this.onLocalAliasChanged}
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => { onItemRemoved={this.onLocalAliasDeleted}
var deleteButton; itemsLabel={_t('Local addresses for this room:')}
if (this.props.canSetAliases) { noItemsLabel={_t('This room has no local addresses')}
deleteButton = ( placeholder={_t(
<img src="img/cancel-small.svg" width="14" height="14" 'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } /> )}
); />
}
return (
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
<EditableText
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
editable={ self.props.canSetAliases }
initialValue={ alias } />
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
{ deleteButton }
</div>
</div>
);
})}
{ this.props.canSetAliases ?
<div className="mx_RoomSettings_aliasesTableRow" key="new">
<EditableText
ref="add_alias"
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasAdded } />
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt={ _t("Add") }
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
</div> : ""
}
</div>
{ remote_aliases_section } { remote_aliases_section }
</div> </div>
); );
} },
}); });