silverstripe-cms/client/src/components/AnchorSelectorField/AnchorSelectorField.js
Will Rossiter 4fbe0fd6b9 Fix linking anchor on the same page (#2388)
To reproduce this issue open a page, add an anchor, save the page, go to link to newly created anchor on the same page.

Incorrect result will be the new anchor does not appear. This patch ensures that the page anchors are reloaded.
2019-04-15 14:34:25 +12:00

204 lines
5.6 KiB
JavaScript

import i18n from 'i18n';
import React from 'react';
import fetch from 'isomorphic-fetch';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { formValueSelector } from 'redux-form';
import SilverStripeComponent from 'lib/SilverStripeComponent';
import * as anchorSelectorActions from 'state/anchorSelector/AnchorSelectorActions';
import anchorSelectorStates from 'state/anchorSelector/AnchorSelectorStates';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import { Creatable } from 'react-select';
import getFormState from 'lib/getFormState';
import classnames from 'classnames';
import PropTypes from 'prop-types';
const noop = () => null;
class AnchorSelectorField extends SilverStripeComponent {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleLoadingError = this.handleLoadingError.bind(this);
}
componentDidMount() {
this.ensurePagesLoaded();
}
componentDidUpdate(prevProps) {
if (this.props.pageId !== prevProps.pageId) {
this.ensurePagesLoaded();
}
}
/**
* Lazy-triggers load of the dropdown based on pageId
*
* @param {Object} props - Props to check
* @return {Promise} The promise object
*/
ensurePagesLoaded(props = this.props) {
// Only load if dirty and a valid ID
if (props.loadingState === anchorSelectorStates.UPDATING || !props.pageId) {
return Promise.resolve();
}
// Mark page updating
props.actions.anchorSelector.beginUpdating(props.pageId);
// Query endpoint for anchors for this page
const fetchURL = props.data.endpoint.replace(/:id/, props.pageId);
return fetch(fetchURL, { credentials: 'same-origin' })
.then(response => response.json())
.then((anchors) => {
// Update anchors
props.actions.anchorSelector.updated(props.pageId, anchors);
return anchors;
})
.catch((error) => {
props.actions.anchorSelector.updateFailed(props.pageId);
this.handleLoadingError(error, props);
});
}
/**
* Get options
*
* @return {Array}
*/
getDropdownOptions() {
const options = this.props.anchors.map(value => ({ value }));
// Ensure value is available in the list
if (this.props.value && !this.props.anchors.find(value => value === this.props.value)) {
options.unshift({ value: this.props.value });
}
return options;
}
/**
* Handles changes to the selected anchor
*
* @param {String} value
*/
handleChange(value) {
if (typeof this.props.onChange === 'function') {
this.props.onChange(value ? value.value : '');
}
}
handleLoadingError(error, props = this.props) {
if (props.onLoadingError === noop) {
throw error;
}
// Custom error handling
return props.onLoadingError({
errors: [
{
value: error.message,
type: 'error',
},
],
});
}
render() {
const inputProps = {
id: this.props.id,
};
const className = classnames('anchorselectorfield', this.props.extraClass);
const options = this.getDropdownOptions();
const value = this.props.value || '';
const placeholder = i18n._t('CMS.ANCHOR_SELECT_OR_TYPE', 'Select or enter anchor');
return (
<Creatable
searchable
options={options}
className={className}
name={this.props.name}
inputProps={inputProps}
onChange={this.handleChange}
onBlurResetsInput
value={value}
placeholder={placeholder}
labelKey="value"
/>
);
}
}
AnchorSelectorField.propTypes = {
extraClass: PropTypes.string,
id: PropTypes.string,
name: PropTypes.string.isRequired,
onChange: PropTypes.func,
value: PropTypes.string,
attributes: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
pageId: PropTypes.number,
anchors: PropTypes.array,
loadingState: PropTypes.oneOf(Object
.keys(anchorSelectorStates)
.map((key) => anchorSelectorStates[key])),
onLoadingError: PropTypes.func,
data: PropTypes.shape({
endpoint: PropTypes.string,
targetFieldName: PropTypes.string,
}),
};
AnchorSelectorField.defaultProps = {
value: '',
extraClass: '',
onLoadingError: noop,
attributes: {},
};
function mapStateToProps(state, ownProps) {
// Get pageId From selector field
const selector = formValueSelector(ownProps.formid, getFormState);
const targetFieldName = (ownProps && ownProps.data && ownProps.data.targetFieldName) || 'PageID';
const pageId = Number(selector(state, targetFieldName) || 0);
// Load anchors from page
let anchors = [];
const page = pageId
? state.cms.anchorSelector.pages.find(next => next.id === pageId)
: null;
if (page && page.loadingState === anchorSelectorStates.SUCCESS) {
// eslint-disable-next-line prefer-destructuring
anchors = page.anchors;
}
// Check status
let loadingState = null;
if (page) {
// eslint-disable-next-line prefer-destructuring
loadingState = page.loadingState;
} else if (pageId) {
// Triggers an update
loadingState = anchorSelectorStates.DIRTY;
} else {
// No page = success
loadingState = anchorSelectorStates.SUCCESS;
}
return { pageId, anchors, loadingState };
}
function mapDispatchToProps(dispatch) {
return {
actions: {
anchorSelector: bindActionCreators(anchorSelectorActions, dispatch),
},
};
}
const ConnectedAnchorSelectorField
= connect(mapStateToProps, mapDispatchToProps)(AnchorSelectorField);
export { AnchorSelectorField as Component, ConnectedAnchorSelectorField };
export default fieldHolder(ConnectedAnchorSelectorField);