diff --git a/scss/maintenance.scss b/scss/maintenance.scss index 17a57f211..779bf9bbb 100644 --- a/scss/maintenance.scss +++ b/scss/maintenance.scss @@ -60,4 +60,4 @@ html { .sortableModalHelper { z-index: 2000; -} \ No newline at end of file +} diff --git a/src/config/field-config/field-order.js b/src/config/field-config/field-order.js index cc1fa4754..5ca08d8a5 100644 --- a/src/config/field-config/field-order.js +++ b/src/config/field-config/field-order.js @@ -148,6 +148,7 @@ const fieldOrderByName = new Map([ 'name', 'shortName', 'code', + 'image', 'description', 'openingDate', 'closedDate', diff --git a/src/config/field-overrides/organisationUnit.js b/src/config/field-overrides/organisationUnit.js index d9016cc8c..f16dc97fc 100644 --- a/src/config/field-overrides/organisationUnit.js +++ b/src/config/field-overrides/organisationUnit.js @@ -1,8 +1,16 @@ import { isStartDateBeforeEndDate } from 'd2-ui/lib/forms/Validators'; import OrgUnitSelectDialogField from '../../forms/form-fields/orgunit-select-dialog-field'; import GeometryField, { validators as GeometryValidators } from '../../forms/form-fields/geometry-field'; +import { ImageSelect, ImageValidators } from '../../forms/form-fields/image-select'; export default new Map([ + [ + 'image', + { + component: ImageSelect, + validator: ImageValidators, + } + ], [ 'parent', { diff --git a/src/forms/form-fields/image-select.js b/src/forms/form-fields/image-select.js new file mode 100644 index 000000000..41eaae9d8 --- /dev/null +++ b/src/forms/form-fields/image-select.js @@ -0,0 +1,364 @@ +import Button from 'd2-ui/lib/button/Button'; +import PropTypes from 'prop-types'; +import React, { Component, createRef } from 'react'; + +const MAX_POLL_TRIES = 5; + +export const ImageValidators = [{ + // validator(value, formState) {}, + validator() {}, + message: 'invalid_image', +}]; + +const ImageSelectText = ({ children }) => { + return ( +
+ {children} +
+ ) +} + +ImageSelectText.propTypes = { + children: PropTypes.any.isRequired, +} + +const ImageSelectButton = ({ disabled, children, onClick }) => ( + { + if (disabled) return + onClick(...args) + }} + style={{ + display: 'inline-block', + marginRight: 4, + background: 'rgb(204, 204, 204)', + color: 'black', + height: 36, + lineHeight: '36px', + textTransform: 'uppercase', + fontWeight: 500, + fontSize: '14px', + letterSpacing: 0, + cursor: 'pointer', + padding: '0 16px', + ...(disabled ? { + color: 'rgba(0, 0, 0, 0.3)', + background: 'transparent', + cursor: 'not-allowed', + } : {}), + }} + > + {children} + +) + +ImageSelectButton.propTypes = { + onClick: PropTypes.func.isRequired, + children: PropTypes.any, + disabled: PropTypes.bool, +} + +export class ImageSelect extends Component { + fileInputRef = null + + constructor(props, context) { + super(props, context); + + this.api = context.d2.Api.getApi(); + this.state = { + initialValue: props.value, + initialized: !props.value, + loading: false, + error: null, + pending: false, + removed: false, + }; + + this.onFileSelect = this.onFileSelect.bind(this) + } + + componentDidMount() { + // no image exists on init + if (this.state.initialized) { + return; + } + + const { id } = this.props.value + this.pollStorageStatusWhilePending(id).finally( + () => this.setState({ initialized: true }) + ); + } + + storageStatusCheckDelay() { + return new Promise(resolve => setTimeout(resolve, 1000)) + } + + checkStorageStatus(id) { + return this.api + .get(`${this.api.baseUrl}/fileResources/${id}`) + .then(({ storageStatus }) => { + if (storageStatus === 'PENDING') { + this.setState({ pending: true }); + } else if (storageStatus === 'STORED') { + this.setState({ pending: false }); + } else { + const errorLabel = this.getTranslation('org_unit_image_storage_status_error'); + const error = new Error(`${errorLabel} ${storageStatus}`); + this.setState({ error }); + } + + return storageStatus; + }) + .catch(error => this.setState({ error })) + } + + pollStorageStatusWhilePending(id, pollCount = 0) { + if (pollCount === MAX_POLL_TRIES) { + const error = new Error('Timed out polling for image storage status update') + this.setState({ error }) + return + } + + return this.checkStorageStatus(id).then(storageStatus => { + if (storageStatus === 'PENDING') { + return this.storageStatusCheckDelay().then(() => + this.pollStorageStatusWhilePending(id, pollCount + 1) + ); + } + }); + } + + onFileSelect(event) { + const { onChange } = this.props; + const file = event.target.files[0]; + + if (!file) { + return; + } + + const formData = new FormData(); + formData.append('file', file); + formData.append('domain', this.props.domain); + + // Send image to server and save image id as avatar + this.setState({ + loading: true, + error: null, + pending: false, + removed: false, + }); + this.api + .post('fileResources', formData) + .then(postResponse => { + const { id: imageId, storageStatus } = postResponse.response.fileResource; + + if (!['PENDING', 'STORED'].includes(storageStatus)) { + throw new Error( + `Org unit image could not be stored, storageStatus is: ${storageStatus}` + ); + } + + const pending = storageStatus === 'PENDING' + this.setState({ pending, loading: false }); + + const imageReference = { id: imageId } + const target = { value: imageReference }; + onChange({ target }); + + if (pending) { + this.pollStorageStatusWhilePending(imageId) + } + }) + .catch(error => { + this.setState({ loading: false, error }) + onChange({ target: { value: null } }) + }); + } + + getTranslation(key) { + return this.context.d2.i18n.getTranslation(key); + } + + render() { + const { value: fileReference } = this.props; + const { id: fileResourceId } = fileReference || {} + const { initialized, initialValue, loading, error, pending } = this.state; + + const fileResourceReady = initialized && !loading && !error && fileResourceId; + const isPending = fileResourceReady && pending; + const displayImage = fileResourceReady && !isPending; + const wasRemoved = !fileResourceId && this.state.removed + const hasNoImage = !fileResourceId && !this.state.removed + const hasInitialValue = initialValue && initialValue.id + const dirty = ( + (fileResourceId && !hasInitialValue) || + (!fileResourceId && hasInitialValue) || + (hasInitialValue && initialValue.id !== fileResourceId) + ) + + return ( +