From b1f92e1639819b8555fbf6e9a1a0699f0659341f Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Fri, 17 Jan 2025 13:12:38 -0300 Subject: [PATCH 01/25] internal(summary-control): add support for rendering count in details summary --- .../InternalSummaryControl.test.ts | 18 ++++++++++++++++++ .../InternalSummaryControl.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.test.ts b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.test.ts index fccdfb39..fcb92fc7 100644 --- a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.test.ts +++ b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.test.ts @@ -25,6 +25,11 @@ describe('InternalSummaryControl', () => { expect(new Control()).to.have.property('layout', null); }); + it('has a reactive property "count" that defaults to null', () => { + expect(Control).to.have.deep.nested.property('properties.count', { type: Number }); + expect(new Control()).to.have.property('count', null); + }); + it('has a reactive property "open" that defaults to false', () => { expect(Control).to.have.deep.nested.property('properties.open', { type: Boolean }); expect(new Control()).to.have.property('open', false); @@ -48,6 +53,19 @@ describe('InternalSummaryControl', () => { expect(control.renderRoot).to.include.text('Foo bar'); }); + it('renders count in label if .count is set and layout is "details"', async () => { + const control = await fixture(html` + + `); + + expect(control.renderRoot).to.include.text('Test'); + expect(control.renderRoot).to.not.include.text('Test (123)'); + + control.count = 123; + await control.requestUpdate(); + expect(control.renderRoot).to.include.text('Test (123)'); + }); + it('renders helper text in default layout', async () => { const control = await fixture(html` diff --git a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts index c2ee5c58..d3ca55c4 100644 --- a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts +++ b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts @@ -9,6 +9,7 @@ export class InternalSummaryControl extends InternalEditableControl { return { ...super.properties, layout: {}, + count: { type: Number }, open: { type: Boolean }, }; } @@ -35,6 +36,8 @@ export class InternalSummaryControl extends InternalEditableControl { layout: null | 'section' | 'details' = null; + count: number | null = null; + open = false; renderLightDom(): void { @@ -50,7 +53,7 @@ export class InternalSummaryControl extends InternalEditableControl { @toggle=${(evt: Event) => { const details = evt.currentTarget as HTMLDetailsElement; this.open = details.open; - if (!evt.composed) this.dispatchEvent(new CustomEvent('toggle')); + if (!evt.composed && !evt.bubbles) this.dispatchEvent(new CustomEvent('toggle')); }} > @@ -63,7 +66,9 @@ export class InternalSummaryControl extends InternalEditableControl { class="font-medium uppercase text-s tracking-wider flex items-center justify-between gap-s" ?hidden=${!this.label} > - ${this.label} + + ${this.label}${typeof this.count === 'number' ? ` (${this.count})` : ''} + Date: Fri, 17 Jan 2025 13:14:42 -0300 Subject: [PATCH 02/25] internal(async-list-control): add support for hiding when empty and rendering in details layout --- .../InternalAsyncListControl.test.ts | 2161 +++++++++-------- .../InternalAsyncListControl.ts | 427 ++-- .../InternalAsyncListControl/index.ts | 2 + 3 files changed, 1405 insertions(+), 1185 deletions(-) diff --git a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.test.ts b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.test.ts index 2c61677f..c9ebab68 100644 --- a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.test.ts +++ b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.test.ts @@ -1,3 +1,4 @@ +import type { InternalSummaryControl } from '../InternalSummaryControl/InternalSummaryControl'; import type { NotificationElement } from '@vaadin/vaadin-notification'; import type { CheckboxElement } from '@vaadin/vaadin-checkbox'; import type { AttributeCard } from '../../public/AttributeCard/AttributeCard'; @@ -20,6 +21,7 @@ import { SwipeActions } from '../../public/SwipeActions/SwipeActions'; import { getTestData } from '../../../testgen/getTestData'; import { FormDialog } from '../../public/FormDialog/FormDialog'; import { Pagination } from '../../public/Pagination/Pagination'; +import { ifDefined } from 'lit-html/directives/if-defined'; import { getByTag } from '../../../testgen/getByTag'; import { getByKey } from '../../../testgen/getByKey'; import { spread } from '@open-wc/lit-helpers'; @@ -30,6 +32,15 @@ import { getByTestClass } from '../../../testgen/getByTestClass'; import { createRouter } from '../../../server/hapi'; import { Type } from '../../public/QueryBuilder/types'; +customElements.define( + 'x-test-control-with-error-message', + class extends Control { + protected get _errorMessage() { + return 'Test error message'; + } + } +); + describe('InternalAsyncListControl', () => { const OriginalResizeObserver = window.ResizeObserver; @@ -150,6 +161,11 @@ describe('InternalAsyncListControl', () => { expect(Control).to.have.deep.nested.property('properties.related', { type: Array }); }); + it('has a reactive property "layout"', () => { + expect(new Control()).to.have.property('layout', null); + expect(Control).to.have.deep.nested.property('properties.layout', {}); + }); + it('has a reactive property "first"', () => { expect(new Control()).to.have.property('first', null); expect(Control).to.have.deep.nested.property('properties.first', {}); @@ -191,6 +207,19 @@ describe('InternalAsyncListControl', () => { expect(Control).to.have.deep.nested.property('properties.wide', { type: Boolean }); }); + it('has a reactive property "open"', () => { + expect(new Control()).to.have.property('open', false); + expect(Control).to.have.deep.nested.property('properties.open', { type: Boolean }); + }); + + it('has a reactive property "hideWhenEmpty"', () => { + expect(new Control()).to.have.property('hideWhenEmpty', false); + expect(Control).to.have.deep.nested.property('properties.hideWhenEmpty', { + type: Boolean, + attribute: 'hide-when-empty', + }); + }); + it('has a reactive property "alert"', () => { expect(new Control()).to.have.property('alert', false); expect(Control).to.have.deep.nested.property('properties.alert', { type: Boolean }); @@ -275,7 +304,7 @@ describe('InternalAsyncListControl', () => { expect(dialog).to.have.attribute('infer', ''); }); - it("renders a translated label if it's defined", async () => { + it("renders a translated label in default layout if it's defined", async () => { const control = await fixture(html` `); @@ -288,1110 +317,1212 @@ describe('InternalAsyncListControl', () => { expect(control.renderRoot).to.include.text('Test label'); }); - it('renders a pagination element for collection items', async () => { + it('renders helper text in default layout', async () => { const control = await fixture(html` `); - const pagination = await getByTag(control, 'foxy-pagination'); - - expect(pagination).to.exist; - expect(pagination).to.not.have.attribute('first'); - expect(pagination).to.have.attribute('infer', 'pagination'); - - control.first = 'https://demo.api/virtual/stall'; - control.limit = 5; - await control.requestUpdate(); - - expect(pagination).to.have.attribute('first', 'https://demo.api/virtual/stall?limit=5'); - }); - - it('renders a collection page under pagination', async () => { - const control = await fixture(html` - - `); - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination?.querySelector('foxy-collection-page'); - - expect(page).to.exist; - expect(page).to.have.attribute('infer', 'card'); - expect(page).to.have.deep.property('related', []); - expect(page).to.have.deep.property('props', {}); - - control.related = ['https://demo.api/virtual/stall']; - await control.requestUpdate(); - expect(page).to.have.deep.property('related', ['https://demo.api/virtual/stall']); + expect(control.renderRoot).to.include.text('helper_text'); - control.itemProps = { foo: 'bar' }; + control.helperText = 'Test helper text'; await control.requestUpdate(); - expect(page).to.have.deep.property('props', { foo: 'bar' }); - }); - - it('can render collection page items as simple list items using local name', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: null, - next: null, - ns: '', - }) - ); - - expect(item).to.have.property('localName', 'foxy-attribute-card'); - expect(item).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); - expect(item).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); - expect(item).to.have.attribute('infer', ''); - expect(item).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); - }); - - it('can render collection page items as simple list items using render function', async () => { - const control = await fixture(html` - - `); - - control.item = () => html`
`; - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: null, - next: null, - ns: '', - }) - ); - - expect(item).to.include.html('
'); - }); - - it('can render collection page items as links using local name', async () => { - const control = await fixture(html` - - `); - - control.getPageHref = href => `https://demo.api/virtual/stall?href=${encodeURIComponent(href)}`; - control.item = 'foxy-attribute-card'; - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const card = item.querySelector('foxy-attribute-card')!; - const a = card.closest('a')!; - - expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); - expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); - expect(card).to.have.attribute('infer', ''); - expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); - - expect(a).to.have.attribute( - 'href', - `https://demo.api/virtual/stall?href=${encodeURIComponent( - 'https://demo.api/virtual/stall?href' - )}` - ); - }); - - it('can render collection page items as links using render function', async () => { - const control = await fixture(html` - - `); - - control.getPageHref = href => `https://demo.api/virtual/stall?href=${encodeURIComponent(href)}`; - control.item = () => html`
`; - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const a = item.querySelector('a')!; - - expect(a).to.include.html('
'); - expect(a).to.have.attribute( - 'href', - `https://demo.api/virtual/stall?href=${encodeURIComponent( - 'https://demo.api/virtual/stall?href' - )}` - ); - }); - - it('can render collection page items as buttons opening a dialog using local name', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const card = item.querySelector('foxy-attribute-card')!; - const button = card.closest('button')!; - const showMethod = stub(dialog, 'show'); - - expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); - expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); - expect(card).to.have.attribute('infer', ''); - expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); - - button.click(); - - expect(showMethod).to.have.been.called; - expect(dialog).to.have.property('header', 'header_update'); - expect(dialog).to.have.property('href', 'https://demo.api/virtual/stall?href'); - }); - - it('can render collection page items as buttons opening a dialog using render function', async () => { - const control = await fixture(html` - - `); - - control.item = () => html`
`; - control.form = 'foxy-attribute-form'; - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const button = item.querySelector('button')!; - expect(button).to.include.html('
'); - - const showMethod = stub(dialog, 'show'); - button.click(); - - expect(showMethod).to.have.been.called; - expect(dialog).to.have.property('header', 'header_update'); - expect(dialog).to.have.property('href', 'https://demo.api/virtual/stall?href'); - }); - - it('renders Delete button for collection page items', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const swipeActions = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const card = swipeActions.querySelector('foxy-attribute-card') as AttributeCard; - const label = swipeActions.querySelector('[key="delete_button_text"]')!; - const button = label.closest('vaadin-button')!; - const confirm = (await getByTag( - control, - 'foxy-internal-confirm-dialog' - )) as InternalConfirmDialog; - - expect(swipeActions).to.have.property('localName', 'foxy-swipe-actions'); - expect(label).to.have.property('localName', 'foxy-i18n'); - expect(label).to.have.attribute('infer', ''); - expect(button).to.have.attribute('slot', 'action'); - - const confirmShowMethod = stub(confirm, 'show'); - button.click(); - - expect(confirmShowMethod).to.have.been.called; - - const cardDeleteMethod = stub(card, 'delete'); - confirm.dispatchEvent(new DialogHideEvent()); - - expect(cardDeleteMethod).to.have.been.called; - }); - - it('hides delete button for collection page items if hideDeleteButton is true', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - control.hideDeleteButton = true; - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const swipeActions = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const selector = 'vaadin-button foxy-i18n[key="delete_button_text"]'; - expect(swipeActions.querySelector(selector)).to.not.exist; - }); - - it('hides delete button for collection page items when readonly', async () => { - const control = await fixture(html` - - `); - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - control.readonly = true; - - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - - const swipeActions = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const selector = 'vaadin-button foxy-i18n[key="delete_button_text"]'; - expect(swipeActions.querySelector(selector)).to.not.exist; - }); - - it('renders Create button when .form is defined', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - - const label = (await getByKey(control, 'create_button_text'))!; - const button = label.closest('vaadin-button')!; - const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - - expect(label).to.have.property('localName', 'foxy-i18n'); - expect(label).to.have.attribute('infer', 'pagination'); - - const dialogShowMethod = stub(dialog, 'show'); - button.click(); - - expect(dialogShowMethod).to.have.been.called; - expect(dialog).to.have.property('header', 'header_create'); - expect(dialog).to.have.property('href', ''); + expect(control.renderRoot).to.not.include.text('helper_text'); + expect(control.renderRoot).to.include.text('Test helper text'); }); - it('renders Create link when .createPageHref is defined', async () => { + it('renders a summary control in details layout', async () => { + const router = createRouter(); const control = await fixture(html` - + router.handleEvent(evt)} + > + `); - control.item = 'foxy-attribute-card'; - control.createPageHref = 'https://example.com'; - - const label = (await getByKey(control, 'create_button_text'))!; - const a = label.closest('a')!; - - expect(label).to.have.property('localName', 'foxy-i18n'); - expect(label).to.have.attribute('infer', ''); - expect(a).to.have.attribute('href', 'https://example.com'); - }); + const summary = control.renderRoot.querySelector( + 'foxy-internal-summary-control' + ) as InternalSummaryControl; - it("hides Create link/button if there's no form", async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; + const collectionPage = summary.querySelector('foxy-collection-page') as CollectionPage; + await waitUntil(() => !!collectionPage.data, undefined, { timeout: 5000 }); await control.requestUpdate(); - const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; - const aSelector = 'a foxy-i18n[key="create_button_text"]'; - - expect(control.querySelector(buttonSelector)).to.not.exist; - expect(control.querySelector(aSelector)).to.not.exist; - }); - - it('hides Create link/button if control is readonly', async () => { - const control = await fixture(html` - - `); + expect(summary).to.exist; + expect(summary).to.have.attribute('layout', 'details'); + expect(summary).to.have.attribute('count', '2'); + expect(summary).to.not.have.attribute('open'); - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - control.readonly = true; + control.open = true; await control.requestUpdate(); + expect(summary).to.have.attribute('open'); + expect(summary).to.not.have.attribute('count'); - const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; - const aSelector = 'a foxy-i18n[key="create_button_text"]'; - - expect(control.querySelector(buttonSelector)).to.not.exist; - expect(control.querySelector(aSelector)).to.not.exist; - }); - - it('hides Create link/button if hideCreateButton is true', async () => { - const control = await fixture(html` - - `); - - control.item = 'foxy-attribute-card'; - control.form = 'foxy-attribute-form'; - control.hideCreateButton = true; + summary.open = false; + summary.dispatchEvent(new CustomEvent('toggle')); await control.requestUpdate(); - - const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; - const aSelector = 'a foxy-i18n[key="create_button_text"]'; - - expect(control.querySelector(buttonSelector)).to.not.exist; - expect(control.querySelector(aSelector)).to.not.exist; + expect(control).to.have.property('open', false); + expect(summary).to.have.attribute('count', '2'); }); - it('renders helper text', async () => { - const control = await fixture(html` - - `); - - expect(control.renderRoot).to.include.text('helper_text'); - - control.helperText = 'Test helper text'; - await control.requestUpdate(); - - expect(control.renderRoot).to.not.include.text('helper_text'); - expect(control.renderRoot).to.include.text('Test helper text'); - }); - - it('render error text if available', async () => { - let control = await fixture(html` - - `); - - expect(control.renderRoot).to.not.include.text('Test error message'); - - customElements.define( - 'x-test-control', - class extends Control { - protected get _errorMessage() { - return 'Test error message'; - } - } - ); - - control = await fixture(html``); - expect(control.renderRoot).to.include.text('Test error message'); - }); - - it('renders actions if configured', async () => { + it('visually hides itself when collection has 0 items and hideWhenEmpty is true', async () => { const router = createRouter(); const control = await fixture(html` router.handleEvent(evt)} > `); - await waitUntil( - () => !!control.renderRoot.querySelector>('foxy-collection-page')?.data - ); + const collectionPage = control.renderRoot.querySelector( + 'foxy-collection-page' + ) as CollectionPage; - let actions = await getByTestClass(control, 'action'); - expect(actions).to.be.empty; + await waitUntil(() => !!collectionPage.data, undefined, { timeout: 5000 }); + await control.requestUpdate(); + const wrapper = control.shadowRoot?.firstElementChild; + expect(wrapper).to.not.have.attribute('class', 'hidden'); - control.actions = [ - { text: 'action_1', theme: 'primary', state: 'idle', onClick: stub() }, - { text: 'action_2', theme: 'secondary', state: 'busy', onClick: stub() }, - ]; + control.hideWhenEmpty = true; + await control.requestUpdate(); + expect(wrapper).to.have.attribute('class', 'hidden'); + control.first = 'https://demo.api/hapi/transactions'; + await control.requestUpdate(); + await waitUntil(() => !!collectionPage.data, undefined, { timeout: 5000 }); await control.requestUpdate(); - actions = await getByTestClass(control, 'action'); + expect(wrapper).to.not.have.attribute('class', 'hidden'); + }); - expect(actions).to.have.length(2); + [undefined, 'details'].forEach(layout => { + const layoutLabel = layout ?? 'default'; - expect(actions[0].closest('foxy-swipe-actions')).to.exist; - expect(actions[1].closest('foxy-swipe-actions')).to.exist; - expect(actions[0].closest('foxy-swipe-actions')).to.equal( - actions[1].closest('foxy-swipe-actions') - ); + it(`renders a pagination element for collection items in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - expect(actions[0]).to.have.attribute('theme', 'primary'); - expect(actions[1]).to.have.attribute('theme', 'secondary'); + const pagination = await getByTag(control, 'foxy-pagination'); - actions[0].click(); - expect(control.actions[0].onClick).to.have.been.calledOnce; - expect(control.actions[1].onClick).to.not.have.been.called; + expect(pagination).to.exist; + expect(pagination).to.not.have.attribute('first'); + expect(pagination).to.have.attribute('infer', 'pagination'); - actions[1].click(); - expect(control.actions[0].onClick).to.have.been.calledOnce; - expect(control.actions[1].onClick).to.have.been.calledOnce; + control.first = 'https://demo.api/virtual/stall'; + control.limit = 5; + await control.requestUpdate(); - const action0Text = actions[0].querySelector('foxy-i18n'); - expect(action0Text).to.have.attribute('infer', ''); - expect(action0Text).to.have.attribute('key', 'action_1'); + expect(pagination).to.have.attribute('first', 'https://demo.api/virtual/stall?limit=5'); + }); - const action1Text = actions[1].querySelector('foxy-i18n'); - expect(action1Text).to.have.attribute('infer', ''); - expect(action1Text).to.have.attribute('key', 'action_2'); + it(`renders a collection page under pagination in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - const action0Spinner = actions[0].querySelector('foxy-spinner'); - expect(action0Spinner).to.have.attribute('infer', 'spinner'); - expect(action0Spinner).to.have.attribute('state', 'idle'); + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination?.querySelector('foxy-collection-page'); - const action1Spinner = actions[1].querySelector('foxy-spinner'); - expect(action1Spinner).to.have.attribute('infer', 'spinner'); - expect(action1Spinner).to.have.attribute('state', 'busy'); - }); + expect(page).to.exist; + expect(page).to.have.attribute('infer', 'card'); + expect(page).to.have.deep.property('related', []); + expect(page).to.have.deep.property('props', {}); - it('renders filters if configured', async () => { - const control = await fixture(html` - - `); + control.related = ['https://demo.api/virtual/stall']; + await control.requestUpdate(); + expect(page).to.have.deep.property('related', ['https://demo.api/virtual/stall']); - expect(await getByKey(control, 'search_button_text')).to.not.exist; - - control.filters = [ - { - label: 'filter_1', - type: Type.Boolean, - path: 'foo', - }, - ]; - - await control.requestUpdate(); - - const buttonLabel = await getByKey(control, 'search_button_text'); - const button = buttonLabel?.closest('vaadin-button'); - const overlay = control.renderRoot.querySelector( - 'foxy-internal-async-list-control-filter-overlay' - ); - - expect(buttonLabel).to.exist; - expect(buttonLabel).to.have.attribute('infer', 'pagination'); - - expect(button).to.exist; - expect(button).to.not.have.attribute('disabled'); - - expect(overlay).to.exist; - expect(overlay).to.have.property('positionTarget', button); - expect(overlay).to.have.deep.property('model', { - options: control.filters, - value: '', - lang: control.lang, - ns: control.ns, + control.itemProps = { foo: 'bar' }; + await control.requestUpdate(); + expect(page).to.have.deep.property('props', { foo: 'bar' }); }); - expect(overlay).to.not.have.attribute('opened'); - - button?.click(); - await control.requestUpdate(); - expect(overlay).to.have.attribute('opened'); - - overlay?.dispatchEvent(new CustomEvent('vaadin-overlay-close')); - await control.requestUpdate(); - expect(overlay).to.not.have.attribute('opened'); - - overlay?.dispatchEvent(new CustomEvent('search', { detail: 'foo=bar' })); - await control.requestUpdate(); - expect(overlay).to.have.deep.property('model', { - options: control.filters, - value: 'foo=bar', - lang: control.lang, - ns: control.ns, + it(`can render collection page items as simple list items using local name in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = 'foxy-attribute-card'; + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const wrapper = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: null, + next: null, + ns: '', + }) + ); + + const item = wrapper.firstElementChild; + + expect(item).to.have.property('localName', 'foxy-attribute-card'); + expect(item).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); + expect(item).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); + expect(item).to.have.attribute('infer', ''); + expect(item).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); }); - }); - it('emits "itemclick" event when collection page item is clicked', async () => { - const control = await fixture(html` - - - `); + it(`can render collection page items as simple list items using render function in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = () => html`
`; + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: null, + next: null, + ns: '', + }) + ); + + expect(item).to.include.html('
'); + }); - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const card = item.querySelector('foxy-attribute-card')!; - const button = card.closest('button')!; - const showMethod = stub(dialog, 'show'); - - const whenGotItemClickEvent = oneEvent(control, 'itemclick'); - button.click(); - const itemClickEvent = await whenGotItemClickEvent; - - expect(itemClickEvent).to.be.instanceOf(CustomEvent); - expect(itemClickEvent).to.have.property('cancelable', true); - expect(itemClickEvent).to.have.property('composed', true); - expect(itemClickEvent).to.have.property('bubbles', true); - expect(itemClickEvent).to.have.property('detail', 'https://demo.api/virtual/stall?href'); - expect(showMethod).to.have.been.called; - }); + it(`can render collection page items as links using local name in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.getPageHref = href => + `https://demo.api/virtual/stall?href=${encodeURIComponent(href)}`; + control.item = 'foxy-attribute-card'; + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const card = item.querySelector('foxy-attribute-card')!; + const a = card.closest('a')!; + + expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); + expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); + expect(card).to.have.attribute('infer', ''); + expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); + + expect(a).to.have.attribute( + 'href', + `https://demo.api/virtual/stall?href=${encodeURIComponent( + 'https://demo.api/virtual/stall?href' + )}` + ); + }); - it('does not show dialog when "itemclick" event is canceled', async () => { - const control = await fixture(html` - - - `); + it(`can render collection page items as links using render function in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.getPageHref = href => + `https://demo.api/virtual/stall?href=${encodeURIComponent(href)}`; + control.item = () => html`
`; + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const a = item.querySelector('a')!; + + expect(a).to.include.html('
'); + expect(a).to.have.attribute( + 'href', + `https://demo.api/virtual/stall?href=${encodeURIComponent( + 'https://demo.api/virtual/stall?href' + )}` + ); + }); - const pagination = await getByTag(control, 'foxy-pagination'); - const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; - const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - const card = item.querySelector('foxy-attribute-card')!; - const button = card.closest('button')!; - const showMethod = stub(dialog, 'show'); - - control.addEventListener('itemclick', evt => evt.preventDefault()); - button.click(); - - expect(showMethod).to.not.have.been.called; - }); + it(`can render collection page items as buttons opening a dialog using local name in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const card = item.querySelector('foxy-attribute-card')!; + const button = card.closest('button')!; + const showMethod = stub(dialog, 'show'); + + expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); + expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); + expect(card).to.have.attribute('infer', ''); + expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); + + button.click(); + + expect(showMethod).to.have.been.called; + expect(dialog).to.have.property('header', 'header_update'); + expect(dialog).to.have.property('href', 'https://demo.api/virtual/stall?href'); + }); - it('renders Select button when "first" and non-empty .bulkActions are defined', async () => { - const control = await fixture(html` - - - `); + it(`can render collection page items as buttons opening a dialog using render function in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + `); + + control.item = () => html`
`; + control.form = 'foxy-attribute-form'; + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const button = item.querySelector('button')!; + expect(button).to.include.html('
'); + + const showMethod = stub(dialog, 'show'); + button.click(); + + expect(showMethod).to.have.been.called; + expect(dialog).to.have.property('header', 'header_update'); + expect(dialog).to.have.property('href', 'https://demo.api/virtual/stall?href'); + }); - expect(await getByKey(control, 'select_button_text')).to.not.exist; + it(`renders Delete button for collection page items in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const swipeActions = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const card = swipeActions.querySelector('foxy-attribute-card') as AttributeCard; + const label = swipeActions.querySelector('[key="delete_button_text"]')!; + const button = label.closest('vaadin-button')!; + const confirm = (await getByTag( + control, + 'foxy-internal-confirm-dialog' + )) as InternalConfirmDialog; + + expect(swipeActions).to.have.property('localName', 'foxy-swipe-actions'); + expect(label).to.have.property('localName', 'foxy-i18n'); + expect(label).to.have.attribute('infer', ''); + expect(button).to.have.attribute('slot', 'action'); + + const confirmShowMethod = stub(confirm, 'show'); + button.click(); + + expect(confirmShowMethod).to.have.been.called; + + const cardDeleteMethod = stub(card, 'delete'); + confirm.dispatchEvent(new DialogHideEvent()); + + expect(cardDeleteMethod).to.have.been.called; + }); - control.first = 'https://demo.api/hapi/customer_attributes'; - await control.requestUpdate(); - expect(await getByKey(control, 'select_button_text')).to.not.exist; + it(`hides Delete button for collection page items if hideDeleteButton is true in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + control.hideDeleteButton = true; + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const swipeActions = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const selector = 'vaadin-button foxy-i18n[key="delete_button_text"]'; + expect(swipeActions.querySelector(selector)).to.not.exist; + }); - control.bulkActions = []; - await control.requestUpdate(); - expect(await getByKey(control, 'select_button_text')).to.not.exist; + it(`hides Delete button for collection page items when readonly in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + control.readonly = true; + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + + const swipeActions = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const selector = 'vaadin-button foxy-i18n[key="delete_button_text"]'; + expect(swipeActions.querySelector(selector)).to.not.exist; + }); - control.bulkActions = [{ name: 'foo', onClick: stub() }]; - await control.requestUpdate(); + it(`renders Create button when .form is defined in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; - expect(label).to.exist; - expect(button).to.exist; - expect(label).to.have.property('localName', 'foxy-i18n'); - expect(label).to.have.attribute('infer', 'pagination'); - }); + const label = (await getByKey(control, 'create_button_text'))!; + const button = label.closest('vaadin-button')!; + const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; - it('renders Cancel button when selection mode is enabled', async () => { - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + expect(label).to.have.property('localName', 'foxy-i18n'); + expect(label).to.have.attribute('infer', 'pagination'); - const page = await getByTag>(control, 'foxy-collection-page'); - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + const dialogShowMethod = stub(dialog, 'show'); + button.click(); - expect(await getByKey(control, 'select_button_text')).to.exist; - expect(await getByKey(control, 'cancel_button_text')).to.not.exist; + expect(dialogShowMethod).to.have.been.called; + expect(dialog).to.have.property('header', 'header_create'); + expect(dialog).to.have.property('href', ''); + }); - (await getByKey(control, 'select_button_text'))?.closest('vaadin-button')?.click(); - await control.requestUpdate(); + it(`renders Create link when .createPageHref is defined in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - expect(await getByKey(control, 'select_button_text')).to.not.exist; - expect(await getByKey(control, 'cancel_button_text')).to.exist; - }); + control.item = 'foxy-attribute-card'; + control.createPageHref = 'https://example.com'; - it('renders bulk actions when selection mode is enabled and at least one item is selected', async () => { - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + const label = (await getByKey(control, 'create_button_text'))!; + const a = label.closest('a')!; - const page = await getByTag>(control, 'foxy-collection-page'); - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); - expect(await getByTestClass(control, 'bulk-action')).to.be.empty; - - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); - button?.click(); - await control.requestUpdate(); - const anyCheckbox = await getByTag(control, 'vaadin-checkbox'); - expect(anyCheckbox).to.exist; - expect(await getByTestClass(control, 'bulk-action')).to.have.length(0); - - anyCheckbox!.checked = true; - anyCheckbox!.dispatchEvent(new CustomEvent('change')); - await control.requestUpdate(); + expect(label).to.have.property('localName', 'foxy-i18n'); + expect(label).to.have.attribute('infer', ''); + expect(a).to.have.attribute('href', 'https://example.com'); + }); - const bulkActions = await getByTestClass(control, 'bulk-action'); - expect(bulkActions).to.have.length(1); + it(`hides Create link/button if there's no form in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - const firstBulkAction = bulkActions[0]; - const firstBulkActionLabel = firstBulkAction.querySelector('foxy-i18n'); - expect(firstBulkActionLabel).to.have.attribute('key', 'foo_bulk_action_caption_idle'); - expect(firstBulkActionLabel).to.have.attribute('infer', 'pagination'); - expect(firstBulkActionLabel).to.have.deep.property('options', { count: 1 }); - }); + control.item = 'foxy-attribute-card'; + await control.requestUpdate(); - it('renders collection items inside vaadin-checkbox elements when selection mode is enabled', async () => { - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; + const aSelector = 'a foxy-i18n[key="create_button_text"]'; - const page = (await getByTag>(control, 'foxy-collection-page'))!; - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); - expect(await getByTestClass(control, 'vaadin-checkbox')).to.be.empty; + expect(control.querySelector(buttonSelector)).to.not.exist; + expect(control.querySelector(aSelector)).to.not.exist; + }); - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); - button?.click(); - await control.requestUpdate(); + it(`hides Create link/button if control is readonly in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - const item = await fixture( - (page.item as ItemRenderer)({ - simplifyNsLoading: false, - readonlyControls: BooleanSelector.False, - disabledControls: BooleanSelector.False, - hiddenControls: BooleanSelector.False, - templates: {}, - readonly: false, - disabled: false, - previous: null, - related: ['https://demo.api/virtual/stall?related'], - hidden: false, - parent: 'https://demo.api/virtual/stall?parent', - spread: spread, - props: {}, - group: '', - html: html, - lang: '', - href: 'https://demo.api/virtual/stall?href', - data: await getTestData('./hapi/customer_attributes/0'), - next: null, - ns: '', - }) - ); - - expect(item).to.have.property('localName', 'vaadin-checkbox'); - expect(item).to.not.have.attribute('checked'); - - const card = item.querySelector('foxy-attribute-card')!; - - expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); - expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); - expect(card).to.have.attribute('infer', ''); - expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); - }); + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + control.readonly = true; + await control.requestUpdate(); - it('can run a bulk action on selected items', async () => { - const onClick = stub(); - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; + const aSelector = 'a foxy-i18n[key="create_button_text"]'; - const page = (await getByTag>(control, 'foxy-collection-page'))!; - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + expect(control.querySelector(buttonSelector)).to.not.exist; + expect(control.querySelector(aSelector)).to.not.exist; + }); - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); - button?.click(); - await control.requestUpdate(); + it(`hides Create link/button if hideCreateButton is true in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); - const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); - checkbox!.checked = true; - checkbox!.dispatchEvent(new CustomEvent('change')); - await control.requestUpdate(); + control.item = 'foxy-attribute-card'; + control.form = 'foxy-attribute-form'; + control.hideCreateButton = true; + await control.requestUpdate(); - const bulkActions = await getByTestClass(control, 'bulk-action'); - const fooAction = bulkActions[0]; - fooAction.click(); + const buttonSelector = 'vaadin-button foxy-i18n[key="create_button_text"]'; + const aSelector = 'a foxy-i18n[key="create_button_text"]'; - const customerAttribute0 = await getTestData('./hapi/customer_attributes/0'); - expect(onClick).to.have.been.calledWithMatch([customerAttribute0]); - }); + expect(control.querySelector(buttonSelector)).to.not.exist; + expect(control.querySelector(aSelector)).to.not.exist; + }); - it('communicates bulk action state to the user (positive path, no errors)', async () => { - let next: (() => void) | null = null; - const onClick = () => new Promise(resolve => (next = resolve)); + it(`renders error text if available in ${layoutLabel} layout`, async () => { + let control = await fixture(html` + + + `); - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + expect(control.renderRoot).to.not.include.text('Test error message'); - const page = (await getByTag>(control, 'foxy-collection-page'))!; - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + control = await fixture( + html`` + ); + expect(control.renderRoot).to.include.text('Test error message'); + }); - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); - button?.click(); - await control.requestUpdate(); + it(`renders actions if configured in ${layoutLabel} layout`, async () => { + const router = createRouter(); + const control = await fixture(html` + router.handleEvent(evt)} + > + + `); + + await waitUntil( + () => !!control.renderRoot.querySelector>('foxy-collection-page')?.data + ); + + let actions = await getByTestClass(control, 'action'); + expect(actions).to.be.empty; + + control.actions = [ + { text: 'action_1', theme: 'primary', state: 'idle', onClick: stub() }, + { text: 'action_2', theme: 'secondary', state: 'busy', onClick: stub() }, + ]; + + await control.requestUpdate(); + actions = await getByTestClass(control, 'action'); + + expect(actions).to.have.length(2); + + expect(actions[0].closest('foxy-swipe-actions')).to.exist; + expect(actions[1].closest('foxy-swipe-actions')).to.exist; + expect(actions[0].closest('foxy-swipe-actions')).to.equal( + actions[1].closest('foxy-swipe-actions') + ); + + expect(actions[0]).to.have.attribute('theme', 'primary'); + expect(actions[1]).to.have.attribute('theme', 'secondary'); + + actions[0].click(); + expect(control.actions[0].onClick).to.have.been.calledOnce; + expect(control.actions[1].onClick).to.not.have.been.called; + + actions[1].click(); + expect(control.actions[0].onClick).to.have.been.calledOnce; + expect(control.actions[1].onClick).to.have.been.calledOnce; + + const action0Text = actions[0].querySelector('foxy-i18n'); + expect(action0Text).to.have.attribute('infer', ''); + expect(action0Text).to.have.attribute('key', 'action_1'); + + const action1Text = actions[1].querySelector('foxy-i18n'); + expect(action1Text).to.have.attribute('infer', ''); + expect(action1Text).to.have.attribute('key', 'action_2'); + + const action0Spinner = actions[0].querySelector('foxy-spinner'); + expect(action0Spinner).to.have.attribute('infer', 'spinner'); + expect(action0Spinner).to.have.attribute('state', 'idle'); + + const action1Spinner = actions[1].querySelector('foxy-spinner'); + expect(action1Spinner).to.have.attribute('infer', 'spinner'); + expect(action1Spinner).to.have.attribute('state', 'busy'); + }); - const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); - checkbox!.checked = true; - checkbox!.dispatchEvent(new CustomEvent('change')); - await control.requestUpdate(); + it(`renders filters if configured in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + expect(await getByKey(control, 'search_button_text')).to.not.exist; + + control.filters = [ + { + label: 'filter_1', + type: Type.Boolean, + path: 'foo', + }, + ]; + + await control.requestUpdate(); + + const buttonLabel = await getByKey(control, 'search_button_text'); + const button = buttonLabel?.closest('vaadin-button'); + const overlay = control.renderRoot.querySelector( + 'foxy-internal-async-list-control-filter-overlay' + ); + + expect(buttonLabel).to.exist; + expect(buttonLabel).to.have.attribute('infer', 'pagination'); + + expect(button).to.exist; + expect(button).to.not.have.attribute('disabled'); + + expect(overlay).to.exist; + expect(overlay).to.have.property('positionTarget', button); + expect(overlay).to.have.deep.property('model', { + options: control.filters, + value: '', + lang: control.lang, + ns: control.ns, + }); + + expect(overlay).to.not.have.attribute('opened'); + + button?.click(); + await control.requestUpdate(); + expect(overlay).to.have.attribute('opened'); + + overlay?.dispatchEvent(new CustomEvent('vaadin-overlay-close')); + await control.requestUpdate(); + expect(overlay).to.not.have.attribute('opened'); + + overlay?.dispatchEvent(new CustomEvent('search', { detail: 'foo=bar' })); + await control.requestUpdate(); + expect(overlay).to.have.deep.property('model', { + options: control.filters, + value: 'foo=bar', + lang: control.lang, + ns: control.ns, + }); + }); - const bulkActions = await getByTestClass(control, 'bulk-action'); - const fooAction = bulkActions[0]; - fooAction.click(); - await control.requestUpdate(); + it(`emits "itemclick" event when collection page item is clicked in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const card = item.querySelector('foxy-attribute-card')!; + const button = card.closest('button')!; + const showMethod = stub(dialog, 'show'); + + const whenGotItemClickEvent = oneEvent(control, 'itemclick'); + button.click(); + const itemClickEvent = await whenGotItemClickEvent; + + expect(itemClickEvent).to.be.instanceOf(CustomEvent); + expect(itemClickEvent).to.have.property('cancelable', true); + expect(itemClickEvent).to.have.property('composed', true); + expect(itemClickEvent).to.have.property('bubbles', true); + expect(itemClickEvent).to.have.property('detail', 'https://demo.api/virtual/stall?href'); + expect(showMethod).to.have.been.called; + }); - expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; - expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.exist; - // @ts-expect-error it's too hard for TS to figure out that `next` is not null at this point - next?.(); - await control.requestUpdate(); - expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; - expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.not.exist; - expect(await getByKey(control, 'cancel_button_text')).to.not.exist; - expect(await getByKey(control, 'select_button_text')).to.exist; - expect(await getByTestClass(control, 'bulk-action')).to.have.length(0); - - const notification = await getByTag(control, 'vaadin-notification'); - expect(notification).to.have.attribute('theme', 'success'); - expect(notification).to.have.property('opened', true); - - const notificationRoot = document.createElement('div'); - notification?.renderer?.(notificationRoot); - - const notificationText = notificationRoot.querySelector('foxy-i18n'); - expect(notificationText).to.have.attribute('lang', 'es'); - expect(notificationText).to.have.attribute('key', 'foo_bulk_action_notification_done'); - expect(notificationText).to.have.attribute('ns', 'test pagination'); - }); + it(`does not show dialog when "itemclick" event is canceled in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + const pagination = await getByTag(control, 'foxy-pagination'); + const page = pagination!.querySelector('foxy-collection-page') as CollectionPage; + const dialog = (await getByTag(control, 'foxy-form-dialog')) as FormDialog; + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + const card = item.querySelector('foxy-attribute-card')!; + const button = card.closest('button')!; + const showMethod = stub(dialog, 'show'); + + control.addEventListener('itemclick', evt => evt.preventDefault()); + button.click(); + + expect(showMethod).to.not.have.been.called; + }); - it('communicates bulk action state to the user (negative path, failure)', async () => { - let next: (() => void) | null = null; - const onClick = () => new Promise((_, reject) => (next = reject)); + it(`renders Select button when "first" and non-empty .bulkActions are defined in ${layoutLabel} layout`, async () => { + const control = await fixture(html` + + + `); + + expect(await getByKey(control, 'select_button_text')).to.not.exist; + + control.first = 'https://demo.api/hapi/customer_attributes'; + await control.requestUpdate(); + expect(await getByKey(control, 'select_button_text')).to.not.exist; + + control.bulkActions = []; + await control.requestUpdate(); + expect(await getByKey(control, 'select_button_text')).to.not.exist; + + control.bulkActions = [{ name: 'foo', onClick: stub() }]; + await control.requestUpdate(); + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + + expect(label).to.exist; + expect(button).to.exist; + expect(label).to.have.property('localName', 'foxy-i18n'); + expect(label).to.have.attribute('infer', 'pagination'); + }); - const router = createRouter(); - const control = await fixture(html` - !evt.defaultPrevented && router.handleEvent(evt)} - > - - `); + it(`renders Cancel button when selection mode is enabled in ${layoutLabel} layout`, async () => { + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = await getByTag>(control, 'foxy-collection-page'); + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + + expect(await getByKey(control, 'select_button_text')).to.exist; + expect(await getByKey(control, 'cancel_button_text')).to.not.exist; + + (await getByKey(control, 'select_button_text'))?.closest('vaadin-button')?.click(); + await control.requestUpdate(); + + expect(await getByKey(control, 'select_button_text')).to.not.exist; + expect(await getByKey(control, 'cancel_button_text')).to.exist; + }); - const page = (await getByTag>(control, 'foxy-collection-page'))!; - await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + it(`renders bulk actions when selection mode is enabled and at least one item is selected in ${layoutLabel} layout`, async () => { + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = await getByTag>(control, 'foxy-collection-page'); + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + expect(await getByTestClass(control, 'bulk-action')).to.be.empty; + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + button?.click(); + await control.requestUpdate(); + const anyCheckbox = await getByTag(control, 'vaadin-checkbox'); + expect(anyCheckbox).to.exist; + expect(await getByTestClass(control, 'bulk-action')).to.have.length(0); + + anyCheckbox!.checked = true; + anyCheckbox!.dispatchEvent(new CustomEvent('change')); + await control.requestUpdate(); + + const bulkActions = await getByTestClass(control, 'bulk-action'); + expect(bulkActions).to.have.length(1); + + const firstBulkAction = bulkActions[0]; + const firstBulkActionLabel = firstBulkAction.querySelector('foxy-i18n'); + expect(firstBulkActionLabel).to.have.attribute('key', 'foo_bulk_action_caption_idle'); + expect(firstBulkActionLabel).to.have.attribute('infer', 'pagination'); + expect(firstBulkActionLabel).to.have.deep.property('options', { count: 1 }); + }); - const label = await getByKey(control, 'select_button_text'); - const button = label?.closest('vaadin-button'); - button?.click(); - await control.requestUpdate(); + it(`renders collection items inside vaadin-checkbox elements when selection mode is enabled in ${layoutLabel} layout`, async () => { + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = (await getByTag>(control, 'foxy-collection-page'))!; + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + expect(await getByTestClass(control, 'vaadin-checkbox')).to.be.empty; + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + button?.click(); + await control.requestUpdate(); + + const item = await fixture( + (page.item as ItemRenderer)({ + simplifyNsLoading: false, + readonlyControls: BooleanSelector.False, + disabledControls: BooleanSelector.False, + hiddenControls: BooleanSelector.False, + templates: {}, + readonly: false, + disabled: false, + previous: null, + related: ['https://demo.api/virtual/stall?related'], + hidden: false, + parent: 'https://demo.api/virtual/stall?parent', + spread: spread, + props: {}, + group: '', + html: html, + lang: '', + href: 'https://demo.api/virtual/stall?href', + data: await getTestData('./hapi/customer_attributes/0'), + next: null, + ns: '', + }) + ); + + expect(item).to.have.property('localName', 'vaadin-checkbox'); + expect(item).to.not.have.attribute('checked'); + + const card = item.querySelector('foxy-attribute-card')!; + + expect(card).to.have.attribute('related', '["https://demo.api/virtual/stall?related"]'); + expect(card).to.have.attribute('parent', 'https://demo.api/virtual/stall?parent'); + expect(card).to.have.attribute('infer', ''); + expect(card).to.have.attribute('href', 'https://demo.api/virtual/stall?href'); + }); - const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); - checkbox!.checked = true; - checkbox!.dispatchEvent(new CustomEvent('change')); - await control.requestUpdate(); + it(`can run a bulk action on selected items in ${layoutLabel} layout`, async () => { + const onClick = stub(); + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = (await getByTag>(control, 'foxy-collection-page'))!; + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + button?.click(); + await control.requestUpdate(); + + const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); + checkbox!.checked = true; + checkbox!.dispatchEvent(new CustomEvent('change')); + await control.requestUpdate(); + + const bulkActions = await getByTestClass(control, 'bulk-action'); + const fooAction = bulkActions[0]; + fooAction.click(); + + const customerAttribute0 = await getTestData('./hapi/customer_attributes/0'); + expect(onClick).to.have.been.calledWithMatch([customerAttribute0]); + }); - const bulkActions = await getByTestClass(control, 'bulk-action'); - const fooAction = bulkActions[0]; - fooAction.click(); - await control.requestUpdate(); + it(`communicates bulk action state to the user (positive path, no errors) in ${layoutLabel} layout`, async () => { + let next: (() => void) | null = null; + const onClick = () => new Promise(resolve => (next = resolve)); + + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = (await getByTag>(control, 'foxy-collection-page'))!; + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + button?.click(); + await control.requestUpdate(); + + const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); + checkbox!.checked = true; + checkbox!.dispatchEvent(new CustomEvent('change')); + await control.requestUpdate(); + + const bulkActions = await getByTestClass(control, 'bulk-action'); + const fooAction = bulkActions[0]; + fooAction.click(); + await control.requestUpdate(); + + expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; + expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.exist; + // @ts-expect-error it's too hard for TS to figure out that `next` is not null at this point + next?.(); + await control.requestUpdate(); + expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; + expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.not.exist; + expect(await getByKey(control, 'cancel_button_text')).to.not.exist; + expect(await getByKey(control, 'select_button_text')).to.exist; + expect(await getByTestClass(control, 'bulk-action')).to.have.length(0); + + const notification = await getByTag(control, 'vaadin-notification'); + expect(notification).to.have.attribute('theme', 'success'); + expect(notification).to.have.property('opened', true); + + const notificationRoot = document.createElement('div'); + notification?.renderer?.(notificationRoot); + + const notificationText = notificationRoot.querySelector('foxy-i18n'); + expect(notificationText).to.have.attribute('lang', 'es'); + expect(notificationText).to.have.attribute('key', 'foo_bulk_action_notification_done'); + expect(notificationText).to.have.attribute('ns', 'test pagination'); + }); - expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; - expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.exist; - // @ts-expect-error it's too hard for TS to figure out that `next` is not null at this point - next?.(); - await control.requestUpdate(); - expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.exist; - expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.not.exist; - expect(await getByKey(control, 'cancel_button_text')).to.exist; - expect(await getByKey(control, 'select_button_text')).to.not.exist; - expect(await getByTestClass(control, 'bulk-action')).to.have.length(1); - - const notification = await getByTag(control, 'vaadin-notification'); - expect(notification).to.have.attribute('theme', 'error'); - expect(notification).to.have.property('opened', true); - - const notificationRoot = document.createElement('div'); - notification?.renderer?.(notificationRoot); - - const notificationText = notificationRoot.querySelector('foxy-i18n'); - expect(notificationText).to.have.attribute('lang', 'es'); - expect(notificationText).to.have.attribute('key', 'foo_bulk_action_notification_fail'); - expect(notificationText).to.have.attribute('ns', 'test pagination'); + it(`communicates bulk action state to the user (negative path, failure) in ${layoutLabel} layout`, async () => { + let next: (() => void) | null = null; + const onClick = () => new Promise((_, reject) => (next = reject)); + + const router = createRouter(); + const control = await fixture(html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + `); + + const page = (await getByTag>(control, 'foxy-collection-page'))!; + await waitUntil(() => !!page?.data, '', { timeout: 5000 }); + + const label = await getByKey(control, 'select_button_text'); + const button = label?.closest('vaadin-button'); + button?.click(); + await control.requestUpdate(); + + const checkbox = control.renderRoot.querySelector('vaadin-checkbox'); + checkbox!.checked = true; + checkbox!.dispatchEvent(new CustomEvent('change')); + await control.requestUpdate(); + + const bulkActions = await getByTestClass(control, 'bulk-action'); + const fooAction = bulkActions[0]; + fooAction.click(); + await control.requestUpdate(); + + expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.not.exist; + expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.exist; + // @ts-expect-error it's too hard for TS to figure out that `next` is not null at this point + next?.(); + await control.requestUpdate(); + expect(await getByKey(control, 'foo_bulk_action_caption_idle')).to.exist; + expect(await getByKey(control, 'foo_bulk_action_caption_busy')).to.not.exist; + expect(await getByKey(control, 'cancel_button_text')).to.exist; + expect(await getByKey(control, 'select_button_text')).to.not.exist; + expect(await getByTestClass(control, 'bulk-action')).to.have.length(1); + + const notification = await getByTag(control, 'vaadin-notification'); + expect(notification).to.have.attribute('theme', 'error'); + expect(notification).to.have.property('opened', true); + + const notificationRoot = document.createElement('div'); + notification?.renderer?.(notificationRoot); + + const notificationText = notificationRoot.querySelector('foxy-i18n'); + expect(notificationText).to.have.attribute('lang', 'es'); + expect(notificationText).to.have.attribute('key', 'foo_bulk_action_notification_fail'); + expect(notificationText).to.have.attribute('ns', 'test pagination'); + }); }); }); diff --git a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts index 7e7a2a64..fbd5fa1c 100644 --- a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts +++ b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts @@ -1,11 +1,13 @@ import type { PropertyDeclarations, TemplateResult } from 'lit-element'; import type { CollectionPage, NucleonElement } from '../../public/index'; import type { BulkAction, SwipeAction } from './types'; +import type { InternalSummaryControl } from '../InternalSummaryControl/InternalSummaryControl'; import type { InternalConfirmDialog } from '../InternalConfirmDialog'; import type { DialogHideEvent } from '../../private/Dialog/DialogHideEvent'; import type { HALJSONResource } from '../../public/NucleonElement/types'; import type { CheckboxElement } from '@vaadin/vaadin-checkbox'; import type { ItemRenderer } from '../../public/CollectionPage/types'; +import type { UpdateEvent } from '../../public/NucleonElement/UpdateEvent'; import type { FormDialog } from '../../index'; import type { Option } from '../../public/QueryBuilder/types'; @@ -26,6 +28,7 @@ export class InternalAsyncListControl extends InternalEditableControl { createPageHref: { attribute: 'create-page-href' }, getPageHref: { attribute: false }, related: { type: Array }, + layout: {}, first: {}, limit: { type: Number }, form: {}, @@ -33,12 +36,15 @@ export class InternalAsyncListControl extends InternalEditableControl { item: {}, itemProps: { type: Object, attribute: 'item-props' }, wide: { type: Boolean }, + open: { type: Boolean }, + hideWhenEmpty: { type: Boolean, attribute: 'hide-when-empty' }, alert: { type: Boolean }, actions: { attribute: false }, bulkActions: { attribute: false }, filters: { type: Array }, __filter: { attribute: false }, __selection: { attribute: false }, + __totalItems: { attribute: false }, __isSelecting: { attribute: false }, __notification: { attribute: false }, __isFilterVisible: { attribute: false }, @@ -61,6 +67,9 @@ export class InternalAsyncListControl extends InternalEditableControl { /** Same as the `related` property of `NucleonElement`. */ related = [] as string[]; + /** When set to `details`, makes the entire control collapsible. */ + layout: 'details' | null = null; + /** Swipe actions. */ actions = [] as SwipeAction[]; @@ -88,6 +97,12 @@ export class InternalAsyncListControl extends InternalEditableControl { /** Same as the `wide` property of `FormDialog`. */ wide = false; + /** Visually hides the control when the collection is empty or loading. */ + hideWhenEmpty = false; + + /** Same as the `open` property of `InternalSummaryControl`. */ + open = false; + /** Same as the `alert` property of `FormDialog`. */ alert = false; @@ -115,15 +130,20 @@ export class InternalAsyncListControl extends InternalEditableControl { private __isSelecting = false; + private __totalItems = 0; + private __selection: HALJSONResource[] = []; private __itemRenderer: ItemRenderer = ctx => { - if (!ctx.data) return this.__cardRenderer(ctx); + const item = this.__cardRenderer(ctx); + const readonlyItem = html`
${item}
`; + + if (!ctx.data) return readonlyItem; if (this.__isSelecting) { return html`