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");
you may not use this file except in compliance with the License.
@ -14,48 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import React from "react";
import { _t } from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export class EditableItem extends React.Component {
static propTypes = {
index: PropTypes.number,
value: PropTypes.string,
onRemove: PropTypes.func,
interface IItemProps {
index?: number;
value?: string;
onRemove?(index: number): void;
}
interface IItemState {
verifyRemove: boolean;
}
export class EditableItem extends React.Component<IItemProps, IItemState> {
public state = {
verifyRemove: false,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
private onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
this.setState({ verifyRemove: true });
};
_onDontRemove = (e) => {
private onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
_onActuallyRemove = (e) => {
private onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onRemove) this.props.onRemove(this.props.index);
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
render() {
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
{_t("Are you sure?")}
</span>
<AccessibleButton
onClick={this._onActuallyRemove}
onClick={this.onActuallyRemove}
kind="primary_sm"
className="mx_EditableItem_confirmBtn"
>
{_t("Yes")}
</AccessibleButton>
<AccessibleButton
onClick={this._onDontRemove}
onClick={this.onDontRemove}
kind="danger_sm"
className="mx_EditableItem_confirmBtn"
>
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
return (
<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>
</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")
export default class EditableItemList extends React.Component {
static propTypes = {
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) => {
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
protected onItemAdded = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
};
_onItemRemoved = (index) => {
protected 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);
};
_renderNewItemField() {
protected renderNewItemField() {
return (
<form
onSubmit={this._onItemAdded}
onSubmit={this.onItemAdded}
autoComplete="off"
noValidate={true}
className="mx_EditableItemList_newItem"
>
<Field label={this.props.placeholder} type="text"
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")}
<Field
label={this.props.placeholder}
type="text"
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") }
</AccessibleButton>
</form>
);
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
key={item}
index={index}
value={item}
onRemove={this._onItemRemoved}
onRemove={this.onItemRemoved}
/>;
});
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 }
return (
<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItemsSection }
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
</div>
{ editableItemsSection }
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
</div>);
);
}
}

View File

@ -1,6 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (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.
*/
import React, { ChangeEvent, createRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import EditableItemList from "../elements/EditableItemList";
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import Spinner from "../elements/Spinner";
import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import RoomPublishSetting from "./RoomPublishSetting";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAliasField from "../elements/RoomAliasField";
class EditableAliasesList extends EditableItemList {
constructor(props) {
super(props);
interface IEditableAliasesListProps {
domain?: string;
}
this._aliasField = createRef();
}
class EditableAliasesList extends EditableItemList<IEditableAliasesListProps> {
private aliasField = createRef<RoomAliasField>();
_onAliasAdded = async () => {
await this._aliasField.current.validate({ allowEmpty: false });
private onAliasAdded = async () => {
await this.aliasField.current.validate({ allowEmpty: false });
if (this._aliasField.current.isValid) {
if (this.aliasField.current.isValid) {
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
return;
}
this._aliasField.current.focus();
this._aliasField.current.validate({ allowEmpty: false, focused: true });
this.aliasField.current.focus();
this.aliasField.current.validate({ allowEmpty: false, focused: true });
};
_renderNewItemField() {
protected renderNewItemField() {
// 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) {
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 (
<form
onSubmit={this._onAliasAdded}
onSubmit={this.onAliasAdded}
autoComplete="off"
noValidate={true}
className="mx_EditableItemList_newItem"
>
<RoomAliasField
ref={this._aliasField}
ref={this.aliasField}
onChange={onChange}
value={this.props.newItem || ""}
domain={this.props.domain} />
<AccessibleButton onClick={this._onAliasAdded} kind="primary">
<AccessibleButton onClick={this.onAliasAdded} kind="primary">
{ _t("Add") }
</AccessibleButton>
</form>
@ -75,15 +75,27 @@ class EditableAliasesList extends EditableItemList {
}
}
@replaceableComponent("views.room_settings.AliasSettings")
export default class AliasSettings extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired,
canonicalAliasEvent: PropTypes.object, // MatrixEvent
};
interface IProps {
roomId: string;
canSetCanonicalAlias: boolean;
canSetAliases: boolean;
canonicalAliasEvent?: MatrixEvent;
aliasEvents?: 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 = {
canSetAliases: false,
canSetCanonicalAlias: false,
@ -122,7 +134,7 @@ export default class AliasSettings extends React.Component {
}
}
async loadLocalAliases() {
private async loadLocalAliases() {
this.setState({ localAliasesLoading: true });
try {
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;
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;
this.setState({
@ -181,7 +193,7 @@ export default class AliasSettings extends React.Component {
const eventContent = {};
if (this.state.canonicalAlias) {
eventContent.alias = this.state.canonicalAlias;
eventContent["alias"] = this.state.canonicalAlias;
}
if (altAliases) {
eventContent["alt_aliases"] = altAliases;
@ -202,11 +214,11 @@ export default class AliasSettings extends React.Component {
});
}
onNewAliasChanged = (value) => {
this.setState({newAlias: value});
private onNewAliasChanged = (value: string) => {
this.setState({ newAlias: value });
};
onLocalAliasAdded = (alias) => {
private onLocalAliasAdded = (alias: string) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
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];
// 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
@ -261,7 +273,7 @@ export default class AliasSettings extends React.Component {
});
};
onLocalAliasesToggled = (event) => {
private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>) => {
// expanded
if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount
@ -269,37 +281,37 @@ export default class AliasSettings extends React.Component {
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);
};
onNewAltAliasChanged = (value) => {
this.setState({newAltAlias: value});
private onNewAltAliasChanged = (value: string) => {
this.setState({ newAltAlias: value });
}
onAltAliasAdded = (alias) => {
private onAltAliasAdded = (alias: string) => {
const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim());
this.changeAltAliases(altAliases);
this.setState({newAltAlias: ""});
this.setState({ newAltAlias: "" });
}
}
onAltAliasDeleted = (index) => {
private onAltAliasDeleted = (index: number) => {
const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1);
this.changeAltAliases(altAliases);
}
_getAliases() {
return this.state.altAliases.concat(this._getLocalNonAltAliases());
private getAliases() {
return this.state.altAliases.concat(this.getLocalNonAltAliases());
}
_getLocalNonAltAliases() {
private getLocalNonAltAliases() {
const {altAliases} = this.state;
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>
{
this._getAliases().map((alias, i) => {
this.getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true;
return (
<option value={alias} key={i}>
@ -340,12 +352,10 @@ export default class AliasSettings extends React.Component {
let localAliasesList;
if (this.state.localAliasesLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = <Spinner />;
} else {
localAliasesList = (<EditableAliasesList
id="roomAliases"
className={"mx_RoomSettings_localAliases"}
items={this.state.localAliases}
newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged}
@ -367,13 +377,12 @@ export default class AliasSettings extends React.Component {
{canonicalAliasSection}
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
<datalist id="mx_AliasSettings_altRecommendations">
{this._getLocalNonAltAliases().map(alias => {
{this.getLocalNonAltAliases().map(alias => {
return <option value={alias} key={alias} />;
})};
</datalist>
<EditableAliasesList
id="roomAltAliases"
className={"mx_RoomSettings_altAliases"}
items={this.state.altAliases}
newItem={this.state.newAltAlias}
onNewItemChanged={this.onNewAltAliasChanged}
@ -386,11 +395,16 @@ export default class AliasSettings extends React.Component {
noItemsLabel={_t('No other published addresses yet, add one below')}
placeholder={_t('New published address (e.g. #alias:server)')}
/>
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>
{ _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}>
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
{localAliasesList}
{ localAliasesList }
</details>
</div>
);