diff --git a/.eslintignore b/.eslintignore index 4a05976ff8..21262b5a35 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -**/src/locales/* **/cypress/assets +**/src/locales/* diff --git a/cypress/integration/Transfer/display-order/index.js b/cypress/integration/Transfer/display-order/index.js index 23eb64c90a..a077264c34 100644 --- a/cypress/integration/Transfer/display-order/index.js +++ b/cypress/integration/Transfer/display-order/index.js @@ -61,9 +61,9 @@ When('the user selects multiple options', () => { }) .each($option => cy.wrap($option).clickWith('ctrl')) .then($options => { - cy.wrap($options.toArray().map(extractOptionFromElement)) - .as('selectedOptions') - .then(console.log.bind(null, 'selectedOptions')) + cy.wrap($options.toArray().map(extractOptionFromElement)).as( + 'selectedOptions' + ) }) cy.get('{transfer-actions-addindividual}').click() @@ -150,7 +150,6 @@ Then( () => cy.get('{transfer-sourceoptions} {transferoption}'), () => cy.get('@deselectedOptions') ).should(([win, $selectableSourceOptions, deselectedOptions]) => { - console.log('deselectedOptions', deselectedOptions) const selectableSourceOptions = $selectableSourceOptions .toArray() .map(extractOptionFromElement) @@ -169,9 +168,6 @@ Then( value === deselectedOption.value ) - if (!result) { - console.log('deselectedOption', deselectedOption) - } return result }) expect(hasAllOptions).to.equal(true) @@ -205,8 +201,6 @@ Then( .slice(transferredOptions.length * -1) .map(extractOptionFromElement) - console.log('transferredOptions', transferredOptions) - console.log('lastSelectedOptions', lastSelectedOptions) expect(transferredOptions).to.eql(lastSelectedOptions) }) } diff --git a/packages/core/src/Transfer/Filter.js b/packages/core/src/Transfer/Filter.js deleted file mode 100644 index c8bbf91afc..0000000000 --- a/packages/core/src/Transfer/Filter.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import propTypes from '@dhis2/prop-types' - -import { spacers } from '@dhis2/ui-constants' - -import { Input } from '../Input/Input.js' -import { Field } from '../Field/Field.js' - -export const Filter = ({ dataTest, filter, onChange, label }) => { - return ( -
- - - - - -
- ) -} - -Filter.propTypes = { - dataTest: propTypes.string.isRequired, - filter: propTypes.string.isRequired, - onChange: propTypes.func.isRequired, - label: propTypes.string, -} diff --git a/packages/core/src/Transfer/PickedOptions.js b/packages/core/src/Transfer/PickedOptions.js deleted file mode 100644 index d0218f9484..0000000000 --- a/packages/core/src/Transfer/PickedOptions.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import propTypes from '@dhis2/prop-types' - -import { spacers } from '@dhis2/ui-constants' - -import { findOption, getModeByModifierKey } from './common.js' - -// TODO: This will be refactored away to match the MultiSelect -export const multiSelectedPropType = propTypes.arrayOf( - propTypes.shape({ - label: propTypes.string, - value: propTypes.string, - }) -) - -export const PickedOptions = ({ - children, - dataTest, - toggleHighlightedPickedOption, - selectedEmptyComponent, - highlightedPickedOptions, - deselectSingleOption, -}) => ( -
- {!React.Children.count(children) && selectedEmptyComponent} - {React.Children.map(children, child => { - const option = { - label: child.props.label, - value: child.props.value, - } - - return React.cloneElement(child, { - onClick: (_, event) => { - const mode = getModeByModifierKey(event) - toggleHighlightedPickedOption({ option, mode }) - }, - onDoubleClick: deselectSingleOption, - highlighted: !!findOption(highlightedPickedOptions, option), - }) - })} - - -
-) - -PickedOptions.propTypes = { - children: propTypes.node.isRequired, - dataTest: propTypes.string.isRequired, - deselectSingleOption: propTypes.func.isRequired, - toggleHighlightedPickedOption: propTypes.func.isRequired, - highlightedPickedOptions: multiSelectedPropType, - selectedEmptyComponent: propTypes.node, -} diff --git a/packages/core/src/Transfer/SourceOptions.js b/packages/core/src/Transfer/SourceOptions.js deleted file mode 100644 index af4a0e1810..0000000000 --- a/packages/core/src/Transfer/SourceOptions.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import propTypes from '@dhis2/prop-types' - -import { spacers } from '@dhis2/ui-constants' - -import { findOption, getModeByModifierKey } from './common.js' - -// TODO: This will be refactored away to match the MultiSelect -export const multiSelectedPropType = propTypes.arrayOf( - propTypes.shape({ - label: propTypes.string, - value: propTypes.string, - }) -) - -export const SourceOptions = ({ - children, - dataTest, - toggleHighlightedSourceOption, - sourceEmptyPlaceholder, - highlightedSourceOptions, - selectSingleOption, -}) => ( -
- {React.Children.map(children, child => { - const option = { - label: child.props.label, - value: child.props.value, - } - - return React.cloneElement(child, { - onClick: (_, event) => { - const mode = getModeByModifierKey(event) - toggleHighlightedSourceOption({ option, mode }) - }, - onDoubleClick: selectSingleOption, - highlighted: !!findOption(highlightedSourceOptions, option), - }) - })} - - {!React.Children.count(children) && sourceEmptyPlaceholder} - - -
-) - -SourceOptions.propTypes = { - dataTest: propTypes.string.isRequired, - selectSingleOption: propTypes.func.isRequired, - toggleHighlightedSourceOption: propTypes.func.isRequired, - children: propTypes.node, - highlightedSourceOptions: multiSelectedPropType, - sourceEmptyPlaceholder: propTypes.node, -} diff --git a/packages/core/src/Transfer/__e2e__/common.js b/packages/core/src/Transfer/__e2e__/common.js deleted file mode 100644 index 9c8f57b50d..0000000000 --- a/packages/core/src/Transfer/__e2e__/common.js +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react' -import { TransferOption } from '../../index.js' - -export const extractOptionFromComponent = ({ props }) => ({ - label: props.label, - value: props.value, -}) - -export const statefulDecorator = ({ - initialState = [], - controlFilter = false, - initialSearchTerm = '', -} = {}) => fn => - React.createElement(() => { - const initialSelected = initialState.map(child => child.props) - const [selected, setSelected] = useState(initialSelected) - const [searchTerm, setSearchTerm] = useState(initialSearchTerm) - - return fn({ - selected, - searchTerm: controlFilter ? searchTerm : undefined, - onChange: payload => setSelected(payload.selected), - onFilterChange: controlFilter - ? ({ value }) => setSearchTerm(value) - : undefined, - }) - }) - -export const options = [ - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , -] diff --git a/packages/core/src/Transfer/__e2e__/display-order.stories.e2e.js b/packages/core/src/Transfer/__e2e__/display-order.stories.e2e.js deleted file mode 100644 index abe2c0338f..0000000000 --- a/packages/core/src/Transfer/__e2e__/display-order.stories.e2e.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react' - -import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' - -export default { title: 'Transfer Display Order' } - -window.options = options.map(child => { - const { label, value } = child.props - return { label, value } -}) - -export const NoSelection = ({ selected, onChange }) => ( - - {options} - -) - -export const SomeSelected = ({ selected, onChange }) => ( - - {options} - -) - -SomeSelected.story = { - decorators: [ - statefulDecorator({ - initialState: options.slice(0, 4), - }), - ], -} diff --git a/packages/core/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js b/packages/core/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js deleted file mode 100644 index 855d09349f..0000000000 --- a/packages/core/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react' - -import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' - -export default { - title: 'Transfer highlight range of options', - decorators: [statefulDecorator()], -} - -export const HasOptions = ({ onChange, selected }) => ( - - {options} - -) - -export const HasSelected = ({ onChange, selected }) => ( - - {options} - -) - -HasSelected.story = { - decorators: [ - statefulDecorator({ - initialState: options.slice(0, 4), - }), - ], -} - -export const AllSelected = ({ onChange, selected }) => ( - - {options} - -) - -AllSelected.story = { - decorators: [ - statefulDecorator({ - initialState: options, - }), - ], -} diff --git a/packages/core/src/Transfer/__tests__/common.test.js b/packages/core/src/Transfer/__tests__/common.test.js deleted file mode 100644 index 8c33215dee..0000000000 --- a/packages/core/src/Transfer/__tests__/common.test.js +++ /dev/null @@ -1,275 +0,0 @@ -import { - addOption, - isOption, - findOption, - findOptionIndex, - removeOption, - toggleOption, - toggleOptions, -} from '../../Transfer/common.js' - -describe('Transfer - isOption', () => { - it('should return true when the options are the same', () => { - const option1 = { label: 'foo', value: 'bar' } - const option2 = { label: 'foo', value: 'bar' } - const actual = isOption(option1, option2) - const expected = true - - expect(actual).toBe(expected) - }) - - it('should return false when the labels do not match', () => { - const option1 = { label: 'foo', value: 'bar' } - const option2 = { label: 'baz', value: 'bar' } - const actual = isOption(option1, option2) - const expected = false - - expect(actual).toBe(expected) - }) - - it('should return false when the values do not match', () => { - const option1 = { label: 'foo', value: 'bar' } - const option2 = { label: 'foo', value: 'baz' } - const actual = isOption(option1, option2) - const expected = false - - expect(actual).toBe(expected) - }) -}) - -describe('Transfer - findOptionIndex', () => { - it('should return index 1', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'foo', value: 'baz' } - const actual = findOptionIndex(options, option) - const expected = 1 - - expect(actual).toBe(expected) - }) - - it('should return -1 when the option is not included', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = findOptionIndex(options, option) - const expected = -1 - - expect(actual).toBe(expected) - }) -}) - -describe('Transfer - findOption', () => { - it('should return the option', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'foo', value: 'baz' } - const actual = findOption(options, option) - const expected = { label: 'foo', value: 'baz' } - - expect(actual).toEqual(expected) - }) - - it('should return undefined when the option is not included', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = findOption(options, option) - const expected = undefined - - expect(actual).toEqual(expected) - }) -}) - -describe('Transfer - toggleOption', () => { - it('should add the option to the collection', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = toggleOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should remove the option from the collection', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = toggleOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) -}) - -describe('Transfer - addOption', () => { - it('should add the option to the collection', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = addOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should not add the option to the collection when already included', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = addOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) -}) - -describe('Transfer - removeOption', () => { - it('should remove the option to the collection', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - { label: 'baz', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = removeOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should not remove the option to the collection when already included', () => { - const options = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - const option = { label: 'baz', value: 'baz' } - const actual = removeOption(options, option) - const expected = [ - { label: 'foo', value: 'bar' }, - { label: 'foo', value: 'baz' }, - ] - - expect(actual).toEqual(expected) - }) -}) - -describe('Transfer - toggleOptions', () => { - it('should add all options when none are inside the colleciton yet', () => { - const collection = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - ] - const optionsToToggle = [ - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - const actual = toggleOptions(collection, optionsToToggle) - const expected = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should remove all options when all are included', () => { - const collection = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - const optionsToToggle = [ - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - const actual = toggleOptions(collection, optionsToToggle) - const expected = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should add some and remove some when only some are given', () => { - const collection = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'baz', value: 'baz' }, - ] - const optionsToToggle = [ - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - const actual = toggleOptions(collection, optionsToToggle) - const expected = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'foobar', value: 'foobar' }, - ] - - expect(actual).toEqual(expected) - }) - - it('should only add the missing one and not remove any when using a custom modifier', () => { - const collection = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'baz', value: 'baz' }, - ] - const optionsToToggle = [ - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - const actual = toggleOptions(collection, optionsToToggle, addOption) - const expected = [ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: 'baz', value: 'baz' }, - { label: 'foobar', value: 'foobar' }, - ] - - expect(actual).toEqual(expected) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/common/createChildren.js b/packages/core/src/Transfer/__tests__/common/createChildren.js deleted file mode 100644 index 3ef92d8119..0000000000 --- a/packages/core/src/Transfer/__tests__/common/createChildren.js +++ /dev/null @@ -1,9 +0,0 @@ -import { createElement } from 'react' - -export const createChildren = (...childElements) => { - const div = createElement('div', {}, childElements) - const { props } = div - const { children } = props - - return children -} diff --git a/packages/core/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js b/packages/core/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js deleted file mode 100644 index 0345c3e2d3..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { createElement } from 'react' - -import { addAllSelectableSourceOptions } from '../../helper/addAllSelectableSourceOptions.js' -import { createChildren } from '../common/createChildren.js' - -describe('Transfer - addAllSelectableSourceOptions', () => { - const onChange = jest.fn() - const setHighlightedSourceOptions = jest.fn() - - const sourceReactOptions = createChildren( - createElement('div', { key: 'foo', label: 'Foo', value: 'foo' }), - createElement('div', { key: 'bar', label: 'Bar', value: 'bar' }), - createElement('div', { key: 'baz', label: 'Baz', value: 'baz' }), - createElement('div', { - key: 'foobar', - label: 'Foobar', - value: 'foobar', - }), - createElement('div', { - key: 'foobaz', - label: 'Foobaz', - value: 'foobaz', - }) - ) - - afterEach(() => { - onChange.mockClear() - setHighlightedSourceOptions.mockClear() - }) - - it('should add all selectable source options to the selectedPlainOptions array', () => { - const selectedPlainOptions = [{ label: 'Barfoo', value: 'barfoo' }] - const expected = { - selected: [ - { label: 'Barfoo', value: 'barfoo' }, - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - { label: 'Foobar', value: 'foobar' }, - { label: 'Foobaz', value: 'foobaz' }, - ], - } - - addAllSelectableSourceOptions({ - sourceReactOptions, - selectedPlainOptions, - onChange, - setHighlightedSourceOptions, - }) - - expect(onChange).toHaveBeenCalledWith(expected) - }) - - it('should reset all highlighted source options', () => { - addAllSelectableSourceOptions({ - sourceReactOptions, - selectedPlainOptions: [{ label: 'Barfoo', value: 'barfoo' }], - onChange, - setHighlightedSourceOptions, - }) - - expect(setHighlightedSourceOptions).toHaveBeenCalledWith([]) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js b/packages/core/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js deleted file mode 100644 index 9962b1a81a..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js +++ /dev/null @@ -1,105 +0,0 @@ -import { createElement } from 'react' - -import { addIndividualSourceOptions } from '../../helper/addIndividualSourceOptions.js' -import { createChildren } from '../common/createChildren.js' - -describe('Transfer - addIndividualSourceOptions', () => { - const onChange = jest.fn() - const setHighlightedSourceOptions = jest.fn() - - const filteredSourcePlainOptions = createChildren( - createElement('div', { key: 'foo', label: 'Foo', value: 'foo' }), - createElement('div', { - key: 'foobar', - label: 'Foobar', - value: 'foobar', - }), - createElement('div', { - key: 'foobaz', - label: 'Foobaz', - value: 'foobaz', - }) - ) - - const highlightedSourcePlainOptions = [ - { label: 'Foobaz', value: 'foobaz' }, - { label: 'Bar', value: 'bar' }, - ] - - const selectedPlainOptions = [{ label: 'Barfoo', value: 'barfoo' }] - - afterEach(() => { - onChange.mockClear() - setHighlightedSourceOptions.mockClear() - }) - - it('should add the highlighted source options to the selectedPlainOptions array', () => { - addIndividualSourceOptions({ - highlightedSourcePlainOptions, - maxSelections: Infinity, - onChange, - selectedPlainOptions, - setHighlightedSourceOptions, - }) - - expect(onChange).toHaveBeenCalledWith({ - selected: [ - { label: 'Barfoo', value: 'barfoo' }, - { label: 'Foobaz', value: 'foobaz' }, - { label: 'Bar', value: 'bar' }, - ], - }) - }) - - it('should reset the highlighted source options', () => { - addIndividualSourceOptions({ - highlightedSourcePlainOptions, - maxSelections: Infinity, - onChange, - selectedPlainOptions, - setHighlightedSourceOptions, - }) - - expect(setHighlightedSourceOptions).toHaveBeenCalledWith([]) - }) - - it('should only select the filtered source options', () => { - addIndividualSourceOptions({ - filterable: true, - filter: 'oo', - filteredSourcePlainOptions, - highlightedSourcePlainOptions, - maxSelections: Infinity, - onChange, - selectedPlainOptions, - setHighlightedSourceOptions, - }) - - expect(onChange).toHaveBeenCalledWith({ - selected: [ - { label: 'Barfoo', value: 'barfoo' }, - { label: 'Foobaz', value: 'foobaz' }, - ], - }) - }) - - it('should only call onChange with the max selection amount', () => { - addIndividualSourceOptions({ - filterable: true, - filter: 'oo', - filteredSourcePlainOptions, - highlightedSourcePlainOptions: highlightedSourcePlainOptions.slice( - 0, - 1 - ), - maxSelections: 1, - onChange, - selectedPlainOptions: selectedPlainOptions.slice(0, 1), - setHighlightedSourceOptions, - }) - - expect(onChange).toHaveBeenCalledWith({ - selected: [{ label: 'Foobaz', value: 'foobaz' }], - }) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/extractPickedReactOptions.test.js b/packages/core/src/Transfer/__tests__/helper/extractPickedReactOptions.test.js deleted file mode 100644 index ce506cf36d..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/extractPickedReactOptions.test.js +++ /dev/null @@ -1,79 +0,0 @@ -import { Children, createElement } from 'react' - -import { createChildren } from '../common/createChildren.js' -import { extractPickedReactOptions } from '../../helper/extractPickedReactOptions.js' - -describe('Transfer - extractPickedReactOptions', () => { - it('should remove all non-picked options', () => { - const toggleHighlightedPickedOptions = jest.fn() - const highlightedPickedOptions = [] - const reactOptions = createChildren( - createElement('div', { key: 'foo', label: 'Foo', value: 'foo' }), - createElement('div', { key: 'bar', label: 'Bar', value: 'bar' }), - createElement('div', { key: 'baz', label: 'Baz', value: 'baz' }) - ) - const selectedPlainOptions = [{ label: 'Baz', value: 'baz' }] - const result = Children.toArray( - extractPickedReactOptions({ - reactOptions, - selectedPlainOptions, - highlightedPickedOptions, - toggleHighlightedPickedOptions, - }) - ) - - expect(result).toHaveLength(1) - expect(result[0].props).toEqual( - expect.objectContaining({ label: 'Baz', value: 'baz' }) - ) - }) - - it('sorts the picked options by the order of the "selectedPlainOptions" array', () => { - const toggleHighlightedPickedOptions = jest.fn() - const highlightedPickedOptions = [] - const reactOptions = createChildren( - createElement('div', { key: 'foo', label: 'Foo', value: 'foo' }), - createElement('div', { key: 'bar', label: 'Bar', value: 'bar' }), - createElement('div', { key: 'baz', label: 'Baz', value: 'baz' }), - createElement('div', { - key: 'foobar', - label: 'Foobar', - value: 'foobar', - }), - createElement('div', { - key: 'foobaz', - label: 'Foobaz', - value: 'foobaz', - }), - createElement('div', { - key: 'barfoo', - label: 'Barfoo', - value: 'barfoo', - }) - ) - const selectedPlainOptions = [ - { label: 'Barfoo', value: 'barfoo' }, - { label: 'Foo', value: 'foo' }, - { label: 'Foobar', value: 'foobar' }, - ] - const result = Children.toArray( - extractPickedReactOptions({ - reactOptions, - selectedPlainOptions, - highlightedPickedOptions, - toggleHighlightedPickedOptions, - }) - ) - - expect(result).toHaveLength(3) - expect(result[0].props).toEqual( - expect.objectContaining({ label: 'Barfoo', value: 'barfoo' }) - ) - expect(result[1].props).toEqual( - expect.objectContaining({ label: 'Foo', value: 'foo' }) - ) - expect(result[2].props).toEqual( - expect.objectContaining({ label: 'Foobar', value: 'foobar' }) - ) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/filterOutOptions.test.js b/packages/core/src/Transfer/__tests__/helper/filterOutOptions.test.js deleted file mode 100644 index 05d3f9887a..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/filterOutOptions.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Children, createElement } from 'react' - -import { createChildren } from '../common/createChildren.js' -import { filterOutOptions } from '../../helper/filterOutOptions.js' - -describe('Transfer - filterOutOptions', () => { - it('should remove all picked options', () => { - const reactOptions = createChildren( - createElement('div', { label: 'Foo', value: 'foo', key: 'foo' }), - createElement('div', { label: 'Bar', value: 'bar', key: 'bar' }), - createElement('div', { label: 'Baz', value: 'baz', key: 'baz' }) - ) - - const plainOptions = [{ label: 'Baz', value: 'baz' }] - - const result = Children.toArray( - filterOutOptions(reactOptions, plainOptions) - ) - - expect(result).toHaveLength(2) - expect(result[0].props).toEqual({ label: 'Foo', value: 'foo' }) - expect(result[1].props).toEqual({ label: 'Bar', value: 'bar' }) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/filterReactOptionsBy.test.js b/packages/core/src/Transfer/__tests__/helper/filterReactOptionsBy.test.js deleted file mode 100644 index b43317ae7f..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/filterReactOptionsBy.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' - -import { TransferOption } from '../../TransferOption.js' -import { createChildren } from '../common/createChildren.js' -import { filterReactOptionsBy } from '../../helper/filterReactOptionsBy.js' - -describe('Transfer - filterReactOptionsBy', () => { - it('should return all enabled options', () => { - const reactOptions = createChildren( - , - , - , - , - - ) - - const expectedChildren = createChildren( - , - , - - ) - - const actualChildren = filterReactOptionsBy( - ({ disabled }) => !disabled, - reactOptions - ) - - const expected = expectedChildren.map(child => child.props) - const actual = actualChildren.map(child => child.props) - - expect(actual).toEqual(expected) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/getPlainOptionFromReactOption.test.js b/packages/core/src/Transfer/__tests__/helper/getPlainOptionFromReactOption.test.js deleted file mode 100644 index 488e2e4baf..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/getPlainOptionFromReactOption.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { getPlainOptionFromReactOption } from '../../../Transfer/helper/getPlainOptionFromReactOption.js' - -describe('Transfer - getPlainOptionFromReactOption', () => { - it('should extract the plain option', () => { - const reactOption = React.createElement('span', { - label: 'Foo', - value: 'foo', - disabled: false, - }) - const expected = { label: 'Foo', value: 'foo', disabled: false } - const actual = getPlainOptionFromReactOption(reactOption) - - expect(actual).toEqual(expected) - }) - - it('should merge additionalData into the option', () => { - const additionalData = { - bar: 'Bar', - baz: 'Baz', - } - const reactOption = React.createElement('span', { - label: 'Foo', - value: 'foo', - disabled: false, - additionalData, - }) - const expected = expect.objectContaining(additionalData) - const actual = getPlainOptionFromReactOption(reactOption) - - expect(actual).toEqual(expected) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/getPlainOptionsFromReactOptions.test.js b/packages/core/src/Transfer/__tests__/helper/getPlainOptionsFromReactOptions.test.js deleted file mode 100644 index 65b7d3e53f..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/getPlainOptionsFromReactOptions.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { createElement } from 'react' - -import { createChildren } from '../common/createChildren.js' -import { getPlainOptionsFromReactOptions } from '../../helper/getPlainOptionsFromReactOptions.js' - -describe('Transfer - getPlainOptionsFromReactOptions', () => { - it('should extract the option objects from the reactOptions and return as array', () => { - const reactOptions = createChildren( - createElement('div', { - label: 'Foo', - value: 'foo', - key: 'foo', - }), - createElement('div', { - label: 'Bar', - value: 'bar', - key: 'bar', - }) - ) - - const expected = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - ] - const actual = getPlainOptionsFromReactOptions(reactOptions) - - expect(actual).toEqual(expected) - }) - - it('should extract additionalData', () => { - const reactOptions = createChildren( - createElement('div', { - label: 'Foo', - value: 'foo', - key: 'foo', - additionalData: { - year: '2020', - relativePeriod: true, - }, - }), - createElement('div', { - label: 'Bar', - value: 'bar', - key: 'bar', - additionalData: { - year: '2019', - relativePeriod: false, - }, - }) - ) - - const expected = [ - { label: 'Foo', value: 'foo', year: '2020', relativePeriod: true }, - { label: 'Bar', value: 'bar', year: '2019', relativePeriod: false }, - ] - const actual = getPlainOptionsFromReactOptions(reactOptions) - - expect(actual).toEqual(expected) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/getSubsetByFilter.test.js b/packages/core/src/Transfer/__tests__/helper/getSubsetByFilter.test.js deleted file mode 100644 index d4b495955c..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/getSubsetByFilter.test.js +++ /dev/null @@ -1,81 +0,0 @@ -import { Children, createElement } from 'react' - -import { createChildren } from '../common/createChildren.js' -import { getSubsetByFilter } from '../../helper/getSubsetByFilter.js' - -describe('Transfer - getSubsetByFilter', () => { - const filterCallback = (options, filter) => - options.filter(({ label }) => label.indexOf(filter) !== -1) - - const reactOptions = createChildren( - createElement('div', { key: 'foo', label: 'Foo', value: 'foo' }), - createElement('div', { key: 'bar', label: 'Bar', value: 'bar' }), - createElement('div', { key: 'baz', label: 'Baz', value: 'baz' }), - createElement('div', { - key: 'foobar', - label: 'Foobar', - value: 'foobar', - }), - createElement('div', { - key: 'foobaz', - label: 'Foobaz', - value: 'foobaz', - }), - createElement('div', { - key: 'barfoo', - label: 'Barfoo', - value: 'barfoo', - }) - ) - - it('should return only the react reactOptions with a matching label', () => { - const result = Children.toArray( - getSubsetByFilter({ - filterable: true, - filter: 'oo', - reactOptions, - filterCallback, - }) - ) - - expect(result).toHaveLength(4) - expect(result[0].props).toEqual( - expect.objectContaining({ label: 'Foo', value: 'foo' }) - ) - expect(result[1].props).toEqual( - expect.objectContaining({ label: 'Foobar', value: 'foobar' }) - ) - expect(result[2].props).toEqual( - expect.objectContaining({ label: 'Foobaz', value: 'foobaz' }) - ) - expect(result[3].props).toEqual( - expect.objectContaining({ label: 'Barfoo', value: 'barfoo' }) - ) - }) - - it('should return all reactOptions when filterable is false', () => { - const result = Children.toArray( - getSubsetByFilter({ - filterable: false, - filter: 'oo', - reactOptions, - filterCallback, - }) - ) - - expect(result).toHaveLength(6) - }) - - it('should return all reactOptions when the filter is empty', () => { - const result = Children.toArray( - getSubsetByFilter({ - filterable: true, - filter: '', - reactOptions, - filterCallback, - }) - ) - - expect(result).toHaveLength(6) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js b/packages/core/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js deleted file mode 100644 index 0a3dc1fc1b..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import { isReorderDownDisabled } from '../../../Transfer/helper/isReorderDownDisabled.js' - -describe('Transfer - isReorderDownDisabled', () => { - const selectedPlainOptions = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - - it('should return true when there are no highlighted picked options', () => { - const highlightedPickedPlainOptions = [] - const actual = isReorderDownDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return true when there are multiple highlighted picked options', () => { - const highlightedPickedPlainOptions = [ - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - const actual = isReorderDownDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return true if the last picked option is highlighted', () => { - const highlightedPickedPlainOptions = [{ label: 'Baz', value: 'baz' }] - - const actual = isReorderDownDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return false when one picked option is highlighted which is not the last one', () => { - const highlightedPickedPlainOptions = [{ label: 'Bar', value: 'bar' }] - - const actual = isReorderDownDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(false) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js b/packages/core/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js deleted file mode 100644 index ccbdf939a0..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import { isReorderUpDisabled } from '../../../Transfer/helper/isReorderUpDisabled.js' - -describe('Transfer - isReorderUpDisabled', () => { - const selectedPlainOptions = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - - it('should return true when there are no highlighted picked options', () => { - const highlightedPickedPlainOptions = [] - const actual = isReorderUpDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return true when there are multiple highlighted picked options', () => { - const highlightedPickedPlainOptions = [ - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - const actual = isReorderUpDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return true if the first picked option is highlighted', () => { - const highlightedPickedPlainOptions = [{ label: 'Foo', value: 'foo' }] - - const actual = isReorderUpDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(true) - }) - - it('should return false when one picked option is highlighted which is not the last one', () => { - const highlightedPickedPlainOptions = [{ label: 'Baz', value: 'baz' }] - - const actual = isReorderUpDisabled({ - highlightedPickedPlainOptions, - selectedPlainOptions, - }) - - expect(actual).toBe(false) - }) -}) diff --git a/packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js b/packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js deleted file mode 100644 index d5598b0fbc..0000000000 --- a/packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { moveHighlightedPickedOptionUp } from '../../../Transfer/helper/moveHighlightedPickedOptionUp.js' - -describe('Transfer - moveHighlightedPickedOptionUp', () => { - const onChange = jest.fn() - - const selectedPlainOptions = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - - afterEach(() => { - onChange.mockClear() - }) - - it('should move the highlighted option up', () => { - const highlighted = [{ label: 'Bar', value: 'bar' }] - - moveHighlightedPickedOptionUp({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, - onChange, - }) - - expect(onChange).toHaveBeenCalledWith({ - selected: [ - { label: 'Bar', value: 'bar' }, - { label: 'Foo', value: 'foo' }, - { label: 'Baz', value: 'baz' }, - ], - }) - }) - - it('should do nothing when trying to move up the first option', () => { - const highlighted = [{ label: 'Foo', value: 'foo' }] - - moveHighlightedPickedOptionUp({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, - onChange, - }) - - expect(onChange).toHaveBeenCalledTimes(0) - }) - - it('should do nothing when trying to move up a non-existing option', () => { - const highlighted = [{ label: 'Foobar', value: 'foobar' }] - - moveHighlightedPickedOptionUp({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, - onChange, - }) - - expect(onChange).toHaveBeenCalledTimes(0) - }) -}) diff --git a/packages/core/src/Transfer/common.js b/packages/core/src/Transfer/common.js deleted file mode 100644 index 55d5c86b86..0000000000 --- a/packages/core/src/Transfer/common.js +++ /dev/null @@ -1,113 +0,0 @@ -import './types.js' -import { colors } from '@dhis2/ui-constants' - -export const borderColor = colors.grey400 -export const borderRadius = '3px' - -/** - * Click modes when clicking on an option with/without - * a modifier key (ctrl, alt, cmd, shift) - */ - -// no or multiple modifier keys -export const REPLACE_MODE = 'REPLACE_MODE' -// add/remove options from selection -export const ADD_MODE = 'ADD_MODE' -// create selection range -export const RANGE_MODE = 'RANGE_MODE' - -/** - * @param {Option} left - * @param {Option} left - * @returns {bool} - */ -export const isOption = (left, right) => - left.label === right.label && left.value === right.value - -/** - * @param {Option[]} options - * @param {Option} option - * @returns {Int} - */ -export const findOptionIndex = (options, option) => - options.findIndex(current => isOption(current, option)) - -/** - * @param {Option[]} options - * @param {Option} option - * @returns {Option} - */ -export const findOption = (options, option) => - options.find(current => isOption(current, option)) - -/** - * @param {Option[]} options - * @param {Option} option - * @returns {Option} - */ -export const addOption = (options, option) => { - const found = findOption(options, option) - if (found) return options - return [...options, option] -} - -/** - * @param {Option[]} options - * @param {Option} option - * @returns {Option} - */ -export const removeOption = (options, option) => { - const index = findOptionIndex(options, option) - - if (index === -1) return options - if (index === 0) return options.slice(1) - - return [...options.slice(0, index), ...options.slice(index + 1)] -} - -/** - * @param {Option[]} options - * @param {Option} option - * @returns {Option} - */ -export const toggleOption = (options, option) => - findOption(options, option) - ? removeOption(options, option) - : addOption(options, option) - -/** - * @param {Option[]} collection - * @param {Option[]} options - * @param {Function} modifier - * @returns {Option} - */ -export const toggleOptions = ( - collection, - optionsToToggle, - modifier = toggleOption -) => { - return optionsToToggle.reduce( - (curSelected, option) => modifier(curSelected, option), - collection - ) -} - -export const getModeByModifierKey = ({ - altKey, - shiftKey, - ctrlKey, - metaKey, -}) => { - const keys = [altKey, shiftKey, ctrlKey, metaKey] - const amountKeyPressed = keys.filter(v => v) - const moreThanOneKeyPressed = amountKeyPressed.length - - if (moreThanOneKeyPressed !== 1) return REPLACE_MODE - - if (altKey || ctrlKey || metaKey) return ADD_MODE - - if (shiftKey) return RANGE_MODE - - // default to replace mode - return REPLACE_MODE -} diff --git a/packages/core/src/Transfer/helper/defaultFilterCallback.js b/packages/core/src/Transfer/helper/defaultFilterCallback.js deleted file mode 100644 index 2cb282ccad..0000000000 --- a/packages/core/src/Transfer/helper/defaultFilterCallback.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @param {Option[]} plainOptions - * @param {string} filter - * @returns {Option[]} - */ -export const defaultFilterCallback = (plainOptions, filter) => - filter === '' - ? plainOptions - : plainOptions.filter(({ label }) => - label.match(new RegExp(filter, 'i')) - ) diff --git a/packages/core/src/Transfer/helper/extractPickedReactOptions.js b/packages/core/src/Transfer/helper/extractPickedReactOptions.js deleted file mode 100644 index d811303ef2..0000000000 --- a/packages/core/src/Transfer/helper/extractPickedReactOptions.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Children } from 'react' -import { findOption, findOptionIndex } from '../common' - -/** - * @param {Object} args - * @param {ReactElement} args.reactOptions - * @param {Option[]} args.selectedPlainOptions - * @returns {ReactElement} React elements - */ -export const extractPickedReactOptions = ({ - reactOptions, - selectedPlainOptions, -}) => { - const pickedOptions = Children.toArray(reactOptions) - .map(child => { - const { props } = child - const isSelected = !!findOption(selectedPlainOptions, props) - - return isSelected ? child : null - }) - // We can ONLY do this because the reactOptions have keys - .filter(child => !!child) - - pickedOptions.sort((left, right) => { - const leftIndex = findOptionIndex(selectedPlainOptions, left.props) - const rightIndex = findOptionIndex(selectedPlainOptions, right.props) - - if (leftIndex < rightIndex) return -1 - return 1 - }) - - return pickedOptions -} diff --git a/packages/core/src/Transfer/helper/filterOutOptions.js b/packages/core/src/Transfer/helper/filterOutOptions.js deleted file mode 100644 index 4701aab1aa..0000000000 --- a/packages/core/src/Transfer/helper/filterOutOptions.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Children } from 'react' -import { findOption } from '../common' - -/** - * @param {ReactElement} reactOptions - * @param {Option[]} plainOptions - * @returns {Object} React elements - */ -export const filterOutOptions = (reactOptions, plainOptions) => { - return Children.map(reactOptions, child => - findOption(plainOptions, child.props) ? null : child - ) -} diff --git a/packages/core/src/Transfer/helper/filterReactOptionsBy.js b/packages/core/src/Transfer/helper/filterReactOptionsBy.js deleted file mode 100644 index b17009cde2..0000000000 --- a/packages/core/src/Transfer/helper/filterReactOptionsBy.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Children } from 'react' -import { getPlainOptionFromReactOption } from './getPlainOptionFromReactOption' - -export const filterReactOptionsBy = (callback, reactOptions) => { - return Children.map(reactOptions, child => { - const plainOption = getPlainOptionFromReactOption(child) - const keep = callback(plainOption) - - if (!keep) return null - return child - }) -} diff --git a/packages/core/src/Transfer/helper/getPlainOptionFromReactOption.js b/packages/core/src/Transfer/helper/getPlainOptionFromReactOption.js deleted file mode 100644 index f78a48e8f3..0000000000 --- a/packages/core/src/Transfer/helper/getPlainOptionFromReactOption.js +++ /dev/null @@ -1,12 +0,0 @@ -import '../types.js' - -/** - * @param {ReactElement} reactOption - * @returns {Option} plainOption - */ -export const getPlainOptionFromReactOption = reactOption => ({ - label: reactOption.props.label, - value: reactOption.props.value, - disabled: reactOption.props.disabled, - ...(reactOption.props.additionalData || {}), -}) diff --git a/packages/core/src/Transfer/helper/getPlainOptionsFromReactOptions.js b/packages/core/src/Transfer/helper/getPlainOptionsFromReactOptions.js deleted file mode 100644 index f2a1e91c97..0000000000 --- a/packages/core/src/Transfer/helper/getPlainOptionsFromReactOptions.js +++ /dev/null @@ -1,10 +0,0 @@ -import '../types.js' -import { Children } from 'react' -import { getPlainOptionFromReactOption } from './getPlainOptionFromReactOption' - -/** - * @param {ReactElement} reactOptions - * @returns {Option[]} plainOption - */ -export const getPlainOptionsFromReactOptions = reactOptions => - Children.toArray(reactOptions).map(getPlainOptionFromReactOption) diff --git a/packages/core/src/Transfer/helper/getSubsetByFilter.js b/packages/core/src/Transfer/helper/getSubsetByFilter.js deleted file mode 100644 index 5231409168..0000000000 --- a/packages/core/src/Transfer/helper/getSubsetByFilter.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Children } from 'react' -import { getPlainOptionsFromReactOptions } from './getPlainOptionsFromReactOptions' -import { isOption } from '../common' - -/** - * @param {Object} args - * @param {ReactElement} args.reactOptions - * @param {string} args.filter - * @param {bool} args.filterable - * @param {Function} args.filterCallback - * @returns {Object} React elements - */ -export const getSubsetByFilter = ({ - reactOptions, - filter, - filterable, - filterCallback, -}) => { - const options = getPlainOptionsFromReactOptions(reactOptions) - - const filtered = filterable ? filterCallback(options, filter) : options - - return Children.map(reactOptions, child => { - if (!filterable) return child - if (!filtered.find(option => isOption(option, child.props))) return null - - return child - }) -} diff --git a/packages/core/src/Transfer/helper/isReorderDownDisabled.js b/packages/core/src/Transfer/helper/isReorderDownDisabled.js deleted file mode 100644 index 2a7e5ebbda..0000000000 --- a/packages/core/src/Transfer/helper/isReorderDownDisabled.js +++ /dev/null @@ -1,17 +0,0 @@ -import { findOptionIndex } from '../common' - -/** - * @param {Object} args - * @param {PlainElement} args.highlightedPickedPlainOptions - * @param {Option[]} args.selectedPlainOptions - * @returns {bool} - */ -export const isReorderDownDisabled = ({ - highlightedPickedPlainOptions, - selectedPlainOptions, -}) => - // only one item can be moved with the buttons - highlightedPickedPlainOptions.length !== 1 || - // can't move an item down if it's the last one - findOptionIndex(selectedPlainOptions, highlightedPickedPlainOptions[0]) === - selectedPlainOptions.length - 1 diff --git a/packages/core/src/Transfer/helper/isReorderUpDisabled.js b/packages/core/src/Transfer/helper/isReorderUpDisabled.js deleted file mode 100644 index c83425136d..0000000000 --- a/packages/core/src/Transfer/helper/isReorderUpDisabled.js +++ /dev/null @@ -1,17 +0,0 @@ -import { findOptionIndex } from '../common' - -/** - * @param {Object} args - * @param {PlainElement} args.highlightedPickedPlainOptions - * @param {Option[]} args.selectedPlainOptions - * @returns {bool} - */ -export const isReorderUpDisabled = ({ - highlightedPickedPlainOptions, - selectedPlainOptions, -}) => - // only one item can be moved with the buttons - highlightedPickedPlainOptions.length !== 1 || - // can't move an item up if it's the first one - findOptionIndex(selectedPlainOptions, highlightedPickedPlainOptions[0]) === - 0 diff --git a/packages/core/src/Transfer/helper/moveHighlightedPickedOptionDown.js b/packages/core/src/Transfer/helper/moveHighlightedPickedOptionDown.js deleted file mode 100644 index 324cad5a99..0000000000 --- a/packages/core/src/Transfer/helper/moveHighlightedPickedOptionDown.js +++ /dev/null @@ -1,33 +0,0 @@ -import { findOptionIndex } from '../common' - -/** - * @param {Object} args - * @param {Option[]} args.selectedPlainOptions - * @param {Option[]} args.highlightedPickedPlainOptions - * @param {Function} args.onChange - * @returns {void} - */ -export const moveHighlightedPickedOptionDown = ({ - selectedPlainOptions, - highlightedPickedPlainOptions, - onChange, -}) => { - const optionIndex = findOptionIndex( - selectedPlainOptions, - highlightedPickedPlainOptions[0] - ) - - // Can't move down last or non-existing option - if (optionIndex === -1 || optionIndex > selectedPlainOptions.length - 2) - return - - // swap with next item - const reordered = [ - ...selectedPlainOptions.slice(0, optionIndex), - selectedPlainOptions[optionIndex + 1], - selectedPlainOptions[optionIndex], - ...selectedPlainOptions.slice(optionIndex + 2), - ] - - onChange({ selected: reordered }) -} diff --git a/packages/core/src/Transfer/helper/moveHighlightedPickedOptionUp.js b/packages/core/src/Transfer/helper/moveHighlightedPickedOptionUp.js deleted file mode 100644 index 983db5cd72..0000000000 --- a/packages/core/src/Transfer/helper/moveHighlightedPickedOptionUp.js +++ /dev/null @@ -1,32 +0,0 @@ -import { findOptionIndex } from '../common' - -/** - * @param {Object} args - * @param {Option[]} args.selectedPlainOptions - * @param {Option[]} args.highlightedPickedPlainOptions - * @param {Function} args.onChange - * @returns {void} - */ -export const moveHighlightedPickedOptionUp = ({ - selectedPlainOptions, - highlightedPickedPlainOptions, - onChange, -}) => { - const optionIndex = findOptionIndex( - selectedPlainOptions, - highlightedPickedPlainOptions[0] - ) - - // Can't move up option at index 0 or non-existing option - if (optionIndex < 1) return - - // swap with previous item - const reordered = [ - ...selectedPlainOptions.slice(0, optionIndex - 1), - selectedPlainOptions[optionIndex], - selectedPlainOptions[optionIndex - 1], - ...selectedPlainOptions.slice(optionIndex + 1), - ] - - onChange({ selected: reordered }) -} diff --git a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleAdd.js b/packages/core/src/Transfer/helper/useHighlightedOptions/toggleAdd.js deleted file mode 100644 index f5d2288026..0000000000 --- a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleAdd.js +++ /dev/null @@ -1,21 +0,0 @@ -import '../../types.js' -import { toggleOption } from '../../common' - -/** - * @param {Object} args - * @param {Option[]} args.highlightedOptions - * @param {number} args.maxSelections - * @param {Option} args.option - * @param {Function} args.setHighlightedOption - * @returns {void} - */ -export const toggleAdd = ({ - highlightedOptions, - maxSelections, - option, - setHighlightedOptions, -}) => { - setHighlightedOptions( - toggleOption(highlightedOptions, option).slice(-1 * maxSelections) - ) -} diff --git a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleReplace.js b/packages/core/src/Transfer/helper/useHighlightedOptions/toggleReplace.js deleted file mode 100644 index 2b002e0f8c..0000000000 --- a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleReplace.js +++ /dev/null @@ -1,23 +0,0 @@ -import '../../types.js' -import { toggleOption } from '../../common' - -/** - * @param {Object} args - * @param {Option[]} args.highlightedOptions - * @param {Option} args.option - * @param {Function} args.setHighlightedOption - * @returns {void} - */ -export const toggleReplace = ({ - option, - highlightedOptions, - setHighlightedOptions, -}) => { - if (highlightedOptions.length > 1) { - setHighlightedOptions([option]) - } else { - setHighlightedOptions( - toggleOption(highlightedOptions, option).slice(-1) - ) - } -} diff --git a/packages/core/src/Transfer/types.js b/packages/core/src/Transfer/types.js deleted file mode 100644 index 55aa9c7e67..0000000000 --- a/packages/core/src/Transfer/types.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * These are instances of react components, - * both built-in and custom ones. - * - * @typedef {Object} ReactElement - */ - -/** - * @typedef {Object} Option - * @prop {string} label - * @prop {string} value - */ diff --git a/packages/core/src/index.js b/packages/core/src/index.js index a8827e3ca6..4254819aab 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -69,6 +69,4 @@ export { TableRow } from './Table/TableRow.js' export { TableRowHead } from './Table/TableRowHead.js' export { Tag } from './Tag/Tag.js' export { TextArea } from './TextArea/TextArea.js' -export { Transfer } from './Transfer/Transfer.js' -export { TransferOption } from './Transfer/TransferOption.js' export { Tooltip } from './Tooltip/Tooltip.js' diff --git a/packages/core/src/Transfer/Actions.js b/packages/widgets/src/Transfer/Actions.js similarity index 100% rename from packages/core/src/Transfer/Actions.js rename to packages/widgets/src/Transfer/Actions.js diff --git a/packages/core/src/Transfer/AddAll.js b/packages/widgets/src/Transfer/AddAll.js similarity index 93% rename from packages/core/src/Transfer/AddAll.js rename to packages/widgets/src/Transfer/AddAll.js index b8880e37e4..34ee7a144c 100644 --- a/packages/core/src/Transfer/AddAll.js +++ b/packages/widgets/src/Transfer/AddAll.js @@ -1,7 +1,7 @@ +import { Button } from '@dhis2/ui-core' import React from 'react' import propTypes from '@dhis2/prop-types' -import { Button } from '../Button/Button.js' import { IconAddAll } from './icons.js' export const AddAll = ({ label, dataTest, disabled, onClick }) => ( diff --git a/packages/core/src/Transfer/AddIndividual.js b/packages/widgets/src/Transfer/AddIndividual.js similarity index 93% rename from packages/core/src/Transfer/AddIndividual.js rename to packages/widgets/src/Transfer/AddIndividual.js index acad012d51..9759e8fd8e 100644 --- a/packages/core/src/Transfer/AddIndividual.js +++ b/packages/widgets/src/Transfer/AddIndividual.js @@ -1,7 +1,7 @@ +import { Button } from '@dhis2/ui-core' import React from 'react' import propTypes from '@dhis2/prop-types' -import { Button } from '../Button/Button.js' import { IconAddIndividual } from './icons.js' export const AddIndividual = ({ label, dataTest, disabled, onClick }) => ( diff --git a/packages/core/src/Transfer/Container.js b/packages/widgets/src/Transfer/Container.js similarity index 100% rename from packages/core/src/Transfer/Container.js rename to packages/widgets/src/Transfer/Container.js diff --git a/packages/widgets/src/Transfer/Filter.js b/packages/widgets/src/Transfer/Filter.js new file mode 100644 index 0000000000..00e2c38137 --- /dev/null +++ b/packages/widgets/src/Transfer/Filter.js @@ -0,0 +1,35 @@ +import { Input, Field } from '@dhis2/ui-core' +import { spacers } from '@dhis2/ui-constants' +import React from 'react' +import propTypes from '@dhis2/prop-types' + +export const Filter = ({ dataTest, filter, onChange, label }) => ( +
+ + + + + +
+) + +Filter.propTypes = { + dataTest: propTypes.string.isRequired, + filter: propTypes.string.isRequired, + onChange: propTypes.func.isRequired, + label: propTypes.string, +} diff --git a/packages/core/src/Transfer/LeftFooter.js b/packages/widgets/src/Transfer/LeftFooter.js similarity index 91% rename from packages/core/src/Transfer/LeftFooter.js rename to packages/widgets/src/Transfer/LeftFooter.js index b062679bf0..98fbdf821c 100644 --- a/packages/core/src/Transfer/LeftFooter.js +++ b/packages/widgets/src/Transfer/LeftFooter.js @@ -3,7 +3,7 @@ import propTypes from '@dhis2/prop-types' import { spacers } from '@dhis2/ui-constants' -import { borderColor } from './common.js' +import { borderColor } from './common/index.js' export const LeftFooter = ({ children, dataTest }) => (
diff --git a/packages/core/src/Transfer/LeftHeader.js b/packages/widgets/src/Transfer/LeftHeader.js similarity index 91% rename from packages/core/src/Transfer/LeftHeader.js rename to packages/widgets/src/Transfer/LeftHeader.js index b118ba4337..738959546d 100644 --- a/packages/core/src/Transfer/LeftHeader.js +++ b/packages/widgets/src/Transfer/LeftHeader.js @@ -3,7 +3,7 @@ import propTypes from '@dhis2/prop-types' import { spacers } from '@dhis2/ui-constants' -import { borderColor } from './common.js' +import { borderColor } from './common/index.js' export const LeftHeader = ({ children, dataTest }) => (
diff --git a/packages/core/src/Transfer/LeftSide.js b/packages/widgets/src/Transfer/LeftSide.js similarity index 93% rename from packages/core/src/Transfer/LeftSide.js rename to packages/widgets/src/Transfer/LeftSide.js index 181444ffd2..fcd5d137d2 100644 --- a/packages/core/src/Transfer/LeftSide.js +++ b/packages/widgets/src/Transfer/LeftSide.js @@ -1,7 +1,7 @@ import React from 'react' import propTypes from '@dhis2/prop-types' -import { borderColor, borderRadius } from './common.js' +import { borderColor, borderRadius } from './common/index.js' export const LeftSide = ({ children, dataTest, width }) => (
diff --git a/packages/widgets/src/Transfer/PickedOptions.js b/packages/widgets/src/Transfer/PickedOptions.js new file mode 100644 index 0000000000..78a41a906d --- /dev/null +++ b/packages/widgets/src/Transfer/PickedOptions.js @@ -0,0 +1,29 @@ +import React from 'react' +import propTypes from '@dhis2/prop-types' + +import { spacers } from '@dhis2/ui-constants' + +export const PickedOptions = ({ + children, + dataTest, + selectedEmptyComponent, +}) => ( +
+ {!React.Children.count(children) && selectedEmptyComponent} + {children} + + +
+) + +PickedOptions.propTypes = { + children: propTypes.node.isRequired, + dataTest: propTypes.string.isRequired, + selectedEmptyComponent: propTypes.node, +} diff --git a/packages/core/src/Transfer/RemoveAll.js b/packages/widgets/src/Transfer/RemoveAll.js similarity index 93% rename from packages/core/src/Transfer/RemoveAll.js rename to packages/widgets/src/Transfer/RemoveAll.js index 104b1c4c55..0359b97aa7 100644 --- a/packages/core/src/Transfer/RemoveAll.js +++ b/packages/widgets/src/Transfer/RemoveAll.js @@ -1,7 +1,7 @@ +import { Button } from '@dhis2/ui-core' import React from 'react' import propTypes from '@dhis2/prop-types' -import { Button } from '../Button/Button.js' import { IconRemoveAll } from './icons.js' export const RemoveAll = ({ label, dataTest, disabled, onClick }) => ( diff --git a/packages/core/src/Transfer/RemoveIndividual.js b/packages/widgets/src/Transfer/RemoveIndividual.js similarity index 93% rename from packages/core/src/Transfer/RemoveIndividual.js rename to packages/widgets/src/Transfer/RemoveIndividual.js index df382e6a0c..7e5aa58fe2 100644 --- a/packages/core/src/Transfer/RemoveIndividual.js +++ b/packages/widgets/src/Transfer/RemoveIndividual.js @@ -1,7 +1,7 @@ +import { Button } from '@dhis2/ui-core' import React from 'react' import propTypes from '@dhis2/prop-types' -import { Button } from '../Button/Button.js' import { IconRemoveIndividual } from './icons.js' export const RemoveIndividual = ({ label, dataTest, disabled, onClick }) => ( diff --git a/packages/core/src/Transfer/ReorderingActions.js b/packages/widgets/src/Transfer/ReorderingActions.js similarity index 91% rename from packages/core/src/Transfer/ReorderingActions.js rename to packages/widgets/src/Transfer/ReorderingActions.js index b349c03fdc..a86ccee158 100644 --- a/packages/core/src/Transfer/ReorderingActions.js +++ b/packages/widgets/src/Transfer/ReorderingActions.js @@ -1,9 +1,8 @@ +import { Button } from '@dhis2/ui-core' +import { spacers } from '@dhis2/ui-constants' import React from 'react' import propTypes from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' - -import { Button } from '../Button/Button.js' import { IconMoveDown, IconMoveUp } from './icons.js' export const ReorderingActions = ({ @@ -34,8 +33,8 @@ export const ReorderingActions = ({ dataTest={`${dataTest}-buttonmoveup`} icon={ } /> diff --git a/packages/core/src/Transfer/RightFooter.js b/packages/widgets/src/Transfer/RightFooter.js similarity index 91% rename from packages/core/src/Transfer/RightFooter.js rename to packages/widgets/src/Transfer/RightFooter.js index 29a03ff865..07cfcad483 100644 --- a/packages/core/src/Transfer/RightFooter.js +++ b/packages/widgets/src/Transfer/RightFooter.js @@ -3,7 +3,7 @@ import propTypes from '@dhis2/prop-types' import { spacers } from '@dhis2/ui-constants' -import { borderColor } from './common.js' +import { borderColor } from './common/index.js' export const RightFooter = ({ children, dataTest }) => (
diff --git a/packages/core/src/Transfer/RightSide.js b/packages/widgets/src/Transfer/RightSide.js similarity index 92% rename from packages/core/src/Transfer/RightSide.js rename to packages/widgets/src/Transfer/RightSide.js index df3ab4e5ef..b435577de2 100644 --- a/packages/core/src/Transfer/RightSide.js +++ b/packages/widgets/src/Transfer/RightSide.js @@ -1,7 +1,7 @@ import React from 'react' import propTypes from '@dhis2/prop-types' -import { borderColor, borderRadius } from './common.js' +import { borderColor, borderRadius } from './common/index.js' export const RightSide = ({ children, dataTest, width }) => (
diff --git a/packages/widgets/src/Transfer/SourceOptions.js b/packages/widgets/src/Transfer/SourceOptions.js new file mode 100644 index 0000000000..bfdd24e7fd --- /dev/null +++ b/packages/widgets/src/Transfer/SourceOptions.js @@ -0,0 +1,29 @@ +import React from 'react' +import propTypes from '@dhis2/prop-types' + +import { spacers } from '@dhis2/ui-constants' + +export const SourceOptions = ({ + children, + dataTest, + sourceEmptyPlaceholder, +}) => ( +
+ {children} + {!React.Children.count(children) && sourceEmptyPlaceholder} + + +
+) + +SourceOptions.propTypes = { + dataTest: propTypes.string.isRequired, + children: propTypes.node, + sourceEmptyPlaceholder: propTypes.node, +} diff --git a/packages/core/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js similarity index 69% rename from packages/core/src/Transfer/Transfer.js rename to packages/widgets/src/Transfer/Transfer.js index 6f2b54c2a5..984dc4ebca 100644 --- a/packages/core/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -1,6 +1,4 @@ -import './types.js' - -import React, { Children, useState } from 'react' +import React, { Fragment, useState } from 'react' import propTypes from '@dhis2/prop-types' import { Actions } from './Actions.js' @@ -18,14 +16,13 @@ import { ReorderingActions } from './ReorderingActions.js' import { RightFooter } from './RightFooter.js' import { RightSide } from './RightSide.js' import { SourceOptions } from './SourceOptions.js' +import { TransferOption } from './TransferOption.js' import { addAllSelectableSourceOptions, addIndividualSourceOptions, createDoubleClickHandlers, - extractPickedReactOptions, defaultFilterCallback, - filterOutOptions, - getSubsetByFilter, + getOptionClickHandlers, isReorderDownDisabled, isReorderUpDisabled, moveHighlightedPickedOptionDown, @@ -33,17 +30,9 @@ import { removeAllPickedOptions, removeIndividualPickedOptions, useHighlightedOptions, -} from './helper/index.js' -import { filterReactOptionsBy } from './helper/filterReactOptionsBy.js' - -// TODO: This will be refactored away to match the MultiSelect -export const singleSelectedPropType = propTypes.shape({ - label: propTypes.string, - value: propTypes.string, -}) +} from './Transfer/index.js' -export const multiSelectedPropType = propTypes.arrayOf(singleSelectedPropType) -// ODOT +const identity = value => value /** * @module @@ -75,9 +64,9 @@ export const multiSelectedPropType = propTypes.arrayOf(singleSelectedPropType) * rest of the library */ export const Transfer = ({ + options, onChange, - children, className, dataTest, disabled, @@ -97,6 +86,7 @@ export const Transfer = ({ leftHeader, maxSelections, optionsWidth, + renderOption, rightFooter, searchTerm, selected, @@ -113,69 +103,78 @@ export const Transfer = ({ */ const [internalFilter, setInternalFilter] = useState(initialSearchTerm) const actualFilter = onFilterChange ? searchTerm : internalFilter + const actualFilterCallback = filterable ? filterCallback : identity /* - * These are all the not-selected option react elements. - * It will replace all selected options with null + * Extract the not-selected options. + * Filters options if filterable is true. */ - const sourceOptions = filterOutOptions(children, selected) - const filteredSourceOptions = getSubsetByFilter({ - reactOptions: sourceOptions, - filter: actualFilter, - filterable, - filterCallback, - }) + const sourceOptions = actualFilterCallback( + options.filter(({ value }) => !selected.includes(value)), + actualFilter + ) /* - * Extract the selected options. This way custom options are supported - * without having to provide a component via the props - * - * Children are sorted by the order given in the "selected" array. - * This is done in order to cover the "append newly selected items - * at the end" feature/behavior. + * Extract the selected options. Can't use `options.filter` + * because we need to keep the order of `selected` */ - const pickedOptions = extractPickedReactOptions({ - reactOptions: children, - selectedPlainOptions: selected, - }) + const pickedOptions = selected + .map(value => options.find(option => value === option.value)) + // filter -> in case a selected value has been provided + // that does not exist as option + .filter(identity) /* - * These are all the highlighted option react elements on the options side. + * These are all the highlighted options on the options side. */ const { highlightedOptions: highlightedSourceOptions, setHighlightedOptions: setHighlightedSourceOptions, toggleHighlightedOption: toggleHighlightedSourceOption, } = useHighlightedOptions({ - reactOptions: filteredSourceOptions, + options: sourceOptions, disabled, maxSelections, }) /* - * These are all the highlighted option react elements on the selected side. + * These are all the highlighted options on the selected side. */ const { highlightedOptions: highlightedPickedOptions, setHighlightedOptions: setHighlightedPickedOptions, toggleHighlightedOption: toggleHighlightedPickedOption, } = useHighlightedOptions({ - reactOptions: pickedOptions, + options: pickedOptions, disabled, maxSelections, }) + /* + * These are the double click handlers for (de-)selection + */ const { selectSingleOption, deselectSingleOption, } = createDoubleClickHandlers({ - selectedPlainOptions: selected, + selected, setHighlightedSourceOptions, setHighlightedPickedOptions, onChange, maxSelections, }) + /** + * Disabled button states + */ + const isAddAllDisabled = + disabled || + sourceOptions.filter(({ disabled }) => !disabled).length === 0 + const isAddIndividualDisabled = disabled || !highlightedSourceOptions.length + const isRemoveAllDisabled = disabled || !selected.length + const isRemoveIndividualDisabled = + disabled || !highlightedPickedOptions.length + return ( @@ -202,13 +201,27 @@ export const Transfer = ({ - {filteredSourceOptions} + {sourceOptions.map(option => { + const highlighted = !!highlightedSourceOptions.find( + highlightedSourceOption => + highlightedSourceOption === option.value + ) + + return ( + + {renderOption({ + ...option, + ...getOptionClickHandlers( + option, + selectSingleOption, + toggleHighlightedSourceOption + ), + highlighted, + })} + + ) + })} {leftFooter && ( @@ -223,19 +236,11 @@ export const Transfer = ({ !disabled, - filteredSourceOptions - ) - ) - } + disabled={isAddAllDisabled} onClick={() => addAllSelectableSourceOptions({ - sourceReactOptions: filteredSourceOptions, - selectedPlainOptions: selected, + sourceOptions, + selected, onChange, setHighlightedSourceOptions, }) @@ -246,13 +251,13 @@ export const Transfer = ({ addIndividualSourceOptions({ filterable, - filteredSourcePlainOptions: filteredSourceOptions, - highlightedSourcePlainOptions: highlightedSourceOptions, - selectedPlainOptions: selected, + sourceOptions, + highlightedSourceOptions, + selected, maxSelections, onChange, setHighlightedSourceOptions, @@ -264,7 +269,7 @@ export const Transfer = ({ removeAllPickedOptions({ setHighlightedPickedOptions, @@ -277,12 +282,12 @@ export const Transfer = ({ removeIndividualPickedOptions({ - highlightedPickedReactOptions: highlightedPickedOptions, + highlightedPickedOptions, onChange, - selectedPlainOptions: selected, + selected, setHighlightedPickedOptions, }) } @@ -293,13 +298,26 @@ export const Transfer = ({ - {pickedOptions} + {pickedOptions.map(option => { + const highlighted = !!highlightedPickedOptions.find( + value => option.value === value + ) + + return ( + + {renderOption({ + ...option, + ...getOptionClickHandlers( + option, + deselectSingleOption, + toggleHighlightedPickedOption + ), + highlighted, + })} + + ) + })} {(rightFooter || enableOrderChange) && ( @@ -308,24 +326,24 @@ export const Transfer = ({ moveHighlightedPickedOptionUp({ - selectedPlainOptions: selected, - highlightedPickedPlainOptions: highlightedPickedOptions, + selected, + highlightedPickedOptions, onChange, }) } onChangeDown={() => { moveHighlightedPickedOptionDown({ - selectedPlainOptions: selected, - highlightedPickedPlainOptions: highlightedPickedOptions, + selected, + highlightedPickedOptions, onChange, }) }} @@ -340,14 +358,17 @@ export const Transfer = ({ ) } +const defaultRenderOption = option => + Transfer.defaultProps = { dataTest: 'dhis2-uicore-transfer', - initialSearchTerm: '', - selected: [], height: '240px', + initialSearchTerm: '', + maxSelections: Infinity, optionsWidth: '320px', + renderOption: defaultRenderOption, + selected: [], selectedWidth: '320px', - maxSelections: Infinity, filterCallback: defaultFilterCallback, } @@ -378,16 +399,22 @@ Transfer.defaultProps = { * @prop {string} [removeIndividualText] * @prop {Node} [rightFooter] * @prop {string} [searchTerm] - * @prop {Option|Option[]} selected + * @prop {string[]} selected * @prop {string} [selectedWidth] * @prop {Function} [onFilterChange] */ Transfer.propTypes = { + options: propTypes.arrayOf( + propTypes.shape({ + label: propTypes.string.isRequired, + value: propTypes.string.isRequired, + disabled: propTypes.bool, + }) + ).isRequired, onChange: propTypes.func.isRequired, addAllText: propTypes.string, addIndividualText: propTypes.string, - children: propTypes.node, className: propTypes.string, dataTest: propTypes.string, disabled: propTypes.bool, @@ -404,12 +431,10 @@ Transfer.propTypes = { optionsWidth: propTypes.string, removeAllText: propTypes.string, removeIndividualText: propTypes.string, + renderOption: propTypes.func, rightFooter: propTypes.node, searchTerm: propTypes.string, - selected: propTypes.oneOfType([ - singleSelectedPropType, - multiSelectedPropType, - ]), + selected: propTypes.arrayOf(propTypes.string), selectedEmptyComponent: propTypes.node, selectedWidth: propTypes.string, sourceEmptyPlaceholder: propTypes.node, diff --git a/packages/core/src/Transfer/Transfer.stories.js b/packages/widgets/src/Transfer/Transfer.stories.js similarity index 50% rename from packages/core/src/Transfer/Transfer.stories.js rename to packages/widgets/src/Transfer/Transfer.stories.js index dc75d78db2..693e1c7439 100644 --- a/packages/core/src/Transfer/Transfer.stories.js +++ b/packages/widgets/src/Transfer/Transfer.stories.js @@ -1,21 +1,13 @@ /* eslint-disable react/prop-types */ +import { SingleSelectOption, Tab, TabBar } from '@dhis2/ui-core' import React, { useState } from 'react' -import { - Field, - SingleSelect, - SingleSelectOption, - Tab, - TabBar, - Transfer, - TransferOption, -} from '../index.js' +import { SingleSelectField, Transfer, TransferOption } from '../index.js' -export default { title: 'Transfer' } +export default { title: 'Components/widgets/Transfer' } const StatefulWrapper = ({ children, initialState }) => { - const initialSelected = initialState.map(child => child.props) - const [selected, setSelected] = useState(initialSelected) + const [selected, setSelected] = useState(initialState) return React.Children.map(children, child => React.cloneElement(child, { @@ -30,116 +22,96 @@ StatefulWrapper.defaultProps = { } const options = [ - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , + { + label: 'ANC 1st visit', + value: 'anc_1st_visit', + }, + { + label: 'ANC 2nd visit', + value: 'anc_2nd_visit', + }, + { + label: 'ANC 3rd visit', + value: 'anc_3rd_visit', + }, + { + label: 'ANC 4th or more visits', + value: 'anc_4th_or_more_visits', + }, + { + label: 'ARI treated with antibiotics (pneumonia) follow-up', + value: 'ari_treated_with_antibiotics_(pneumonia)_follow-up', + }, + { + label: 'ARI treated with antibiotics (pneumonia) new', + value: 'ari_treated_with_antibiotics_(pneumonia)_new', + }, + { + label: 'ARI treated with antibiotics (pneumonia) referrals', + value: 'ari_treated_with_antibiotics_(pneumonia)_referrals', + }, + { + label: 'ARI treated without antibiotics (cough) follow-up', + value: 'ari_treated_without_antibiotics_(cough)_follow-up', + }, + { + label: 'ARI treated without antibiotics (cough) new', + value: 'ari_treated_without_antibiotics_(cough)_new', + }, + { + label: 'ARI treated without antibiotics (cough) referrals', + value: 'ari_treated_without_antibiotics_(cough)_referrals', + }, + { + label: 'ART No clients who stopped TRT due to TRT failure', + value: 'art_no_clients_who_stopped_trt_due_to_trt_failure', + }, + { + label: + 'ART No clients who stopped TRT due to adverse clinical status/event', + value: + 'art_no_clients_who_stopped_trt_due_to_adverse_clinical_status/event', + }, + { + label: 'ART No clients with change of regimen due to drug toxicity', + value: 'art_no_clients_with_change_of_regimen_due_to_drug_toxicity', + }, + { + label: 'ART No clients with new adverse drug reaction', + value: 'art_no_clients_with_new_adverse_drug_reaction', + }, + { + label: 'ART No started Opportunist Infection prophylaxis', + value: 'art_no_started_opportunist_infection_prophylaxis', + }, + { + label: 'ART clients with new adverse clinical event', + value: 'art_clients_with_new_adverse_clinical_event', + }, + { + label: 'ART defaulters', + value: 'art_defaulters', + }, + { + label: 'ART enrollment stage 1', + value: 'art_enrollment_stage_1', + }, + { + label: 'ART enrollment stage 2', + value: 'art_enrollment_stage_2', + }, + { + label: 'ART enrollment stage 3', + value: 'art_enrollment_stage_3', + }, + { + label: 'ART enrollment stage 4', + value: 'art_enrollment_stage_4', + }, + { + label: 'ART entry point: No PMTCT', + value: 'art_entry_point:_no_pmtct', + }, ] export const SingleSelection = () => ( @@ -147,17 +119,17 @@ export const SingleSelection = () => ( console.log('Will be overriden')} - > - {options} - + options={options} + /> ) export const Multiple = () => ( - console.log('Will be overriden')}> - {options} - + console.log('Will be overriden')} + options={options.slice(0, 3)} + /> ) @@ -166,9 +138,8 @@ export const Header = () => ( console.log('Will be overriden')} leftHeader={

Header on the left side

} - > - {options} -
+ options={options} + /> ) @@ -188,9 +159,8 @@ export const OptionsFooter = () => ( Reload list } - > - {options} - + options={options} + /> ) @@ -201,13 +171,12 @@ export const Filtered = () => ( onChange={() => console.log('Will be overriden by StatefulWrapper')} initialSearchTerm="ANC" leftHeader={

Header on the left side

} - > - {options} - + options={options} + /> ) -const CustomOption = ({ label, value, onClick, highlighted }) => ( +const renderOption = ({ label, value, onClick, highlighted }) => (

onClick({ label, value }, event)} style={{ background: highlighted ? 'green' : 'blue' }} @@ -220,7 +189,7 @@ export const CustomListOptions = () => ( <> Custom option code: -

{`const CustomOption = ({ label, value, onClick, highlighted }) => (
+            
{`const renderOption = ({ label, value, onClick, highlighted }) => (
     

onClick({ label, value }, event)} style={{ background: highlighted ? 'green' : 'blue' }} @@ -234,19 +203,33 @@ export const CustomListOptions = () => ( onChange={() => console.log('Will be overriden by StatefulWrapper') } - optionsComponent={CustomOption} - > - - - - - - - + renderOption={renderOption} + options={options} + /> ) +export const IndividualCustomOption = () => ( + + console.log('Will be overriden')} + addAllText="Add all" + addIndividualText="Add individual" + removeAllText="Remove all" + removeIndividualText="Remove individual" + renderOption={args => { + if (args.option.value === options[0].value) { + return renderOption(args) + } + + return + }} + options={options} + /> + +) + export const CustomButtonText = () => ( ( addIndividualText="Add individual" removeAllText="Remove all" removeIndividualText="Remove individual" - > - {options} - + options={options} + /> ) export const SourceEmptyPlaceholder = () => ( console.log('Will be overriden')} + options={[]} sourceEmptyPlaceholder={

No options found. @@ -285,16 +268,19 @@ export const PickedEmptyComponent = () => (

} - > - {options} - + options={options} + /> ) export const Reordering = () => ( - - null}> - {options.slice(0, 4)} - + value)} + > + null} + options={options.slice(0, 4)} + /> ) @@ -309,9 +295,8 @@ export const IncreasedOptionsHeight = () => ( } height="400px" leftHeader={

Header on the left side

} - > - {options} - + options={options} + />
) @@ -325,34 +310,23 @@ export const DifferentWidths = () => ( leftHeader={

Header on the left side

} optionsWidth="500px" selectedWidth="240px" - > - {options} - + options={options} + /> ) const createCustomFilteringInHeader = hideFilterInput => { - const relativePeriods = React.Children.map( - options.slice(0, 10), - (child, index) => - React.cloneElement(child, { - additionalData: { - relativePeriod: true, - year: index < 5 ? '2020' : '2019', - }, - }) - ) - - const fixedPeriods = React.Children.map( - options.slice(10, 20), - (child, index) => - React.cloneElement(child, { - additionalData: { - relativePeriod: false, - year: index < 5 ? '2020' : '2019', - }, - }) - ) + const relativePeriods = options.slice(0, 10).map((option, index) => ({ + ...option, + relativePeriod: true, + year: index < 5 ? '2020' : '2019', + })) + + const fixedPeriods = options.slice(10, 20).map((option, index) => ({ + ...option, + relativePeriod: false, + year: index < 5 ? '2020' : '2019', + })) const allOptions = [...relativePeriods, ...fixedPeriods] @@ -381,25 +355,24 @@ const createCustomFilteringInHeader = hideFilterInput => {

- - - - - - + + + + ) const CustomTransfer = props => { const [filter, setFilter] = useState('') const [relativePeriod, setRelativePeriod] = useState(true) - const [year, setYear] = useState({ label: '2020', value: '2020' }) + const [year, setYear] = useState('2020') const filterCallback = (options, filter) => { const optionsWithYear = options.filter( - option => option.year === year.value + option => option.year === year ) const optionsWithPeriod = optionsWithYear.filter( @@ -442,7 +415,7 @@ const createCustomFilteringInHeader = hideFilterInput => { // eslint-disable-next-line react/display-name return () => ( - {allOptions} + ) } diff --git a/packages/core/src/Transfer/helper/addAllSelectableSourceOptions.js b/packages/widgets/src/Transfer/Transfer/addAllSelectableSourceOptions.js similarity index 54% rename from packages/core/src/Transfer/helper/addAllSelectableSourceOptions.js rename to packages/widgets/src/Transfer/Transfer/addAllSelectableSourceOptions.js index a5a23d495a..55735e3517 100644 --- a/packages/core/src/Transfer/helper/addAllSelectableSourceOptions.js +++ b/packages/widgets/src/Transfer/Transfer/addAllSelectableSourceOptions.js @@ -1,26 +1,27 @@ -import { addOption, toggleOptions } from '../common' -import { getPlainOptionsFromReactOptions } from './getPlainOptionsFromReactOptions' - /** * @param {Object} args - * @param {ReactElement} args.sourceReactOptions - * @param {Option[]} args.selectedPlainOptions + * @param {Object[]} args.sourceOptions + * @param {string[]} args.selected * @param {Function} args.onChange * @param {Function} arg.setHighlightedSourceOptions * @returns {void} */ export const addAllSelectableSourceOptions = ({ - sourceReactOptions, + sourceOptions, onChange, - selectedPlainOptions, + selected, setHighlightedSourceOptions, }) => { - const all = getPlainOptionsFromReactOptions(sourceReactOptions) - const allEnabled = all.filter(({ disabled }) => !disabled) - const newSelected = toggleOptions( - selectedPlainOptions, - allEnabled, - addOption + const enabledSourceOptions = sourceOptions.filter( + ({ disabled }) => !disabled + ) + + const newSelected = enabledSourceOptions.reduce( + (accumulatedSelected, enabledSourceOption) => [ + ...accumulatedSelected, + enabledSourceOption.value, + ], + selected ) setHighlightedSourceOptions([]) diff --git a/packages/core/src/Transfer/helper/addIndividualSourceOptions.js b/packages/widgets/src/Transfer/Transfer/addIndividualSourceOptions.js similarity index 60% rename from packages/core/src/Transfer/helper/addIndividualSourceOptions.js rename to packages/widgets/src/Transfer/Transfer/addIndividualSourceOptions.js index 404a38c9f3..52f187298d 100644 --- a/packages/core/src/Transfer/helper/addIndividualSourceOptions.js +++ b/packages/widgets/src/Transfer/Transfer/addIndividualSourceOptions.js @@ -1,23 +1,20 @@ -import { Children } from 'react' -import { addOption, isOption, toggleOptions } from '../common' - /** * @param {Object} args * @param {bool} args.filterable - * @param {ReactElement} args.filteredSourcePlainOptions - * @param {Option[]} args.highlightedSourcePlainOptions + * @param {Object[]} args.sourceOptions + * @param {string[]} args.highlightedSourceOptions + * @param {string[]} args.selected * @param {Function} args.onChange - * @param {Option[]} args.selectedPlainOptions * @param {Function} args.setHighlightedSourceOptions * @returns void */ export const addIndividualSourceOptions = ({ filterable, - filteredSourcePlainOptions, - highlightedSourcePlainOptions, + sourceOptions, + highlightedSourceOptions, maxSelections, onChange, - selectedPlainOptions, + selected, setHighlightedSourceOptions, }) => { /** @@ -32,17 +29,22 @@ export const addIndividualSourceOptions = ({ * clicks the "add individuals" button */ const filteredHighlightedSourceOptions = filterable - ? highlightedSourcePlainOptions.filter(option => - Children.toArray(filteredSourcePlainOptions) - .map(({ props }) => props) - .find(filteredOption => isOption(filteredOption, option)) + ? highlightedSourceOptions.filter(value => + sourceOptions.find( + filteredOption => filteredOption.value === value + ) ) - : highlightedSourcePlainOptions + : highlightedSourceOptions - const newSelected = toggleOptions( - selectedPlainOptions, - filteredHighlightedSourceOptions, - addOption + const newSelected = filteredHighlightedSourceOptions.reduce( + (accumulatedSelected, value) => [ + ...accumulatedSelected, + filteredHighlightedSourceOptions.find( + filteredHighlightedSourceOption => + filteredHighlightedSourceOption === value + ), + ], + selected ) setHighlightedSourceOptions([]) diff --git a/packages/core/src/Transfer/helper/createDoubleClickHandlers.js b/packages/widgets/src/Transfer/Transfer/createDoubleClickHandlers.js similarity index 64% rename from packages/core/src/Transfer/helper/createDoubleClickHandlers.js rename to packages/widgets/src/Transfer/Transfer/createDoubleClickHandlers.js index 37637e6e33..c5b7898b59 100644 --- a/packages/core/src/Transfer/helper/createDoubleClickHandlers.js +++ b/packages/widgets/src/Transfer/Transfer/createDoubleClickHandlers.js @@ -1,10 +1,8 @@ -import { addOption, removeOption } from '../common' - /** * @param {Object} args * @param {number} args.maxSelections + * @param {string[]} args.selected * @param {Function} args.onChange - * @param {Option[]} args.selectedPlainOptions * @param {Function} args.setHighlightedSourceOptions * @param {Function} args.setHighlightedPickedOptions * @returns void @@ -12,18 +10,24 @@ import { addOption, removeOption } from '../common' export const createDoubleClickHandlers = ({ maxSelections, onChange, - selectedPlainOptions, + selected, setHighlightedPickedOptions, setHighlightedSourceOptions, }) => { - const selectSingleOption = ({ option }) => { - const newSelected = addOption(selectedPlainOptions, option) + const selectSingleOption = ({ value }) => { + const newSelected = selected.includes(value) + ? selected + : [...selected, value] + setHighlightedSourceOptions([]) onChange({ selected: newSelected.slice(-1 * maxSelections) }) } - const deselectSingleOption = ({ option }) => { - const newSelected = removeOption(selectedPlainOptions, option) + const deselectSingleOption = ({ value }) => { + const newSelected = selected.filter( + curSelected => curSelected !== value + ) + setHighlightedPickedOptions([]) onChange({ selected: newSelected }) } diff --git a/packages/widgets/src/Transfer/Transfer/defaultFilterCallback.js b/packages/widgets/src/Transfer/Transfer/defaultFilterCallback.js new file mode 100644 index 0000000000..c895f141d2 --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/defaultFilterCallback.js @@ -0,0 +1,9 @@ +/** + * @param {Object[]} options + * @param {string} filter + * @returns {Object[]} + */ +export const defaultFilterCallback = (options, filter) => + filter === '' + ? options + : options.filter(({ label }) => label.match(new RegExp(filter, 'i'))) diff --git a/packages/widgets/src/Transfer/Transfer/getOptionClickHandlers.js b/packages/widgets/src/Transfer/Transfer/getOptionClickHandlers.js new file mode 100644 index 0000000000..5ee5de09a1 --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/getOptionClickHandlers.js @@ -0,0 +1,19 @@ +import { getModeByModifierKey } from '../common/index.js' + +/** + * @param {Object} option + * @param {Function} selectionHandler + * @param {Function} toggleHighlightedOption + * @returns {Object} + */ +export const getOptionClickHandlers = ( + option, + selectionHandler, + toggleHighlightedOption +) => ({ + onClick: (_, event) => { + const mode = getModeByModifierKey(event) + toggleHighlightedOption({ option, mode }) + }, + onDoubleClick: selectionHandler, +}) diff --git a/packages/core/src/Transfer/helper/index.js b/packages/widgets/src/Transfer/Transfer/index.js similarity index 74% rename from packages/core/src/Transfer/helper/index.js rename to packages/widgets/src/Transfer/Transfer/index.js index 1f8f66c186..8e7e3662f8 100644 --- a/packages/core/src/Transfer/helper/index.js +++ b/packages/widgets/src/Transfer/Transfer/index.js @@ -1,11 +1,8 @@ export * from './addAllSelectableSourceOptions.js' export * from './addIndividualSourceOptions.js' export * from './createDoubleClickHandlers.js' -export * from './extractPickedReactOptions.js' export * from './defaultFilterCallback.js' -export * from './filterOutOptions.js' -export * from './getPlainOptionsFromReactOptions.js' -export * from './getSubsetByFilter.js' +export * from './getOptionClickHandlers.js' export * from './isReorderDownDisabled.js' export * from './isReorderUpDisabled.js' export * from './moveHighlightedPickedOptionDown.js' diff --git a/packages/widgets/src/Transfer/Transfer/isReorderDownDisabled.js b/packages/widgets/src/Transfer/Transfer/isReorderDownDisabled.js new file mode 100644 index 0000000000..10f1042db6 --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/isReorderDownDisabled.js @@ -0,0 +1,11 @@ +/** + * @param {Object} args + * @param {string} args.highlightedPickedOptions + * @param {string[]} args.selected + * @returns {bool} + */ +export const isReorderDownDisabled = ({ highlightedPickedOptions, selected }) => + // only one item can be moved with the buttons + highlightedPickedOptions.length !== 1 || + // can't move an item down if it's the last one + selected.indexOf(highlightedPickedOptions[0]) === selected.length - 1 diff --git a/packages/widgets/src/Transfer/Transfer/isReorderUpDisabled.js b/packages/widgets/src/Transfer/Transfer/isReorderUpDisabled.js new file mode 100644 index 0000000000..9386ac169e --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/isReorderUpDisabled.js @@ -0,0 +1,11 @@ +/** + * @param {Object} args + * @param {string} args.highlightedPickedOptions + * @param {string[]} args.selected + * @returns {bool} + */ +export const isReorderUpDisabled = ({ highlightedPickedOptions, selected }) => + // only one item can be moved with the buttons + highlightedPickedOptions.length !== 1 || + // can't move an item up if it's the first one + selected.indexOf(highlightedPickedOptions[0]) === 0 diff --git a/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionDown.js b/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionDown.js new file mode 100644 index 0000000000..eb015b392e --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionDown.js @@ -0,0 +1,29 @@ +/** + * @param {Object} args + * @param {string[]} args.selected + * @param {string[]} args.highlightedPickedOptions + * @param {Function} args.onChange + * @returns {void} + */ +export const moveHighlightedPickedOptionDown = ({ + selected, + highlightedPickedOptions, + onChange, +}) => { + const optionIndex = selected.findIndex( + selectedOption => selectedOption === highlightedPickedOptions[0] + ) + + // Can't move down last or non-existing option + if (optionIndex === -1 || optionIndex > selected.length - 2) return + + // swap with next item + const reordered = [ + ...selected.slice(0, optionIndex), + selected[optionIndex + 1], + selected[optionIndex], + ...selected.slice(optionIndex + 2), + ] + + onChange({ selected: reordered }) +} diff --git a/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionUp.js b/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionUp.js new file mode 100644 index 0000000000..44611779ca --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/moveHighlightedPickedOptionUp.js @@ -0,0 +1,29 @@ +/** + * @param {Object} args + * @param {string[]} args.selected + * @param {string[]} args.highlightedPickedOptions + * @param {Function} args.onChange + * @returns {void} + */ +export const moveHighlightedPickedOptionUp = ({ + selected, + highlightedPickedOptions, + onChange, +}) => { + const optionIndex = selected.findIndex( + selectedOption => selectedOption === highlightedPickedOptions[0] + ) + + // Can't move up option at index 0 or non-existing option + if (optionIndex < 1) return + + // swap with previous item + const reordered = [ + ...selected.slice(0, optionIndex - 1), + selected[optionIndex], + selected[optionIndex - 1], + ...selected.slice(optionIndex + 1), + ] + + onChange({ selected: reordered }) +} diff --git a/packages/core/src/Transfer/helper/removeAllPickedOptions.js b/packages/widgets/src/Transfer/Transfer/removeAllPickedOptions.js similarity index 100% rename from packages/core/src/Transfer/helper/removeAllPickedOptions.js rename to packages/widgets/src/Transfer/Transfer/removeAllPickedOptions.js diff --git a/packages/core/src/Transfer/helper/removeIndividualPickedOptions.js b/packages/widgets/src/Transfer/Transfer/removeIndividualPickedOptions.js similarity index 58% rename from packages/core/src/Transfer/helper/removeIndividualPickedOptions.js rename to packages/widgets/src/Transfer/Transfer/removeIndividualPickedOptions.js index 90b6a022cc..d5462487b8 100644 --- a/packages/core/src/Transfer/helper/removeIndividualPickedOptions.js +++ b/packages/widgets/src/Transfer/Transfer/removeIndividualPickedOptions.js @@ -1,23 +1,19 @@ -import { removeOption, toggleOptions } from '../common' - /** * @param {Object} args - * @param {ReactElement} args.highlightedPickedReactOptions + * @param {string[]} args.highlightedPickedOptions + * @param {string[]} args.selected * @param {Function} args.setHighlightedPickedOptions - * @param {Option[]} args.selectedPlainOptions * @param {Function} args.onChange * @returns {void} */ export const removeIndividualPickedOptions = ({ - highlightedPickedReactOptions, + highlightedPickedOptions, onChange, - selectedPlainOptions, + selected, setHighlightedPickedOptions, }) => { - const newSelected = toggleOptions( - selectedPlainOptions, - highlightedPickedReactOptions, - removeOption + const newSelected = selected.filter( + selectedOption => !highlightedPickedOptions.includes(selectedOption) ) setHighlightedPickedOptions([]) diff --git a/packages/core/src/Transfer/helper/useHighlightedOptions.js b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions.js similarity index 79% rename from packages/core/src/Transfer/helper/useHighlightedOptions.js rename to packages/widgets/src/Transfer/Transfer/useHighlightedOptions.js index d4a3d18aae..90175adc1a 100644 --- a/packages/core/src/Transfer/helper/useHighlightedOptions.js +++ b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions.js @@ -1,21 +1,15 @@ -import '../types.js' - import { useState } from 'react' -import { createToggleHighlightedOption } from './useHighlightedOptions/createToggleHighlightedOption' +import { createToggleHighlightedOption } from './useHighlightedOptions/createToggleHighlightedOption.js' /** * @param {Object} args * @param {bool} args.disabled * @param {number} args.maxSelection - * @param {ReactElement} args.reactOptions + * @param {Object[]} args.options * @returns {Object} highlighted options & helpers */ -export const useHighlightedOptions = ({ - disabled, - maxSelections, - reactOptions, -}) => { +export const useHighlightedOptions = ({ disabled, maxSelections, options }) => { /** * These are important so the stored element can be used * as range-start when using shift multiple times consecutively @@ -29,7 +23,7 @@ export const useHighlightedOptions = ({ setHighlightedOptions, maxSelections, setLastClicked, - reactOptions, + options, lastClicked, }) diff --git a/packages/core/src/Transfer/helper/useHighlightedOptions/createToggleHighlightedOption.js b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/createToggleHighlightedOption.js similarity index 55% rename from packages/core/src/Transfer/helper/useHighlightedOptions/createToggleHighlightedOption.js rename to packages/widgets/src/Transfer/Transfer/useHighlightedOptions/createToggleHighlightedOption.js index 153eeae2b7..fc2ceba8e3 100644 --- a/packages/core/src/Transfer/helper/useHighlightedOptions/createToggleHighlightedOption.js +++ b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/createToggleHighlightedOption.js @@ -1,15 +1,26 @@ -import { ADD_MODE, RANGE_MODE } from '../../common' -import { toggleAdd } from './toggleAdd' -import { toggleRange } from './toggleRange' -import { toggleReplace } from './toggleReplace' +import { ADD_MODE, RANGE_MODE } from '../../common/index.js' +import { toggleAdd } from './toggleAdd.js' +import { toggleRange } from './toggleRange.js' +import { toggleReplace } from './toggleReplace.js' +/** + * @param {Object} args + * @param {bool} args.disabled + * @param {string[]} args.highlightedOptions + * @param {Function} args.setHighlightedOptions + * @param {number} args.maxSelections + * @param {Function} args.setLastClicked + * @param {Object[]} args.options + * @param {string} args.lastClicked + * @returns {void} + */ export const createToggleHighlightedOption = ({ disabled, highlightedOptions, setHighlightedOptions, maxSelections, setLastClicked, - reactOptions, + options, lastClicked, }) => ({ option, mode }) => { if (disabled) return @@ -17,7 +28,7 @@ export const createToggleHighlightedOption = ({ setHighlightedOptions([]) if (mode === ADD_MODE) { - setLastClicked(option) + setLastClicked(option.value) return toggleAdd({ highlightedOptions, @@ -30,7 +41,7 @@ export const createToggleHighlightedOption = ({ if (mode === RANGE_MODE) { return toggleRange({ highlightedOptions, - reactOptions, + options, option, setHighlightedOptions, lastClicked, @@ -39,7 +50,7 @@ export const createToggleHighlightedOption = ({ } // REPLACE_MODE - setLastClicked(option) + setLastClicked(option.value) return toggleReplace({ option, diff --git a/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleAdd.js b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleAdd.js new file mode 100644 index 0000000000..30e7f086c6 --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleAdd.js @@ -0,0 +1,20 @@ +import { toggleValue } from '../../common' + +/** + * @param {Object} args + * @param {number} args.maxSelections + * @param {string[]} args.highlightedOptions + * @param {Object} args.option + * @param {Function} args.setHighlightedOption + * @returns {void} + */ +export const toggleAdd = ({ + highlightedOptions, + maxSelections, + option, + setHighlightedOptions, +}) => { + const afterToggled = toggleValue(highlightedOptions, option.value) + const capped = afterToggled.slice(-1 * maxSelections) + setHighlightedOptions(capped) +} diff --git a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleRange.js b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleRange.js similarity index 72% rename from packages/core/src/Transfer/helper/useHighlightedOptions/toggleRange.js rename to packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleRange.js index 0db1b82fb6..415a2ac4e6 100644 --- a/packages/core/src/Transfer/helper/useHighlightedOptions/toggleRange.js +++ b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleRange.js @@ -1,34 +1,31 @@ -import '../../types.js' -import { findOption, findOptionIndex } from '../../common' -import { getPlainOptionsFromReactOptions } from '../getPlainOptionsFromReactOptions' +import { findOptionIndex } from '../../common/index.js' /** * @param {Object} args - * @param {Option[]} args.highlightedOptions - * @param {ReactElement} args.reactOptions - * @param {Option} args.option - * @param {Function} args.setHighlightedOption * @param {number} args.maxSelections - * @param {Option} args.lastClicked + * @param {string[]} args.highlightedOptions + * @param {Object[]} args.options + * @param {Object} args.option + * @param {string} args.lastClicked + * @param {Function} args.setHighlightedOption * @returns {void} */ export const toggleRange = ({ highlightedOptions, - reactOptions, + options, option, setHighlightedOptions, lastClicked, maxSelections, }) => { if (highlightedOptions.length === 0) { - setHighlightedOptions([option]) + setHighlightedOptions([option.value]) } else { let from, to - const options = getPlainOptionsFromReactOptions(reactOptions) const clickedOptionIndex = findOptionIndex(options, option) const lastClickedSourceOptionWithoutRangeModeIndex = lastClicked - ? findOptionIndex(options, lastClicked) + ? options.findIndex(curOption => curOption.value === lastClicked) : -1 if (lastClickedSourceOptionWithoutRangeModeIndex !== -1) { @@ -39,7 +36,9 @@ export const toggleRange = ({ * A filter-change has removed the most recently highlighted option */ const firstHighlightedInList = options.findIndex(option => - findOption(highlightedOptions, option) + highlightedOptions.find( + highlightedOption => highlightedOption === option.value + ) ) from = firstHighlightedInList @@ -55,6 +54,7 @@ export const toggleRange = ({ .slice(lower, higher + 1) .filter(option => !option.disabled) .slice(maxSelections * -1) + .map(({ value }) => value) setHighlightedOptions(newHighlightedSourceOptions) } diff --git a/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleReplace.js b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleReplace.js new file mode 100644 index 0000000000..9e676e1119 --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/useHighlightedOptions/toggleReplace.js @@ -0,0 +1,26 @@ +/** + * @param {Object} args + * @param {string[]} args.highlightedOptions + * @param {Object} args.option + * @param {Function} args.setHighlightedOption + * @returns {void} + */ +export const toggleReplace = ({ + option, + highlightedOptions, + setHighlightedOptions, +}) => { + if (highlightedOptions.length > 1) { + setHighlightedOptions([option.value]) + } else { + const optionIndex = highlightedOptions.findIndex( + highlightedOption => highlightedOption === option.value + ) + + if (optionIndex === -1) { + setHighlightedOptions([option.value]) + } else { + setHighlightedOptions([]) + } + } +} diff --git a/packages/core/src/Transfer/TransferOption.js b/packages/widgets/src/Transfer/TransferOption.js similarity index 92% rename from packages/core/src/Transfer/TransferOption.js rename to packages/widgets/src/Transfer/TransferOption.js index dcb4379fda..0c0d7c4860 100644 --- a/packages/core/src/Transfer/TransferOption.js +++ b/packages/widgets/src/Transfer/TransferOption.js @@ -20,12 +20,11 @@ export const TransferOption = ({ className, disabled, dataTest, - label, highlighted, onClick, onDoubleClick, + label, value, - additionalData, }) => { const doubleClickTimeout = useRef(null) @@ -35,20 +34,18 @@ export const TransferOption = ({ onClick={event => { if (disabled) return - const option = { label, value, ...additionalData } - if (doubleClickTimeout.current) { clearTimeout(doubleClickTimeout.current) doubleClickTimeout.current = null - onDoubleClick({ option }, event) + onDoubleClick({ value }, event) } else { doubleClickTimeout.current = setTimeout(() => { clearTimeout(doubleClickTimeout.current) doubleClickTimeout.current = null }, DOUBLE_CLICK_MAX_DELAY) - onClick({ option }, event) + onClick({ value }, event) } }} data-value={value} @@ -104,7 +101,6 @@ TransferOption.defaultProps = { TransferOption.propTypes = { label: propTypes.string.isRequired, value: propTypes.string.isRequired, - additionalData: propTypes.object, className: propTypes.string, dataTest: propTypes.string, disabled: propTypes.bool, diff --git a/packages/core/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js similarity index 53% rename from packages/core/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js index 0fe632cb53..ab73f7b5b1 100644 --- a/packages/core/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/add_remove-highlighted-options.stories.e2e.js @@ -2,7 +2,8 @@ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Transfer add & remove highlighted options', @@ -10,21 +11,22 @@ export default { } export const HasOptions = ({ onChange, selected }) => ( - - {options} - + ) export const HasSelected = ({ onChange, selected }) => ( - - {options} - + ) HasSelected.story = { decorators: [ statefulDecorator({ - initialState: options.slice(0, 4), + initialState: options.slice(0, 4).map(({ value }) => value), }), ], } diff --git a/packages/widgets/src/Transfer/__e2e__/common.js b/packages/widgets/src/Transfer/__e2e__/common.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/widgets/src/Transfer/__e2e__/common/options.js b/packages/widgets/src/Transfer/__e2e__/common/options.js new file mode 100644 index 0000000000..8010b73e27 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/common/options.js @@ -0,0 +1,92 @@ +export const options = [ + { + label: 'ANC 1st visit', + value: 'anc_1st_visit', + }, + { + label: 'ANC 2nd visit', + value: 'anc_2nd_visit', + }, + { + label: 'ANC 3rd visit', + value: 'anc_3rd_visit', + }, + { + label: 'ANC 4th or more visits', + value: 'anc_4th_or_more_visits', + }, + { + label: 'ARI treated with antibiotics (pneumonia) follow-up', + value: 'ari_treated_with_antibiotics_(pneumonia)_follow-up', + }, + { + label: 'ARI treated with antibiotics (pneumonia) new', + value: 'ari_treated_with_antibiotics_(pneumonia)_new', + }, + { + label: 'ARI treated with antibiotics (pneumonia) referrals', + value: 'ari_treated_with_antibiotics_(pneumonia)_referrals', + }, + { + label: 'ARI treated without antibiotics (cough) follow-up', + value: 'ari_treated_without_antibiotics_(cough)_follow-up', + }, + { + label: 'ARI treated without antibiotics (cough) new', + value: 'ari_treated_without_antibiotics_(cough)_new', + }, + { + label: 'ARI treated without antibiotics (cough) referrals', + value: 'ari_treated_without_antibiotics_(cough)_referrals', + }, + { + label: 'ART No clients who stopped TRT due to TRT failure', + value: 'art_no_clients_who_stopped_trt_due_to_trt_failure', + }, + { + label: + 'ART No clients who stopped TRT due to adverse clinical status/event', + value: + 'art_no_clients_who_stopped_trt_due_to_adverse_clinical_status/event', + }, + { + label: 'ART No clients with change of regimen due to drug toxicity', + value: 'art_no_clients_with_change_of_regimen_due_to_drug_toxicity', + }, + { + label: 'ART No clients with new adverse drug reaction', + value: 'art_no_clients_with_new_adverse_drug_reaction', + }, + { + label: 'ART No started Opportunist Infection prophylaxis', + value: 'art_no_started_opportunist_infection_prophylaxis', + }, + { + label: 'ART clients with new adverse clinical event', + value: 'art_clients_with_new_adverse_clinical_event', + }, + { + label: 'ART defaulters', + value: 'art_defaulters', + }, + { + label: 'ART enrollment stage 1', + value: 'art_enrollment_stage_1', + }, + { + label: 'ART enrollment stage 2', + value: 'art_enrollment_stage_2', + }, + { + label: 'ART enrollment stage 3', + value: 'art_enrollment_stage_3', + }, + { + label: 'ART enrollment stage 4', + value: 'art_enrollment_stage_4', + }, + { + label: 'ART entry point: No PMTCT', + value: 'art_entry_point:_no_pmtct', + }, +] diff --git a/packages/widgets/src/Transfer/__e2e__/common/statefulDecorator.js b/packages/widgets/src/Transfer/__e2e__/common/statefulDecorator.js new file mode 100644 index 0000000000..792cd958a5 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/common/statefulDecorator.js @@ -0,0 +1,20 @@ +import React, { useState } from 'react' + +export const statefulDecorator = ({ + initialState = [], + controlFilter = false, + initialSearchTerm = '', +} = {}) => fn => + React.createElement(() => { + const [selected, setSelected] = useState(initialState) + const [searchTerm, setSearchTerm] = useState(initialSearchTerm) + + return fn({ + selected, + searchTerm: controlFilter ? searchTerm : undefined, + onChange: payload => setSelected(payload.selected), + onFilterChange: controlFilter + ? ({ value }) => setSearchTerm(value) + : undefined, + }) + }) diff --git a/packages/core/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js similarity index 52% rename from packages/core/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js index f0d7d4f072..b2046a8f48 100644 --- a/packages/core/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/disabled-transfer-buttons.stories.e2e.js @@ -2,7 +2,8 @@ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Transfer Disabled Transfer Buttons', @@ -10,45 +11,41 @@ export default { } export const NoOptions = ({ selected, onChange }) => ( - + ) export const HasOptions = ({ selected, onChange }) => ( - - {options} - + ) export const SomeOptionsSelected = ({ selected, onChange }) => ( - - {options} - + ) export const OnlyDisabledSourceOptions = ({ selected, onChange }) => ( - - {React.cloneElement(options[0], { disabled: true })} - + ) SomeOptionsSelected.story = { decorators: [ statefulDecorator({ - initialState: options.slice(0, 4), + initialState: options.slice(0, 4).map(({ value }) => value), }), ], } export const AllOptionsSelected = ({ selected, onChange }) => ( - - {options} - + ) AllOptionsSelected.story = { decorators: [ statefulDecorator({ - initialState: options, + initialState: options.map(({ value }) => value), }), ], } diff --git a/packages/core/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js similarity index 58% rename from packages/core/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js index 8529a093e9..8fbefb927d 100644 --- a/packages/core/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/disabled-transfer-options.stories.e2e.js @@ -2,7 +2,8 @@ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Disabled Source Options', @@ -10,11 +11,13 @@ export default { } export const OneDisabled = ({ selected, onChange }) => ( - - {[ + + /> ) diff --git a/packages/widgets/src/Transfer/__e2e__/display-order.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/display-order.stories.e2e.js new file mode 100644 index 0000000000..8b3f2f98b4 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/display-order.stories.e2e.js @@ -0,0 +1,26 @@ +/* eslint-disable react/prop-types */ +import React from 'react' + +import { Transfer } from '../../index.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' + +export default { title: 'Transfer Display Order' } + +window.options = options + +export const NoSelection = ({ selected, onChange }) => ( + +) + +export const SomeSelected = ({ selected, onChange }) => ( + +) + +SomeSelected.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 4).map(({ value }) => value), + }), + ], +} diff --git a/packages/core/src/Transfer/__e2e__/filter-options-list.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/filter-options-list.stories.e2e.js similarity index 86% rename from packages/core/src/Transfer/__e2e__/filter-options-list.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/filter-options-list.stories.e2e.js index 932deb7bdf..1d1eedac39 100644 --- a/packages/core/src/Transfer/__e2e__/filter-options-list.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/filter-options-list.stories.e2e.js @@ -1,7 +1,8 @@ /* eslint-disable react/prop-types */ import React from 'react' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' import { Transfer } from '../../index.js' export default { @@ -16,9 +17,8 @@ export const EmptyResult = ({ selected, onChange }) => ( selected={selected} onChange={onChange} sourceEmptyPlaceholder={No results} - > - {options} - + options={options} + /> ) export const SomeResults = ({ selected, onChange }) => ( @@ -27,9 +27,8 @@ export const SomeResults = ({ selected, onChange }) => ( filterable initialSearchTerm="ANC" onChange={onChange} - > - {options} - + options={options} + /> ) export const UppercaseSearch = ({ selected, onChange }) => ( @@ -38,9 +37,8 @@ export const UppercaseSearch = ({ selected, onChange }) => ( filterable initialSearchTerm="ANC" onChange={onChange} - > - {options} - + options={options} + /> ) export const LowercaseSearch = ({ selected, onChange }) => ( @@ -49,9 +47,8 @@ export const LowercaseSearch = ({ selected, onChange }) => ( initialSearchTerm="anc" selected={selected} onChange={onChange} - > - {options} - + options={options} + /> ) export const AncCustomFilter = ({ selected, onChange }) => ( @@ -62,9 +59,8 @@ export const AncCustomFilter = ({ selected, onChange }) => ( filterCallback={(options, filter) => options.filter(({ label }) => label.match(`(^| )ANC .*${filter}`)) } - > - {options} - + options={options} + /> ) window.customFilterCallback = (options, filter) => { @@ -86,9 +82,8 @@ export const ControlledFilter = ({ filterCallback={window.customFilterCallback} searchTerm={filter} onFilterChange={onFilterChange} - > - {options} - + options={options} + /> ) ControlledFilter.story = { diff --git a/packages/widgets/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js new file mode 100644 index 0000000000..3c28283f83 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/highlight-range-of-options.stories.e2e.js @@ -0,0 +1,54 @@ +/* eslint-disable react/prop-types */ +import React from 'react' + +import { Transfer } from '../../index.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' + +export default { + title: 'Transfer highlight range of options', + decorators: [statefulDecorator()], +} + +export const HasOptions = ({ onChange, selected }) => ( + +) + +export const HasSelected = ({ onChange, selected }) => ( + +) + +HasSelected.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 4).map(({ value }) => value), + }), + ], +} + +export const AllSelected = ({ onChange, selected }) => ( + +) + +AllSelected.story = { + decorators: [ + statefulDecorator({ + initialState: options.map(({ value }) => value), + }), + ], +} diff --git a/packages/core/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js similarity index 50% rename from packages/core/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js index c39bc2ba71..073009ef9e 100644 --- a/packages/core/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/reorder-with-buttons.stories.e2e.js @@ -1,20 +1,24 @@ /* eslint-disable react/prop-types */ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Transfer Reorder Buttons' } export const HasSomeSelected = ({ selected, onChange }) => ( - - {options} - + ) HasSomeSelected.story = { decorators: [ statefulDecorator({ - initialState: options.slice(0, 3), + initialState: options.slice(0, 3).map(({ value }) => value), }), ], } diff --git a/packages/core/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js similarity index 53% rename from packages/core/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js index 263ba5ec68..519ac0a41a 100644 --- a/packages/core/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/set_unset-highlighted-option.stories.e2e.js @@ -2,7 +2,8 @@ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Transfer set & unset higlighted options', @@ -10,21 +11,22 @@ export default { } export const HasOptions = ({ onChange, selected }) => ( - - {options} - + ) export const HasSelected = ({ onChange, selected }) => ( - - {options} - + ) HasSelected.story = { decorators: [ statefulDecorator({ - initialState: options.slice(0, 4), + initialState: options.slice(0, 4).map(({ value }) => value), }), ], } diff --git a/packages/core/src/Transfer/__e2e__/transferring-items.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/transferring-items.stories.e2e.js similarity index 51% rename from packages/core/src/Transfer/__e2e__/transferring-items.stories.e2e.js rename to packages/widgets/src/Transfer/__e2e__/transferring-items.stories.e2e.js index 78c22e5aff..a6a3d5a8bd 100644 --- a/packages/core/src/Transfer/__e2e__/transferring-items.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/transferring-items.stories.e2e.js @@ -1,34 +1,28 @@ /* eslint-disable react/prop-types */ import React from 'react' import { Transfer } from '../../index.js' -import { options, statefulDecorator } from './common.js' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' export default { title: 'Transfer Transferring Items', decorators: [statefulDecorator()], } -window.options = options.map(child => { - const { label, value } = child.props - return { label, value } -}) +window.options = options export const HasOptions = ({ selected, onChange }) => ( - - {options} - + ) export const SomeSelected = ({ selected, onChange }) => ( - - {options} - + ) SomeSelected.story = { decorators: [ statefulDecorator({ - initialState: options.slice(0, 4), + initialState: options.slice(0, 4).map(({ value }) => value), }), ], } diff --git a/packages/widgets/src/Transfer/__tests__/common.test.js b/packages/widgets/src/Transfer/__tests__/common.test.js new file mode 100644 index 0000000000..e8935b45df --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/common.test.js @@ -0,0 +1,131 @@ +import { + ADD_MODE, + RANGE_MODE, + REPLACE_MODE, + findOptionIndex, + getModeByModifierKey, + isOption, + toggleValue, +} from '../common/index' + +describe('Transfer - isOption', () => { + it('should return true when the options are the same', () => { + const option1 = { label: 'foo', value: 'bar' } + const option2 = { label: 'foo', value: 'bar' } + const actual = isOption(option1, option2) + const expected = true + + expect(actual).toBe(expected) + }) + + it('should return false when the labels do not match', () => { + const option1 = { label: 'foo', value: 'bar' } + const option2 = { label: 'baz', value: 'bar' } + const actual = isOption(option1, option2) + const expected = false + + expect(actual).toBe(expected) + }) + + it('should return false when the values do not match', () => { + const option1 = { label: 'foo', value: 'bar' } + const option2 = { label: 'foo', value: 'baz' } + const actual = isOption(option1, option2) + const expected = false + + expect(actual).toBe(expected) + }) +}) + +describe('Transfer - findOptionIndex', () => { + it('should return index 1', () => { + const options = [ + { label: 'foo', value: 'bar' }, + { label: 'foo', value: 'baz' }, + ] + const option = { label: 'foo', value: 'baz' } + const actual = findOptionIndex(options, option) + const expected = 1 + + expect(actual).toBe(expected) + }) + + it('should return -1 when the option is not included', () => { + const options = [ + { label: 'foo', value: 'bar' }, + { label: 'foo', value: 'baz' }, + ] + const option = { label: 'baz', value: 'baz' } + const actual = findOptionIndex(options, option) + const expected = -1 + + expect(actual).toBe(expected) + }) +}) + +describe('Transfer - toggleValue', () => { + it('should remove the last value from the array when value inside array', () => { + const values = ['foo', 'bar', 'baz'] + const value = 'baz' + const expected = ['foo', 'bar'] + const actual = toggleValue(values, value) + + expect(actual).toEqual(expected) + }) + + it('should remove the first value from the array when value inside array', () => { + const values = ['foo', 'bar', 'baz'] + const value = 'foo' + const expected = ['bar', 'baz'] + const actual = toggleValue(values, value) + + expect(actual).toEqual(expected) + }) + + it('should add the value if not inside the array', () => { + const values = ['foo', 'bar'] + const value = 'baz' + const expected = ['foo', 'bar', 'baz'] + const actual = toggleValue(values, value) + + expect(actual).toEqual(expected) + }) +}) + +describe('Transfer - getModeByModifierKey', () => { + it('should return REPLACE_MODE when more than one key is pressed', () => { + const expected = REPLACE_MODE + const actual = getModeByModifierKey({ altKey: true, ctrlKey: true }) + expect(actual).toBe(expected) + }) + + it('should return ADD_MODE if alt key is pressed', () => { + const expected = ADD_MODE + const actual = getModeByModifierKey({ altKey: true }) + expect(actual).toBe(expected) + }) + + it('should return ADD_MODE if ctrl key is pressed', () => { + const expected = ADD_MODE + const actual = getModeByModifierKey({ ctrlKey: true }) + expect(actual).toBe(expected) + }) + + it('should return ADD_MODE if meta key is pressed', () => { + const expected = ADD_MODE + const actual = getModeByModifierKey({ metaKey: true }) + expect(actual).toBe(expected) + }) + + it('should return RANGE_MODE if shift key is pressed', () => { + const expected = RANGE_MODE + const actual = getModeByModifierKey({ shiftKey: true }) + expect(actual).toBe(expected) + }) + + it('should return REPLACE_MODE if no key is pressed', () => { + const expected = REPLACE_MODE + const actual = getModeByModifierKey({}) + expect(actual).toBe(expected) + }) +}) diff --git a/packages/widgets/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js b/packages/widgets/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js new file mode 100644 index 0000000000..64e6b938a3 --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/helper/addAllSelectableSourceOptions.test.js @@ -0,0 +1,46 @@ +import { addAllSelectableSourceOptions } from '../../Transfer/addAllSelectableSourceOptions.js' + +describe('Transfer - addAllSelectableSourceOptions', () => { + const onChange = jest.fn() + const setHighlightedSourceOptions = jest.fn() + + const sourceOptions = [ + { label: 'Foo', value: 'foo' }, + { label: 'Bar', value: 'bar' }, + { label: 'Baz', value: 'baz' }, + { label: 'Foobar', value: 'foobar' }, + { label: 'Foobaz', value: 'foobaz' }, + ] + + afterEach(() => { + onChange.mockClear() + setHighlightedSourceOptions.mockClear() + }) + + it('should add all selectable source options to the selected array', () => { + const selected = ['barfoo'] + const expected = { + selected: ['barfoo', 'foo', 'bar', 'baz', 'foobar', 'foobaz'], + } + + addAllSelectableSourceOptions({ + sourceOptions, + selected, + onChange, + setHighlightedSourceOptions, + }) + + expect(onChange).toHaveBeenCalledWith(expected) + }) + + it('should reset all highlighted source options', () => { + addAllSelectableSourceOptions({ + sourceOptions, + selected: ['barfoo'], + onChange, + setHighlightedSourceOptions, + }) + + expect(setHighlightedSourceOptions).toHaveBeenCalledWith([]) + }) +}) diff --git a/packages/widgets/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js b/packages/widgets/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js new file mode 100644 index 0000000000..bcaee6656f --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/helper/addIndividualSourceOptions.test.js @@ -0,0 +1,80 @@ +import { addIndividualSourceOptions } from '../../Transfer/addIndividualSourceOptions.js' + +describe('Transfer - addIndividualSourceOptions', () => { + const onChange = jest.fn() + const setHighlightedSourceOptions = jest.fn() + + const sourceOptions = [ + { label: 'Foo', value: 'foo' }, + { label: 'Foobar', value: 'foobar' }, + { label: 'Foobaz', value: 'foobaz' }, + ] + + const highlightedSourceOptions = ['foobaz', 'bar'] + const selected = ['barfoo'] + + afterEach(() => { + onChange.mockClear() + setHighlightedSourceOptions.mockClear() + }) + + it('should add the highlighted source options to the selected array', () => { + addIndividualSourceOptions({ + highlightedSourceOptions, + maxSelections: Infinity, + onChange, + selected, + setHighlightedSourceOptions, + }) + + expect(onChange).toHaveBeenCalledWith({ + selected: ['barfoo', 'foobaz', 'bar'], + }) + }) + + it('should reset the highlighted source options', () => { + addIndividualSourceOptions({ + highlightedSourceOptions, + maxSelections: Infinity, + onChange, + selected, + setHighlightedSourceOptions, + }) + + expect(setHighlightedSourceOptions).toHaveBeenCalledWith([]) + }) + + it('should only select the filtered source options', () => { + addIndividualSourceOptions({ + filterable: true, + filter: 'oo', + sourceOptions, + highlightedSourceOptions, + maxSelections: Infinity, + onChange, + selected, + setHighlightedSourceOptions, + }) + + expect(onChange).toHaveBeenCalledWith({ + selected: ['barfoo', 'foobaz'], + }) + }) + + it('should only call onChange with the max selection amount', () => { + addIndividualSourceOptions({ + filterable: true, + filter: 'oo', + sourceOptions, + highlightedSourceOptions: highlightedSourceOptions.slice(0, 1), + maxSelections: 1, + onChange, + selected: selected, + setHighlightedSourceOptions, + }) + + expect(onChange).toHaveBeenCalledWith({ + selected: ['foobaz'], + }) + }) +}) diff --git a/packages/core/src/Transfer/__tests__/helper/defaultFilterCallback.test.js b/packages/widgets/src/Transfer/__tests__/helper/defaultFilterCallback.test.js similarity index 70% rename from packages/core/src/Transfer/__tests__/helper/defaultFilterCallback.test.js rename to packages/widgets/src/Transfer/__tests__/helper/defaultFilterCallback.test.js index b12c98df99..2a869137ad 100644 --- a/packages/core/src/Transfer/__tests__/helper/defaultFilterCallback.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/defaultFilterCallback.test.js @@ -1,7 +1,7 @@ -import { defaultFilterCallback } from '../../../Transfer/helper/defaultFilterCallback.js' +import { defaultFilterCallback } from '../../../Transfer/Transfer/defaultFilterCallback.js' describe('Transfer - defaultFilterCallback', () => { - const plainOptions = [ + const options = [ { label: 'Foo', value: 'foo' }, { label: 'FOO', value: 'fOO' }, { label: 'Bar', value: 'bar' }, @@ -10,8 +10,8 @@ describe('Transfer - defaultFilterCallback', () => { it('should returl all options when the filter is empty', () => { const filter = '' - const expected = plainOptions - const actual = defaultFilterCallback(plainOptions, filter) + const expected = options + const actual = defaultFilterCallback(options, filter) expect(actual).toEqual(expected) }) @@ -22,7 +22,7 @@ describe('Transfer - defaultFilterCallback', () => { { label: 'Foo', value: 'foo' }, { label: 'FOO', value: 'fOO' }, ] - const actual = defaultFilterCallback(plainOptions, filter) + const actual = defaultFilterCallback(options, filter) expect(actual).toEqual(expected) }) @@ -30,7 +30,7 @@ describe('Transfer - defaultFilterCallback', () => { it('should return an empty array when there are no matches', () => { const filter = 'Baz' const expected = [] - const actual = defaultFilterCallback(plainOptions, filter) + const actual = defaultFilterCallback(options, filter) expect(actual).toEqual(expected) }) diff --git a/packages/widgets/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js b/packages/widgets/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js new file mode 100644 index 0000000000..64910e3141 --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/helper/isReorderDownDisabled.test.js @@ -0,0 +1,47 @@ +import { isReorderDownDisabled } from '../../../Transfer/Transfer/isReorderDownDisabled.js' + +describe('Transfer - isReorderDownDisabled', () => { + const selected = ['foo', 'bar', 'baz'] + + it('should return true when there are no highlighted picked options', () => { + const highlightedPickedOptions = [] + const actual = isReorderDownDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return true when there are multiple highlighted picked options', () => { + const highlightedPickedOptions = ['bar', 'foo'] + const actual = isReorderDownDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return true if the last picked option is highlighted', () => { + const highlightedPickedOptions = ['baz'] + + const actual = isReorderDownDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return false when one picked option is highlighted which is not the last one', () => { + const highlightedPickedOptions = ['bar'] + + const actual = isReorderDownDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(false) + }) +}) diff --git a/packages/widgets/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js b/packages/widgets/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js new file mode 100644 index 0000000000..d4176becaf --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/helper/isReorderUpDisabled.test.js @@ -0,0 +1,47 @@ +import { isReorderUpDisabled } from '../../../Transfer/Transfer/isReorderUpDisabled.js' + +describe('Transfer - isReorderUpDisabled', () => { + const selected = ['foo', 'bar', 'baz'] + + it('should return true when there are no highlighted picked options', () => { + const highlightedPickedOptions = [] + const actual = isReorderUpDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return true when there are multiple highlighted picked options', () => { + const highlightedPickedOptions = ['bar', 'baz'] + const actual = isReorderUpDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return true if the first picked option is highlighted', () => { + const highlightedPickedOptions = ['foo'] + + const actual = isReorderUpDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(true) + }) + + it('should return false when one picked option is highlighted which is not the last one', () => { + const highlightedPickedOptions = ['baz'] + + const actual = isReorderUpDisabled({ + highlightedPickedOptions, + selected, + }) + + expect(actual).toBe(false) + }) +}) diff --git a/packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js b/packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js similarity index 50% rename from packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js rename to packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js index d0eacee6a5..b178e77dd5 100644 --- a/packages/core/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionDown.test.js @@ -1,42 +1,33 @@ -import { moveHighlightedPickedOptionDown } from '../../../Transfer/helper/moveHighlightedPickedOptionDown.js' +import { moveHighlightedPickedOptionDown } from '../../../Transfer/Transfer/moveHighlightedPickedOptionDown.js' describe('Transfer - moveHighlightedPickedOptionDown', () => { const onChange = jest.fn() - - const selectedPlainOptions = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] + const selected = ['foo', 'bar', 'baz'] afterEach(() => { onChange.mockClear() }) it('should move the highlighted option down', () => { - const highlighted = [{ label: 'Bar', value: 'bar' }] + const highlighted = ['bar'] moveHighlightedPickedOptionDown({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, + selected, + highlightedPickedOptions: highlighted, onChange, }) expect(onChange).toHaveBeenCalledWith({ - selected: [ - { label: 'Foo', value: 'foo' }, - { label: 'Baz', value: 'baz' }, - { label: 'Bar', value: 'bar' }, - ], + selected: ['foo', 'baz', 'bar'], }) }) it('should do nothing when trying to move down the last option', () => { - const highlighted = [{ label: 'Baz', value: 'baz' }] + const highlighted = ['baz'] moveHighlightedPickedOptionDown({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, + selected, + highlightedPickedOptions: highlighted, onChange, }) @@ -44,11 +35,11 @@ describe('Transfer - moveHighlightedPickedOptionDown', () => { }) it('should do nothing when trying to move down a non-existing option', () => { - const highlighted = [{ label: 'Foobar', value: 'foobar' }] + const highlighted = ['foobar'] moveHighlightedPickedOptionDown({ - selectedPlainOptions, - highlightedPickedPlainOptions: highlighted, + selected, + highlightedPickedOptions: highlighted, onChange, }) diff --git a/packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js b/packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js new file mode 100644 index 0000000000..0919b22908 --- /dev/null +++ b/packages/widgets/src/Transfer/__tests__/helper/moveHighlightedPickedOptionUp.test.js @@ -0,0 +1,48 @@ +import { moveHighlightedPickedOptionUp } from '../../../Transfer/Transfer/moveHighlightedPickedOptionUp.js' + +describe('Transfer - moveHighlightedPickedOptionUp', () => { + const onChange = jest.fn() + const selected = ['foo', 'bar', 'baz'] + + afterEach(() => { + onChange.mockClear() + }) + + it('should move the highlighted option up', () => { + const highlighted = ['bar'] + + moveHighlightedPickedOptionUp({ + selected, + highlightedPickedOptions: highlighted, + onChange, + }) + + expect(onChange).toHaveBeenCalledWith({ + selected: ['bar', 'foo', 'baz'], + }) + }) + + it('should do nothing when trying to move up the first option', () => { + const highlighted = ['foo'] + + moveHighlightedPickedOptionUp({ + selected, + highlightedPickedOptions: highlighted, + onChange, + }) + + expect(onChange).toHaveBeenCalledTimes(0) + }) + + it('should do nothing when trying to move up a non-existing option', () => { + const highlighted = ['foobar'] + + moveHighlightedPickedOptionUp({ + selected, + highlightedPickedOptions: highlighted, + onChange, + }) + + expect(onChange).toHaveBeenCalledTimes(0) + }) +}) diff --git a/packages/core/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js b/packages/widgets/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js similarity index 88% rename from packages/core/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js rename to packages/widgets/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js index ea705374c4..ff80ac779d 100644 --- a/packages/core/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/removeAllPickedOptions.test.js @@ -1,4 +1,4 @@ -import { removeAllPickedOptions } from '../../../Transfer/helper/removeAllPickedOptions.js' +import { removeAllPickedOptions } from '../../../Transfer/Transfer/removeAllPickedOptions.js' describe('Transfer - removeAllPickedOptions', () => { const onChange = jest.fn() diff --git a/packages/core/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js b/packages/widgets/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js similarity index 60% rename from packages/core/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js rename to packages/widgets/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js index 0517e95e13..392d0f594e 100644 --- a/packages/core/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/removeIndividualPickedOptions.test.js @@ -1,4 +1,4 @@ -import { removeIndividualPickedOptions } from '../../../Transfer/helper/removeIndividualPickedOptions.js' +import { removeIndividualPickedOptions } from '../../../Transfer/Transfer/removeIndividualPickedOptions.js' describe('Transfer - removeIndividualPickedOptions', () => { const onChange = jest.fn() @@ -9,34 +9,26 @@ describe('Transfer - removeIndividualPickedOptions', () => { setHighlightedPickedOptions.mockClear() }) - const selectedPlainOptions = [ - { label: 'Foo', value: 'foo' }, - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] - - const highlightedPickedReactOptions = [ - { label: 'Bar', value: 'bar' }, - { label: 'Baz', value: 'baz' }, - ] + const selected = ['foo', 'bar', 'baz'] + const highlightedPickedOptions = ['bar', 'baz'] it('should remove the highlighted picked options', () => { removeIndividualPickedOptions({ - highlightedPickedReactOptions, - selectedPlainOptions, + highlightedPickedOptions, + selected, onChange, setHighlightedPickedOptions, }) expect(onChange).toHaveBeenCalledWith({ - selected: [{ label: 'Foo', value: 'foo' }], + selected: ['foo'], }) }) it('should reset the highlighted picked options', () => { removeIndividualPickedOptions({ - highlightedPickedReactOptions, - selectedPlainOptions, + highlightedPickedOptions, + selected, onChange, setHighlightedPickedOptions, }) diff --git a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js similarity index 79% rename from packages/core/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js rename to packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js index 1cbe909867..8f16a0024b 100644 --- a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/createToggleHighlightedOption.test.js @@ -1,22 +1,21 @@ -import { ADD_MODE, RANGE_MODE, REPLACE_MODE } from '../../../common.js' -import { createChildren } from '../../common/createChildren.js' -import { createToggleHighlightedOption } from '../../../helper/useHighlightedOptions/createToggleHighlightedOption.js' -import { toggleAdd } from '../../../helper/useHighlightedOptions/toggleAdd.js' -import { toggleRange } from '../../../helper/useHighlightedOptions/toggleRange.js' -import { toggleReplace } from '../../../helper/useHighlightedOptions/toggleReplace.js' +import { ADD_MODE, RANGE_MODE, REPLACE_MODE } from '../../../common/index.js' +import { createToggleHighlightedOption } from '../../../Transfer/useHighlightedOptions/createToggleHighlightedOption.js' +import { toggleAdd } from '../../../Transfer/useHighlightedOptions/toggleAdd.js' +import { toggleRange } from '../../../Transfer/useHighlightedOptions/toggleRange.js' +import { toggleReplace } from '../../../Transfer/useHighlightedOptions/toggleReplace.js' jest.mock( - '../../../../Transfer/helper/useHighlightedOptions/toggleAdd', + '../../../../Transfer/Transfer/useHighlightedOptions/toggleAdd', () => ({ toggleAdd: jest.fn() }) ) jest.mock( - '../../../../Transfer/helper/useHighlightedOptions/toggleRange', + '../../../../Transfer/Transfer/useHighlightedOptions/toggleRange', () => ({ toggleRange: jest.fn() }) ) jest.mock( - '../../../../Transfer/helper/useHighlightedOptions/toggleReplace', + '../../../../Transfer/Transfer/useHighlightedOptions/toggleReplace', () => ({ toggleReplace: jest.fn() }) ) @@ -28,10 +27,10 @@ describe('Transfer- useHighlightedOptions - createToggleHighlightedOption', () = const highlightedOptions = [] const maxSelections = Infinity const setHighlightedOptions = jest.fn() - const reactOptions = createChildren() + const options = [] const option = { value: 'foo', label: 'Foo' } const createToggleHighlightedOptionDefaultPayload = { - reactOptions, + options, disabled, highlightedOptions, lastClicked, @@ -55,12 +54,12 @@ describe('Transfer- useHighlightedOptions - createToggleHighlightedOption', () = it('should set the lastClicked to the clicked option when mode is ADD_MODE', () => { toggleHighlightedOption({ option, mode: ADD_MODE }) - expect(setLastClicked).toHaveBeenCalledWith(option) + expect(setLastClicked).toHaveBeenCalledWith(option.value) }) it('should set the lastClicked to the clicked option when mode is REPLACE_MODE', () => { toggleHighlightedOption({ option, mode: REPLACE_MODE }) - expect(setLastClicked).toHaveBeenCalledWith(option) + expect(setLastClicked).toHaveBeenCalledWith(option.value) }) it('should not overwrite the lastClicked to the clicked option when mode is RANGE_MODE', () => { diff --git a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js similarity index 78% rename from packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js rename to packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js index a22c0a0828..e4dbb2c604 100644 --- a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleAdd.test.js @@ -1,4 +1,4 @@ -import { toggleAdd } from '../../../../Transfer/helper/useHighlightedOptions/toggleAdd.js' +import { toggleAdd } from '../../../../Transfer/Transfer/useHighlightedOptions/toggleAdd.js' describe('Transfer - useHighlightedOptions - toggleAdd', () => { const setHighlightedOptions = jest.fn() @@ -11,9 +11,9 @@ describe('Transfer - useHighlightedOptions - toggleAdd', () => { const maxSelections = 1 it('should replace the current highlighted option', () => { - const highlightedOptions = [{ value: 'foo', label: 'Foo' }] + const highlightedOptions = ['foo'] const option = { value: 'bar', label: 'Bar' } - const expected = [option] + const expected = [option.value] toggleAdd({ maxSelections, @@ -26,7 +26,7 @@ describe('Transfer - useHighlightedOptions - toggleAdd', () => { }) it('should empty the highlighted options', () => { - const highlightedOptions = [{ value: 'foo', label: 'Foo' }] + const highlightedOptions = ['foo'] const option = { value: 'foo', label: 'Foo' } const expected = [] @@ -45,9 +45,9 @@ describe('Transfer - useHighlightedOptions - toggleAdd', () => { const maxSelections = Infinity it('should add the option to the highlighted options when maxSelections is Infinity', () => { - const highlightedOptions = [{ value: 'foo', label: 'Foo' }] + const highlightedOptions = ['foo'] const option = { value: 'bar', label: 'Bar' } - const expected = [...highlightedOptions, option] + const expected = [...highlightedOptions, option.value] toggleAdd({ maxSelections, @@ -63,8 +63,12 @@ describe('Transfer - useHighlightedOptions - toggleAdd', () => { const optionOne = { value: 'foo', label: 'Foo' } const optionTwo = { value: 'bar', label: 'Bar' } const optionThree = { value: 'baz', label: 'Baz' } - const highlightedOptions = [optionOne, optionTwo, optionThree] - const expected = [optionOne, optionThree] + const highlightedOptions = [ + optionOne.value, + optionTwo.value, + optionThree.value, + ] + const expected = [optionOne, optionThree].map(({ value }) => value) const option = optionTwo toggleAdd({ diff --git a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js similarity index 65% rename from packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js rename to packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js index 22c692d7ff..4974931b4c 100644 --- a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleRange.test.js @@ -1,11 +1,8 @@ -import React from 'react' - -import { createChildren } from '../../common/createChildren.js' -import { toggleRange } from '../../../helper/useHighlightedOptions/toggleRange.js' +import { toggleRange } from '../../../Transfer/useHighlightedOptions/toggleRange.js' describe('Transfer- useHighlightedOptions - toggleRange', () => { const setHighlightedOptions = jest.fn() - const plainOptions = [ + const options = [ { label: 'Foo', value: 'foo' }, { label: 'Bar', value: 'bar' }, { label: 'Baz', value: 'baz' }, @@ -13,14 +10,6 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { { label: 'Foobaz', value: 'foobaz' }, { label: 'Foobarbaz', value: 'foobarbaz' }, ] - const reactOptions = createChildren( - ...plainOptions.map(plainOption => - React.createElement('span', { - ...plainOption, - key: plainOption.value, - }) - ) - ) afterEach(() => { setHighlightedOptions.mockClear() @@ -28,12 +17,12 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { describe('maxSelections=1', () => { const maxSelections = 1 - const highlightedOptions = plainOptions.slice(0, 1) - const lastClicked = plainOptions[0] + const highlightedOptions = options.slice(0, 1).map(({ value }) => value) + const lastClicked = options[0].value it('should replace the current highlighted option', () => { - const option = plainOptions[1] - const expected = [option] + const option = options[1] + const expected = [option.value] toggleRange({ lastClicked, @@ -41,14 +30,14 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { highlightedOptions, option, setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) }) it('should not change the highlighted options', () => { - const option = plainOptions[0] + const option = options[0] const expected = highlightedOptions toggleRange({ @@ -57,7 +46,7 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { highlightedOptions, option, setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) @@ -66,11 +55,11 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { describe('maxSelections=Infinity', () => { const maxSelections = Infinity - const highlightedOptions = plainOptions.slice(0, 1) - const lastClicked = plainOptions[0] + const highlightedOptions = options.slice(0, 1).map(({ value }) => value) + const lastClicked = options[0].value it('should not change the highlighted options', () => { - const option = plainOptions[0] + const option = options[0] const expected = highlightedOptions toggleRange({ @@ -79,37 +68,37 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { highlightedOptions, option, setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) }) it('should highlight all option from the highlighted one to the clicked one', () => { - const expected = plainOptions.slice(0, 4) + const expected = options.slice(0, 4).map(({ value }) => value) toggleRange({ - option: plainOptions[3], + option: options[3], lastClicked, maxSelections, highlightedOptions, setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) }) it('should highlight from the lastClicked to the most recently clicked one', () => { - const expected = plainOptions.slice(1, 4) + const expected = options.slice(1, 4).map(({ value }) => value) toggleRange({ - option: plainOptions[3], - lastClicked: plainOptions[1], + option: options[3], + lastClicked: options[1].value, maxSelections, highlightedOptions, setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) @@ -119,21 +108,18 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { * E. g. lastClicked is hidden because of a filter change */ it('should highlight from the highest in list to the clicked when last clicked not visible anymore', () => { - const expected = plainOptions.slice(1, 6) + const expected = options.slice(1, 6).map(({ value }) => value) toggleRange({ - option: plainOptions[5], - lastClicked: { - value: 'not visible anymore', - label: 'irrelevant', - }, + option: options[5], + lastClicked: 'not visible anymore', maxSelections, highlightedOptions: [ - ...plainOptions.slice(1, 2), - ...plainOptions.slice(3, 5), - ], + ...options.slice(1, 2), + ...options.slice(3, 5), + ].map(({ value }) => value), setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) @@ -144,18 +130,18 @@ describe('Transfer- useHighlightedOptions - toggleRange', () => { * have been clicked with shift */ it('should highlight from the highest in list to the clicked when last clicked null', () => { - const expected = plainOptions.slice(1, 6) + const expected = options.slice(1, 6).map(({ value }) => value) toggleRange({ - option: plainOptions[5], + option: options[5], lastClicked: null, maxSelections, highlightedOptions: [ - ...plainOptions.slice(1, 2), - ...plainOptions.slice(3, 5), - ], + ...options.slice(1, 2), + ...options.slice(3, 5), + ].map(({ value }) => value), setHighlightedOptions, - reactOptions, + options, }) expect(setHighlightedOptions).toHaveBeenCalledWith(expected) diff --git a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js similarity index 77% rename from packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js rename to packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js index 59a129de95..9cfff0c590 100644 --- a/packages/core/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOption/toggleReplace.test.js @@ -1,4 +1,4 @@ -import { toggleReplace } from '../../../../Transfer/helper/useHighlightedOptions/toggleReplace.js' +import { toggleReplace } from '../../../../Transfer/Transfer/useHighlightedOptions/toggleReplace.js' describe('Transfer - useHighlightedOptions - toggleReplace', () => { const setHighlightedOptions = jest.fn() @@ -9,9 +9,9 @@ describe('Transfer - useHighlightedOptions - toggleReplace', () => { describe('maxSelections=1', () => { it('should replace the current highlighted option', () => { - const highlightedOptions = [{ value: 'foo', label: 'Foo' }] + const highlightedOptions = ['foo'] const option = { value: 'bar', label: 'Bar' } - const expected = [option] + const expected = [option.value] toggleReplace({ highlightedOptions, @@ -23,7 +23,7 @@ describe('Transfer - useHighlightedOptions - toggleReplace', () => { }) it('should empty the highlighted options', () => { - const highlightedOptions = [{ value: 'foo', label: 'Foo' }] + const highlightedOptions = ['foo'] const option = { value: 'foo', label: 'Foo' } const expected = [] diff --git a/packages/core/src/Transfer/__tests__/helper/useHighlightedOptions.test.js b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOptions.test.js similarity index 84% rename from packages/core/src/Transfer/__tests__/helper/useHighlightedOptions.test.js rename to packages/widgets/src/Transfer/__tests__/helper/useHighlightedOptions.test.js index f20ebda6d6..8809d8fa50 100644 --- a/packages/core/src/Transfer/__tests__/helper/useHighlightedOptions.test.js +++ b/packages/widgets/src/Transfer/__tests__/helper/useHighlightedOptions.test.js @@ -1,8 +1,8 @@ -import { createToggleHighlightedOption } from '../../../Transfer/helper/useHighlightedOptions/createToggleHighlightedOption.js' -import { useHighlightedOptions } from '../../../Transfer/helper/useHighlightedOptions.js' +import { createToggleHighlightedOption } from '../../../Transfer/Transfer/useHighlightedOptions/createToggleHighlightedOption.js' +import { useHighlightedOptions } from '../../../Transfer/Transfer/useHighlightedOptions.js' jest.mock( - '../../../Transfer/helper/useHighlightedOptions/createToggleHighlightedOption', + '../../../Transfer/Transfer/useHighlightedOptions/createToggleHighlightedOption', () => ({ createToggleHighlightedOption: jest.fn(), }) diff --git a/packages/widgets/src/Transfer/common/addOption.js b/packages/widgets/src/Transfer/common/addOption.js new file mode 100644 index 0000000000..40e8e8e06b --- /dev/null +++ b/packages/widgets/src/Transfer/common/addOption.js @@ -0,0 +1,12 @@ +import { findOption } from './findOption.js' + +/** + * @param {Object[]} options + * @param {Object} option + * @returns {Object} + */ +export const addOption = (options, option) => { + const found = findOption(options, option) + if (found) return options + return [...options, option] +} diff --git a/packages/widgets/src/Transfer/common/findOptionIndex.js b/packages/widgets/src/Transfer/common/findOptionIndex.js new file mode 100644 index 0000000000..9a19e4f95d --- /dev/null +++ b/packages/widgets/src/Transfer/common/findOptionIndex.js @@ -0,0 +1,9 @@ +import { isOption } from './isOption.js' + +/** + * @param {Object[]} options + * @param {Object} option + * @returns {Int} + */ +export const findOptionIndex = (options, option) => + options.findIndex(current => isOption(current, option)) diff --git a/packages/widgets/src/Transfer/common/getModeByModifierKey.js b/packages/widgets/src/Transfer/common/getModeByModifierKey.js new file mode 100644 index 0000000000..cd4dbbeb15 --- /dev/null +++ b/packages/widgets/src/Transfer/common/getModeByModifierKey.js @@ -0,0 +1,29 @@ +import { ADD_MODE, RANGE_MODE, REPLACE_MODE } from './modes.js' + +/** + * @param {Object} args + * @param {bool} args.altKey + * @param {bool} args.shiftKey + * @param {bool} args.ctrlKey + * @param {bool} args.metaKey + * @return {string} + */ +export const getModeByModifierKey = ({ + altKey, + shiftKey, + ctrlKey, + metaKey, +}) => { + const keys = [altKey, shiftKey, ctrlKey, metaKey] + const amountKeyPressed = keys.filter(v => v) + const moreThanOneKeyPressed = amountKeyPressed.length + + if (moreThanOneKeyPressed !== 1) return REPLACE_MODE + + if (altKey || ctrlKey || metaKey) return ADD_MODE + + if (shiftKey) return RANGE_MODE + + // default to replace mode + return REPLACE_MODE +} diff --git a/packages/widgets/src/Transfer/common/index.js b/packages/widgets/src/Transfer/common/index.js new file mode 100644 index 0000000000..eafe245e1e --- /dev/null +++ b/packages/widgets/src/Transfer/common/index.js @@ -0,0 +1,6 @@ +export { ADD_MODE, RANGE_MODE, REPLACE_MODE } from './modes.js' +export { borderColor, borderRadius } from './styles.js' +export { findOptionIndex } from './findOptionIndex.js' +export { getModeByModifierKey } from './getModeByModifierKey.js' +export { isOption } from './isOption.js' +export { toggleValue } from './toggleValue.js' diff --git a/packages/widgets/src/Transfer/common/isOption.js b/packages/widgets/src/Transfer/common/isOption.js new file mode 100644 index 0000000000..b66213dbc1 --- /dev/null +++ b/packages/widgets/src/Transfer/common/isOption.js @@ -0,0 +1,7 @@ +/** + * @param {Object} left + * @param {Object} left + * @returns {bool} + */ +export const isOption = (left, right) => + left.label === right.label && left.value === right.value diff --git a/packages/widgets/src/Transfer/common/modes.js b/packages/widgets/src/Transfer/common/modes.js new file mode 100644 index 0000000000..f837eed4be --- /dev/null +++ b/packages/widgets/src/Transfer/common/modes.js @@ -0,0 +1,11 @@ +/** + * Click modes when clicking on an option with/without + * a modifier key (ctrl, alt, cmd, shift) + */ + +// no or multiple modifier keys +export const REPLACE_MODE = 'REPLACE_MODE' +// add/remove options from selection +export const ADD_MODE = 'ADD_MODE' +// create selection range +export const RANGE_MODE = 'RANGE_MODE' diff --git a/packages/widgets/src/Transfer/common/removeOption.js b/packages/widgets/src/Transfer/common/removeOption.js new file mode 100644 index 0000000000..4a9555a590 --- /dev/null +++ b/packages/widgets/src/Transfer/common/removeOption.js @@ -0,0 +1,15 @@ +import { findOptionIndex } from './findOptionIndex.js' + +/** + * @param {Object[]} options + * @param {Object} option + * @returns {Object} + */ +export const removeOption = (options, option) => { + const index = findOptionIndex(options, option) + + if (index === -1) return options + if (index === 0) return options.slice(1) + + return [...options.slice(0, index), ...options.slice(index + 1)] +} diff --git a/packages/widgets/src/Transfer/common/styles.js b/packages/widgets/src/Transfer/common/styles.js new file mode 100644 index 0000000000..5451a45b6d --- /dev/null +++ b/packages/widgets/src/Transfer/common/styles.js @@ -0,0 +1,4 @@ +import { colors } from '@dhis2/ui-constants' + +export const borderColor = colors.grey400 +export const borderRadius = '3px' diff --git a/packages/widgets/src/Transfer/common/toggleValue.js b/packages/widgets/src/Transfer/common/toggleValue.js new file mode 100644 index 0000000000..ab5fdbe36c --- /dev/null +++ b/packages/widgets/src/Transfer/common/toggleValue.js @@ -0,0 +1,18 @@ +/** + * @param {string[]} values + * @param {string} value + * @returns {string[]} + */ +export const toggleValue = (values, value) => { + const index = values.indexOf(value) + + if (index === -1) { + return [...values, value] + } else if (index === 0) { + values.slice(1) + } + + const prevSlice = values.slice(0, index) + const nextSlice = values.slice(index + 1) + return [...prevSlice, ...nextSlice] +} diff --git a/packages/core/src/Transfer/icons.js b/packages/widgets/src/Transfer/icons.js similarity index 100% rename from packages/core/src/Transfer/icons.js rename to packages/widgets/src/Transfer/icons.js diff --git a/packages/widgets/src/index.js b/packages/widgets/src/index.js index eddd92d94d..95d59460de 100755 --- a/packages/widgets/src/index.js +++ b/packages/widgets/src/index.js @@ -10,3 +10,5 @@ export { MultiSelectField } from './MultiSelectField/MultiSelectField.js' export { SingleSelectField } from './SingleSelectField/SingleSelectField.js' export { SwitchField } from './SwitchField/SwitchField.js' export { TextAreaField } from './TextAreaField/TextAreaField.js' +export { Transfer } from './Transfer/Transfer.js' +export { TransferOption } from './Transfer/TransferOption.js'