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);