Convert EditableItemList & AliasSettings to Typescript

pull/21833/head
Michael Telatynski 2021-06-07 15:48:55 +01:00
parent 6d2a7390d7
commit a7eb09af1e
2 changed files with 146 additions and 121 deletions

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2017, 2019 New Vector Ltd. Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,43 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "./Field"; import Field from "./Field";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
export class EditableItem extends React.Component { interface IItemProps {
static propTypes = { index?: number;
index: PropTypes.number, value?: string;
value: PropTypes.string, onRemove?(index: number): void;
onRemove: PropTypes.func,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
} }
_onRemove = (e) => { interface IItemState {
verifyRemove: boolean;
}
export class EditableItem extends React.Component<IItemProps, IItemState> {
public state = {
verifyRemove: false,
};
private onRemove = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: true }); this.setState({ verifyRemove: true });
}; };
_onDontRemove = (e) => { private onDontRemove = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.setState({ verifyRemove: false }); this.setState({ verifyRemove: false });
}; };
_onActuallyRemove = (e) => { private onActuallyRemove = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
{_t("Are you sure?")} {_t("Are you sure?")}
</span> </span>
<AccessibleButton <AccessibleButton
onClick={this._onActuallyRemove} onClick={this.onActuallyRemove}
kind="primary_sm" kind="primary_sm"
className="mx_EditableItem_confirmBtn" className="mx_EditableItem_confirmBtn"
> >
{_t("Yes")} {_t("Yes")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
onClick={this._onDontRemove} onClick={this.onDontRemove}
kind="danger_sm" kind="danger_sm"
className="mx_EditableItem_confirmBtn" className="mx_EditableItem_confirmBtn"
> >
@ -85,58 +85,67 @@ export class EditableItem extends React.Component {
return ( return (
<div className="mx_EditableItem"> <div className="mx_EditableItem">
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" /> <div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
<span className="mx_EditableItem_item">{this.props.value}</span> <span className="mx_EditableItem_item">{this.props.value}</span>
</div> </div>
); );
} }
} }
interface IProps {
id: string;
items: string[];
itemsLabel?: string;
noItemsLabel?: string;
placeholder?: string;
newItem?: string;
canEdit?: boolean;
canRemove?: boolean;
suggestionsListId?: string;
onItemAdded?(item: string): void;
onItemRemoved?(index: number): void;
onNewItemChanged?(item: string): void;
}
@replaceableComponent("views.elements.EditableItemList") @replaceableComponent("views.elements.EditableItemList")
export default class EditableItemList extends React.Component { export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
static propTypes = { protected onItemAdded = (e) => {
id: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
placeholder: PropTypes.string,
newItem: PropTypes.string,
onItemAdded: PropTypes.func,
onItemRemoved: PropTypes.func,
onNewItemChanged: PropTypes.func,
canEdit: PropTypes.bool,
canRemove: PropTypes.bool,
};
_onItemAdded = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
}; };
_onItemRemoved = (index) => { protected onItemRemoved = (index) => {
if (this.props.onItemRemoved) this.props.onItemRemoved(index); if (this.props.onItemRemoved) this.props.onItemRemoved(index);
}; };
_onNewItemChanged = (e) => { protected onNewItemChanged = (e) => {
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value); if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
}; };
_renderNewItemField() { protected renderNewItemField() {
return ( return (
<form <form
onSubmit={this._onItemAdded} onSubmit={this.onItemAdded}
autoComplete="off" autoComplete="off"
noValidate={true} noValidate={true}
className="mx_EditableItemList_newItem" className="mx_EditableItemList_newItem"
> >
<Field label={this.props.placeholder} type="text" <Field
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} label={this.props.placeholder}
list={this.props.suggestionsListId} /> type="text"
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}> autoComplete="off"
value={this.props.newItem || ""}
onChange={this.onNewItemChanged}
list={this.props.suggestionsListId}
/>
<AccessibleButton
onClick={this.onItemAdded}
kind="primary"
type="submit"
disabled={!this.props.newItem}
>
{ _t("Add") } { _t("Add") }
</AccessibleButton> </AccessibleButton>
</form> </form>
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
key={item} key={item}
index={index} index={index}
value={item} value={item}
onRemove={this._onItemRemoved} onRemove={this.onItemRemoved}
/>; />;
}); });
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>; const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList"> return (
<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label"> <div className="mx_EditableItemList_label">
{ label } { label }
</div> </div>
{ editableItemsSection } { editableItemsSection }
{ this.props.canEdit ? this._renderNewItemField() : <div /> } { this.props.canEdit ? this.renderNewItemField() : <div /> }
</div>); </div>
);
} }
} }

View File

