-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(icons): support uploading icons (#2818)
* feat(icons): custom icons * fix: default key to filename * fix: upload icons and pagination * fix: fix button width * refactor: cleanup * fix: hide hidden upload field * refactor: move uploadiconfield to separate component
- Loading branch information
Showing
9 changed files
with
603 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
src/forms/form-fields/helpers/IconPickerDialog/IconList.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import CircularProgress from 'd2-ui/lib/circular-progress/CircularProgress.js'; | ||
import Icon from './Icon.js'; | ||
|
||
export class IconList extends Component { | ||
constructor(props, context) { | ||
super(props, context); | ||
|
||
this.d2 = context.d2; | ||
|
||
this.state = { | ||
loading: false, | ||
pager: { | ||
page: 1, | ||
}, | ||
icons: [], | ||
}; | ||
|
||
this.intersectionObserver = null; | ||
this.loadingRef = null; | ||
} | ||
|
||
componentDidMount() { | ||
// init observer only after first page is fetched | ||
// this is needed so that the observer doesnt return early due to loading state | ||
this.fetchIcons(1).then(() => this.initIntersectionObserver()); | ||
} | ||
|
||
initIntersectionObserver() { | ||
this.intersectionObserver = new IntersectionObserver( | ||
entries => { | ||
if (this.state.loading) { | ||
return; | ||
} | ||
const [{ isIntersecting }] = entries; | ||
|
||
if (isIntersecting) { | ||
this.fetchIcons(this.state.pager.page + 1); | ||
} | ||
}, | ||
{ threshold: 0.8 } | ||
); | ||
|
||
this.intersectionObserver.observe(this.loadingRef); | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
if (prevProps.textFilter !== this.props.textFilter) { | ||
this.setState({ icons: [] }, () => this.fetchIcons(1)); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
this.intersectionObserver.disconnect(); | ||
} | ||
|
||
fetchIconAndAddToStartOfList = async key => { | ||
const icon = await this.d2.Api.getApi().get(`/icons/${key}`); | ||
this.setState(prevState => ({ | ||
icons: [icon, ...prevState.icons], | ||
})); | ||
}; | ||
|
||
fetchIcons = async page => { | ||
const typeFilter = this.props.type; | ||
|
||
const filter = this.props.textFilter || ''; | ||
|
||
this.setState({ loading: true }); | ||
const response = await this.d2.Api.getApi().get('/icons', { | ||
type: typeFilter, | ||
paging: true, | ||
page: page, | ||
search: filter, | ||
pageSize: 200, | ||
}); | ||
|
||
this.setState(prevState => ({ | ||
pager: response.pager, | ||
icons: [...prevState.icons, ...response.icons], | ||
loading: false, | ||
})); | ||
}; | ||
|
||
render() { | ||
const isLastPage = | ||
this.state.pager.total === 0 || | ||
(this.state.pager.pageCount && | ||
this.state.pager.page === this.state.pager.pageCount); | ||
|
||
const shouldShowLoading = !!this.state.loading || !isLastPage; | ||
const icons = this.state.icons; | ||
|
||
return ( | ||
<div> | ||
<div> | ||
{this.state.selectedIcon && ( | ||
<Icon | ||
icon={this.state.selectedIcon} | ||
selectedIconKey={this.props.selectedIconKey} | ||
handleClick={this.props.onIconSelect} | ||
/> | ||
)} | ||
{icons.map(icon => ( | ||
<Icon | ||
icon={icon} | ||
key={icon.key} | ||
selectedIconKey={this.props.selectedIconKey} | ||
handleClick={this.props.onIconSelect} | ||
/> | ||
))} | ||
</div> | ||
|
||
<div | ||
className="icon-picker__list-loader" | ||
style={shouldShowLoading ? undefined : { display: 'none' }} | ||
ref={ref => (this.loadingRef = ref)} | ||
> | ||
<CircularProgress /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
IconList.propTypes = { | ||
type: PropTypes.oneOf(['default', 'custom', 'all']).isRequired, | ||
textFilter: PropTypes.string, | ||
selectedIconKey: PropTypes.string, | ||
onIconSelect: PropTypes.func.isRequired, | ||
}; | ||
|
||
IconList.contextTypes = { | ||
d2: PropTypes.object, | ||
}; |
140 changes: 140 additions & 0 deletions
140
src/forms/form-fields/helpers/IconPickerDialog/IconPickerCustomTab.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import React, { Component } from 'react'; | ||
import RaisedButton from 'material-ui/RaisedButton'; | ||
import PropTypes from 'prop-types'; | ||
import Divider from 'material-ui/Divider'; | ||
import TextField from 'material-ui/TextField/TextField'; | ||
import { uploadIcon } from './uploadIcon.js'; | ||
import ErrorMessage from 'd2-ui/lib/messages/ErrorMessage.component'; | ||
import classes from 'classnames'; | ||
import { UploadIconField } from '../../upload-icon-field.js'; | ||
|
||
export class IconPickerCustomTab extends Component { | ||
constructor(props, context) { | ||
super(props, context); | ||
|
||
this.t = context.d2.i18n.getTranslation.bind(context.d2.i18n); | ||
this.d2 = context.d2; | ||
this.state = { | ||
iconFile: null, | ||
iconMetadata: { | ||
key: '', | ||
description: '', | ||
keywords: [], | ||
}, | ||
uploading: false, | ||
uploadError: null, | ||
}; | ||
} | ||
|
||
handleIconMetadataChange = event => { | ||
const value = event.target.value && event.target.value.trim(); | ||
const name = event.target.name; | ||
|
||
this.setState(prevState => ({ | ||
iconMetadata: { | ||
...prevState.iconMetadata, | ||
[name]: value, | ||
}, | ||
})); | ||
}; | ||
|
||
handleIconKeywordsChange = event => { | ||
const keywords = event.target.value | ||
.split(/[,\r\n]+/) | ||
.map(kw => kw.trim()) | ||
.filter(kw => !!kw); | ||
this.setState(prevState => ({ | ||
iconMetadata: { | ||
...prevState.iconMetadata, | ||
keywords, | ||
}, | ||
})); | ||
}; | ||
|
||
handleFileUpload = () => { | ||
const iconData = this.state.iconMetadata; | ||
this.setState({ uploading: true }); | ||
uploadIcon(this.state.iconFile, iconData) | ||
.then(() => { | ||
if (typeof this.props.onIconUpload === 'function') { | ||
this.props.onIconUpload(iconData.key); | ||
} | ||
this.setState({ uploading: false, uploadError: null }); | ||
}) | ||
.catch(e => this.setState({ uploading: false, uploadError: e })); | ||
}; | ||
|
||
handleFileChange = async event => { | ||
const file = event.target.files[0]; | ||
if (!file) { | ||
return; | ||
} | ||
|
||
this.setState(prevState => ({ | ||
iconFile: file, | ||
iconMetadata: { | ||
...prevState.iconMetadata, | ||
// remove file extension and replace hyphens with underscores | ||
key: file.name.replace(/\..*$/, '').replaceAll(/[-\s()]/g, '_'), | ||
}, | ||
})); | ||
}; | ||
|
||
render() { | ||
const iconKey = this.state.iconMetadata.key; | ||
|
||
return ( | ||
<div className={classes('icon-picker-custom', 'wrapper')}> | ||
<div className={'form'}> | ||
<UploadIconField onChange={this.handleFileChange} /> | ||
{this.state.iconFile && ( | ||
<div className="form-fields"> | ||
<TextField | ||
name="key" | ||
onChange={this.handleIconMetadataChange} | ||
floatingLabelText="Icon Key" | ||
value={iconKey} | ||
required | ||
/> | ||
<TextField | ||
name="description" | ||
onChange={this.handleIconMetadataChange} | ||
floatingLabelText="Description" | ||
/> | ||
<TextField | ||
name="keywords" | ||
onChange={this.handleIconKeywordsChange} | ||
floatingLabelText="Keywords" | ||
multiLine | ||
hintText="Separate keywords by ," | ||
/> | ||
{this.state.uploadError && ( | ||
<ErrorMessage | ||
message={this.state.uploadError.message} | ||
/> | ||
)} | ||
<RaisedButton | ||
className="upload-button" | ||
onClick={this.handleFileUpload} | ||
label={this.t('upload_icon')} | ||
primary | ||
disabled={this.state.uploading || !this.state.iconMetadata.key} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
<Divider /> | ||
<div>{this.props.children}</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
IconPickerCustomTab.propTypes = { | ||
children: PropTypes.node, | ||
onIconUpload: PropTypes.func, | ||
}; | ||
|
||
IconPickerCustomTab.contextTypes = { | ||
d2: PropTypes.object, | ||
}; |
Oops, something went wrong.