/ common / components / AddressBookTable.tsx
AddressBookTable.tsx
  1  import React from 'react';
  2  import { connect, MapStateToProps } from 'react-redux';
  3  import classnames from 'classnames';
  4  
  5  import translate, { translateRaw } from 'translations';
  6  import { AppState } from 'features/reducers';
  7  import { getChecksumAddressFn } from 'features/config';
  8  import {
  9    addressBookConstants,
 10    addressBookActions,
 11    addressBookSelectors
 12  } from 'features/addressBook';
 13  import { Input, Identicon } from 'components/ui';
 14  import AddressBookTableRow from './AddressBookTableRow';
 15  import './AddressBookTable.scss';
 16  
 17  interface DispatchProps {
 18    changeAddressLabelEntry: addressBookActions.TChangeAddressLabelEntry;
 19    saveAddressLabelEntry: addressBookActions.TSaveAddressLabelEntry;
 20    removeAddressLabelEntry: addressBookActions.TRemoveAddressLabelEntry;
 21  }
 22  
 23  interface StateProps {
 24    rows: ReturnType<typeof addressBookSelectors.getAddressLabelRows>;
 25    entry: ReturnType<typeof addressBookSelectors.getAddressBookTableEntry>;
 26    addressLabels: ReturnType<typeof addressBookSelectors.getAddressLabels>;
 27    labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
 28    toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
 29  }
 30  
 31  type Props = DispatchProps & StateProps;
 32  
 33  interface State {
 34    editingRow: number | null;
 35    addressTouched: boolean;
 36    addressBlurred: boolean;
 37    labelTouched: boolean;
 38    labelBlurred: boolean;
 39  }
 40  
 41  class AddressBookTable extends React.Component<Props, State> {
 42    public state: State = {
 43      editingRow: null,
 44      addressTouched: false,
 45      addressBlurred: false,
 46      labelTouched: false,
 47      labelBlurred: false
 48    };
 49  
 50    private addressInput: HTMLInputElement | null = null;
 51  
 52    private labelInput: HTMLInputElement | null = null;
 53  
 54    public render() {
 55      const {
 56        entry: { temporaryAddress = '', addressError = '', temporaryLabel = '', labelError = '' },
 57        rows
 58      } = this.props;
 59      const { addressTouched, addressBlurred, labelTouched, labelBlurred } = this.state;
 60  
 61      // Classnames
 62      const addressTouchedWithError = addressTouched && addressError;
 63      const labelTouchedWithError = labelTouched && labelError;
 64      const nonMobileTemporaryInputErrorClassName =
 65        'AddressBookTable-row-error-temporary-input--non-mobile';
 66  
 67      const nonMobileTemporaryAddressErrorClassName = classnames({
 68        [nonMobileTemporaryInputErrorClassName]: true,
 69        [`${nonMobileTemporaryInputErrorClassName}-address`]: true,
 70        'is-visible': !!addressTouchedWithError
 71      });
 72  
 73      const nonMobileTemporaryLabelErrorClassName = classnames({
 74        [nonMobileTemporaryInputErrorClassName]: true,
 75        [`${nonMobileTemporaryInputErrorClassName}-label`]: true,
 76        'is-visible': !!labelTouchedWithError
 77      });
 78  
 79      return (
 80        <section className="AddressBookTable" onKeyDown={this.handleKeyDown}>
 81          <div className="AddressBookTable-row AddressBookTable-row-labels">
 82            <label className="AddressBookTable-row-label" htmlFor="temporaryAddress">
 83              {translate('ADDRESS')}
 84            </label>
 85            <label className="AddressBookTable-row-label" htmlFor="temporaryLabel">
 86              {translate('LABEL')}
 87            </label>
 88          </div>
 89          <div className="AddressBookTable-row AddressBookTable-row-inputs">
 90            <div className="AddressBookTable-row-input">
 91              <div className="AddressBookTable-row-input-wrapper">
 92                <label
 93                  className="AddressBookTable-row-input-wrapper-label"
 94                  htmlFor="temporaryAddress"
 95                >
 96                  {translate('ADDRESS')}
 97                </label>
 98                <Input
 99                  name="temporaryAddress"
100                  placeholder={translateRaw('NEW_ADDRESS')}
101                  value={temporaryAddress}
102                  onChange={this.handleAddressChange}
103                  onFocus={this.setAddressTouched}
104                  onBlur={this.setAddressBlurred}
105                  setInnerRef={this.setAddressInputRef}
106                  isValid={!addressTouchedWithError}
107                />
108              </div>
109              <div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-non-mobile">
110                <Identicon address={temporaryAddress} />
111              </div>
112              <div className="AddressBookTable-row-identicon AddressBookTable-row-identicon-mobile">
113                <Identicon address={temporaryAddress} size="3rem" />
114              </div>
115            </div>
116            <div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--mobile">
117              <label className="AddressBookTable-row-input-wrapper-error">
118                {addressBlurred && addressError}
119              </label>
120            </div>
121            <div className="AddressBookTable-row-input">
122              <div className="AddressBookTable-row-input-wrapper">
123                <label className="AddressBookTable-row-input-wrapper-label" htmlFor="temporaryLabel">
124                  {translate('LABEL')}
125                </label>
126                <Input
127                  name="temporaryLabel"
128                  placeholder={translateRaw('NEW_LABEL')}
129                  value={temporaryLabel}
130                  onChange={this.handleLabelChange}
131                  onFocus={this.setLabelTouched}
132                  onBlur={this.setLabelBlurred}
133                  setInnerRef={this.setLabelInputRef}
134                  isValid={!labelTouchedWithError}
135                />
136              </div>
137              <button
138                title={translateRaw('ADD_LABEL')}
139                className="btn btn-sm btn-success"
140                onClick={this.handleAddEntry}
141              >
142                <i className="fa fa-plus" />
143              </button>
144            </div>
145            <div className="AddressBookTable-row AddressBookTable-row-error AddressBookTable-row-error--mobile">
146              <label className="AddressBookTable-row-input-wrapper-error">
147                {labelBlurred && labelError}
148              </label>
149            </div>
150          </div>
151          <div className="AddressBookTable-row AddressBookTable-row-error">
152            <label className={nonMobileTemporaryAddressErrorClassName}>
153              {addressBlurred && addressError}
154            </label>
155            <label className={nonMobileTemporaryLabelErrorClassName}>
156              {labelBlurred && labelError}
157            </label>
158          </div>
159          {rows.map(this.makeLabelRow)}
160        </section>
161      );
162    }
163  
164    private handleAddEntry = () => {
165      const { entry: { temporaryAddress, addressError, labelError } } = this.props;
166  
167      if (!temporaryAddress || addressError || temporaryAddress.length === 0) {
168        return this.addressInput && this.addressInput.focus();
169      }
170  
171      if (labelError && this.labelInput) {
172        this.labelInput.focus();
173      }
174  
175      this.props.saveAddressLabelEntry(addressBookConstants.ADDRESS_BOOK_TABLE_ID);
176  
177      if (!addressError && !labelError) {
178        this.clearFieldStatuses();
179        this.setEditingRow(null);
180      }
181    };
182  
183    private handleKeyDown = (e: React.KeyboardEvent<HTMLTableElement>) => {
184      if (e.key === 'Enter') {
185        this.handleAddEntry();
186      }
187    };
188  
189    private setEditingRow = (editingRow: number | null) => this.setState({ editingRow });
190  
191    private clearEditingRow = () => this.setEditingRow(null);
192  
193    private makeLabelRow = (row: any, index: number) => {
194      const { editingRow } = this.state;
195      const { id, label, temporaryLabel, labelError } = row;
196      const address = this.props.toChecksumAddress(row.address);
197      const isEditing = index === editingRow;
198      const onChange = (newLabel: string) =>
199        this.props.changeAddressLabelEntry({
200          id,
201          address,
202          label: newLabel,
203          isEditing: true
204        });
205      const onSave = () => {
206        this.props.saveAddressLabelEntry(id);
207        this.setEditingRow(null);
208      };
209      const onLabelInputBlur = () => {
210        // If the new changes aren't valid, undo them.
211        if (labelError) {
212          this.props.changeAddressLabelEntry({
213            id,
214            address,
215            temporaryAddress: address,
216            label,
217            temporaryLabel: label,
218            overrideValidation: true
219          });
220        }
221  
222        this.clearEditingRow();
223      };
224  
225      return (
226        <AddressBookTableRow
227          key={address}
228          index={index}
229          address={address}
230          label={label}
231          temporaryLabel={temporaryLabel}
232          labelError={labelError}
233          isEditing={isEditing}
234          onChange={onChange}
235          onSave={onSave}
236          onLabelInputBlur={onLabelInputBlur}
237          onEditClick={() => this.setEditingRow(index)}
238          onRemoveClick={() => this.props.removeAddressLabelEntry(id)}
239        />
240      );
241    };
242  
243    private setAddressInputRef = (node: HTMLInputElement) => (this.addressInput = node);
244  
245    private setAddressTouched = () =>
246      !this.state.addressTouched && this.setState({ addressTouched: true });
247  
248    private clearAddressTouched = () => this.setState({ addressTouched: false });
249  
250    private setAddressBlurred = () => this.setState({ addressBlurred: true });
251  
252    private handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
253      const { entry } = this.props;
254      const address = e.target.value;
255      const label = entry.temporaryLabel || '';
256  
257      this.props.changeAddressLabelEntry({
258        id: addressBookConstants.ADDRESS_BOOK_TABLE_ID,
259        address,
260        label
261      });
262  
263      this.setState(
264        { addressTouched: true },
265        () => address.length === 0 && this.clearAddressTouched()
266      );
267    };
268  
269    private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node);
270  
271    private setLabelTouched = () => !this.state.labelTouched && this.setState({ labelTouched: true });
272  
273    private clearLabelTouched = () => this.setState({ labelTouched: false });
274  
275    private setLabelBlurred = () => this.setState({ labelBlurred: true });
276  
277    private handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
278      const { entry } = this.props;
279      const address = entry.temporaryAddress || '';
280      const label = e.target.value;
281  
282      this.props.changeAddressLabelEntry({
283        id: addressBookConstants.ADDRESS_BOOK_TABLE_ID,
284        address,
285        label
286      });
287  
288      this.setState({ labelTouched: true }, () => label.length === 0 && this.clearLabelTouched());
289    };
290  
291    private clearFieldStatuses = () =>
292      this.setState({
293        addressTouched: false,
294        addressBlurred: false,
295        labelTouched: false,
296        labelBlurred: false
297      });
298  }
299  
300  const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = state => ({
301    rows: addressBookSelectors.getAddressLabelRows(state),
302    entry: addressBookSelectors.getAddressBookTableEntry(state),
303    addressLabels: addressBookSelectors.getAddressLabels(state),
304    labelAddresses: addressBookSelectors.getLabelAddresses(state),
305    toChecksumAddress: getChecksumAddressFn(state)
306  });
307  
308  const mapDispatchToProps: DispatchProps = {
309    changeAddressLabelEntry: addressBookActions.changeAddressLabelEntry,
310    saveAddressLabelEntry: addressBookActions.saveAddressLabelEntry,
311    removeAddressLabelEntry: addressBookActions.removeAddressLabelEntry
312  };
313  
314  export default connect(mapStateToProps, mapDispatchToProps)(AddressBookTable);