Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

DEP Upgrade frontend build stack #229

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10
18
6 changes: 6 additions & 0 deletions babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions client/src/bundles/bundle.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
require('legacy/entwine/TagField');
require('boot');
import 'legacy/entwine/TagField';
import 'boot';
96 changes: 78 additions & 18 deletions client/src/components/TagField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React, { Component } from 'react';
import Select from 'react-select';
import AsyncSelect from 'react-select/async';
import AsyncCreatableSelect from 'react-select/async-creatable';
import CreatableSelect from 'react-select/creatable';
import EmotionCssCacheProvider from 'containers/EmotionCssCacheProvider/EmotionCssCacheProvider';
import i18n from 'i18n';
import fetch from 'isomorphic-fetch';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import url from 'url';
Expand All @@ -18,6 +23,7 @@ class TagField extends Component {

this.handleChange = this.handleChange.bind(this);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.isValidNewOption = this.isValidNewOption.bind(this);
this.getOptions = this.getOptions.bind(this);
this.fetchOptions = debounce(this.fetchOptions, 500);
}
Expand All @@ -33,11 +39,11 @@ class TagField extends Component {
const { lazyLoad, options } = this.props;

if (!lazyLoad) {
return Promise.resolve({ options });
return Promise.resolve(options);
}

if (!input) {
return Promise.resolve({ options: [] });
return Promise.resolve([]);
}

return this.fetchOptions(input);
Expand Down Expand Up @@ -91,17 +97,62 @@ class TagField extends Component {

return fetch(url.format(fetchURL), { credentials: 'same-origin' })
.then((response) => response.json())
.then((json) => ({
options: json.items.map((item) => ({
.then((json) => json.items.map(
(item) => ({
[labelKey]: item.Title,
[valueKey]: item.Value,
Selected: item.Selected,
})),
}));
})
));
}

/**
* Check if a new option can be created based on a given input
* @param {string} inputValue
* @param {array|object} value
* @param {array} currentOptions
* @returns {boolean}
*/
isValidNewOption(inputValue, value, currentOptions) {
const { valueKey } = this.props;

// Don't allow empty options
if (!inputValue) {
return false;
}

// Don't repeat the currently selected option
if (Array.isArray(value)) {
if (this.valueInOptions(inputValue, value, valueKey)) {
return false;
}
} else if (inputValue === value[valueKey]) {
return false;
}

// Don't repeat any existing option
return !this.valueInOptions(inputValue, currentOptions, valueKey);
}

/**
* Check if a value is in an array of options already
* @param {string} value
* @param {array} options
* @param {string} valueKey
* @returns {boolean}
*/
valueInOptions(value, options, valueKey) {
// eslint-disable-next-line no-restricted-syntax
for (const item of options) {
if (value === item[valueKey]) {
return true;
}
}
return false;
}

render() {
const { lazyLoad, options, creatable, ...passThroughAttributes } =
const { lazyLoad, options, creatable, multi, disabled, labelKey, valueKey, ...passThroughAttributes } =
this.props;

const optionAttributes = lazyLoad
Expand All @@ -110,11 +161,11 @@ class TagField extends Component {

let SelectComponent = Select;
if (lazyLoad && creatable) {
SelectComponent = Select.AsyncCreatable;
SelectComponent = AsyncCreatableSelect;
} else if (lazyLoad) {
SelectComponent = Select.Async;
SelectComponent = AsyncSelect;
} else if (creatable) {
SelectComponent = Select.Creatable;
SelectComponent = CreatableSelect;
}

// Update the value to passthrough with the kept state provided this component is not
Expand All @@ -124,7 +175,7 @@ class TagField extends Component {
}

// if this is a single select then we just need the first value
if (!passThroughAttributes.multi && passThroughAttributes.value) {
if (!multi && passThroughAttributes.value) {
Comment on lines -127 to +178
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a passthrough anymore - missed this the first attempt when I pulled multi out as a distinct variable.

if (Object.keys(passThroughAttributes.value).length > 0) {
const value =
passThroughAttributes.value[
Expand All @@ -138,13 +189,22 @@ class TagField extends Component {
}

return (
<SelectComponent
{...passThroughAttributes}
onChange={this.handleChange}
onBlur={this.handleOnBlur}
inputProps={{ className: 'no-change-track' }}
{...optionAttributes}
/>
<EmotionCssCacheProvider>
<SelectComponent
{...passThroughAttributes}
isMulti={multi}
isDisabled={disabled}
cacheOptions
onChange={this.handleChange}
{...optionAttributes}
getOptionLabel={(option) => option[labelKey]}
getOptionValue={(option) => option[valueKey]}
noOptionsMessage={({ inputValue }) => (inputValue ? i18n._t('TagField.NO_OPTIONS', 'No options') : i18n._t('TagField.TYPE_TO_SEARCH', 'Type to search'))}
isValidNewOption={this.isValidNewOption}
getNewOptionData={(inputValue, label) => ({ [labelKey]: label, [valueKey]: inputValue })}
classNamePrefix="ss-tag-field"
/>
</EmotionCssCacheProvider>
);
}
}
Expand Down
111 changes: 108 additions & 3 deletions client/src/components/TagField.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,110 @@
@import "~react-select/scss/default";

.ss-tag-field .Select--multi .Select-value {
.ss-tag-field__multi-value {
margin-top: 3px;
}

/* Styles below here are duplicates of the treedropdownfield styles in silverstripe/admin, but with the appropriate classnames for tagfield. */
.ss-tag-field__control {
border-color: $gray-200;
box-shadow: none;
}

.ss-tag-field__control--is-focused {
border-color: $brand-primary;
box-shadow: none;
}

.ss-tag-field__option+.ss-tag-field__option {
border-top: 1px solid $border-color-light;
}

.ss-tag-field__option-button {
border: 1px solid $border-color-light;
border-radius: $border-radius;
background: $white;
// needed to override the width rule in .fill-width
width: auto !important; // sass-lint:disable-line no-important
max-width: 25%;
margin: -4px -5px -4px 5px;
padding: 4px 5px 4px 4px;
cursor: pointer;

&:hover {
background: $gray-200;
}

.font-icon-right-open-big {
margin: 2px 0 0 -1px;
width: 24px;
}
}

.ss-tag-field__option-count-icon {
padding: 0 ($spacer / 2);
line-height: 0.8;
}

.ss-tag-field__option-context {
color: $gray-600;
font-size: $font-size-sm;
}

.ss-tag-field__option--is-focused {
background-color: $list-group-hover-bg;
}

.ss-tag-field__option--is-selected {
background: $link-color;
color: $white;

.ss-tag-field__option-button {
border-color: $brand-primary;
background: none;
color: $white;

&:hover {
background: rgba(0, 0, 0, 0.2);
}
}
}

.ss-tag-field__option-title--highlighted {
font-weight: bold;
}

.ss-tag-field__indicator {
cursor: pointer;
}

.ss-tag-field__clear-indicator {
&:hover,
&:focus {
color: $brand-danger;
}
}

.ss-tag-field__dropdown-indicator {
&:hover,
&:focus {
color: $body-color-dark;
}
}

.ss-tag-field__multi-value {
color: $body-color;
background-color: $white;
border: 1px solid $input-focus-border-color;
border-radius: $border-radius;
}

.ss-tag-field__multi-value__remove {
font-size: $font-size-lg;
padding: 0 5px 2px;
border-left: 1px solid $input-focus-border-color;
border-radius: 0;

&:focus,
&:hover {
background-color: rgba(0, 113, 230, .08);
color: #0071e6;
}
}
13 changes: 8 additions & 5 deletions client/src/components/tests/TagField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ jest.mock('isomorphic-fetch');

import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-15.4';
import Adapter from 'enzyme-adapter-react-16';
import { Component as TagField } from '../TagField';
import Select from 'react-select';
import AsyncSelect from 'react-select/async';
import AsyncCreatableSelect from 'react-select/async-creatable';
import CreatableSelect from 'react-select/creatable';
import fetch from 'isomorphic-fetch';

Enzyme.configure({ adapter: new Adapter() });
Expand Down Expand Up @@ -38,22 +41,22 @@ describe('TagField', () => {
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.Creatable).length).toBe(1);
expect(wrapper.find(CreatableSelect).length).toBe(1);
});
it('Select.Async with lazyLoad option', () => {
props.lazyLoad = true;
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.Async).length).toBe(1);
expect(wrapper.find(AsyncSelect).length).toBe(1);
});
it('Select.AsyncCreatable with both creatable and lazyLoad options', () => {
props.creatable = true;
props.lazyLoad = true;
const wrapper = shallow(
<TagField {...props} />
);
expect(wrapper.find(Select.AsyncCreatable).length).toBe(1);
expect(wrapper.find(AsyncCreatableSelect).length).toBe(1);
});
});

Expand All @@ -69,7 +72,7 @@ describe('TagField', () => {
);

fetch.mockImplementation(() => Promise.resolve({
json: () => ({}),
json: () => ({ items: [{ Title: 'item1', Value: 'item1' }] }),
}));
});

Expand Down
Loading