Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix AliasSettings and RelatedGroups UX #2679

Merged
merged 12 commits into from
Feb 22, 2019
1 change: 0 additions & 1 deletion .eslintignore.errorfiles
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ src/components/views/globals/UpdateCheckBar.js
src/components/views/messages/MFileBody.js
src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js
src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js
src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js
Expand Down
49 changes: 20 additions & 29 deletions res/css/views/elements/_EditableItemList.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2017, 2019 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.
Expand All @@ -16,47 +16,38 @@ limitations under the License.

.mx_EditableItemList {
margin-top: 12px;
margin-bottom: 0px;
margin-bottom: 10px;
}

.mx_EditableItem {
display: flex;
margin-left: 56px;
margin-bottom: 5px;
margin-left: 15px;
}

.mx_EditableItem .mx_EditableItem_editable {
border: 0px;
border-bottom: 1px solid $strong-input-border-color;
padding: 0px;
min-width: 240px;
max-width: 400px;
margin-bottom: 16px;
.mx_EditableItem_delete {
margin-right: 5px;
cursor: pointer;
vertical-align: middle;
}

.mx_EditableItem .mx_EditableItem_editable:focus {
border-bottom: 1px solid $accent-color;
outline: none;
box-shadow: none;
.mx_EditableItem_email {
vertical-align: middle;
}

.mx_EditableItem .mx_EditableItem_editablePlaceholder {
color: $settings-grey-fg-color;
.mx_EditableItem_promptText {
margin-right: 10px;
}

.mx_EditableItem .mx_EditableItem_addButton,
.mx_EditableItem .mx_EditableItem_removeButton {
padding-left: 0.5em;
position: relative;
cursor: pointer;

visibility: hidden;
.mx_EditableItem_confirmBtn {
margin-right: 5px;
}

.mx_EditableItem:hover .mx_EditableItem_addButton,
.mx_EditableItem:hover .mx_EditableItem_removeButton {
visibility: visible;
.mx_EditableItemList_newItem .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe box-sizing instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Nope - tried border-box, padding-box, and content-box and all 3 have very similar results.

}

.mx_EditableItemList_label {
margin-bottom: 8px;
}
margin-bottom: 5px;
}
31 changes: 31 additions & 0 deletions src/TextForEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,36 @@ function textForGuestAccessEvent(ev) {
}
}

function textForRelatedGroupsEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || [];
const added = groups.filter((g) => !prevGroups.includes(g));
const removed = prevGroups.filter((g) => !groups.includes(g));

if (added.length && !removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: added.join(', '),
});
} else if (!added.length && removed.length) {
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: removed.join(', '),
});
} else if (added.length && removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', {
senderDisplayName,
newGroups: added.join(', '),
oldGroups: removed.join(', '),
});
} else {
// Don't bother rendering this change (because there were no changes)
return '';
}
}

function textForServerACLEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent();
Expand Down Expand Up @@ -473,6 +503,7 @@ const stateHandlers = {
'm.room.tombstone': textForTombstoneEvent,
'm.room.join_rules': textForJoinRulesEvent,
'm.room.guest_access': textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent,

'im.vector.modular.widgets': textForWidgetEvent,
};
Expand Down
225 changes: 114 additions & 111 deletions src/components/views/elements/EditableItemList.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2017, 2019 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.
Expand All @@ -16,142 +16,145 @@ limitations under the License.

import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler.js';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";

const EditableItem = React.createClass({
displayName: 'EditableItem',

propTypes: {
initialValue: PropTypes.string,
export class EditableItem extends React.Component {
static propTypes = {
index: PropTypes.number,
placeholder: PropTypes.string,

onChange: PropTypes.func,
value: PropTypes.string,
onRemove: PropTypes.func,
onAdd: PropTypes.func,
};

addOnChange: PropTypes.bool,
},
constructor() {
super();

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);
},
this.state = {
verifyRemove: false,
};
}

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={require("../../../../res/img/plus.svg")} width="14" height="14"
alt={_t("Add")} onClick={this.onAdd} />
</div>
:
<div className="mx_EditableItem_removeButton">
<img className="mx_filterFlipColor"
src={require("../../../../res/img/cancel-small.svg")} width="14" height="14"
alt={_t("Delete")} onClick={this.onRemove} />
</div>
}
</div>;
},
});
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();

// TODO: Make this use the new Field element
module.exports = React.createClass({
displayName: 'EditableItemList',
this.setState({verifyRemove: true});
};

propTypes: {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
onNewItemChanged: PropTypes.func,
onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func,
onItemRemoved: PropTypes.func,
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();

canEdit: PropTypes.bool,
},

getDefaultProps: function() {
return {
onItemAdded: () => {},
onItemEdited: () => {},
onItemRemoved: () => {},
onNewItemChanged: () => {},
};
},
this.setState({verifyRemove: false});
};

onItemAdded: function(value) {
this.props.onItemAdded(value);
},
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();

onItemEdited: function(value, index) {
if (value.length === 0) {
this.onItemRemoved(index);
} else {
this.props.onItemEdited(value, index);
if (this.props.onRemove) this.props.onRemove(this.props.index);
this.setState({verifyRemove: false});
};

render() {
if (this.state.verifyRemove) {
return (
<div className="mx_EditableItem">
<span className="mx_EditableItem_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_EditableItem_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_EditableItem_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
);
}
},

onItemRemoved: function(index) {
this.props.onItemRemoved(index);
},
return (
<div className="mx_EditableItem">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);
}
}

onNewItemChanged: function(value) {
this.props.onNewItemChanged(value);
},
export default class EditableItemList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
placeholder: PropTypes.string,
newItem: PropTypes.string,

render: function() {
onItemAdded: PropTypes.func,
onItemRemoved: PropTypes.func,
onNewItemChanged: PropTypes.func,

canEdit: PropTypes.bool,
canRemove: PropTypes.bool,
};

_onItemAdded = (e) => {
e.stopPropagation();
e.preventDefault();

if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
};

_onItemRemoved = (index) => {
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
};

_onNewItemChanged = (e) => {
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
};

_renderNewItemField() {
return (
<form onSubmit={this._onItemAdded} autoComplete={false}
noValidate={true} className="mx_EditableItemList_newItem">
<Field id="newEmailAddress" label={this.props.placeholder}
type="text" autoComplete="off" value={this.props.newItem}
onChange={this._onNewItemChanged}
/>
<AccessibleButton onClick={this._onItemAdded} kind="primary">
{_t("Add")}
</AccessibleButton>
</form>
);
}

render() {
const editableItems = this.props.items.map((item, index) => {
if (!this.props.canRemove) {
return <li>{item}</li>;
}

return <EditableItem
key={index}
index={index}
initialValue={item}
onChange={this.onItemEdited}
onRemove={this.onItemRemoved}
placeholder={this.props.placeholder}
value={item}
onRemove={this._onItemRemoved}
/>;
});

const label = this.props.items.length > 0 ?
this.props.itemsLabel : this.props.noItemsLabel;
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
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 }
{ this.props.canEdit ?
// This is slightly evil; we want a new instance of
// EditableItem when the list grows. To make sure it's
// reset to its initial state.
<EditableItem
key={editableItems.length}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}
addOnChange={true}
placeholder={this.props.placeholder}
/> : <div />
}
{ editableItemsSection }
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
</div>);
},
});
}
}
Loading