diff --git a/src/PropertiesPanel.js b/src/PropertiesPanel.js index 98e124d0..e2f57fd3 100644 --- a/src/PropertiesPanel.js +++ b/src/PropertiesPanel.js @@ -46,7 +46,8 @@ const DEFAULT_LAYOUT = { * id: String, * items: Array, * label: String, - * shouldSort?: Boolean + * shouldSort?: Boolean, + * shouldOpen?: Boolean * } } ListGroupDefinition * * @typedef { { diff --git a/src/components/ListGroup.js b/src/components/ListGroup.js index d61b1bd6..4849a6fd 100644 --- a/src/components/ListGroup.js +++ b/src/components/ListGroup.js @@ -34,7 +34,8 @@ export default function ListGroup(props) { items, label, add: AddContainer, - shouldSort = true + shouldSort = true, + shouldOpen = true } = props; @@ -50,7 +51,7 @@ export default function ListGroup(props) { const prevElement = usePrevious(element); const elementChanged = element !== prevElement; - const shouldHandleEffects = !elementChanged && shouldSort; + const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen); // reset initial ordering when element changes (before first render) if (elementChanged) { @@ -80,17 +81,23 @@ export default function ListGroup(props) { let newOrdering = ordering; - // sort + open if closed - if (!open) { - newOrdering = createOrdering(sortItems(items)); + // open if not open and configured + if (!open && shouldOpen) { toggleOpen(); + + // if I opened and I should sort, then sort items + if (shouldSort) { + newOrdering = createOrdering(sortItems(items)); + } } - // add new items on top - newOrdering = removeDuplicates([ - ...add, - ...newOrdering - ]); + // add new items on top or bottom depending on sorting behavior + newOrdering = newOrdering.filter(item => !add.includes(item)); + if (shouldSort) { + newOrdering.unshift(...add); + } else { + newOrdering.push(...add); + } setOrdering(newOrdering); setNewItemAdded(true); @@ -99,14 +106,13 @@ export default function ListGroup(props) { } }, [ items, open, shouldHandleEffects ]); - // (2) sort items on open + // (2) sort items on open if shouldSort is set useEffect(() => { - // we already sorted as items were added - if (shouldHandleEffects && open && !newItemAdded) { + if (shouldSort && open && !newItemAdded) { setOrdering(createOrdering(sortItems(items))); } - }, [ open, shouldHandleEffects ]); + }, [ open, shouldSort ]); // (3) items were deleted useEffect(() => { @@ -184,7 +190,9 @@ export default function ListGroup(props) { return ( ); }) @@ -211,12 +219,8 @@ function createOrdering(items) { return items.map(i => i.id); } -function removeDuplicates(items) { - return items.filter((i, index) => items.indexOf(i) === index); -} - function getTitleAttribute(label, items) { const count = items.length; return label + (count ? ` (${count} item${count != 1 ? 's' : ''})` : ''); -} \ No newline at end of file +} diff --git a/src/components/ListItem.js b/src/components/ListItem.js index e80a2d2e..053ddd4d 100644 --- a/src/components/ListItem.js +++ b/src/components/ListItem.js @@ -45,4 +45,4 @@ export default function ListItem(props) { ); -} \ No newline at end of file +} diff --git a/test/spec/components/ListGroup.spec.js b/test/spec/components/ListGroup.spec.js index 98acb92e..7c141946 100644 --- a/test/spec/components/ListGroup.spec.js +++ b/test/spec/components/ListGroup.spec.js @@ -121,42 +121,6 @@ describe('', function() { }); - it('should open on adding new item', function() { - - // given - const items = [ - { - id: 'item-1', - label: 'Item 1' - } - ]; - - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); - - const list = domQuery('.bio-properties-panel-list', container); - - // assume - expect(domClasses(list).has('open')).to.be.false; - - const newItems = [ - ...items, - { - id: 'item-2', - label: 'Item 2' - } - ]; - - // when - createListGroup({ items: newItems }, rerender); - - // then - expect(domClasses(list).has('open')).to.be.true; - }); - - it('should wrap add container', async function() { // given @@ -190,494 +154,807 @@ describe('', function() { }); - describe('ordering', function() { - - it('should create initial ordering from items', function() { - - // given - const items = [ - { - id: 'item-1', - label: 'Item 1' - }, - { - id: 'item-2', - label: 'Item 2' - }, - { - id: 'item-3', - label: 'Item 2' - } - ]; - - const { container } = createListGroup({ container: parentContainer, items }); - - const list = domQuery('.bio-properties-panel-list', container); - - // then - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-2', - 'item-3' - ]); - }); - - - it('should re-iniate ordering when element changed (unsorted)', async function() { - - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'ab' - }, - { - id: 'item-3', - label: 'def03' - } - ]; - - const { - container, - rerender - } = createListGroup({ container: parentContainer, items, shouldSort: false }); - - const list = domQuery('.bio-properties-panel-list', container); - - // when - const newElement = { - ...noopElement, - id: 'bar' - }; - - // when - createListGroup({ element: newElement, items, shouldSort: false }, rerender); - - // then - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-2', - 'item-3' - ]); - }); - - - it('should re-iniate ordering when element changed (sorted)', async function() { - - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'ab' - }, - { - id: 'item-3', - label: 'def03' - } - ]; - - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); - - const list = domQuery('.bio-properties-panel-list', container); - - // when - const newElement = { - ...noopElement, - id: 'bar' - }; - - // when - createListGroup({ element: newElement, items }, rerender); - - // then - expect(getListOrdering(list)).to.eql([ - 'item-2', - 'item-3', - 'item-1' - ]); - }); - - - it('should NOT sort if configured', async function() { - - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'ab' - }, - { - id: 'item-3', - label: 'def03' - }, - { - id: 'item-4', - label: 'def04' - } - ]; + describe('behaviors', function() { + + describe('sorting', function() { + + it('should sort per default', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + }, + { + id: 'item-3', + label: 'def03' + }, + { + id: 'item-4', + label: 'def04' + } + ]; + + const { container } = createListGroup({ container: parentContainer, items }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + waitFor(async () => { + await header.click(); + }); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-4', + 'item-1' + ]); + }); - const { container } = createListGroup({ container: parentContainer, items, shouldSort: false }); - const header = domQuery('.bio-properties-panel-group-header', container); + it('should create initial sorting from items', function() { + + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + }, + { + id: 'item-2', + label: 'Item 2' + }, + { + id: 'item-3', + label: 'Item 2' + } + ]; + + const { container } = createListGroup({ container: parentContainer, items }); + + const list = domQuery('.bio-properties-panel-list', container); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2', + 'item-3' + ]); + }); - const list = domQuery('.bio-properties-panel-list', container); - // when - waitFor(async () => { - await header.click(); + it('should re-iniate sorting when element changed (unsorted)', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + }, + { + id: 'item-3', + label: 'def03' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items, shouldSort: false }); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + const newElement = { + ...noopElement, + id: 'bar' + }; + + // when + createListGroup({ element: newElement, items, shouldSort: false }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2', + 'item-3' + ]); }); - // then - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-2', - 'item-3', - 'item-4' - ]); - }); + it('should re-iniate sorting when element changed (sorted)', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + }, + { + id: 'item-3', + label: 'def03' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + const newElement = { + ...noopElement, + id: 'bar' + }; + + // when + createListGroup({ element: newElement, items }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-1' + ]); + }); - it('should order alphanumeric on open (label)', async function() { - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'ab' - }, - { - id: 'item-3', - label: 'def03' - }, - { - id: 'item-4', - label: 'def04' - } - ]; + it('should NOT sort if configured', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + }, + { + id: 'item-3', + label: 'def03' + }, + { + id: 'item-4', + label: 'def04' + } + ]; + + const { container } = createListGroup({ container: parentContainer, items, shouldSort: false }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + waitFor(async () => { + await header.click(); + }); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2', + 'item-3', + 'item-4' + ]); + }); - const { container } = createListGroup({ container: parentContainer, items }); - const header = domQuery('.bio-properties-panel-group-header', container); + it('should order alphanumeric on open (label)', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + }, + { + id: 'item-3', + label: 'def03' + }, + { + id: 'item-4', + label: 'def04' + } + ]; + + const { container } = createListGroup({ container: parentContainer, items }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + waitFor(async () => { + await header.click(); + }); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-4', + 'item-1' + ]); + }); - const list = domQuery('.bio-properties-panel-list', container); - // when - waitFor(async () => { - await header.click(); + it('should NOT add new items on top - element changed', function() { + + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + }, + { + id: 'item-2', + label: 'Item 2' + }, + { + id: 'item-3', + label: 'Item 3' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const list = domQuery('.bio-properties-panel-list', container); + + const newItems = [ + ...items, + { + id: 'item-4', + label: 'Item 4' + } + ]; + + const newElement = { + ...noopElement, + id: 'bar' + }; + + // when + createListGroup({ element: newElement, items: newItems }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2', + 'item-3', + 'item-4' + ]); }); - // then - expect(getListOrdering(list)).to.eql([ - 'item-2', - 'item-3', - 'item-4', - 'item-1' - ]); - }); + it('should sort items - closed + added new one', function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'ab' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, label: 'List', items }); + + const newItems = [ + ...items, + { + id: 'item-3', + label: 'foo' + } + ]; + + const list = domQuery('.bio-properties-panel-list', container); + + // assume + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2' + ]); + + // when + createListGroup({ items: newItems }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-3', + 'item-2', + 'item-1' + ]); + }); - it('should add new items on top', function() { - // given - const items = [ - { - id: 'item-1', - label: 'Item 1' - }, - { - id: 'item-2', - label: 'Item 2' - }, - { - id: 'item-3', - label: 'Item 3' - } - ]; + it('should keep sorting when items count did not change', async function() { + + // given + const items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'abc' + }, + { + id: 'item-3', + label: 'foo' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + waitFor(async () => { + await header.click(); + }); + + // assume + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-1' + ]); + + items[2].label = 'aaa'; + + // when + createListGroup({ items }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-1' + ]); + }); - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); - const list = domQuery('.bio-properties-panel-list', container); + it('should NOT sort when open on adding items', async function() { + + // given + let items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'abc' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + // assume + // (1) open + waitFor(async () => { + await header.click(); + }); + + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-1' + ]); + + // (1) when + // add + items = [ + ...items, + { + id: 'item-3', + label: 'foo' + } + ]; + + createListGroup({ items }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-3', + 'item-2', + 'item-1' + ]); + + // (2) when + // add + items = [ + ...items, + { + id: 'item-4', + label: 'goo' + } + ]; + + createListGroup({ items }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-4', + 'item-3', + 'item-2', + 'item-1' + ]); + + + // (5) when + // close + open + waitFor(async () => { + await header.click(); + }); + + waitFor(async () => { + await header.click(); + }); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-3', + 'item-4', + 'item-1' + ]); + }); - const newItems = [ - ...items, - { - id: 'item-4', - label: 'Item 4' - } - ]; - // when - createListGroup({ items: newItems }, rerender); + it('complex (open -> add -> change -> remove -> close -> open)', async function() { + + // given + let items = [ + { + id: 'item-1', + label: 'xyz' + }, + { + id: 'item-2', + label: 'abc' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const header = domQuery('.bio-properties-panel-group-header', container); + + const list = domQuery('.bio-properties-panel-list', container); + + // when + + // (1) open + waitFor(async () => { + await header.click(); + }); + + expect(getListOrdering(list)).to.eql([ + 'item-2', + 'item-1' + ]); + + // (2) add + items = [ + ...items, + { + id: 'item-3', + label: 'foo' + } + ]; + + createListGroup({ items }, rerender); + + expect(getListOrdering(list)).to.eql([ + 'item-3', + 'item-2', + 'item-1' + ]); + + // (3) change + items[0].label = 'aaa'; + + createListGroup({ items }, rerender); + + expect(getListOrdering(list)).to.eql([ + 'item-3', + 'item-2', + 'item-1' + ]); + + // (4) remove + items.splice(1, 1); + + createListGroup({ items }, rerender); + + expect(getListOrdering(list)).to.eql([ + 'item-3', + 'item-1' + ]); + + // (5) close + open + waitFor(async () => { + await header.click(); + }); + + waitFor(async () => { + await header.click(); + }); + + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-3' + ]); + }); - // then - expect(getListOrdering(list)).to.eql([ - 'item-4', - 'item-1', - 'item-2', - 'item-3' - ]); }); - it('should NOT add new items on top - element changed', function() { - - // given - const items = [ - { - id: 'item-1', - label: 'Item 1' - }, - { - id: 'item-2', - label: 'Item 2' - }, - { - id: 'item-3', - label: 'Item 3' - } - ]; - - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); - - const list = domQuery('.bio-properties-panel-list', container); - - const newItems = [ - ...items, - { - id: 'item-4', - label: 'Item 4' - } - ]; + describe('insert top vs bottom', function() { + + it('should insert new items to top given sorting enabled', function() { + + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + }, + { + id: 'item-2', + label: 'Item 2' + }, + { + id: 'item-3', + label: 'Item 3' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); + + const list = domQuery('.bio-properties-panel-list', container); + + const newItems = [ + ...items, + { + id: 'item-4', + label: 'Item 4' + } + ]; + + // when + createListGroup({ items: newItems }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-4', + 'item-1', + 'item-2', + 'item-3' + ]); + }); - const newElement = { - ...noopElement, - id: 'bar' - }; - // when - createListGroup({ element: newElement, items: newItems }, rerender); + it('should insert new items to bottom given sorting disabled', function() { + + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + }, + { + id: 'item-2', + label: 'Item 2' + }, + { + id: 'item-3', + label: 'Item 3' + } + ]; + + const { + container, + rerender + } = createListGroup({ container: parentContainer, items, shouldSort: false }); + + const list = domQuery('.bio-properties-panel-list', container); + + const newItems = [ + ...items, + { + id: 'item-4', + label: 'Item 4' + } + ]; + + // when + createListGroup({ items: newItems, shouldSort: false }, rerender); + + // then + expect(getListOrdering(list)).to.eql([ + 'item-1', + 'item-2', + 'item-3', + 'item-4' + ]); + }); - // then - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-2', - 'item-3', - 'item-4' - ]); }); - it('should sort items - closed + added new one', function() { + describe('open', function() { - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'ab' - } - ]; + it('should open on adding new item per default', function() { - const { - container, - rerender - } = createListGroup({ container: parentContainer, label: 'List', items }); + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + } + ]; - const newItems = [ - ...items, - { - id: 'item-3', - label: 'foo' - } - ]; + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); - const list = domQuery('.bio-properties-panel-list', container); + const list = domQuery('.bio-properties-panel-list', container); - // assume - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-2' - ]); + // assume + expect(domClasses(list).has('open')).to.be.false; - // when - createListGroup({ items: newItems }, rerender); + const newItems = [ + ...items, + { + id: 'item-2', + label: 'Item 2' + } + ]; - // then - expect(getListOrdering(list)).to.eql([ - 'item-3', - 'item-2', - 'item-1' - ]); - }); + // when + createListGroup({ items: newItems }, rerender); + // then + const newItem = domQuery('[data-entry-id="item-2"]', container); + const oldItem = domQuery('[data-entry-id="item-1"]', container); - it('should keep ordering when items count did not change', async function() { - - // given - const items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'abc' - }, - { - id: 'item-3', - label: 'foo' - } - ]; - - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); - - const header = domQuery('.bio-properties-panel-group-header', container); - - const list = domQuery('.bio-properties-panel-list', container); - - waitFor(async () => { - await header.click(); + expect(domClasses(newItem).has('open')).to.be.true; + expect(domClasses(oldItem).has('open')).to.be.false; }); - // assume - expect(getListOrdering(list)).to.eql([ - 'item-2', - 'item-3', - 'item-1' - ]); - items[2].label = 'aaa'; + it('should open on adding new item per default given no sorting', function() { - // when - createListGroup({ items }, rerender); + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + } + ]; - // then - expect(getListOrdering(list)).to.eql([ - 'item-2', - 'item-3', - 'item-1' - ]); - }); - - - it('complex (open -> add -> change -> remove -> close -> open)', async function() { + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); - // given - let items = [ - { - id: 'item-1', - label: 'xyz' - }, - { - id: 'item-2', - label: 'abc' - } - ]; + const list = domQuery('.bio-properties-panel-list', container); - const { - container, - rerender - } = createListGroup({ container: parentContainer, items }); + // assume + expect(domClasses(list).has('open')).to.be.false; - const header = domQuery('.bio-properties-panel-group-header', container); + const newItems = [ + ...items, + { + id: 'item-2', + label: 'Item 2' + } + ]; - const list = domQuery('.bio-properties-panel-list', container); + // when + createListGroup({ items: newItems, shouldSort: false }, rerender); - // when + // then + const newItem = domQuery('[data-entry-id="item-2"]', container); + const oldItem = domQuery('[data-entry-id="item-1"]', container); - // (1) open - waitFor(async () => { - await header.click(); + expect(domClasses(newItem).has('open')).to.be.true; + expect(domClasses(oldItem).has('open')).to.be.false; + expect(domClasses(list).has('open')).to.be.true; }); - expect(getListOrdering(list)).to.eql([ - 'item-2', - 'item-1' - ]); - // (2) add - items = [ - ...items, - { - id: 'item-3', - label: 'foo' - } - ]; + it('should NOT open on adding new item given disabled', function() { - createListGroup({ items }, rerender); + // given + const items = [ + { + id: 'item-1', + label: 'Item 1' + } + ]; - expect(getListOrdering(list)).to.eql([ - 'item-3', - 'item-2', - 'item-1' - ]); + const { + container, + rerender + } = createListGroup({ container: parentContainer, items }); - // (3) change - items[0].label = 'aaa'; + const list = domQuery('.bio-properties-panel-list', container); - createListGroup({ items }, rerender); + // assume + expect(domClasses(list).has('open')).to.be.false; - expect(getListOrdering(list)).to.eql([ - 'item-3', - 'item-2', - 'item-1' - ]); + const newItems = [ + ...items, + { + id: 'item-2', + label: 'Item 2' + } + ]; - // (4) remove - items.splice(1, 1); + // when + createListGroup({ items: newItems, shouldOpen: false }, rerender); - createListGroup({ items }, rerender); - - expect(getListOrdering(list)).to.eql([ - 'item-3', - 'item-1' - ]); - - // (5) close + open - waitFor(async () => { - await header.click(); - }); - - waitFor(async () => { - await header.click(); + // then + expect(domClasses(list).has('open')).to.be.false; }); - expect(getListOrdering(list)).to.eql([ - 'item-1', - 'item-3' - ]); }); + }); @@ -774,6 +1051,7 @@ function createListGroup(options = {}, renderFn = render) { items = [], add, shouldSort, + shouldOpen, container } = options; @@ -784,7 +1062,8 @@ function createListGroup(options = {}, renderFn = render) { label={ label } items={ items } add={ add } - shouldSort={ shouldSort } />, + shouldSort={ shouldSort } + shouldOpen={ shouldOpen } />, { container } @@ -803,4 +1082,4 @@ function getListOrdering(list) { }); return ordering; -} \ No newline at end of file +}