@ -1,6 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016-2021 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,59 +14,60 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ChangeEvent, createRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import EditableItemList from "../elements/EditableItemList"; import EditableItemList from "../elements/EditableItemList";
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import Spinner from "../elements/Spinner";
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import RoomPublishSetting from "./RoomPublishSetting"; import RoomPublishSetting from "./RoomPublishSetting";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAliasField from "../elements/RoomAliasField";
class EditableAliasesList extends EditableItemList { interface IEditableAliasesListProps {
constructor(props) { domain?: string;
super(props);
this._aliasField = createRef();
} }
_onAliasAdded = async () => { class EditableAliasesList extends EditableItemList<IEditableAliasesListProps> {
await this._aliasField.current.validate({ allowEmpty: false }); private aliasField = createRef<RoomAliasField>();
if (this._aliasField.current.isValid) { private onAliasAdded = async () => {
await this.aliasField.current.validate({ allowEmpty: false });
if (this.aliasField.current.isValid) {
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
return; return;
} }
this._aliasField.current.focus(); this.aliasField.current.focus();
this._aliasField.current.validate({ allowEmpty: false, focused: true }); this.aliasField.current.validate({ allowEmpty: false, focused: true });
}; };
_renderNewItemField() { protected renderNewItemField() {
// if we don't need the RoomAliasField, // if we don't need the RoomAliasField,
// we don't need to overriden version of _renderNewItemField // we don't need to overriden version of renderNewItemField
if (!this.props.domain) { if (!this.props.domain) {
return super._renderNewItemField(); return super.renderNewItemField();
} }
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
return ( return (
<form <form
onSubmit={this._onAliasAdded} onSubmit={this.onAliasAdded}
autoComplete="off" autoComplete="off"
noValidate={true} noValidate={true}
className="mx_EditableItemList_newItem" className="mx_EditableItemList_newItem"
> >
<RoomAliasField <RoomAliasField
ref={this._aliasField} ref={this.aliasField}
onChange={onChange} onChange={onChange}
value={this.props.newItem || ""} value={this.props.newItem || ""}
domain={this.props.domain} /> domain={this.props.domain} />
<AccessibleButton onClick={this._onAliasAdded} kind="primary"> <AccessibleButton onClick={this.onAliasAdded} kind="primary">
{ _t("Add") } { _t("Add") }
</AccessibleButton> </AccessibleButton>
</form> </form>
@ -75,15 +75,27 @@ class EditableAliasesList extends EditableItemList {
} }
} }
@replaceableComponent("views.room_settings.AliasSettings") interface IProps {
export default class AliasSettings extends React.Component { roomId: string;
static propTypes = { canSetCanonicalAlias: boolean;
roomId: PropTypes.string.isRequired, canSetAliases: boolean;
canSetCanonicalAlias: PropTypes.bool.isRequired, canonicalAliasEvent?: MatrixEvent;
canSetAliases: PropTypes.bool.isRequired, aliasEvents?: MatrixEvent[];
canonicalAliasEvent: PropTypes.object, // MatrixEvent }
};
interface IState {
altAliases: string[];
localAliases: string[];
canonicalAlias?: string;
updatingCanonicalAlias: boolean;
localAliasesLoading: boolean;
detailsOpen: boolean;
newAlias?: string;
newAltAlias?: string;
}
@replaceableComponent("views.room_settings.AliasSettings")
export default class AliasSettings extends React.Component<IProps, IState> {
static defaultProps = { static defaultProps = {
canSetAliases: false, canSetAliases: false,
canSetCanonicalAlias: false, canSetCanonicalAlias: false,
@ -122,7 +134,7 @@ export default class AliasSettings extends React.Component {
} }
} }
async loadLocalAliases() { private async loadLocalAliases() {
this.setState({ localAliasesLoading: true }); this.setState({ localAliasesLoading: true });
try { try {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -139,7 +151,7 @@ export default class AliasSettings extends React.Component {
} }
} }
changeCanonicalAlias(alias) { private changeCanonicalAlias(alias: string) {
if (!this.props.canSetCanonicalAlias) return; if (!this.props.canSetCanonicalAlias) return;
const oldAlias = this.state.canonicalAlias; const oldAlias = this.state.canonicalAlias;
@ -170,7 +182,7 @@ export default class AliasSettings extends React.Component {
}); });
} }
changeAltAliases(altAliases) { private changeAltAliases(altAliases: string[]) {
if (!this.props.canSetCanonicalAlias) return; if (!this.props.canSetCanonicalAlias) return;
this.setState({ this.setState({
@ -181,7 +193,7 @@ export default class AliasSettings extends React.Component {
const eventContent = {}; const eventContent = {};
if (this.state.canonicalAlias) { if (this.state.canonicalAlias) {
eventContent.alias = this.state.canonicalAlias; eventContent["alias"] = this.state.canonicalAlias;
} }
if (altAliases) { if (altAliases) {
eventContent["alt_aliases"] = altAliases; eventContent["alt_aliases"] = altAliases;
@ -202,11 +214,11 @@ export default class AliasSettings extends React.Component {
}); });
} }
onNewAliasChanged = (value) => { private onNewAliasChanged = (value: string) => {
this.setState({ newAlias: value }); this.setState({ newAlias: value });
}; };
onLocalAliasAdded = (alias) => { private onLocalAliasAdded = (alias: string) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
@ -232,7 +244,7 @@ export default class AliasSettings extends React.Component {
}); });
}; };
onLocalAliasDeleted = (index) => { private onLocalAliasDeleted = (index: number) => {
const alias = this.state.localAliases[index]; const alias = this.state.localAliases[index];
// TODO: In future, we should probably be making sure that the alias actually belongs // TODO: In future, we should probably be making sure that the alias actually belongs
// to this room. See https://github.com/vector-im/element-web/issues/7353 // to this room. See https://github.com/vector-im/element-web/issues/7353
@ -261,7 +273,7 @@ export default class AliasSettings extends React.Component {
}); });
}; };
onLocalAliasesToggled = (event) => { private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>) => {
// expanded // expanded
if (event.target.open) { if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount // if local aliases haven't been preloaded yet at component mount
@ -269,18 +281,18 @@ export default class AliasSettings extends React.Component {
this.loadLocalAliases(); this.loadLocalAliases();
} }
} }
this.setState({detailsOpen: event.target.open}); this.setState({ detailsOpen: event.currentTarget.open });
}; };
onCanonicalAliasChange = (event) => { private onCanonicalAliasChange = (event: ChangeEvent<HTMLSelectElement>) => {
this.changeCanonicalAlias(event.target.value); this.changeCanonicalAlias(event.target.value);
}; };
onNewAltAliasChanged = (value) => { private onNewAltAliasChanged = (value: string) => {
this.setState({ newAltAlias: value }); this.setState({ newAltAlias: value });
} }
onAltAliasAdded = (alias) => { private onAltAliasAdded = (alias: string) => {
const altAliases = this.state.altAliases.slice(); const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) { if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim()); altAliases.push(alias.trim());
@ -289,17 +301,17 @@ export default class AliasSettings extends React.Component {
} }
} }
onAltAliasDeleted = (index) => { private onAltAliasDeleted = (index: number) => {
const altAliases = this.state.altAliases.slice(); const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1); altAliases.splice(index, 1);
this.changeAltAliases(altAliases); this.changeAltAliases(altAliases);
} }
_getAliases() { private getAliases() {
return this.state.altAliases.concat(this._getLocalNonAltAliases()); return this.state.altAliases.concat(this.getLocalNonAltAliases());
} }
_getLocalNonAltAliases() { private getLocalNonAltAliases() {
const {altAliases} = this.state; const {altAliases} = this.state;
return this.state.localAliases.filter(alias => !altAliases.includes(alias)); return this.state.localAliases.filter(alias => !altAliases.includes(alias));
} }
@ -320,7 +332,7 @@ export default class AliasSettings extends React.Component {
> >
<option value="" key="unset">{ _t('not specified') }</option> <option value="" key="unset">{ _t('not specified') }</option>
{ {
this._getAliases().map((alias, i) => { this.getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true; if (alias === this.state.canonicalAlias) found = true;
return ( return (
<option value={alias} key={i}> <option value={alias} key={i}>
@ -340,12 +352,10 @@ export default class AliasSettings extends React.Component {
let localAliasesList; let localAliasesList;
if (this.state.localAliasesLoading) { if (this.state.localAliasesLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = <Spinner />; localAliasesList = <Spinner />;
} else { } else {
localAliasesList = (<EditableAliasesList localAliasesList = (<EditableAliasesList
id="roomAliases" id="roomAliases"
className={"mx_RoomSettings_localAliases"}
items={this.state.localAliases} items={this.state.localAliases}
newItem={this.state.newAlias} newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged} onNewItemChanged={this.onNewAliasChanged}
@ -367,13 +377,12 @@ export default class AliasSettings extends React.Component {
{canonicalAliasSection} {canonicalAliasSection}
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} /> <RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
<datalist id="mx_AliasSettings_altRecommendations"> <datalist id="mx_AliasSettings_altRecommendations">
{this._getLocalNonAltAliases().map(alias => { {this.getLocalNonAltAliases().map(alias => {
return <option value={alias} key={alias} />; return <option value={alias} key={alias} />;
})}; })};
</datalist> </datalist>
<EditableAliasesList <EditableAliasesList
id="roomAltAliases" id="roomAltAliases"
className={"mx_RoomSettings_altAliases"}
items={this.state.altAliases} items={this.state.altAliases}
newItem={this.state.newAltAlias} newItem={this.state.newAltAlias}
onNewItemChanged={this.onNewAltAliasChanged} onNewItemChanged={this.onNewAltAliasChanged}
@ -386,8 +395,13 @@ export default class AliasSettings extends React.Component {
noItemsLabel={_t('No other published addresses yet, add one below')} noItemsLabel={_t('No other published addresses yet, add one below')}
placeholder={_t('New published address (e.g. #alias:server)')} placeholder={_t('New published address (e.g. #alias:server)')}
/> />
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span> <span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p> { _t("Local Addresses") }
</span>
<p>
{ _t("Set addresses for this room so users can find this room " +
"through your homeserver (%(localDomain)s)", { localDomain }) }
</p>
<details onToggle={this.onLocalAliasesToggled}> <details onToggle={this.onLocalAliasesToggled}>
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary> <summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
{ localAliasesList } { localAliasesList }