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);
},
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 (this.isAliasValid(alias)) {
// add this alias to the domain to aliases dict
var domain = alias.replace(/^.*?:/, '');
// XXX: do we need to deep copy aliases before editing it?
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
this.state.domainToAliases[domain].push(alias);
this.setState({
domainToAliases: this.state.domainToAliases
});
const localDomain = MatrixClientPeg.get().getDomain();
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
this.state.domainToAliases[localDomain].push(alias);
// reset the add field
this.refs.add_alias.setValue(''); // FIXME
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
this.setState({
domainToAliases: this.state.domainToAliases,
// Reset the add field
newAlias: "",
});
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
title: _t('Invalid alias format'),
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
var oldAlias;
if (this.isAliasValid(alias)) {
oldAlias = this.state.domainToAliases[domain][index];
this.state.domainToAliases[domain][index] = alias;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const localDomain = MatrixClientPeg.get().getDomain();
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
this.state.domainToAliases[localDomain][index] = alias;
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
title: _t('Invalid address format'),
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
// 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
// would be to arbitrarily deepcopy to a temp variable and then setState
// 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({
domainToAliases: this.state.domainToAliases
domainToAliases: this.state.domainToAliases,
});
},
@ -198,6 +198,7 @@ module.exports = React.createClass({
render: function() {
var self = this;
var EditableText = sdk.getComponent("elements.EditableText");
var EditableItemList = sdk.getComponent("elements.EditableItemList");
var localDomain = MatrixClientPeg.get().getDomain();
var canonical_alias_section;
@ -257,58 +258,25 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_aliasLabel">
{ _t('The main address for this room is') }: { canonical_alias_section }
</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 ?
<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="Add"
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
</div> : ""
}
</div>
<EditableItemList
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias}
onNewItemChanged={onNewAliasChanged}
onItemAdded={this.onLocalAliasAdded}
onItemEdited={this.onLocalAliasChanged}
onItemRemoved={this.onLocalAliasDeleted}
itemsLabel={_t('Local addresses for this room:')}
noItemsLabel={_t('This room has no local addresses')}
placeholder={_t(
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)}
/>
{ remote_aliases_section }
</div>
);
}
},
});