keystone
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
250 lines (238 loc) • 6.99 kB
JavaScript
import async from 'async';
import Field from '../Field';
import { listsByKey } from '../../../admin/client/utils/lists';
import React from 'react';
import Select from 'react-select';
import xhr from 'xhr';
import {
Button,
FormInput,
InlineGroup as Group,
InlineGroupSection as Section,
} from '../../../admin/client/App/elemental';
import _ from 'lodash';
function compareValues (current, next) {
const currentLength = current ? current.length : 0;
const nextLength = next ? next.length : 0;
if (currentLength !== nextLength) return false;
for (let i = 0; i < currentLength; i++) {
if (current[i] !== next[i]) return false;
}
return true;
}
module.exports = Field.create({
displayName: 'RelationshipField',
statics: {
type: 'Relationship',
},
getInitialState () {
return {
value: null,
createIsOpen: false,
};
},
componentDidMount () {
this._itemsCache = {};
this.loadValue(this.props.value);
this.__isMounted = true;
},
componentWillUnmount () {
this.__isMounted = false;
},
componentWillReceiveProps (nextProps) {
if (nextProps.value === this.props.value || nextProps.many && compareValues(this.props.value, nextProps.value)) return;
this.loadValue(nextProps.value);
},
shouldCollapse () {
if (this.props.many) {
// many:true relationships have an Array for a value
return this.props.collapse && !this.props.value.length;
}
return this.props.collapse && !this.props.value;
},
buildFilters () {
var filters = {};
_.forEach(this.props.filters, (value, key) => {
if (typeof value === 'string' && value[0] === ':') {
var fieldName = value.slice(1);
var val = this.props.values[fieldName];
if (val) {
filters[key] = val;
return;
}
// check if filtering by id and item was already saved
if (fieldName === '_id' && Keystone.item) {
filters[key] = Keystone.item.id;
return;
}
} else {
filters[key] = value;
}
}, this);
var parts = [];
_.forEach(filters, function (val, key) {
parts.push('filters[' + key + '][value]=' + encodeURIComponent(val));
});
return parts.join('&');
},
cacheItem (item) {
item.href = Keystone.adminPath + '/' + this.props.refList.path + '/' + item.id;
this._itemsCache[item.id] = item;
},
loadValue (values) {
if (!values) {
return this.setState({
loading: false,
value: null,
});
};
values = Array.isArray(values) ? values : values.split(',');
const cachedValues = values.map(i => this._itemsCache[i]).filter(i => i);
if (cachedValues.length === values.length) {
this.setState({
loading: false,
value: this.props.many ? cachedValues : cachedValues[0],
});
return;
}
this.setState({
loading: true,
value: null,
});
async.map(values, (value, done) => {
xhr({
url: Keystone.adminPath + '/api/' + this.props.refList.path + '/' + value + '?basic',
responseType: 'json',
}, (err, resp, data) => {
if (err || !data) return done(err);
this.cacheItem(data);
done(err, data);
});
}, (err, expanded) => {
if (!this.__isMounted) return;
this.setState({
loading: false,
value: this.props.many ? expanded : expanded[0],
});
});
},
// NOTE: this seems like the wrong way to add options to the Select
loadOptionsCallback: {},
loadOptions (input, callback) {
// NOTE: this seems like the wrong way to add options to the Select
this.loadOptionsCallback = callback;
const filters = this.buildFilters();
xhr({
url: Keystone.adminPath + '/api/' + this.props.refList.path + '?basic&search=' + input + '&' + filters,
responseType: 'json',
}, (err, resp, data) => {
if (err) {
console.error('Error loading items:', err);
return callback(null, []);
}
data.results.forEach(this.cacheItem);
callback(null, {
options: data.results,
complete: data.results.length === data.count,
});
});
},
valueChanged (value) {
this.props.onChange({
path: this.props.path,
value: value,
});
},
openCreate () {
this.setState({
createIsOpen: true,
});
},
closeCreate () {
this.setState({
createIsOpen: false,
});
},
onCreate (item) {
this.cacheItem(item);
if (Array.isArray(this.state.value)) {
// For many relationships, append the new item to the end
const values = this.state.value.map((item) => item.id);
values.push(item.id);
this.valueChanged(values.join(','));
} else {
this.valueChanged(item.id);
}
// NOTE: this seems like the wrong way to add options to the Select
this.loadOptionsCallback(null, {
complete: true,
options: Object.keys(this._itemsCache).map((k) => this._itemsCache[k]),
});
this.closeCreate();
},
renderSelect (noedit) {
const inputName = this.getInputName(this.props.path);
const emptyValueInput = (this.props.many && (!this.state.value || !this.state.value.length) || (!this.props.many && !this.state.value))
? <input type="hidden" name={inputName} value="" /> : null;
return (
<div>
{/* This input ensures that an empty value is submitted when no related items are selected */}
{emptyValueInput}
{/* This input element fools Safari's autocorrect in certain situations that completely break react-select */}
<input type="text" style={{ position: 'absolute', width: 1, height: 1, zIndex: -1, opacity: 0 }} tabIndex="-1"/>
<Select.Async
multi={this.props.many}
disabled={noedit}
loadOptions={this.loadOptions}
labelKey="name"
name={inputName}
onChange={this.valueChanged}
simpleValue
value={this.state.value}
valueKey="id"
/>
</div>
);
},
renderInputGroup () {
// TODO: find better solution
// when importing the CreateForm using: import CreateForm from '../../../admin/client/App/shared/CreateForm';
// CreateForm was imported as a blank object. This stack overflow post suggested lazilly requiring it:
// http://stackoverflow.com/questions/29807664/cyclic-dependency-returns-empty-object-in-react-native
// TODO: Implement this somewhere higher in the app, it breaks the encapsulation of the RelationshipField component
const CreateForm = require('../../../admin/client/App/shared/CreateForm');
return (
<Group block>
<Section grow>
{this.renderSelect()}
</Section>
<Section>
<Button onClick={this.openCreate}>+</Button>
</Section>
<CreateForm
list={listsByKey[this.props.refList.key]}
isOpen={this.state.createIsOpen}
onCreate={this.onCreate}
onCancel={this.closeCreate} />
</Group>
);
},
renderValue () {
const { many } = this.props;
const { value } = this.state;
const props = {
children: value ? value.name : null,
component: value ? 'a' : 'span',
href: value ? value.href : null,
noedit: true,
};
return many ? this.renderSelect(true) : <FormInput {...props} />;
},
renderField () {
if (this.props.createInline) {
return this.renderInputGroup();
} else {
return this.renderSelect();
}
},
});