Factor out EditableItemList from AliasSettings

Such that we can reuse the same UI elsewhere, namely when editing related groups of a room (which is an upcoming feature).
This commit is contained in:
Luke Barnard 2017-10-04 10:00:01 +01:00
parent 03581adf85
commit 8243c39d83
2 changed files with 204 additions and 74 deletions

View file

@ -0,0 +1,162 @@
/*
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,
},
onChange: function(value) {
this.setState({ value });
if (this.props.onChange) this.props.onChange(value, this.props.index);
},
onRemove: function() {
this.props.onRemove(this.props.index);
},
onAdd: function() {
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: () => {},
};
},
getInitialState: function() {
return {};
},
componentWillReceiveProps: function(nextProps) {
},
componentWillMount: function() {
},
componentDidMount: function() {
},
onItemAdded: function(value) {
console.info('onItemAdded', value);
this.props.onItemAdded(value);
},
onItemEdited: function(value, index) {
console.info('onItemEdited', value, index);
if (value.length === 0) {
this.onItemRemoved(index);
} else {
this.onItemEdited(value, index);
}
},
onItemRemoved: function(index) {
console.info('onItemRemoved', 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;
console.info('New item:', this.props.newItem);
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}
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,25 @@ 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">
{ (this.state.domainToAliases[localDomain] &&
this.state.domainToAliases[localDomain].length > 0)
? _t('Local addresses for this room:')
: _t('This room has no local addresses') }
</div>
<div className="mx_RoomSettings_aliasesTable">
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
var deleteButton;
if (this.props.canSetAliases) {
deleteButton = (
<img src="img/cancel-small.svg" width="14" height="14"
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 ? <EditableItemList
<div className="mx_RoomSettings_aliasesTableRow" key="new"> className={"mx_RoomSettings_localAliases"}
<EditableText items={this.state.domainToAliases[localDomain] || []}
ref="add_alias" newItem={this.state.newAlias}
className="mx_RoomSettings_alias mx_RoomSettings_editable" onNewItemChanged={onNewAliasChanged}
placeholderClassName="mx_RoomSettings_aliasPlaceholder" onItemAdded={this.onLocalAliasAdded}
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) } onItemEdited={this.onLocalAliasChanged}
blurToCancel={ false } onItemRemoved={this.onLocalAliasDeleted}
onValueChanged={ self.onAliasAdded } /> itemsLabel={_t('Local addresses for this room:')}
<div className="mx_RoomSettings_addAlias mx_filterFlipColor"> noItemsLabel={_t('This room has no local addresses')}
<img src="img/plus.svg" width="14" height="14" alt="Add" placeholder={_t(
onClick={ self.onAliasAdded.bind(self, undefined) }/> 'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
</div> )}
</div> : "" />
}
</div>
{ remote_aliases_section } { remote_aliases_section }
</div> </div>
); );
} },
}); });