From b0fdcc6917bebd46450795171f59c550c997639e Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 27 Aug 2024 12:50:04 -0300 Subject: [PATCH 01/15] perf(foxy-internal-async-resource-link-list-control): reduce the number of api requests when loading checked options --- .../InternalAsyncResourceLinkListControl.ts | 98 ++++++++++++------- .../types.ts | 21 ++++ 2 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src/elements/internal/InternalAsyncResourceLinkListControl/types.ts diff --git a/src/elements/internal/InternalAsyncResourceLinkListControl/InternalAsyncResourceLinkListControl.ts b/src/elements/internal/InternalAsyncResourceLinkListControl/InternalAsyncResourceLinkListControl.ts index 8db3c432..54efb6df 100644 --- a/src/elements/internal/InternalAsyncResourceLinkListControl/InternalAsyncResourceLinkListControl.ts +++ b/src/elements/internal/InternalAsyncResourceLinkListControl/InternalAsyncResourceLinkListControl.ts @@ -2,10 +2,10 @@ import type { CSSResultArray, PropertyDeclarations } from 'lit-element'; import type { CheckboxElement } from '@vaadin/vaadin-checkbox'; import type { TemplateResult } from 'lit-html'; import type { ItemRenderer } from '../../public/CollectionPage/types'; +import type { Collection } from './types'; import { InternalEditableControl } from '../InternalEditableControl/InternalEditableControl'; import { NucleonElement } from '../../public/NucleonElement/NucleonElement'; -import { getResourceId } from '@foxy.io/sdk/core'; import { ifDefined } from 'lit-html/directives/if-defined'; import { classMap } from '../../../utils/class-map'; import { html } from 'lit-html'; @@ -85,53 +85,31 @@ export class InternalAsyncResourceLinkListControl extends InternalEditableContro `; if (!ctx.href || ctx.href.startsWith('foxy://')) return wrap(render(ctx)); - let linkHref: string | undefined; - const id = getResourceId(ctx.data?._links.self.href ?? ''); + if (this.readonly) return wrap(render(ctx)); - try { - const url = new URL(this.linksHref ?? ''); - url.searchParams.set(this.foreignKeyForId ?? '', String(id ?? '')); - url.searchParams.set('limit', '1'); - linkHref = url.toString(); - } catch { - linkHref = undefined; - } - - const content = html` - this.requestUpdate()} - > - ${render(ctx)} - - `; - - if (this.readonly) return wrap(content); + const foreignKeyForUri = this.foreignKeyForUri; + const linkResource = foreignKeyForUri + ? this.__allLinks?.find(link => link[foreignKeyForUri] === ctx.href) + : undefined; - const nucleon = this.renderRoot.querySelector(`#link-${id}`) as NucleonElement | null; - const checked = !!nucleon?.data?.returned_items; - const isDisabled = this.disabled || !nucleon?.in('idle') || this.__isFetching; + const isDisabled = this.disabled || !this.__allLinks || this.__isFetching; return wrap(html` { const target = evt.currentTarget as CheckboxElement; if (target.checked) { this.__insertLink(ctx.data?._links.self.href ?? ''); } else { - this.__deleteLink( - nucleon?.data?._embedded?.[this.embedKey ?? '']?.[0]?._links.self.href ?? '' - ); + this.__deleteLink(linkResource?._links.self.href ?? ''); } }} >
- ${content} + ${render(ctx)}
`); @@ -150,11 +128,7 @@ export class InternalAsyncResourceLinkListControl extends InternalEditableContro firstHref = undefined; } - const nucleons = [ - ...(this.renderRoot.querySelectorAll('foxy-nucleon') as NodeListOf>), - ]; - - const isStatusVisible = this.__isFetching || nucleons.some(n => !n.in('idle')); + const isStatusVisible = this.__isFetching || !this.__allLinks; return html`
@@ -203,6 +177,8 @@ export class InternalAsyncResourceLinkListControl extends InternalEditableContro > ${this._errorMessage}
+ + ${this.__renderLinkResourceLoaders()} `; } @@ -256,4 +232,52 @@ export class InternalAsyncResourceLinkListControl extends InternalEditableContro this.__isFetching = false; } + + private __renderLinkResourceLoaders() { + const maxApiLimit = 200; + const firstPage = this.renderRoot.querySelector>('[data-link-page]'); + const totalItems = Number(firstPage?.data?.total_items ?? maxApiLimit); // sometimes total_items is a string in hAPI + const links: string[] = []; + + try { + for (let i = 0; i < Math.max(1, Math.ceil(totalItems / maxApiLimit)); i++) { + const url = new URL(this.linksHref ?? ''); + url.searchParams.set('offset', String(i * maxApiLimit)); + url.searchParams.set('limit', String(maxApiLimit)); + links.push(url.toString()); + } + } catch { + // Do nothing. + } + + return links.map( + href => html` + + ` + ); + } + + private get __allLinks() { + const embedKey = this.embedKey; + if (!embedKey) return null; + + type Loader = NucleonElement; + const loaders = this.renderRoot.querySelectorAll('[data-link-page]'); + const allLinks: any[] = []; + + for (const loader of loaders) { + const embedded = loader.data?._embedded?.[embedKey]; + if (!embedded) return null; + allLinks.push(...embedded); + } + + return allLinks; + } } diff --git a/src/elements/internal/InternalAsyncResourceLinkListControl/types.ts b/src/elements/internal/InternalAsyncResourceLinkListControl/types.ts new file mode 100644 index 00000000..2ac877d1 --- /dev/null +++ b/src/elements/internal/InternalAsyncResourceLinkListControl/types.ts @@ -0,0 +1,21 @@ +import type { Graph, Resource } from '@foxy.io/sdk/core'; + +import type { + CollectionGraphLinks, + CollectionGraphProps, +} from '@foxy.io/sdk/dist/types/core/defaults'; + +interface CollectionResourceItem extends Graph { + curie: string; + props: Record; + links: { self: CollectionResourceItem; [key: string]: any }; +} + +interface CollectionResource extends Graph { + curie: string; + props: CollectionGraphProps; + links: CollectionGraphLinks; + child: CollectionResourceItem; +} + +export type Collection = Resource; From 9727499ae91f623c29b5b8793fb5564ea5a098c4 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 27 Aug 2024 14:36:32 -0300 Subject: [PATCH 02/15] feat(foxy-store-shipping-method-form): bump the limit for additional services to 200 --- .../StoreShippingMethodForm/StoreShippingMethodForm.test.ts | 1 + .../public/StoreShippingMethodForm/StoreShippingMethodForm.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts index f2d5bc4f..e5d3cd3c 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts @@ -674,6 +674,7 @@ describe('StoreShippingMethodForm', () => { expect(control).to.have.attribute('own-key-for-uri', 'shipping_method_uri'); expect(control).to.have.attribute('embed-key', 'fx:store_shipping_services'); expect(control).to.have.attribute('infer', 'services'); + expect(control).to.have.attribute('limit', '200'); expect(control).to.have.attribute('item', 'foxy-shipping-service-card'); expect(control).to.have.attribute( 'options-href', diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts index fb888907..df35d2cf 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts @@ -211,6 +211,7 @@ export class StoreShippingMethodForm extends Base { options-href=${ifDefined(shippingMethod?._links['fx:shipping_services'].href)} links-href=${ifDefined(this.data?._links['fx:store_shipping_services'].href)} infer="services" + limit="200" item="foxy-shipping-service-card" > From 7aaee7f1ecb9ce99b37005b356df16d6194b96ee Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 27 Aug 2024 15:54:19 -0300 Subject: [PATCH 03/15] internal(password-control): add support for summary item layout --- .../InternalPasswordControl.test.ts | 16 ++++ .../InternalPasswordControl.ts | 4 + .../internal/InternalPasswordControl/index.ts | 2 + .../InternalPasswordControl/vaadinStyles.ts | 78 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/elements/internal/InternalPasswordControl/vaadinStyles.ts diff --git a/src/elements/internal/InternalPasswordControl/InternalPasswordControl.test.ts b/src/elements/internal/InternalPasswordControl/InternalPasswordControl.test.ts index 3e9667b6..755d710a 100644 --- a/src/elements/internal/InternalPasswordControl/InternalPasswordControl.test.ts +++ b/src/elements/internal/InternalPasswordControl/InternalPasswordControl.test.ts @@ -76,6 +76,11 @@ describe('InternalTextControl', () => { expect(new Control()).to.have.property('showGenerator', false); }); + it('has a reactive property "layout"', () => { + expect(Control).to.have.deep.nested.property('properties.layout', {}); + expect(new Control()).to.have.property('layout', null); + }); + it('extends InternalEditableControl', () => { expect(new Control()).to.be.instanceOf(InternalEditableControl); }); @@ -140,6 +145,17 @@ describe('InternalTextControl', () => { expect(field).to.have.property('label', 'test label'); }); + it('sets "theme" on vaadin-password-field from "layout" on itself', async () => { + const layout = html``; + const control = await fixture(layout); + const field = control.renderRoot.querySelector('vaadin-password-field')!; + expect(field).to.not.have.attribute('theme'); + + control.layout = 'summary-item'; + await control.requestUpdate(); + expect(field).to.have.attribute('theme', 'summary-item'); + }); + it('sets "disabled" on vaadin-password-field from "disabled" on itself', async () => { const layout = html``; const control = await fixture(layout); diff --git a/src/elements/internal/InternalPasswordControl/InternalPasswordControl.ts b/src/elements/internal/InternalPasswordControl/InternalPasswordControl.ts index 0e75c7f5..5ec6a72e 100644 --- a/src/elements/internal/InternalPasswordControl/InternalPasswordControl.ts +++ b/src/elements/internal/InternalPasswordControl/InternalPasswordControl.ts @@ -20,6 +20,7 @@ export class InternalPasswordControl extends InternalEditableControl { ...super.properties, generatorOptions: { type: Object, attribute: 'generator-options' }, showGenerator: { type: Boolean, attribute: 'show-generator' }, + layout: {}, }; } @@ -28,6 +29,8 @@ export class InternalPasswordControl extends InternalEditableControl { /** If true, renders the password generator button. */ showGenerator = false; + layout: 'standalone' | 'summary-item' | null = null; + renderControl(): TemplateResult { return html` Date: Tue, 27 Aug 2024 15:56:18 -0300 Subject: [PATCH 04/15] fix: fix summary item separators being shown for hidden items --- .../InternalSummaryControl/InternalSummaryControl.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts index 0b3b6174..8944aaa8 100644 --- a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts +++ b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts @@ -10,12 +10,9 @@ export class InternalSummaryControl extends InternalEditableControl { ...super.styles, css` ::slotted(*) { + background-color: var(--lumo-base-color); padding: calc(0.625em + (var(--lumo-border-radius) / 4) - 1px); } - - ::slotted(:not(:first-child)) { - border-top: thin var(--foxy-border-style, solid) var(--lumo-contrast-10pct) !important; - } `, ]; } @@ -27,7 +24,12 @@ export class InternalSummaryControl extends InternalEditableControl { renderControl(): TemplateResult { return html`

${this.label}

-
+
+ +

${this.helperText}

`; } From e7159e566cb75ed173dca47ecdd38e0d18b4912a Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 27 Aug 2024 15:57:47 -0300 Subject: [PATCH 05/15] feat(foxy-store-shipping-form): group controls using summary layout --- .../StoreShippingMethodForm.stories.ts | 19 +- .../StoreShippingMethodForm.test.ts | 175 ++++++----- .../StoreShippingMethodForm.ts | 122 ++++---- .../public/StoreShippingMethodForm/index.ts | 3 +- .../store-shipping-method-form/en.json | 278 ++++++++++-------- 5 files changed, 315 insertions(+), 282 deletions(-) diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts index 2c367f31..5e30554c 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts @@ -11,18 +11,19 @@ const summary: Summary = { localName: 'foxy-store-shipping-method-form', translatable: true, configurable: { - sections: ['timestamps', 'header'], + sections: ['timestamps', 'header', 'general', 'destinations', 'account'], buttons: ['delete', 'create', 'submit', 'undo', 'header:copy-id', 'header:copy-json'], inputs: [ - 'shipping-method-uri', - 'shipping-container-uri', - 'shipping-drop-type-uri', - 'destinations', - 'authentication-key', - 'meter-number', + 'general:shipping-method-uri', + 'general:shipping-container-uri', + 'general:shipping-drop-type-uri', + 'destinations:use-for-domestic', + 'destinations:use-for-international', + 'account:authentication-key', + 'account:meter-number', + 'account:accountid', + 'account:password', 'endpoint', - 'accountid', - 'password', 'custom-code', 'services', ], diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts index e5d3cd3c..321cf4e8 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts @@ -7,8 +7,9 @@ import { InternalAsyncResourceLinkListControl } from '../../internal/InternalAsy import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { StoreShippingMethodForm as Form } from './StoreShippingMethodForm'; import { InternalResourcePickerControl } from '../../internal/InternalResourcePickerControl/InternalResourcePickerControl'; -import { InternalCheckboxGroupControl } from '../../internal/InternalCheckboxGroupControl/InternalCheckboxGroupControl'; import { InternalPasswordControl } from '../../internal/InternalPasswordControl/InternalPasswordControl'; +import { InternalSummaryControl } from '../../internal/InternalSummaryControl/InternalSummaryControl'; +import { InternalSwitchControl } from '../../internal/InternalSwitchControl/InternalSwitchControl'; import { InternalSourceControl } from '../../internal/InternalSourceControl/InternalSourceControl'; import { InternalTextControl } from '../../internal/InternalTextControl/InternalTextControl'; import { NucleonElement } from '../NucleonElement/NucleonElement'; @@ -36,9 +37,14 @@ describe('StoreShippingMethodForm', () => { expect(element).to.equal(InternalResourcePickerControl); }); - it('imports and defines foxy-internal-checkbox-group-control', () => { - const element = customElements.get('foxy-internal-checkbox-group-control'); - expect(element).to.equal(InternalCheckboxGroupControl); + it('imports and defines foxy-internal-summary-control', () => { + const element = customElements.get('foxy-internal-summary-control'); + expect(element).to.equal(InternalSummaryControl); + }); + + it('imports and defines foxy-internal-switch-control', () => { + const element = customElements.get('foxy-internal-switch-control'); + expect(element).to.equal(InternalSwitchControl); }); it('imports and defines foxy-internal-source-control', () => { @@ -244,7 +250,7 @@ describe('StoreShippingMethodForm', () => { it('hides everything except for shipping method uri, timestamps, create and delete buttons by default', () => { const form = new Form(); expect(form.hiddenSelector.toString()).to.equal( - 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password endpoint custom-code services undo submit delete timestamps' + 'general:shipping-container-uri general:shipping-drop-type-uri destinations account endpoint custom-code services undo submit delete timestamps' ); }); @@ -271,7 +277,7 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password custom-code services undo submit delete timestamps' + 'general destinations account custom-code services undo submit delete timestamps' ); }); @@ -298,7 +304,7 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password endpoint services undo submit delete timestamps' + 'general destinations account endpoint services undo submit delete timestamps' ); }); @@ -325,7 +331,7 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'shipping-container-uri shipping-drop-type-uri authentication-key meter-number accountid password endpoint custom-code services undo submit delete timestamps' + 'general account endpoint custom-code services undo submit delete timestamps' ); }); @@ -379,7 +385,7 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'authentication-key meter-number accountid password endpoint custom-code services undo submit delete timestamps' + 'account endpoint custom-code services undo submit delete timestamps' ); }); @@ -435,7 +441,18 @@ describe('StoreShippingMethodForm', () => { expect(form.headerSubtitleOptions).to.deep.equal({ id: form.headerCopyIdValue }); }); - it('renders resource picker control for shipping method uri', async () => { + it('renders summary control for General section', async () => { + const router = createRouter(); + const element = await fixture
(html` + router.handleEvent(evt)}> + + `); + + const control = element.renderRoot.querySelector('[infer="general"]') as InternalSummaryControl; + expect(control).to.be.instanceOf(InternalSummaryControl); + }); + + it('renders resource picker control for shipping method uri inside of the General section', async () => { const router = createRouter(); const element = await fixture(html` @@ -447,15 +464,16 @@ describe('StoreShippingMethodForm', () => { `); const control = element.renderRoot.querySelector( - '[infer="shipping-method-uri"]' + '[infer="general"] [infer="shipping-method-uri"]' ) as InternalResourcePickerControl; expect(control).to.be.instanceOf(InternalResourcePickerControl); + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.attribute('first', 'https://demo.api/hapi/shipping_methods'); expect(control).to.have.attribute('item', 'foxy-shipping-method-card'); }); - it('renders resource picker control for shipping container uri', async () => { + it('renders resource picker control for shipping container uri inside of the General section', async () => { const router = createRouter(); const element = await fixture(html` @@ -475,10 +493,11 @@ describe('StoreShippingMethodForm', () => { await element.requestUpdate(); const control = element.renderRoot.querySelector( - '[infer="shipping-container-uri"]' + '[infer="general"] [infer="shipping-container-uri"]' ) as InternalResourcePickerControl; expect(control).to.be.instanceOf(InternalResourcePickerControl); + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.attribute('item', 'foxy-shipping-container-card'); expect(control).to.have.attribute( 'first', @@ -486,7 +505,7 @@ describe('StoreShippingMethodForm', () => { ); }); - it('renders resource picker control for shipping drop type uri', async () => { + it('renders resource picker control for shipping drop type uri inside of the General section', async () => { const router = createRouter(); const element = await fixture(html` @@ -506,10 +525,11 @@ describe('StoreShippingMethodForm', () => { await element.requestUpdate(); const control = element.renderRoot.querySelector( - '[infer="shipping-drop-type-uri"]' + '[infer="general"] [infer="shipping-drop-type-uri"]' ) as InternalResourcePickerControl; expect(control).to.be.instanceOf(InternalResourcePickerControl); + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.attribute('item', 'foxy-shipping-drop-type-card'); expect(control).to.have.attribute( 'first', @@ -517,7 +537,7 @@ describe('StoreShippingMethodForm', () => { ); }); - it('renders checkbox group control for destinations', async () => { + it('renders summary control for Destinations section', async () => { const router = createRouter(); const element = await fixture(html` @@ -527,60 +547,95 @@ describe('StoreShippingMethodForm', () => { const control = element.renderRoot.querySelector( '[infer="destinations"]' - ) as InternalCheckboxGroupControl; - - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { value: 'domestic', label: 'domestic' }, - { value: 'international', label: 'international' }, - ]); + ) as InternalSummaryControl; - expect(control.getValue()).to.be.empty; + expect(control).to.be.instanceOf(InternalSummaryControl); + }); - element.edit({ use_for_domestic: true }); - expect(control.getValue()).to.deep.equal(['domestic']); + it('renders a switch control for domestic destinations inside of the Destinations section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + + `); - element.edit({ use_for_international: true }); - expect(control.getValue()).to.deep.equal(['domestic', 'international']); + expect( + element.renderRoot.querySelector('[infer="destinations"] [infer="use-for-domestic"]') + ).to.be.instanceOf(InternalSwitchControl); + }); - element.edit({ use_for_domestic: false }); - expect(control.getValue()).to.deep.equal(['international']); + it('renders a switch control for international destinations inside of the Destinations section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + + `); - control.setValue([]); - expect(element).to.have.nested.property('form.use_for_domestic', false); - expect(element).to.have.nested.property('form.use_for_international', false); + expect( + element.renderRoot.querySelector('[infer="destinations"] [infer="use-for-international"]') + ).to.be.instanceOf(InternalSwitchControl); + }); - control.setValue(['domestic']); - expect(element).to.have.nested.property('form.use_for_domestic', true); - expect(element).to.have.nested.property('form.use_for_international', false); + it('renders summary control for Account section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + + `); - control.setValue(['domestic', 'international']); - expect(element).to.have.nested.property('form.use_for_domestic', true); - expect(element).to.have.nested.property('form.use_for_international', true); + const control = element.renderRoot.querySelector('[infer="account"]') as InternalSummaryControl; + expect(control).to.be.instanceOf(InternalSummaryControl); }); - it('renders text control for authentication key', async () => { + it('renders text control for authentication key inside of the Account section', async () => { const router = createRouter(); const element = await fixture(html` router.handleEvent(evt)}> `); - expect(element.renderRoot.querySelector('[infer="authentication-key"]')).to.be.instanceOf( - InternalTextControl + const control = element.renderRoot.querySelector( + '[infer="account"] [infer="authentication-key"]' ); + + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.be.instanceOf(InternalTextControl); }); - it('renders text control for meter number', async () => { + it('renders text control for meter number inside of the Account section', async () => { const router = createRouter(); const element = await fixture(html` router.handleEvent(evt)}> `); - expect(element.renderRoot.querySelector('[infer="meter-number"]')).to.be.instanceOf( - InternalTextControl - ); + const control = element.renderRoot.querySelector('[infer="account"] [infer="meter-number"]'); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.be.instanceOf(InternalTextControl); + }); + + it('renders text control for account id inside of the Account section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + + `); + + const control = element.renderRoot.querySelector('[infer="account"] [infer="accountid"]'); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.be.instanceOf(InternalTextControl); + }); + + it('renders password control for password inside of the Account section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + + `); + + const control = element.renderRoot.querySelector('[infer="account"] [infer="password"]'); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.be.instanceOf(InternalPasswordControl); }); it('renders text control for endpoint for "CUSTOM-ENDPOINT-POST" shipping method', async () => { @@ -601,43 +656,15 @@ describe('StoreShippingMethodForm', () => { `); - expect(element.renderRoot.querySelector('[infer="endpoint"]')).to.not.exist; - expect(element.renderRoot.querySelector('[infer="accountid"]')).to.exist; - element.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); // @ts-expect-error type is not resolved for some reason await waitUntil(() => !!element.form._embedded?.['fx:shipping_method'], '', { timeout: 5000 }); const control = element.renderRoot.querySelector('[infer="endpoint"]') as InternalTextControl; - expect(element.renderRoot.querySelector('[infer="accountid"]')).to.not.exist; expect(control).to.be.instanceOf(InternalTextControl); expect(control).to.have.attribute('property', 'accountid'); }); - it('renders text control for account id', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)}> - - `); - - expect(element.renderRoot.querySelector('[infer="accountid"]')).to.be.instanceOf( - InternalTextControl - ); - }); - - it('renders password control for password', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)}> - - `); - - expect(element.renderRoot.querySelector('[infer="password"]')).to.be.instanceOf( - InternalPasswordControl - ); - }); - it('renders source control for custom code', async () => { const router = createRouter(); const element = await fixture(html` diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts index df35d2cf..2f0c85a6 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts @@ -94,45 +94,20 @@ export class StoreShippingMethodForm extends Base { this.edit({ shipping_method_uri: newValue }); }; - private readonly __destinationsGetValue = () => { - const value: string[] = []; - if (this.form.use_for_domestic) value.push('domestic'); - if (this.form.use_for_international) value.push('international'); - return value; - }; - - private readonly __destinationsSetValue = (newValue: string[]) => { - this.edit({ - use_for_domestic: newValue.includes('domestic'), - use_for_international: newValue.includes('international'), - }); - }; - - private readonly __destinationsOptions = [ - { value: 'domestic', label: 'domestic' }, - { value: 'international', label: 'international' }, - ]; - get hiddenSelector(): BooleanSelector { const hasData = !!this.data; const code = this.__shippingMethod?.code; // prettier-ignore - let hiddenControls = 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password endpoint custom-code'; + let hiddenControls = 'general:shipping-container-uri general:shipping-drop-type-uri destinations account endpoint custom-code'; if (code) { const codeToHiddenControls: Record = { - // prettier-ignore - 'CUSTOM-ENDPOINT-POST': 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password custom-code', - // prettier-ignore - 'CUSTOM-CODE': 'shipping-container-uri shipping-drop-type-uri destinations authentication-key meter-number accountid password endpoint', - // prettier-ignore - 'CUSTOM': 'shipping-container-uri shipping-drop-type-uri authentication-key meter-number accountid password endpoint custom-code', - // prettier-ignore + 'CUSTOM-ENDPOINT-POST': 'general destinations account custom-code', + 'CUSTOM-CODE': 'general destinations account endpoint', + 'CUSTOM': 'general account endpoint custom-code', 'FedEx': 'endpoint custom-code', - // prettier-ignore - 'USPS': 'authentication-key meter-number accountid password endpoint custom-code', - // prettier-ignore + 'USPS': 'account endpoint custom-code', 'UPS': 'endpoint custom-code', }; @@ -140,7 +115,7 @@ export class StoreShippingMethodForm extends Base { } if (!hasData || code?.startsWith('CUSTOM')) hiddenControls += ' services'; - if (hasData) hiddenControls = `shipping-method-uri ${hiddenControls}`; + if (hasData) hiddenControls = `general:shipping-method-uri ${hiddenControls}`; return new BooleanSelector(`${hiddenControls} ${super.hiddenSelector}`.trim()); } @@ -159,47 +134,52 @@ export class StoreShippingMethodForm extends Base { return html` ${this.renderHeader()} - - - - - - - - - - - - - - - - ${shippingMethod?.code === 'CUSTOM-ENDPOINT-POST' - ? html` - - - ` - : html``} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Date: Tue, 27 Aug 2024 16:47:02 -0300 Subject: [PATCH 06/15] feat(foxy-store-shipping-method-form): hide custom account fields behind a checkbox --- .../StoreShippingMethodForm.stories.ts | 1 + .../StoreShippingMethodForm.test.ts | 55 ++++++++++++++++++- .../StoreShippingMethodForm.ts | 40 ++++++++++++++ .../store-shipping-method-form/en.json | 6 ++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts index 5e30554c..b9f8a141 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts @@ -19,6 +19,7 @@ const summary: Summary = { 'general:shipping-drop-type-uri', 'destinations:use-for-domestic', 'destinations:use-for-international', + 'account:use-custom-account', 'account:authentication-key', 'account:meter-number', 'account:accountid', diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts index 321cf4e8..3dcfa0d5 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts @@ -358,7 +358,7 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'endpoint custom-code services undo submit delete timestamps' + 'endpoint custom-code services account:accountid account:password account:authentication-key account:meter-number undo submit delete timestamps' ); }); @@ -412,10 +412,61 @@ describe('StoreShippingMethodForm', () => { }); expect(form.hiddenSelector.toString()).to.equal( - 'endpoint custom-code services undo submit delete timestamps' + 'endpoint custom-code services account:accountid account:password account:authentication-key account:meter-number undo submit delete timestamps' ); }); + it('hides custom account fields by default when they are empty', async () => { + const form = new Form(); + + expect(form.hiddenSelector.matches('account:accountid', true)).to.be.true; + expect(form.hiddenSelector.matches('account:password', true)).to.be.true; + expect(form.hiddenSelector.matches('account:authentication-key', true)).to.be.true; + expect(form.hiddenSelector.matches('account:meter-number', true)).to.be.true; + + const method = await getTestData('./hapi/shipping_methods/0'); + + form.edit({ + authentication_key: '123', + meter_number: '123', + accountid: '123', + password: '123', + _embedded: { + 'fx:shipping_method': { ...method, code: 'FedEx' }, + }, + }); + + expect(form.hiddenSelector.matches('account:accountid', true)).to.be.false; + expect(form.hiddenSelector.matches('account:password', true)).to.be.false; + expect(form.hiddenSelector.matches('account:authentication-key', true)).to.be.false; + expect(form.hiddenSelector.matches('account:meter-number', true)).to.be.false; + }); + + it('hides custom account field when they are empty unless use-custom-account is checked', async () => { + const form = await fixture(html` + + `); + + const method = await getTestData('./hapi/shipping_methods/0'); + form.edit({ _embedded: { 'fx:shipping_method': { ...method, code: 'FedEx' } } }); + + expect(form.hiddenSelector.matches('account:accountid', true)).to.be.true; + expect(form.hiddenSelector.matches('account:password', true)).to.be.true; + expect(form.hiddenSelector.matches('account:authentication-key', true)).to.be.true; + expect(form.hiddenSelector.matches('account:meter-number', true)).to.be.true; + + const checkbox = form.renderRoot.querySelector( + '[infer="use-custom-account"]' + ) as InternalSwitchControl; + + checkbox.setValue(true); + + expect(form.hiddenSelector.matches('account:accountid', true)).to.be.false; + expect(form.hiddenSelector.matches('account:password', true)).to.be.false; + expect(form.hiddenSelector.matches('account:authentication-key', true)).to.be.false; + expect(form.hiddenSelector.matches('account:meter-number', true)).to.be.false; + }); + it('renders a form header', async () => { const form = new Form(); const renderHeaderMethod = stub(form, 'renderHeader'); diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts index 2f0c85a6..35746c17 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts @@ -38,6 +38,7 @@ export class StoreShippingMethodForm extends Base { return { ...super.properties, shippingMethods: { attribute: 'shipping-methods' }, + __useCustomAccount: { attribute: false }, }; } @@ -94,6 +95,31 @@ export class StoreShippingMethodForm extends Base { this.edit({ shipping_method_uri: newValue }); }; + private __useCustomAccount = false; + + private readonly __useCustomAccountGetValue = () => { + return Boolean( + this.__useCustomAccount || + this.form.authentication_key || + this.form.meter_number || + this.form.accountid || + this.form.password + ); + }; + + private readonly __useCustomAccountSetValue = (value: boolean) => { + if (!value) { + this.edit({ + authentication_key: '', + meter_number: '', + accountid: '', + password: '', + }); + } + + this.__useCustomAccount = value; + }; + get hiddenSelector(): BooleanSelector { const hasData = !!this.data; const code = this.__shippingMethod?.code; @@ -117,6 +143,9 @@ export class StoreShippingMethodForm extends Base { if (!hasData || code?.startsWith('CUSTOM')) hiddenControls += ' services'; if (hasData) hiddenControls = `general:shipping-method-uri ${hiddenControls}`; + // prettier-ignore + if (!this.__useCustomAccountGetValue()) hiddenControls += ' account:accountid account:password account:authentication-key account:meter-number'; + return new BooleanSelector(`${hiddenControls} ${super.hiddenSelector}`.trim()); } @@ -167,6 +196,12 @@ export class StoreShippingMethodForm extends Base { + + @@ -213,6 +248,11 @@ export class StoreShippingMethodForm extends Base { `; } + updated(changes: Map): void { + super.updated(changes); + if (changes.has('href')) this.__useCustomAccount = false; + } + private get __shippingMethodLoader() { type Loader = NucleonElement>; return this.renderRoot.querySelector(`#${this.__shippingMethodLoaderId}`); diff --git a/src/static/translations/store-shipping-method-form/en.json b/src/static/translations/store-shipping-method-form/en.json index b12663a7..52bbebb9 100644 --- a/src/static/translations/store-shipping-method-form/en.json +++ b/src/static/translations/store-shipping-method-form/en.json @@ -160,6 +160,12 @@ "account": { "label": "Account", "helper_text": "", + "use-custom-account": { + "label": "Use custom credentials", + "helper_text": "", + "checked": "Yes", + "unchecked": "No" + }, "authentication-key": { "label": "Authentication Key", "placeholder": "N/A", From 3c20b7eaca1197fdb17a7d5043d764e35509bbb9 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 27 Aug 2024 17:08:00 -0300 Subject: [PATCH 07/15] feat(foxy-store-card): add active/inactive status indicator --- .../public/StoreCard/StoreCard.test.ts | 21 +++++++++++++++++++ src/elements/public/StoreCard/StoreCard.ts | 14 ++++++++++++- src/static/translations/store-card/en.json | 4 +++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/elements/public/StoreCard/StoreCard.test.ts b/src/elements/public/StoreCard/StoreCard.test.ts index a36ac4eb..46ab6050 100644 --- a/src/elements/public/StoreCard/StoreCard.test.ts +++ b/src/elements/public/StoreCard/StoreCard.test.ts @@ -95,4 +95,25 @@ describe('StoreCard', () => { expect(await getByTestId(element, 'subtitle')).to.include.text('example.com'); }); + + it('renders store activity status in the subtitle', async () => { + const router = createRouter(); + const href = 'https://demo.api/hapi/stores/0'; + const element = await fixture(html` + router.handleEvent(evt)}> + + `); + + await waitUntil(() => !!element.data, undefined, { timeout: 5000 }); + element.data = { ...element.data!, is_active: true }; + await element.requestUpdate(); + const subtitle = await getByTestId(element, 'subtitle'); + expect(subtitle?.querySelector('[aria-label="status_active"]')).to.exist; + expect(subtitle?.querySelector('[aria-label="status_inactive"]')).to.not.exist; + + element.data = { ...element.data!, is_active: false }; + await element.requestUpdate(); + expect(subtitle?.querySelector('[aria-label="status_active"]')).to.not.exist; + expect(subtitle?.querySelector('[aria-label="status_inactive"]')).to.exist; + }); }); diff --git a/src/elements/public/StoreCard/StoreCard.ts b/src/elements/public/StoreCard/StoreCard.ts index 4ce2a027..758ff5ea 100644 --- a/src/elements/public/StoreCard/StoreCard.ts +++ b/src/elements/public/StoreCard/StoreCard.ts @@ -4,6 +4,7 @@ import type { Data } from './types'; import { TranslatableMixin } from '../../../mixins/translatable'; import { TwoLineCard } from '../CustomFieldCard/TwoLineCard'; +import { classMap } from '../../../utils/class-map'; import { html } from 'lit-html'; const NS = 'store-card'; @@ -32,7 +33,18 @@ export class StoreCard extends Base { subtitle: data => { const defaultD = this.defaultDomain; const domain = data?.store_domain; - return html`${domain?.includes('.') || !defaultD ? domain : `${domain}.${defaultD}`}`; + return html` + + ${domain?.includes('.') || !defaultD ? domain : `${domain}.${defaultD}`} + `; }, }); } diff --git a/src/static/translations/store-card/en.json b/src/static/translations/store-card/en.json index 1bdc9c50..c8b3bf4d 100644 --- a/src/static/translations/store-card/en.json +++ b/src/static/translations/store-card/en.json @@ -1,7 +1,9 @@ { + "status_active": "Active", + "status_inactive": "Inactive", "spinner": { "loading_busy": "Loading", "loading_empty": "No data", "loading_error": "Unknown error" } -} +} \ No newline at end of file From fd1c6afbad98d86f6d531d99bdbd491b686d02a0 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Wed, 28 Aug 2024 11:32:09 -0300 Subject: [PATCH 08/15] fix(foxy-store-shipping-method-form): display services for `CUSTOM` shipping method --- .../public/StoreShippingMethodForm/StoreShippingMethodForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts index 35746c17..67a73ff8 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts @@ -140,7 +140,7 @@ export class StoreShippingMethodForm extends Base { if (codeToHiddenControls[code]) hiddenControls = codeToHiddenControls[code]; } - if (!hasData || code?.startsWith('CUSTOM')) hiddenControls += ' services'; + if (!hasData || code?.startsWith('CUSTOM-')) hiddenControls += ' services'; if (hasData) hiddenControls = `general:shipping-method-uri ${hiddenControls}`; // prettier-ignore From 452b78b6a2062776bcbd9aa0a570b7704e004799 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Wed, 28 Aug 2024 12:47:10 -0300 Subject: [PATCH 09/15] fix(foxy-store-shipping-method-form): fix form being in snapshot:dirty state without any user-initiated edits --- .../StoreShippingMethodForm.stories.ts | 6 +- .../StoreShippingMethodForm.test.ts | 420 ++++++++---------- .../StoreShippingMethodForm.ts | 67 +-- .../public/StoreShippingMethodForm/types.ts | 4 +- src/server/hapi/createDataset.ts | 44 +- .../store-shipping-method-form/en.json | 3 +- 6 files changed, 260 insertions(+), 284 deletions(-) diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts index b9f8a141..d8f170c6 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.stories.ts @@ -5,8 +5,8 @@ import { getMeta } from '../../../storygen/getMeta'; import { getStory } from '../../../storygen/getStory'; const summary: Summary = { - href: 'https://demo.api/hapi/store_shipping_methods/0?zoom=shipping_method', - parent: 'https://demo.api/hapi/store_shipping_methods?zoom=shipping_method', + href: 'https://demo.api/hapi/store_shipping_methods/0', + parent: 'https://demo.api/hapi/store_shipping_methods', nucleon: true, localName: 'foxy-store-shipping-method-form', translatable: true, @@ -41,7 +41,7 @@ export const Empty = getStory({ ...summary, ext }); export const Error = getStory({ ...summary, ext }); export const Busy = getStory({ ...summary, ext }); -CustomCode.args.href = `https://demo.api/hapi/store_shipping_methods/1?zoom=shipping_method`; +CustomCode.args.href = `https://demo.api/hapi/store_shipping_methods/1`; Empty.args.href = ''; Error.args.href = 'https://demo.api/virtual/empty?status=404'; Busy.args.href = 'https://demo.api/virtual/stall'; diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts index 3dcfa0d5..d9873849 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.test.ts @@ -1,5 +1,4 @@ import type { FetchEvent } from '../NucleonElement/FetchEvent'; -import type { Data } from './types'; import './index'; @@ -17,8 +16,6 @@ import { InternalForm } from '../../internal/InternalForm/InternalForm'; import { createRouter } from '../../../server/index'; import { getTestData } from '../../../testgen/getTestData'; import { stub } from 'sinon'; -import { Resource } from '@foxy.io/sdk/core'; -import { Rels } from '@foxy.io/sdk/backend'; describe('StoreShippingMethodForm', () => { const OriginalResizeObserver = window.ResizeObserver; @@ -156,124 +153,99 @@ describe('StoreShippingMethodForm', () => { expect(form.errors).to.not.include('custom-code:v8n_too_long'); }); - it('produces the endpoint:v8n_required error for CUSTOM-ENDPOINT-POST method if accountid is empty', () => { - const form = new Form(); - expect(form.errors).to.not.include('endpoint:v8n_required'); + it('produces the shipping-container-uri:v8n_required error on 400 API response', async () => { + const router = createRouter(); + const form = await fixture(html` + { + if (evt.request.method === 'POST') { + evt.respondWith( + Promise.resolve( + new Response( + JSON.stringify({ + _embedded: { + 'fx:errors': [{ message: 'shipping_container_id must be greater than 0' }], + }, + }), + { status: 400 } + ) + ) + ); + } else { + router.handleEvent(evt); + } + }} + > + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Custom Shipping Code', - code: 'CUSTOM-ENDPOINT-POST', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], - }); + expect(form.errors).to.not.include('shipping-container-uri:v8n_required'); - expect(form.errors).to.include('endpoint:v8n_required'); - - form.edit({ accountid: 'https://example.com' }); - expect(form.errors).to.not.include('endpoint:v8n_required'); - }); - - ['USPS', 'FedEx', 'UPS'].forEach(code => { - it(`produces the shipping-container-uri:v8n_required error for ${code} method if uri is empty`, () => { - const form = new Form(); - expect(form.errors).to.not.include('shipping-container-uri:v8n_required'); - - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Test', - code: code, - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], - }); - - expect(form.errors).to.include('shipping-container-uri:v8n_required'); - - form.edit({ shipping_container_uri: 'https://demo.api/virtual/stall' }); - expect(form.errors).to.not.include('shipping-container-uri:v8n_required'); - }); + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); + form.submit(); + await waitUntil(() => form.in('idle')); + expect(form.errors).to.include('shipping-container-uri:v8n_required'); + + form.edit({ shipping_container_uri: 'https://demo.api/virtual/stall' }); + expect(form.errors).to.not.include('shipping-container-uri:v8n_required'); }); - ['FedEx', 'UPS'].forEach(code => { - it(`produces the shipping-drop-type-uri:v8n_required error for ${code} method if uri is empty`, () => { - const form = new Form(); - expect(form.errors).to.not.include('shipping-drop-type-uri:v8n_required'); - - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Test', - code: code, - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], - }); - - expect(form.errors).to.include('shipping-drop-type-uri:v8n_required'); - - form.edit({ shipping_drop_type_uri: 'https://demo.api/virtual/stall' }); - expect(form.errors).to.not.include('shipping-drop-type-uri:v8n_required'); - }); + it('produces the shipping-drop-type-uri:v8n_required error on 400 API response', async () => { + const router = createRouter(); + const form = await fixture(html` + { + if (evt.request.method === 'POST') { + evt.respondWith( + Promise.resolve( + new Response( + JSON.stringify({ + _embedded: { + 'fx:errors': [{ message: 'shipping_drop_type_id must be greater than 0' }], + }, + }), + { status: 400 } + ) + ) + ); + } else { + router.handleEvent(evt); + } + }} + > + + `); + + expect(form.errors).to.not.include('shipping-drop-type-uri:v8n_required'); + + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); + form.submit(); + await waitUntil(() => form.in('idle')); + expect(form.errors).to.include('shipping-drop-type-uri:v8n_required'); + + form.edit({ shipping_drop_type_uri: 'https://demo.api/virtual/stall' }); + expect(form.errors).to.not.include('shipping-drop-type-uri:v8n_required'); }); - it('hides everything except for shipping method uri, timestamps, create and delete buttons by default', () => { + it('shows only relevant controls by default', () => { const form = new Form(); expect(form.hiddenSelector.toString()).to.equal( 'general:shipping-container-uri general:shipping-drop-type-uri destinations account endpoint custom-code services undo submit delete timestamps' ); }); - it('hides everything except for shipping method uri, endpoint, timestamps, create and delete buttons for CUSTOM-ENDPOINT-POST method', () => { - const form = new Form(); + it('shows only relevant controls for CUSTOM-ENDPOINT-POST method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Custom Shipping Code', - code: 'CUSTOM-ENDPOINT-POST', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/6' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -281,26 +253,18 @@ describe('StoreShippingMethodForm', () => { ); }); - it('hides everything except for shipping method uri, custom code, timestamps, create and delete buttons for CUSTOM-CODE method', () => { - const form = new Form(); + it('shows only relevant controls for CUSTOM-CODE method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Custom Code', - code: 'CUSTOM-CODE', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/7' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -308,26 +272,18 @@ describe('StoreShippingMethodForm', () => { ); }); - it('hides everything except for shipping method uri, destinations, services, timestamps, create and delete buttons for CUSTOM method', () => { - const form = new Form(); + it('shows only relevant controls for CUSTOM method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'Custom', - code: 'CUSTOM', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/3' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -335,26 +291,18 @@ describe('StoreShippingMethodForm', () => { ); }); - it('hides everything except for shipping method uri, shipping container uri, shipping drop type uri, destinations, authentication key, meter number, accountid, password, services, timestamps, create and delete buttons for FedEx method', () => { - const form = new Form(); + it('shows only relevant controls for FedEx method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'FedEx', - code: 'FedEx', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/1' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -362,26 +310,18 @@ describe('StoreShippingMethodForm', () => { ); }); - it('hides everything except for shipping method uri, shipping container uri, shipping drop type uri, destinations, services, timestamps, create and delete buttons for USPS method', () => { - const form = new Form(); + it('shows only relevant controls for USPS method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'USPS', - code: 'USPS', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -389,26 +329,18 @@ describe('StoreShippingMethodForm', () => { ); }); - it('hides everything except for shipping method uri, shipping container uri, shipping drop type uri, destinations, authentication key, meter number, accountid, password, services, timestamps, create and delete buttons for UPS method', () => { - const form = new Form(); + it('shows only relevant controls for UPS method', async () => { + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); - form.edit({ - _embedded: { - 'fx:shipping_method': { - _links: { - 'self': { href: 'https://demo.api/virtual/stall' }, - 'fx:property_helpers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_containers': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_drop_types': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_methods': { href: 'https://demo.api/virtual/stall' }, - 'fx:shipping_services': { href: 'https://demo.api/virtual/stall' }, - }, - name: 'UPS', - code: 'UPS', - date_created: null, - date_modified: null, - }, - } as Data['_embedded'], + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/2' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.toString()).to.equal( @@ -417,23 +349,29 @@ describe('StoreShippingMethodForm', () => { }); it('hides custom account fields by default when they are empty', async () => { - const form = new Form(); + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)}> + + `); expect(form.hiddenSelector.matches('account:accountid', true)).to.be.true; expect(form.hiddenSelector.matches('account:password', true)).to.be.true; expect(form.hiddenSelector.matches('account:authentication-key', true)).to.be.true; expect(form.hiddenSelector.matches('account:meter-number', true)).to.be.true; - const method = await getTestData('./hapi/shipping_methods/0'); - form.edit({ + shipping_method_uri: 'https://demo.api/hapi/shipping_methods/1', authentication_key: '123', meter_number: '123', accountid: '123', password: '123', - _embedded: { - 'fx:shipping_method': { ...method, code: 'FedEx' }, - }, + }); + + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); }); expect(form.hiddenSelector.matches('account:accountid', true)).to.be.false; @@ -443,12 +381,18 @@ describe('StoreShippingMethodForm', () => { }); it('hides custom account field when they are empty unless use-custom-account is checked', async () => { + const router = createRouter(); const form = await fixture(html` - + router.handleEvent(evt)}> + `); - const method = await getTestData('./hapi/shipping_methods/0'); - form.edit({ _embedded: { 'fx:shipping_method': { ...method, code: 'FedEx' } } }); + form.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/1' }); + await form.requestUpdate(); + await waitUntil(() => { + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); + }); expect(form.hiddenSelector.matches('account:accountid', true)).to.be.true; expect(form.hiddenSelector.matches('account:password', true)).to.be.true; @@ -475,14 +419,25 @@ describe('StoreShippingMethodForm', () => { }); it('uses custom header title options', async () => { - const form = new Form(); - form.data = await getTestData('./hapi/store_shipping_methods/0?zoom=shipping_method'); - type Embed = { 'fx:shipping_method': Resource }; - const shippingMethod = (form.data!._embedded as Embed)['fx:shipping_method']; + const router = createRouter(); + const form = await fixture(html` + router.handleEvent(evt)} + > + + `); + + await waitUntil(() => { + if (!form.data) return false; + const nucleons = form.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); + }); + expect(form.headerTitleOptions).to.deep.equal({ ...form.data!, context: 'existing', - provider: shippingMethod.name, + provider: 'United States Postal Service', }); }); @@ -535,13 +490,12 @@ describe('StoreShippingMethodForm', () => { `); - element.edit({ - _embedded: { - 'fx:shipping_method': await getTestData('./hapi/shipping_methods/0', router), - }, - }); - + element.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); await element.requestUpdate(); + await waitUntil(() => { + const nucleons = element.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); + }); const control = element.renderRoot.querySelector( '[infer="general"] [infer="shipping-container-uri"]' @@ -567,13 +521,12 @@ describe('StoreShippingMethodForm', () => { `); - element.edit({ - _embedded: { - 'fx:shipping_method': await getTestData('./hapi/shipping_methods/0', router), - }, - }); - + element.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); await element.requestUpdate(); + await waitUntil(() => { + const nucleons = element.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); + }); const control = element.renderRoot.querySelector( '[infer="general"] [infer="shipping-drop-type-uri"]' @@ -707,9 +660,12 @@ describe('StoreShippingMethodForm', () => { `); - element.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/0' }); - // @ts-expect-error type is not resolved for some reason - await waitUntil(() => !!element.form._embedded?.['fx:shipping_method'], '', { timeout: 5000 }); + element.edit({ shipping_method_uri: 'https://demo.api/hapi/shipping_methods/6' }); + await waitUntil(() => { + const nucleons = element.renderRoot.querySelectorAll>('foxy-nucleon'); + return [...nucleons].every(n => !!n.in('idle')); + }); + const control = element.renderRoot.querySelector('[infer="endpoint"]') as InternalTextControl; expect(control).to.be.instanceOf(InternalTextControl); diff --git a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts index 67a73ff8..4deefc8a 100644 --- a/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts +++ b/src/elements/public/StoreShippingMethodForm/StoreShippingMethodForm.ts @@ -1,7 +1,6 @@ import type { PropertyDeclarations } from 'lit-element'; import type { NucleonElement } from '../NucleonElement/NucleonElement'; import type { TemplateResult } from 'lit-html'; -import type { UpdateEvent } from '../NucleonElement/UpdateEvent'; import type { NucleonV8N } from '../NucleonElement/types'; import type { Resource } from '@foxy.io/sdk/core'; import type { Rels } from '@foxy.io/sdk/backend'; @@ -13,8 +12,6 @@ import { InternalForm } from '../../internal/InternalForm/InternalForm'; import { ifDefined } from 'lit-html/directives/if-defined'; import { html } from 'lit-html'; -type Embed = { 'fx:shipping_method': Resource } | undefined; - const NS = 'store-shipping-method-form'; const Base = TranslatableMixin(InternalForm, NS); const getKbSize = (value: string) => new Blob([value]).size / 1024; @@ -50,38 +47,6 @@ export class StoreShippingMethodForm extends Base { ({ meter_number: v }) => !v || v.length <= 50 || 'meter-number:v8n_too_long', ({ authentication_key: v }) => !v || v.length <= 50 || 'authentication-key:v8n_too_long', ({ custom_code: v }) => !v || getKbSize(v) <= 64 || 'custom-code:v8n_too_long', - - form => { - if ((form._embedded as Embed)?.['fx:shipping_method']?.code === 'CUSTOM-ENDPOINT-POST') { - return (form.accountid && isURL(form.accountid)) || 'endpoint:v8n_required'; - } else { - return true; - } - }, - - form => { - const url = form.shipping_container_uri; - const code = (form._embedded as Embed)?.['fx:shipping_method']?.code; - const codes = ['USPS', 'FedEx', 'UPS']; - - if (code && codes.includes(code)) { - return (url && isURL(url)) || 'shipping-container-uri:v8n_required'; - } else { - return true; - } - }, - - form => { - const url = form.shipping_drop_type_uri; - const code = (form._embedded as Embed)?.['fx:shipping_method']?.code; - const codes = ['FedEx', 'UPS']; - - if (code && codes.includes(code)) { - return (url && isURL(url)) || 'shipping-drop-type-uri:v8n_required'; - } else { - return true; - } - }, ]; } @@ -236,11 +201,7 @@ export class StoreShippingMethodForm extends Base { infer="" href=${ifDefined(this.form.shipping_method_uri || undefined)} id=${this.__shippingMethodLoaderId} - @update=${(evt: UpdateEvent) => { - const nucleon = evt.target as NucleonElement>; - const embed = nucleon.data; - this.edit({ _embedded: embed ? { 'fx:shipping_method': embed } : {} }); - }} + @update=${() => this.requestUpdate()} > @@ -253,14 +214,34 @@ export class StoreShippingMethodForm extends Base { if (changes.has('href')) this.__useCustomAccount = false; } + protected async _fetch(...args: Parameters): Promise { + try { + return await super._fetch(...args); + } catch (err) { + const errors: string[] = []; + + try { + for (const error of (await (err as Response).json())._embedded['fx:errors']) { + if (error.message.startsWith('shipping_container_id must be')) { + errors.push('shipping-container-uri:v8n_required'); + } else if (error.message.startsWith('shipping_drop_type_id must be')) { + errors.push('shipping-drop-type-uri:v8n_required'); + } + } + } catch { + // no-op + } + + throw errors.length > 0 ? errors : err; + } + } + private get __shippingMethodLoader() { type Loader = NucleonElement>; return this.renderRoot.querySelector(`#${this.__shippingMethodLoaderId}`); } private get __shippingMethod() { - return ( - this.__shippingMethodLoader?.data ?? (this.form._embedded as Embed)?.['fx:shipping_method'] - ); + return this.__shippingMethodLoader?.data; } } diff --git a/src/elements/public/StoreShippingMethodForm/types.ts b/src/elements/public/StoreShippingMethodForm/types.ts index 4e86b9a3..c4920522 100644 --- a/src/elements/public/StoreShippingMethodForm/types.ts +++ b/src/elements/public/StoreShippingMethodForm/types.ts @@ -1,6 +1,4 @@ import type { Resource } from '@foxy.io/sdk/core'; import type { Rels } from '@foxy.io/sdk/backend'; -export type Data = - | Resource - | Resource; +export type Data = Resource; diff --git a/src/server/hapi/createDataset.ts b/src/server/hapi/createDataset.ts index 9357ec87..c6148969 100644 --- a/src/server/hapi/createDataset.ts +++ b/src/server/hapi/createDataset.ts @@ -1192,7 +1192,7 @@ export const createDataset: () => Dataset = () => ({ id: 1, store_id: 0, shipping_method_id: 1, - shipping_method_uri: 'https://demo.api/hapi/shipping_methods/1', + shipping_method_uri: 'https://demo.api/hapi/shipping_methods/7', use_for_domestic: true, use_for_international: true, deployment_status: 'deployed', @@ -1226,6 +1226,48 @@ export const createDataset: () => Dataset = () => ({ }, { id: 1, + name: 'Federal Express', + code: 'FedEx', + date_created: null, + date_modified: null, + }, + { + id: 2, + name: 'United Parcel Service of America', + code: 'UPS', + date_created: null, + date_modified: null, + }, + { + id: 3, + name: 'Custom Options', + code: 'CUSTOM', + date_created: null, + date_modified: null, + }, + { + id: 4, + name: 'FoxyCart Zzyzx', + code: 'Zzyzx', + date_created: null, + date_modified: null, + }, + { + id: 5, + name: 'Custom Shipping Endpoint', + code: 'CUSTOM-ENDPOINT', + date_created: null, + date_modified: null, + }, + { + id: 6, + name: 'Custom Shipping Endpoint (With Native Rates)', + code: 'CUSTOM-ENDPOINT-POST', + date_created: null, + date_modified: null, + }, + { + id: 7, name: 'Custom Shipping Code', code: 'CUSTOM-CODE', date_created: null, diff --git a/src/static/translations/store-shipping-method-form/en.json b/src/static/translations/store-shipping-method-form/en.json index 52bbebb9..875eb6eb 100644 --- a/src/static/translations/store-shipping-method-form/en.json +++ b/src/static/translations/store-shipping-method-form/en.json @@ -194,8 +194,7 @@ "endpoint": { "label": "Endpoint URL", "placeholder": "https://example.com/rates", - "helper_text": "", - "v8n_required": "Please enter a valid endpoint URL." + "helper_text": "" }, "custom-code": { "label": "Custom Code", From 150fa8019a93c07dfa841d40e91b17596b690e6e Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Thu, 29 Aug 2024 11:55:51 -0300 Subject: [PATCH 10/15] fix(foxy-store-form): render text control for countries with no regions in the api --- .../public/StoreForm/StoreForm.test.ts | 53 ++++++++++++++++--- src/elements/public/StoreForm/StoreForm.ts | 8 ++- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/elements/public/StoreForm/StoreForm.test.ts b/src/elements/public/StoreForm/StoreForm.test.ts index b9834c90..078ee98b 100644 --- a/src/elements/public/StoreForm/StoreForm.test.ts +++ b/src/elements/public/StoreForm/StoreForm.test.ts @@ -998,7 +998,7 @@ describe('StoreForm', () => { ]); }); - it('renders a select control for region', async () => { + it('renders a select control for region when there are predefined regions', async () => { const router = createRouter(); const element = await fixture(html` { `); await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="region"]') as InternalSelectControl; - - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalSelectControl); await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/4', { method: 'PATCH', @@ -1036,15 +1032,60 @@ describe('StoreForm', () => { }), }); + let control: InternalTextControl | null = null; element.regions = 'https://demo.api/hapi/property_helpers/4'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); + await waitUntil( + () => { + control = element.renderRoot.querySelector('foxy-internal-select-control[infer="region"]'); + return !!control; + }, + '', + { timeout: 5000 } + ); + + expect(control).to.be.instanceOf(InternalSelectControl); expect(control).to.have.deep.property('options', [ { value: 'SD', label: 'South Dakota' }, { value: 'TN', label: 'Tennessee' }, ]); }); + it('renders a text control for region when there are no predefined regions', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)} + > + + `); + + await waitUntil(() => !!element.data, '', { timeout: 5000 }); + + await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/4', { + method: 'PATCH', + body: JSON.stringify({ + values: {}, + }), + }); + + let control: InternalTextControl | null = null; + element.regions = 'https://demo.api/hapi/property_helpers/4'; + + await waitUntil( + () => { + control = element.renderRoot.querySelector('foxy-internal-text-control[infer="region"]'); + return !!control; + }, + '', + { timeout: 5000 } + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + }); + it('renders a text control for postal code', async () => { const router = createRouter(); const element = await fixture(html` diff --git a/src/elements/public/StoreForm/StoreForm.ts b/src/elements/public/StoreForm/StoreForm.ts index a8c7774f..307d8395 100644 --- a/src/elements/public/StoreForm/StoreForm.ts +++ b/src/elements/public/StoreForm/StoreForm.ts @@ -448,8 +448,12 @@ export class StoreForm extends Base { - - + ${regionOptions.length > 0 + ? html` + + + ` + : html``} From 9d5faca4b1cbf75af7afa341651988b1146d1d7f Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 3 Sep 2024 13:22:39 -0300 Subject: [PATCH 11/15] feat(foxy-store-form): use summary layout for controls, remove unused fields --- .../public/StoreForm/StoreForm.stories.ts | 132 +- .../public/StoreForm/StoreForm.test.ts | 2748 ++++++++--------- src/elements/public/StoreForm/StoreForm.ts | 1407 ++++----- src/elements/public/StoreForm/index.ts | 8 +- src/elements/public/StoreForm/types.ts | 36 + src/static/translations/store-form/en.json | 599 ++-- 6 files changed, 2369 insertions(+), 2561 deletions(-) diff --git a/src/elements/public/StoreForm/StoreForm.stories.ts b/src/elements/public/StoreForm/StoreForm.stories.ts index 2156b188..28954a0c 100644 --- a/src/elements/public/StoreForm/StoreForm.stories.ts +++ b/src/elements/public/StoreForm/StoreForm.stories.ts @@ -12,67 +12,93 @@ const summary: Summary = { translatable: true, configurable: { sections: ['timestamps', 'header'], - buttons: [ - 'is-maintenance-mode', - 'create', - 'delete', - 'undo', - 'submit', - 'header:copy-id', - 'header:copy-json', - ], + buttons: ['create', 'delete', 'undo', 'submit', 'header:copy-id', 'header:copy-json'], inputs: [ - 'store-name', - 'logo-url', - 'store-domain', - 'store-url', - 'store-email', - 'timezone', - 'store-version-uri', - 'from-email', - 'bcc-on-receipt-email', - 'use-email-dns', - 'use-smtp-config', - 'smtp-config', - 'smtp-config-host', - 'smtp-config-port', - 'smtp-config-username', - 'smtp-config-password', - 'smtp-config-security', - 'country', - 'region', - 'postal-code', - 'shipping-address-type', - 'features-multiship', - 'require-signed-shipping-rates', - 'language', - 'locale-code', - 'currency-style', + 'essentials', + 'essentials:store-name', + 'essentials:logo-url', + 'essentials:store-domain', + 'essentials:store-url', + 'essentials:is-maintenance-mode', + 'essentials:store-email', + 'essentials:timezone', + 'essentials:country', + 'essentials:region', + 'essentials:postal-code', + 'essentials:currency-style', + + 'api-legacy', + 'api-legacy:webhook-key-api-legacy', + + 'emails', + 'emails:from-email', + 'emails:use-email-dns', + 'emails:use-smtp-config', + 'emails:smtp-config-host', + 'emails:smtp-config-port', + 'emails:smtp-config-username', + 'emails:smtp-config-password', + 'emails:smtp-config-security', + + 'shipping', + 'shipping:shipping-address-type', + 'shipping:features-multiship', + 'shipping:require-signed-shipping-rates', + + 'cart', + 'cart:app-session-time', + 'cart:products-require-expires-property', + 'cart:use-cart-validation', + 'cart:webhook-key-cart-signing', + + 'checkout', + 'checkout:checkout-type', + 'checkout:customer-password-hash-type', + 'checkout:customer-password-hash-config', + 'checkout:unified-order-entry-password', + 'checkout:use-single-sign-on-url', + 'checkout:single-sign-on-url', + 'checkout:webhook-key-sso', + + 'receipt', + 'receipt:receipt-continue-url', + 'receipt:bcc-on-receipt-email', + 'custom-display-id-config', - 'receipt-continue-url', - 'app-session-time', - 'products-require-expires-property', - 'use-cart-validation', - 'checkout-type', - 'customer-password-hash-type', - 'customer-password-hash-config', - 'unified-order-entry-password', - 'single-sign-on-url', - 'webhook-url', - 'webhook-key-cart-signing', - 'webhook-key-xml-datafeed', - 'webhook-key-api-legacy', - 'webhook-key-sso', + 'custom-display-id-config:custom-display-id-config-enabled', + 'custom-display-id-config:custom-display-id-config-start', + 'custom-display-id-config:custom-display-id-config-length', + 'custom-display-id-config:custom-display-id-config-prefix', + 'custom-display-id-config:custom-display-id-config-suffix', + 'custom-display-id-config-transaction-journal-entries-enabled', + 'custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-authcapture-prefix', + 'custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-capture-prefix', + 'custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-void-prefix', + 'custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-refund-prefix', + 'custom-display-id-config-transaction-journal-entries-transaction-separator', + + 'xml-datafeed', + 'xml-datafeed:use-webhook', + 'xml-datafeed:webhook-url', + 'xml-datafeed:webhook-key-xml-datafeed', ], }, }; export default getMeta(summary); -export const Playground = getStory({ ...summary, code: true }); -export const Empty = getStory(summary); -export const Error = getStory(summary); -export const Busy = getStory(summary); +const ext = ` + customer-password-hash-types="https://demo.api/hapi/property_helpers/9" + shipping-address-types="https://demo.api/hapi/property_helpers/5" + timezones="https://demo.api/hapi/property_helpers/2" + countries="https://demo.api/hapi/property_helpers/3" + regions="https://demo.api/hapi/property_helpers/4" +`; + +export const Playground = getStory({ ...summary, ext, code: true }); +export const Empty = getStory({ ...summary, ext }); +export const Error = getStory({ ...summary, ext }); +export const Busy = getStory({ ...summary, ext }); Empty.args.href = ''; Error.args.href = 'https://demo.api/virtual/empty?status=404'; diff --git a/src/elements/public/StoreForm/StoreForm.test.ts b/src/elements/public/StoreForm/StoreForm.test.ts index 078ee98b..f7d25a3e 100644 --- a/src/elements/public/StoreForm/StoreForm.test.ts +++ b/src/elements/public/StoreForm/StoreForm.test.ts @@ -3,13 +3,11 @@ import type { FetchEvent } from '../NucleonElement/FetchEvent'; import './index'; import { expect, fixture, html, waitUntil } from '@open-wc/testing'; -import { InternalAsyncComboBoxControl } from '../../internal/InternalAsyncComboBoxControl/InternalAsyncComboBoxControl'; -import { InternalCheckboxGroupControl } from '../../internal/InternalCheckboxGroupControl/InternalCheckboxGroupControl'; import { InternalEditableListControl } from '../../internal/InternalEditableListControl/InternalEditableListControl'; -import { InternalRadioGroupControl } from '../../internal/InternalRadioGroupControl/InternalRadioGroupControl'; import { InternalFrequencyControl } from '../../internal/InternalFrequencyControl/InternalFrequencyControl'; import { InternalPasswordControl } from '../../internal/InternalPasswordControl/InternalPasswordControl'; -import { InternalIntegerControl } from '../../internal/InternalIntegerControl/InternalIntegerControl'; +import { InternalSummaryControl } from '../../internal/InternalSummaryControl/InternalSummaryControl'; +import { InternalSwitchControl } from '../../internal/InternalSwitchControl/InternalSwitchControl'; import { InternalNumberControl } from '../../internal/InternalNumberControl/InternalNumberControl'; import { InternalSelectControl } from '../../internal/InternalSelectControl/InternalSelectControl'; import { InternalTextControl } from '../../internal/InternalTextControl/InternalTextControl'; @@ -17,13 +15,8 @@ import { StoreForm as Form } from './StoreForm'; import { NucleonElement } from '../NucleonElement/NucleonElement'; import { InternalForm } from '../../internal/InternalForm/InternalForm'; import { createRouter } from '../../../server/index'; -import { getTestData } from '../../../testgen/getTestData'; import { I18n } from '../I18n/I18n'; -import { getByKey } from '../../../testgen/getByKey'; -import { getByTag } from '../../../testgen/getByTag'; -import { getByTestId } from '../../../testgen/getByTestId'; import { stub } from 'sinon'; -import { set } from 'lodash-es'; describe('StoreForm', () => { const OriginalResizeObserver = window.ResizeObserver; @@ -32,30 +25,11 @@ describe('StoreForm', () => { before(() => (window.ResizeObserver = undefined)); after(() => (window.ResizeObserver = OriginalResizeObserver)); - it('imports and defines vaadin-button', () => { - expect(customElements.get('vaadin-button')).to.exist; - }); - - it('imports and defines foxy-internal-checkbox-group-control', () => { - const element = customElements.get('foxy-internal-checkbox-group-control'); - expect(element).to.equal(InternalCheckboxGroupControl); - }); - - it('imports and defines foxy-internal-async-combo-box-control', () => { - const element = customElements.get('foxy-internal-async-combo-box-control'); - expect(element).to.equal(InternalAsyncComboBoxControl); - }); - it('imports and defines foxy-internal-editable-list-control', () => { const element = customElements.get('foxy-internal-editable-list-control'); expect(element).to.equal(InternalEditableListControl); }); - it('imports and defines foxy-internal-radio-group-control', () => { - const element = customElements.get('foxy-internal-radio-group-control'); - expect(element).to.equal(InternalRadioGroupControl); - }); - it('imports and defines foxy-internal-frequency-control', () => { const element = customElements.get('foxy-internal-frequency-control'); expect(element).to.equal(InternalFrequencyControl); @@ -66,9 +40,14 @@ describe('StoreForm', () => { expect(element).to.equal(InternalPasswordControl); }); - it('imports and defines foxy-internal-integer-control', () => { - const element = customElements.get('foxy-internal-integer-control'); - expect(element).to.equal(InternalIntegerControl); + it('imports and defines foxy-internal-summary-control', () => { + const element = customElements.get('foxy-internal-summary-control'); + expect(element).to.equal(InternalSummaryControl); + }); + + it('imports and defines foxy-internal-switch-control', () => { + const element = customElements.get('foxy-internal-switch-control'); + expect(element).to.equal(InternalSwitchControl); }); it('imports and defines foxy-internal-number-control', () => { @@ -473,13 +452,13 @@ describe('StoreForm', () => { expect(form.errors).to.not.include('unified-order-entry-password:v8n_too_long'); }); - it('produces the custom-display-id-config-enabled:v8n_too_long error if custom display id config is longer than 100 characters', () => { + it('produces the custom-display-id-config-enabled:v8n_too_long error if custom display id config is longer than 500 characters', () => { const form = new Form(); - form.edit({ custom_display_id_config: 'A'.repeat(101) }); + form.edit({ custom_display_id_config: 'A'.repeat(501) }); expect(form.errors).to.include('custom-display-id-config-enabled:v8n_too_long'); - form.edit({ custom_display_id_config: 'A'.repeat(100) }); + form.edit({ custom_display_id_config: 'A'.repeat(500) }); expect(form.errors).to.not.include('custom-display-id-config-enabled:v8n_too_long'); }); @@ -490,68 +469,54 @@ describe('StoreForm', () => { expect(renderHeaderMethod).to.have.been.called; }); - it('renders a text control for store name', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="store-name"]'); + it('renders a summary control for Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="essentials"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); }); - it('renders a text control for logo url', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="logo-url"]'); + it('renders a text control for store name in the Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="essentials"] foxy-internal-text-control[infer="store-name"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); }); - it('renders a text control for store domain', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for logo url in the Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="essentials"] foxy-internal-text-control[infer="logo-url"]' + ); + + expect(control).to.exist; + expect(control).to.have.attribute('layout', 'summary-item'); + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + it('renders a text control for store domain in the Essentials section', async () => { + const element = await fixture(html``); const control = element.renderRoot.querySelector( - '[infer="store-domain"]' + '[infer="essentials"] foxy-internal-text-control[infer="store-domain"]' ) as InternalTextControl; expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); element.edit({ use_remote_domain: false, store_domain: 'test' }); await element.requestUpdate(); - expect(control).to.have.attribute('helper-text', 'store-domain.helper_text'); + expect(control).to.have.attribute('helper-text', 'essentials.store-domain.helper_text'); expect(control).to.have.attribute('suffix', '.foxycart.com'); element.edit({ use_remote_domain: true, store_domain: 'test.com' }); await element.requestUpdate(); - expect(control).to.have.attribute('helper-text', 'store-domain.custom_domain_note'); + expect(control).to.have.attribute('helper-text', 'essentials.store-domain.custom_domain_note'); expect(control).to.have.attribute('suffix', ''); control.setValue('test'); @@ -563,40 +528,33 @@ describe('StoreForm', () => { expect(element).to.have.nested.property('form.store_domain', 'test.com'); }); - it('renders a text control for store url', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="store-url"]'); + it('renders a text control for store url in the Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="essentials"] foxy-internal-text-control[infer="store-url"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); }); - it('renders an editable list control for store email', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a switch control for maintenance mode in the Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="essentials"] foxy-internal-switch-control[infer="is-maintenance-mode"]' + ); + + expect(control).to.exist; + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + it('renders an editable list control for store email in the Essentials section', async () => { + const element = await fixture(html``); const control = element.renderRoot.querySelector( - '[infer="store-email"]' + '[infer="essentials"] foxy-internal-editable-list-control[infer="store-email"]' ) as InternalEditableListControl; expect(control).to.exist; - expect(control).to.be.instanceOf(InternalEditableListControl); + expect(control).to.have.attribute('layout', 'summary-item'); control.setValue([{ value: 'a@b.c' }, { value: 'd@e.f' }]); expect(element).to.have.nested.property('form.store_email', 'a@b.c,d@e.f'); @@ -608,21 +566,24 @@ describe('StoreForm', () => { ]); }); - it('renders a select control for timezone', async () => { + it('renders a select control for timezone in the Essentials section', async () => { const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + const element = await fixture( + html` + !evt.defaultPrevented && router.handleEvent(evt)} + > + + ` + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="timezone"]') as InternalSelectControl; + const control = element.renderRoot.querySelector( + '[infer="essentials"] [infer="timezone"]' + ) as InternalSelectControl; expect(control).to.exist; expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/2', { method: 'PATCH', @@ -643,321 +604,34 @@ describe('StoreForm', () => { }); element.timezones = 'https://demo.api/hapi/property_helpers/2'; + await element.requestUpdate(); await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); expect(control).to.have.deep.property('options', [ { value: 'America/Los_Angeles', - label: '(GMT-08:00) Pacific Time (US and Canada)', + rawLabel: '(GMT-08:00) Pacific Time (US and Canada)', }, { value: 'America/Denver', - label: '(GMT-07:00) Mountain Time (US and Canada)', + rawLabel: '(GMT-07:00) Mountain Time (US and Canada)', }, ]); }); - it('renders an async combo box control for store version', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="store-version-uri"]' - ) as InternalAsyncComboBoxControl; - - element.data!.store_version_uri = ''; - element.data = { ...element.data! }; - await element.requestUpdate(); - - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalAsyncComboBoxControl); - expect(control).to.have.attribute('item-label-path', 'version'); - expect(control).to.have.attribute('item-value-path', '_links.self.href'); - expect(control).to.not.have.attribute('first'); - expect(control).to.have.property('selectedItem', null); - - element.storeVersions = 'https://demo.api/hapi/store_versions'; - await element.requestUpdate(); - - expect(control).to.have.attribute('first', 'https://demo.api/hapi/store_versions'); - - element.edit({ store_version_uri: 'https://demo.api/hapi/store_versions/0' }); - const storeVersion = await getTestData('./hapi/store_versions/0', router); - await element.requestUpdate(); - await waitUntil(() => !!control.selectedItem, '', { timeout: 5000 }); - - expect(control).to.have.deep.property('selectedItem', storeVersion); - }); - - it('renders a text control for "from" email', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="from-email"]'); - - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); - - element.edit({ store_email: 'a@test.com,b@test.com' }); - await element.requestUpdate(); - expect(control).to.have.attribute('placeholder', 'a@test.com'); - - element.edit({ store_email: '' }); - await element.requestUpdate(); - expect(control).to.have.attribute('placeholder', 'from-email.placeholder'); - }); - - it('renders a checkbox for the "bcc_on_receipt_email" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="bcc-on-receipt-email"]' - ); - - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); - - element.edit({ bcc_on_receipt_email: false }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); - - element.edit({ bcc_on_receipt_email: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); - - it('renders a checkbox for the "use_email_dns" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="use-email-dns"]' - ) as InternalCheckboxGroupControl; - - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); - - element.edit({ use_email_dns: false }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); - - element.edit({ use_email_dns: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); - - it('renders a warning when "use_email_dns" is enabled', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByKey(element, 'use_email_dns_helper_text')).to.not.exist; - - element.data = { ...element.data!, use_email_dns: false }; - element.edit({ use_email_dns: true }); - await element.requestUpdate(); - - const control = element.renderRoot.querySelector( - '[infer="use-email-dns"]' - ) as InternalCheckboxGroupControl; - - const warning = control.nextElementSibling as HTMLDivElement; - const warningText = await getByKey(warning, 'use_email_dns_helper_text'); - const warningLink = await getByTag(warning, 'a'); - - expect(warningText).to.exist; - expect(warningText).to.have.attribute('infer', ''); - - expect(warningLink).to.exist; - expect(warningLink).to.include.text('How Emails Are Sent (SPF, DKIM, DMARC, etc.)'); - expect(warningLink).to.have.attribute( - 'href', - 'https://wiki.foxycart.com/v/1.1/emails#how_emails_are_sent_spf_dkim_dmarc_etc' - ); - }); - - it('hides the email dns warning when "use-email-dns" checkbox is hidden', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByKey(element, 'use_email_dns_helper_text')).to.not.exist; - - element.data = { ...element.data!, use_email_dns: false }; - element.edit({ use_email_dns: true }); - await element.requestUpdate(); - expect(await getByKey(element, 'use_email_dns_helper_text')).to.not.exist; - }); - - it('renders a checkbox for smtp_config', async () => { + it('renders a select control for country in the Essentials section', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); const control = element.renderRoot.querySelector( - '[infer="use-smtp-config"]' - ) as InternalCheckboxGroupControl; - - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); - - element.edit({ smtp_config: '' }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); - - element.edit({ - smtp_config: JSON.stringify({ - username: '', - password: '', - security: '', - host: '', - port: '', - }), - }); - - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); - - it('renders smtp config fields when "smtp_config" is not empty', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.data = { ...element.data!, smtp_config: '' }; - await element.requestUpdate(); - - ['host', 'port', 'username', 'password', 'security'].forEach(prop => { - const control = element.renderRoot.querySelector(`[infer="smtp-config-${prop}"]`); - expect(control).to.not.exist; - }); - - element.edit({ - smtp_config: JSON.stringify({ - username: '', - password: '', - security: '', - host: '', - port: '', - }), - }); - - await element.requestUpdate(); - - const $ = (selector: string) => element.renderRoot.querySelector(selector); - const hostControl = $('[infer="smtp-config-host"]') as InternalTextControl; - const portControl = $('[infer="smtp-config-port"]') as InternalIntegerControl; - const usernameControl = $('[infer="smtp-config-username"]') as InternalTextControl; - const passwordControl = $('[infer="smtp-config-password"]') as InternalPasswordControl; - const securityControl = $('[infer="smtp-config-security"]') as InternalRadioGroupControl; - - expect(hostControl).to.be.instanceOf(InternalTextControl); - hostControl.setValue('test.host'); - expect(JSON.parse(element.form.smtp_config!)).to.have.property('host', 'test.host'); - expect(hostControl.getValue()).to.equal('test.host'); - - expect(portControl).to.be.instanceOf(InternalIntegerControl); - portControl.setValue(1234); - expect(JSON.parse(element.form.smtp_config!)).to.have.property('port', 1234); - expect(portControl.getValue()).to.equal(1234); - - expect(usernameControl).to.be.instanceOf(InternalTextControl); - usernameControl.setValue('test-user'); - expect(JSON.parse(element.form.smtp_config!)).to.have.property('username', 'test-user'); - expect(usernameControl.getValue()).to.equal('test-user'); - - expect(passwordControl).to.be.instanceOf(InternalPasswordControl); - passwordControl.setValue('testpw'); - expect(JSON.parse(element.form.smtp_config!)).to.have.property('password', 'testpw'); - expect(passwordControl.getValue()).to.equal('testpw'); - - expect(securityControl).to.be.instanceOf(InternalRadioGroupControl); - expect(securityControl).to.have.deep.property('options', [ - { label: 'option_ssl', value: 'ssl' }, - { label: 'option_tls', value: 'tls' }, - { label: 'option_none', value: '' }, - ]); - - securityControl.setValue('ssl'); - expect(JSON.parse(element.form.smtp_config!)).to.have.property('security', 'ssl'); - expect(securityControl.getValue()).to.equal('ssl'); - }); - - it('renders a select control for country', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="country"]') as InternalSelectControl; + '[infer="essentials"] [infer="country"]' + ) as InternalSelectControl; expect(control).to.exist; expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/3', { method: 'PATCH', @@ -990,26 +664,21 @@ describe('StoreForm', () => { }); element.countries = 'https://demo.api/hapi/property_helpers/3'; + await element.requestUpdate(); await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); expect(control).to.have.deep.property('options', [ - { value: 'GB', label: 'United Kingdom' }, - { value: 'US', label: 'United States' }, + { value: 'GB', rawLabel: 'United Kingdom' }, + { value: 'US', rawLabel: 'United States' }, ]); }); - it('renders a select control for region when there are predefined regions', async () => { + it('renders a select control for region in the Essentials section when there are predefined regions', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/4', { method: 'PATCH', body: JSON.stringify({ @@ -1034,6 +703,7 @@ describe('StoreForm', () => { let control: InternalTextControl | null = null; element.regions = 'https://demo.api/hapi/property_helpers/4'; + await element.requestUpdate(); await waitUntil( () => { @@ -1045,24 +715,19 @@ describe('StoreForm', () => { ); expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.deep.property('options', [ - { value: 'SD', label: 'South Dakota' }, - { value: 'TN', label: 'Tennessee' }, + { value: 'SD', rawLabel: 'South Dakota' }, + { value: 'TN', rawLabel: 'Tennessee' }, ]); }); - it('renders a text control for region when there are no predefined regions', async () => { + it('renders a text control for region in the Essentials section when there are no predefined regions', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/4', { method: 'PATCH', body: JSON.stringify({ @@ -1072,10 +737,13 @@ describe('StoreForm', () => { let control: InternalTextControl | null = null; element.regions = 'https://demo.api/hapi/property_helpers/4'; + await element.requestUpdate(); await waitUntil( () => { - control = element.renderRoot.querySelector('foxy-internal-text-control[infer="region"]'); + control = element.renderRoot.querySelector( + '[infer="essentials"] foxy-internal-text-control[infer="region"]' + ); return !!control; }, '', @@ -1084,612 +752,810 @@ describe('StoreForm', () => { expect(control).to.exist; expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); }); - it('renders a text control for postal code', async () => { + it('renders a text control for postal code in the Essentials section', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="postal-code"]'); + const control = element.renderRoot.querySelector('[infer="essentials"] [infer="postal-code"]'); expect(control).to.exist; expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); }); - it('renders a select control for shipping address type', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="shipping-address-type"]' - ) as InternalSelectControl; + it('renders a select control for currency style in the Essentials section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="essentials"] [infer="currency-style"]' + ); expect(control).to.exist; expect(control).to.be.instanceOf(InternalSelectControl); - - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/5', { - method: 'PATCH', - body: JSON.stringify({ - values: { - residential: 'Rate as Residential', - commercial: 'Rate as Commercial', - }, - }), - }); - - element.shippingAddressTypes = 'https://demo.api/hapi/property_helpers/5'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); - + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.deep.property('options', [ - { value: 'residential', label: 'Rate as Residential' }, - { value: 'commercial', label: 'Rate as Commercial' }, + { rawLabel: '12.34', value: '101' }, + { rawLabel: 'USD 12.34', value: '001' }, + { rawLabel: '$12.34', value: '000' }, + { rawLabel: '12', value: '111' }, + { rawLabel: 'USD 12', value: '011' }, + { rawLabel: '$12', value: '010' }, ]); - }); - it('renders a checkbox for the "features_multiship" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + control?.setValue('101'); + expect(element).to.have.nested.property('form.hide_currency_symbol', true); + expect(element).to.have.nested.property('form.hide_decimal_characters', false); + expect(element).to.have.nested.property('form.use_international_currency_symbol', true); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="features-multiship"]' - ) as InternalCheckboxGroupControl; + control?.setValue('001'); + expect(element).to.have.nested.property('form.hide_currency_symbol', false); + expect(element).to.have.nested.property('form.hide_decimal_characters', false); + expect(element).to.have.nested.property('form.use_international_currency_symbol', true); - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); + control?.setValue('000'); + expect(element).to.have.nested.property('form.hide_currency_symbol', false); + expect(element).to.have.nested.property('form.hide_decimal_characters', false); + expect(element).to.have.nested.property('form.use_international_currency_symbol', false); - element.edit({ features_multiship: false }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); + control?.setValue('111'); + expect(element).to.have.nested.property('form.hide_currency_symbol', true); + expect(element).to.have.nested.property('form.hide_decimal_characters', true); + expect(element).to.have.nested.property('form.use_international_currency_symbol', true); - element.edit({ features_multiship: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); + control?.setValue('011'); + expect(element).to.have.nested.property('form.hide_currency_symbol', false); + expect(element).to.have.nested.property('form.hide_decimal_characters', true); + expect(element).to.have.nested.property('form.use_international_currency_symbol', true); - it('renders a checkbox for the "require_signed_shipping_rates" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + control?.setValue('010'); + expect(element).to.have.nested.property('form.hide_currency_symbol', false); + expect(element).to.have.nested.property('form.hide_decimal_characters', true); + expect(element).to.have.nested.property('form.use_international_currency_symbol', false); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="require-signed-shipping-rates"]' - ) as InternalCheckboxGroupControl; + element.edit({ + hide_currency_symbol: true, + hide_decimal_characters: false, + use_international_currency_symbol: true, + }); - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); + expect(control?.getValue()).to.equal('101'); - element.edit({ require_signed_shipping_rates: false }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); + element.edit({ + hide_currency_symbol: false, + hide_decimal_characters: false, + use_international_currency_symbol: true, + }); - element.edit({ require_signed_shipping_rates: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); + expect(control?.getValue()).to.equal('001'); - it('renders a warning when "require_signed_shipping_rates" is enabled', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + element.edit({ + hide_currency_symbol: false, + hide_decimal_characters: false, + use_international_currency_symbol: false, + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByKey(element, 'require_signed_shipping_rates_helper_text')).to.not.exist; + expect(control?.getValue()).to.equal('000'); - element.data = { ...element.data!, require_signed_shipping_rates: false }; - element.edit({ require_signed_shipping_rates: true }); - await element.requestUpdate(); + element.edit({ + hide_currency_symbol: true, + hide_decimal_characters: true, + use_international_currency_symbol: true, + }); - const control = element.renderRoot.querySelector( - '[infer="require-signed-shipping-rates"]' - ) as InternalCheckboxGroupControl; + expect(control?.getValue()).to.equal('111'); + + element.edit({ + hide_currency_symbol: false, + hide_decimal_characters: true, + use_international_currency_symbol: true, + }); + + expect(control?.getValue()).to.equal('011'); - const warning = control.nextElementSibling as HTMLDivElement; - const warningText = await getByKey(warning, 'require_signed_shipping_rates_helper_text'); + element.edit({ + hide_currency_symbol: false, + hide_decimal_characters: true, + use_international_currency_symbol: false, + }); - expect(warningText).to.exist; - expect(warningText).to.have.attribute('infer', ''); + expect(control?.getValue()).to.equal('010'); }); - it('renders a select control for language', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a summary control for Legacy API section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="legacy-api"]' + ); + + expect(control).to.exist; + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="language"]') as InternalSelectControl; + it('renders a password control for the legacy api key inside of the Legacy API section (JSON keys)', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="legacy-api"] foxy-internal-password-control[infer="webhook-key-api-legacy"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/6', { - method: 'PATCH', - body: JSON.stringify({ - values: { - dutch: 'Dutch', - english: 'English', - }, - }), - }); - - element.languages = 'https://demo.api/hapi/property_helpers/6'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); + element.edit({ webhook_key: JSON.stringify({ api_legacy: 'test' }) }); + expect(control?.getValue()).to.equal('test'); - expect(control).to.have.deep.property('options', [ - { value: 'dutch', label: 'Dutch' }, - { value: 'english', label: 'English' }, - ]); + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ api_legacy: 'foo' }) + ); }); - it('renders a select control for locale code', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="locale-code"]' - ) as InternalSelectControl; + it('renders a password control for the legacy api key inside of the Legacy API section (string key)', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="legacy-api"] foxy-internal-password-control[infer="webhook-key-api-legacy"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/7', { - method: 'PATCH', - body: JSON.stringify({ - values: { - en_AU: 'English locale for Australia (Currency: AUD:$)', - en_BW: 'English locale for Botswana (Currency: BWP:Pu)', - }, - }), - }); + element.edit({ webhook_key: 'test' }); + expect(control?.getValue()).to.equal('test'); - element.localeCodes = 'https://demo.api/hapi/property_helpers/7'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'test', xml_datafeed: 'test', api_legacy: 'foo', sso: 'test' }) + ); + }); - expect(control).to.have.deep.property('options', [ - { value: 'en_AU', label: 'English locale for Australia (Currency: AUD:$)' }, - { value: 'en_BW', label: 'English locale for Botswana (Currency: BWP:Pu)' }, - ]); + it('renders a summary control for Emails section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="emails"]' + ); + + expect(control).to.exist; }); - it('renders currency style selector', async () => { + it('renders a text control for "from" email in the Emails section', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - - const control = (await getByTestId(element, 'currency-style')) as HTMLDivElement; - const labels = control.querySelectorAll('label'); - const inputs = control.querySelectorAll('label input'); - - expect(labels).to.have.length(6); + const control = element.renderRoot.querySelector('[infer="emails"] [infer="from-email"]'); - inputs.forEach(input => { - expect(input).to.have.attribute('name', 'currency-style'); - expect(input).to.have.attribute('type', 'radio'); - expect(input).to.not.have.attribute('disabled'); - expect(input).to.not.have.attribute('readonly'); - }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); - expect(labels[0]).to.include.text('12.34'); - expect(labels[1]).to.include.text('USD 12.34'); - expect(labels[2]).to.include.text('$12.34'); - expect(labels[3]).to.include.text('12'); - expect(labels[4]).to.include.text('USD 12'); - expect(labels[5]).to.include.text('$12'); + element.edit({ store_email: 'a@test.com,b@test.com' }); + await element.requestUpdate(); + expect(control).to.have.attribute('placeholder', 'a@test.com'); - element.edit({ hide_currency_symbol: false }); - element.edit({ use_international_currency_symbol: false }); - element.edit({ hide_decimal_characters: false }); + element.edit({ store_email: '' }); await element.requestUpdate(); + expect(control).to.have.attribute('placeholder', 'emails.from-email.placeholder'); + }); - expect(inputs[2]).to.have.attribute('checked'); + it('renders a switch control for "Use Email DNS" setting in the Emails section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector('[infer="emails"] [infer="use-email-dns"]'); - element.edit({ hide_currency_symbol: true }); - element.edit({ use_international_currency_symbol: false }); - element.edit({ hide_decimal_characters: false }); - await element.requestUpdate(); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); + }); - expect(inputs[0]).to.have.attribute('checked'); + it('renders a switch control for "Use SMTP Config" setting in the Emails section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector('[infer="emails"] [infer="use-smtp-config"]'); - element.edit({ hide_currency_symbol: false }); - element.edit({ use_international_currency_symbol: true }); - element.edit({ hide_decimal_characters: false }); - await element.requestUpdate(); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); + }); - expect(inputs[1]).to.have.attribute('checked'); + it('renders a text control for SMTP host in the Emails section when "Use SMTP Config" is enabled', async () => { + const element = await fixture(html``); - element.edit({ hide_currency_symbol: false }); - element.edit({ hide_decimal_characters: false }); - element.edit({ use_international_currency_symbol: true }); - await element.requestUpdate(); + expect( + element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-host"]' + ) + ).to.not.exist; - expect(inputs[1]).to.have.attribute('checked'); + element.edit({ + smtp_config: JSON.stringify({ + enabled: true, + host: 'test.com', + port: '123', + username: 'test', + password: 'test', + security: 'ssl', + }), + }); - element.edit({ hide_currency_symbol: true }); - element.edit({ hide_decimal_characters: false }); - element.edit({ use_international_currency_symbol: true }); await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-host"]' + ); - expect(inputs[0]).to.have.attribute('checked'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('test.com'); - element.edit({ hide_currency_symbol: false }); - element.edit({ hide_decimal_characters: true }); - element.edit({ use_international_currency_symbol: true }); - await element.requestUpdate(); + control?.setValue('foo.bar'); + expect(JSON.parse(element.form.smtp_config!)).to.have.property('host', 'foo.bar'); + }); - expect(inputs[4]).to.have.attribute('checked'); + it('renders a number control for SMTP port in the Emails section when "Use SMTP Config" is enabled', async () => { + const element = await fixture(html``); - element.edit({ hide_currency_symbol: true }); - element.edit({ hide_decimal_characters: true }); - element.edit({ use_international_currency_symbol: true }); - await element.requestUpdate(); + expect( + element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-port"]' + ) + ).to.not.exist; - expect(inputs[3]).to.have.attribute('checked'); + element.edit({ + smtp_config: JSON.stringify({ + enabled: true, + host: 'test.com', + port: '123', + username: 'test', + password: 'test', + security: 'ssl', + }), + }); - element.edit({ hide_currency_symbol: true }); - element.edit({ hide_decimal_characters: true }); - element.edit({ use_international_currency_symbol: false }); await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-port"]' + ); - expect(inputs[3]).to.have.attribute('checked'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalNumberControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('step', '1'); + expect(control).to.have.attribute('min', '0'); + expect(control?.getValue()).to.equal(123); + + control?.setValue(456); + expect(JSON.parse(element.form.smtp_config!)).to.have.property('port', '456'); }); - it('disables currency style selector if it matches disabled selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for SMTP username in the Emails section when "Use SMTP Config" is enabled', async () => { + const element = await fixture(html``); + + expect( + element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-username"]' + ) + ).to.not.exist; + + element.edit({ + smtp_config: JSON.stringify({ + enabled: true, + host: 'test.com', + port: '123', + username: 'test', + password: 'test', + security: 'ssl', + }), + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-username"]' + ); - const control = (await getByTestId(element, 'currency-style')) as HTMLDivElement; - const inputs = control.querySelectorAll('label input'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('test'); - inputs.forEach(input => expect(input).to.have.attribute('disabled')); + control?.setValue('foo'); + expect(JSON.parse(element.form.smtp_config!)).to.have.property('username', 'foo'); }); - it('makes currency style selector readonly if it matches readonly selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a password control for SMTP password in the Emails section when "Use SMTP Config" is enabled', async () => { + const element = await fixture(html``); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + expect( + element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-password"]' + ) + ).to.not.exist; - const control = (await getByTestId(element, 'currency-style')) as HTMLDivElement; - const inputs = control.querySelectorAll('label input'); + element.edit({ + smtp_config: JSON.stringify({ + enabled: true, + host: 'test.com', + port: '123', + username: 'test', + password: 'test', + security: 'ssl', + }), + }); - inputs.forEach(input => expect(input).to.have.attribute('readonly')); - }); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-password"]' + ); - it('hides currency style selector if it matches hidden selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalPasswordControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('test'); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByTestId(element, 'currency-style')).to.not.exist; + control?.setValue('foo'); + expect(JSON.parse(element.form.smtp_config!)).to.have.property('password', 'foo'); }); - it('renders before and after slots/templates for currency style selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a select control for SMTP security in the Emails section when "Use SMTP Config" is enabled', async () => { + const element = await fixture(html``); - stub(element, 'renderTemplateOrSlot').callsFake(name => html`
`); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + expect( + element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-security"]' + ) + ).to.not.exist; - const control = (await getByTestId(element, 'currency-style')) as HTMLDivElement; - expect(control.firstElementChild).to.have.attribute('data-slot', 'currency-style:before'); - expect(control.lastElementChild).to.have.attribute('data-slot', 'currency-style:after'); - }); + element.edit({ + smtp_config: JSON.stringify({ + enabled: true, + host: 'test.com', + port: '123', + username: 'test', + password: 'test', + security: 'ssl', + }), + }); - it('renders currency style label and helper text', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="emails"] [infer="smtp-config-security"]' + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('ssl'); + expect(control).to.have.deep.property('options', [ + { label: 'option_ssl', value: 'ssl' }, + { label: 'option_tls', value: 'tls' }, + { label: 'option_none', value: '' }, + ]); - const control = (await getByTestId(element, 'currency-style')) as HTMLDivElement; - const label = await getByKey(control, 'currency_style_label'); - const helperText = await getByKey(control, 'currency_style_helper_text'); + control?.setValue('tls'); + expect(JSON.parse(element.form.smtp_config!)).to.have.property('security', 'tls'); + }); - expect(label).to.exist; - expect(label).to.have.attribute('infer', ''); + it('renders a summary control for Shipping section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="shipping"]' + ); - expect(helperText).to.exist; - expect(helperText).to.have.attribute('infer', ''); + expect(control).to.exist; }); - it('renders a checkbox enabling custom transaction id display', async () => { + it('renders a select control for shipping address type in the Shipping section', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="custom-display-id-config-enabled"]' - ) as InternalCheckboxGroupControl; + await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/5', { + method: 'PATCH', + body: JSON.stringify({ + values: { + residential: 'Rate as Residential', + commercial: 'Rate as Commercial', + determine_by_company: 'Rate based on Company field', + }, + }), + }); + + element.shippingAddressTypes = 'https://demo.api/hapi/property_helpers/5'; + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="shipping"] [infer="shipping-address-type"]' + ); - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); + await waitUntil(() => !!control?.options.length, '', { timeout: 5000 }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, + { value: 'residential', rawLabel: 'Rate as Residential' }, + { value: 'commercial', rawLabel: 'Rate as Commercial' }, + { value: 'determine_by_company', rawLabel: 'Rate based on Company field' }, ]); + }); - element.edit({ custom_display_id_config: '' }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); + it('renders a switch control for Features Multiship flag in the Shipping section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="shipping"] [infer="features-multiship"]' + ); - element.edit({ - custom_display_id_config: JSON.stringify({ - enabled: true, - start: '0', - length: '0', - prefix: '', - suffix: '', - transaction_journal_entries: { - enabled: false, - transaction_separator: '', - log_detail_request_types: { - transaction_authcapture: { prefix: '' }, - transaction_capture: { prefix: '' }, - transaction_refund: { prefix: '' }, - transaction_void: { prefix: '' }, - }, - }, - }), - }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); + }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); + it('renders a switch control for Require Signed Shipping Rates flag in the Shipping section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="shipping"] [infer="require-signed-shipping-rates"]' + ); - control.setValue([]); - expect(JSON.parse(element.form.custom_display_id_config!)).to.have.property('enabled', false); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); + }); - control.setValue(['checked']); - expect(JSON.parse(element.form.custom_display_id_config!)).to.have.property('enabled', true); + it('renders a summary control for Cart section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector('foxy-internal-summary-control[infer="cart"]'); + expect(control).to.exist; }); - it('renders start, length, prefix, suffix controls if custom transaction id display is on', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a frequency control for App Session Time setting in the Cart section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="cart"] [infer="app-session-time"]' + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ custom_display_id_config: '' }); - await element.requestUpdate(); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalFrequencyControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.deep.property('options', [ + { value: 's', label: 'second' }, + { value: 'm', label: 'minute' }, + { value: 'h', label: 'hour' }, + { value: 'd', label: 'day' }, + ]); - ['start', 'length', 'prefix', 'suffix'].forEach(field => { - const selector = `[infer="custom-display-id-config-${field}"]`; - const control = element.renderRoot.querySelector(selector); - expect(control).to.not.exist; - }); + expect(control?.getValue()).to.equal('12h'); - element.edit({ - custom_display_id_config: JSON.stringify({ - enabled: true, - start: '0', - length: '0', - prefix: '', - suffix: '', - transaction_journal_entries: { - enabled: false, - transaction_separator: '', - log_detail_request_types: { - transaction_authcapture: { prefix: '' }, - transaction_capture: { prefix: '' }, - transaction_refund: { prefix: '' }, - transaction_void: { prefix: '' }, - }, - }, - }), - }); + element.edit({ app_session_time: 45 }); + expect(control?.getValue()).to.equal('45s'); - await element.requestUpdate(); + element.edit({ app_session_time: 45 * 60 }); + expect(control?.getValue()).to.equal('45m'); - for (const field of ['start', 'length', 'prefix', 'suffix']) { - const control = element.renderRoot.querySelector( - `[infer="custom-display-id-config-${field}"]` - ) as InternalTextControl | InternalIntegerControl; + element.edit({ app_session_time: 45 * 60 * 60 }); + expect(control?.getValue()).to.equal('45h'); - const config = JSON.parse(element.form.custom_display_id_config as string); + element.edit({ app_session_time: 45 * 60 * 60 * 24 }); + expect(control?.getValue()).to.equal('45d'); - if (field === 'start' || field === 'length') { - config[field] = 123; - element.edit({ custom_display_id_config: JSON.stringify(config) }); - await element.requestUpdate(); + control?.setValue('45s'); + expect(element).to.have.nested.property('form.app_session_time', 45); + + control?.setValue('45m'); + expect(element).to.have.nested.property('form.app_session_time', 45 * 60); + + control?.setValue('45h'); + expect(element).to.have.nested.property('form.app_session_time', 45 * 60 * 60); + + control?.setValue('45d'); + expect(element).to.have.nested.property('form.app_session_time', 45 * 60 * 60 * 24); + }); - expect(control).to.be.instanceOf(InternalIntegerControl); - expect(control.getValue()).to.equal(123); + it('renders a switch control for Products Require Expires Property flag in the Cart section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="cart"] [infer="products-require-expires-property"]' + ); - control.setValue(456); - const newConfig = JSON.parse(element.form.custom_display_id_config as string); - expect(newConfig).to.have.property(field, '456'); - } else { - config[field] = 'foobar'; - element.edit({ custom_display_id_config: JSON.stringify(config) }); - await element.requestUpdate(); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); + }); - expect(control).to.be.instanceOf(InternalTextControl); - expect(control.getValue()).to.equal('foobar'); + it('renders a switch control for Use Cart Validation flag in the Cart section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="cart"] [infer="use-cart-validation"]' + ); - control.setValue('bazqux'); - const newConfig = JSON.parse(element.form.custom_display_id_config as string); - expect(newConfig).to.have.property(field, 'bazqux'); - } - } + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control).to.have.attribute('helper-text-as-tooltip'); }); - it('renders examples if custom transaction id display is on', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a password control for the cart signing key in the Cart section (JSON keys)', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="cart"] foxy-internal-password-control[infer="webhook-key-cart-signing"]' + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ custom_display_id_config: '' }); - expect(await getByTestId(element, 'custom-display-id-config-examples')).to.not.exist; + expect(control).to.exist; + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); element.edit({ - custom_display_id_config: JSON.stringify({ - enabled: true, - start: '8', - length: '12', - prefix: 'FOO-', - suffix: '-BAR', - transaction_journal_entries: { - enabled: false, - transaction_separator: '', - log_detail_request_types: { - transaction_authcapture: { prefix: '' }, - transaction_capture: { prefix: '' }, - transaction_refund: { prefix: '' }, - transaction_void: { prefix: '' }, - }, - }, + webhook_key: JSON.stringify({ + cart_signing: 'test', + xml_datafeed: 'test', + api_legacy: 'test', + sso: 'test', }), }); - const examples = (await getByTestId( - element, - 'custom-display-id-config-examples' - )) as HTMLTableElement; + expect(control?.getValue()).to.equal('test'); + + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'foo', xml_datafeed: 'test', api_legacy: 'test', sso: 'test' }) + ); + }); + + it('renders a password control for the cart signing key in the Cart section (string key)', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="cart"] foxy-internal-password-control[infer="webhook-key-cart-signing"]' + ); - expect(examples).to.exist; - expect(examples.rows).to.have.length(2); - expect(examples.rows[0].cells).to.have.length(2); - expect(examples.rows[1].cells).to.have.length(2); + expect(control).to.exist; + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); - const label1Selector = 'foxy-i18n[infer=""][key="custom-display-id-config-first-example"]'; - expect(examples.rows[0].cells[0].querySelector(label1Selector)).to.exist; - expect(examples.rows[0].cells[1].textContent?.trim()).to.equal('FOO-0008-BAR'); + element.edit({ webhook_key: 'test' }); + expect(control?.getValue()).to.equal('test'); - const label2Selector = 'foxy-i18n[infer=""][key="custom-display-id-config-random-example"]'; - expect(examples.rows[1].cells[0].querySelector(label2Selector)).to.exist; + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'foo', xml_datafeed: 'test', api_legacy: 'test', sso: 'test' }) + ); + }); - const randomExample = examples.rows[1].cells[1].textContent?.trim(); - const randomExamplePrefix = randomExample!.split('-')[0] as string; - const randomExampleId = randomExample!.split('-')[1] as string; - const randomExampleSuffix = randomExample!.split('-')[2] as string; + it('renders a summary control for Checkout section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="checkout"]' + ); - expect(randomExamplePrefix).to.equal('FOO'); - expect(parseInt(randomExampleId)).to.be.greaterThanOrEqual(8); - expect(randomExampleId).to.have.length(4); - expect(randomExampleSuffix).to.equal('BAR'); + expect(control).to.exist; }); - it('renders a checkbox enabling custom transaction journal entry id display when custom transaction id display is on', async () => { + it('renders a select control for customer password hash type in the Checkout section', async () => { const router = createRouter(); const element = await fixture(html` - router.handleEvent(evt)} - > - + router.handleEvent(evt)}> `); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - let control = element.renderRoot.querySelector( - '[infer="custom-display-id-config-transaction-journal-entries-enabled"]' - ) as InternalCheckboxGroupControl; + const control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="customer-password-hash-type"]' + ) as InternalSelectControl; - expect(control).to.not.exist; + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.have.attribute('layout', 'summary-item'); - element.edit({ + await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/9', { + method: 'PATCH', + body: JSON.stringify({ + values: { + concrete5: { description: 'Concrete5', config: 'PASSWORD_SALT' }, + phpass: { description: 'phpass', config: 8 }, + }, + }), + }); + + element.customerPasswordHashTypes = 'https://demo.api/hapi/property_helpers/9'; + await element.requestUpdate(); + await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); + + expect(control).to.have.deep.property('options', [ + { value: 'concrete5', rawLabel: 'Concrete5' }, + { value: 'phpass', rawLabel: 'phpass' }, + ]); + + control.setValue('concrete5'); + expect(element).to.have.nested.property('form.customer_password_hash_config', 'PASSWORD_SALT'); + + control.setValue('phpass'); + expect(element).to.have.nested.property('form.customer_password_hash_config', 8); + }); + + it('renders a text or a number control for customer password hash config in the Checkout section', async () => { + const router = createRouter(); + const element = await fixture(html` + router.handleEvent(evt)}> + `); + + element.edit({ customer_password_hash_config: 'foo' }); + await element.requestUpdate(); + let control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="customer-password-hash-config"]' + ); + + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + + element.edit({ customer_password_hash_config: 8 }); + await element.requestUpdate(); + control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="customer-password-hash-config"]' + ); + + expect(control).to.be.instanceOf(InternalNumberControl); + expect(control).to.have.attribute('layout', 'summary-item'); + }); + + it('renders a password control for UOE password in the Checkout section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="checkout"] foxy-internal-password-control[infer="unified-order-entry-password"]' + ); + + expect(control).to.exist; + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + }); + + it('renders a switch control for Use Single Sign-On flag in the Checkout section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="use-single-sign-on"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + }); + + it('renders a text control for SSO URL in the Checkout section when SSO is enabled', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="checkout"] [infer="single-sign-on-url"]' + ) + ).to.not.exist; + + element.edit({ use_single_sign_on: true }); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="single-sign-on-url"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + }); + + it('renders a password control for SSO key in the Checkout section when SSO is enabled (JSON keys)', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="checkout"] [infer="webhook-key-sso"]' + ) + ).to.not.exist; + + element.edit({ + use_single_sign_on: true, + webhook_key: JSON.stringify({ + cart_signing: 'test', + xml_datafeed: 'test', + api_legacy: 'test', + sso: 'test', + }), + }); + + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="webhook-key-sso"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalPasswordControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); + expect(control?.getValue()).to.equal('test'); + + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'test', xml_datafeed: 'test', api_legacy: 'test', sso: 'foo' }) + ); + }); + + it('renders a password control for SSO key in the Checkout section when SSO is enabled (string key)', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="checkout"] [infer="webhook-key-sso"]' + ) + ).to.not.exist; + + element.edit({ use_single_sign_on: true, webhook_key: 'test' }); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="checkout"] [infer="webhook-key-sso"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalPasswordControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); + expect(control?.getValue()).to.equal('test'); + + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'test', xml_datafeed: 'test', api_legacy: 'test', sso: 'foo' }) + ); + }); + + it('renders a summary control for Receipt section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="receipt"]' + ); + + expect(control).to.exist; + }); + + it('renders a text control for Receipt Continue URL setting in the Receipt section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="receipt"] [infer="receipt-continue-url"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + }); + + it('renders a switch control for BCC on Receipt Email flag in the Receipt section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="receipt"] [infer="bcc-on-receipt-email"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + }); + + it('renders a summary control for Custom Display ID Config section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + 'foxy-internal-summary-control[infer="custom-display-id-config"]' + ); + + expect(control).to.exist; + }); + + it('renders a switch control enabling custom Display ID in the Custom Display ID Config section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-enabled"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control?.getValue()).to.be.false; + + element.edit({ custom_display_id_config: JSON.stringify({ enabled: true, start: '0', @@ -1710,25 +1576,29 @@ describe('StoreForm', () => { }); await element.requestUpdate(); - control = element.renderRoot.querySelector( - '[infer="custom-display-id-config-transaction-journal-entries-enabled"]' - ) as InternalCheckboxGroupControl; + expect(control?.getValue()).to.be.true; + control?.setValue(false); + expect(JSON.parse(element.form.custom_display_id_config!)).to.have.property('enabled', false); + }); - expect(control?.getValue()).to.deep.equal([]); - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); + it('renders a number control for custom Display ID start in the Custom Display ID Config section when Custom Display ID is enabled', async () => { + const element = await fixture(html``); + + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-start"]' + ) + ).to.not.exist; element.edit({ custom_display_id_config: JSON.stringify({ enabled: true, - start: '0', + start: '2', length: '0', prefix: '', suffix: '', transaction_journal_entries: { - enabled: true, + enabled: false, transaction_separator: '', log_detail_request_types: { transaction_authcapture: { prefix: '' }, @@ -1741,59 +1611,39 @@ describe('StoreForm', () => { }); await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - - control.setValue([]); - expect(JSON.parse(element.form.custom_display_id_config!)).to.have.nested.property( - 'transaction_journal_entries.enabled', - false + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-start"]' ); - control.setValue(['checked']); - expect(JSON.parse(element.form.custom_display_id_config!)).to.have.nested.property( - 'transaction_journal_entries.enabled', - true - ); - }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalNumberControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('step', '1'); + expect(control).to.have.attribute('min', '0'); + expect(control?.getValue()).to.equal(2); - it('renders prefix and separator settings if custom transaction journal id display is on', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + control?.setValue(1); + expect(JSON.parse(element.form.custom_display_id_config!).start).to.equal('1'); + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ custom_display_id_config: '' }); - await element.requestUpdate(); + it('renders a number control for custom Display ID length in the Custom Display ID Config section when Custom Display ID is enabled', async () => { + const element = await fixture(html``); - const paths = [ - 'transaction_separator', - 'log_detail_request_types.transaction_authcapture.prefix', - 'log_detail_request_types.transaction_capture.prefix', - 'log_detail_request_types.transaction_void.prefix', - 'log_detail_request_types.transaction_refund.prefix', - ]; - - paths.forEach(path => { - const field = path.replace(/\.|_/g, '-'); - const selector = `[infer="custom-display-id-config-transaction-journal-entries-${field}"]`; - const control = element.renderRoot.querySelector(selector); - expect(control).to.not.exist; - }); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-length"]' + ) + ).to.not.exist; element.edit({ custom_display_id_config: JSON.stringify({ enabled: true, start: '0', - length: '0', + length: '2', prefix: '', suffix: '', transaction_journal_entries: { - enabled: true, + enabled: false, transaction_separator: '', log_detail_request_types: { transaction_authcapture: { prefix: '' }, @@ -1806,102 +1656,122 @@ describe('StoreForm', () => { }); await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-length"]' + ); + + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalNumberControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('step', '1'); + expect(control).to.have.attribute('min', '0'); + expect(control?.getValue()).to.equal(2); - for (const path of paths) { - const field = path.replace(/\.|_/g, '-'); - const selector = `[infer="custom-display-id-config-transaction-journal-entries-${field}"]`; - const control = element.renderRoot.querySelector(selector) as InternalTextControl; + control?.setValue(1); + expect(JSON.parse(element.form.custom_display_id_config!).length).to.equal('1'); + }); + + it('renders a text control for custom Display ID prefix in the Custom Display ID Config section when Custom Display ID is enabled', async () => { + const element = await fixture(html``); - const topConfig = JSON.parse(element.form.custom_display_id_config as string); - const config = topConfig.transaction_journal_entries; + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-prefix"]' + ) + ).to.not.exist; + + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: true, + start: '0', + length: '0', + prefix: 'foo', + suffix: '', + transaction_journal_entries: { + enabled: false, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, + }, + }, + }), + }); - set(config, path, 'foobar'); - element.edit({ custom_display_id_config: JSON.stringify(topConfig) }); - await element.requestUpdate(); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-prefix"]' + ); - expect(control).to.be.instanceOf(InternalTextControl); - expect(control.getValue()).to.equal('foobar'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); - control.setValue('bazqux'); - const newTopConfig = JSON.parse(element.form.custom_display_id_config as string); - const newConfig = newTopConfig.transaction_journal_entries; - expect(newConfig).to.have.nested.property(path, 'bazqux'); - } + control?.setValue('bar'); + expect(JSON.parse(element.form.custom_display_id_config!).prefix).to.equal('bar'); }); - it('renders examples if custom transaction journal id display is on', async () => { - const testId = 'custom-display-id-config-transaction-journal-entries-examples'; - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for custom Display ID suffix in the Custom Display ID Config section when Custom Display ID is enabled', async () => { + const element = await fixture(html``); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ custom_display_id_config: '' }); - expect(await getByTestId(element, testId)).to.not.exist; + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-suffix"]' + ) + ).to.not.exist; element.edit({ custom_display_id_config: JSON.stringify({ enabled: true, - start: '8', - length: '12', - prefix: 'FOO-', - suffix: '-BAR', + start: '0', + length: '0', + prefix: '', + suffix: 'foo', transaction_journal_entries: { - enabled: true, - transaction_separator: '|', + enabled: false, + transaction_separator: '', log_detail_request_types: { - transaction_authcapture: { prefix: 'AU-' }, - transaction_capture: { prefix: 'CA-' }, - transaction_refund: { prefix: 'RE-' }, - transaction_void: { prefix: 'VO-' }, + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, }, }, }), }); - const examples = (await getByTestId(element, testId)) as HTMLTableElement; - - expect(examples).to.exist; - expect(examples.rows).to.have.length(4); - Array.from(examples.rows).forEach(row => expect(row).to.have.length(2)); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-suffix"]' + ); - const getLabelSelector = (type: string) => { - return `foxy-i18n[infer=""][key="custom-display-id-config-transaction-journal-entries-${type}-example"]`; - }; + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); - expect(examples.rows[0].cells[0].querySelector(getLabelSelector('authcapture'))).to.exist; - expect(examples.rows[1].cells[0].querySelector(getLabelSelector('capture'))).to.exist; - expect(examples.rows[2].cells[0].querySelector(getLabelSelector('void'))).to.exist; - expect(examples.rows[3].cells[0].querySelector(getLabelSelector('refund'))).to.exist; + control?.setValue('bar'); + expect(JSON.parse(element.form.custom_display_id_config!).suffix).to.equal('bar'); + }); - const authcaptureExample = examples.rows[0].cells[1].textContent?.trim(); - const captureExample = examples.rows[1].cells[1].textContent?.trim(); - const voidExample = examples.rows[2].cells[1].textContent?.trim(); - const refundExample = examples.rows[3].cells[1].textContent?.trim(); + it('renders examples in the Custom Display ID Config section when Custom Display ID is enabled', async () => { + const element = await fixture(html``); - expect(authcaptureExample).to.match(/FOO-\d{4}-BAR\|AU-\d+/); - expect(captureExample).to.match(/FOO-\d{4}-BAR\|CA-\d+/); - expect(voidExample).to.match(/FOO-\d{4}-BAR\|VO-\d+/); - expect(refundExample).to.match(/FOO-\d{4}-BAR\|RE-\d+/); - }); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-first-example"]' + ) + ).to.not.exist; - it('hides custom transaction id display settings if targeted by hidden selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-random-example"]' + ) + ).to.not.exist; - await waitUntil(() => !!element.data, '', { timeout: 5000 }); element.edit({ custom_display_id_config: JSON.stringify({ enabled: true, @@ -1910,545 +1780,509 @@ describe('StoreForm', () => { prefix: 'FOO-', suffix: '-BAR', transaction_journal_entries: { - enabled: true, - transaction_separator: '|', + enabled: false, + transaction_separator: '', log_detail_request_types: { - transaction_authcapture: { prefix: 'AU-' }, - transaction_capture: { prefix: 'CA-' }, - transaction_refund: { prefix: 'RE-' }, - transaction_void: { prefix: 'VO-' }, + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, }, }, }), }); await element.requestUpdate(); - const controls = element.renderRoot.querySelectorAll('[infer^="custom-display-id-config-"]'); - expect(controls).to.be.empty; - }); - it('renders a text control for receipt continue url', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + const firstExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-first-example"]' + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="receipt-continue-url"]'); + const randomExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-random-example"]' + ); - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); - }); + expect(firstExample).to.exist; + expect(firstExample).to.be.instanceOf(I18n); + expect(firstExample).to.have.attribute('infer', ''); + expect(firstExample?.nextElementSibling?.textContent?.trim()).to.equal('FOO-0008-BAR'); - it('renders a frequency control for app session time', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + expect(randomExample).to.exist; + expect(randomExample).to.be.instanceOf(I18n); + expect(randomExample).to.have.attribute('infer', ''); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="app-session-time"]' - ) as InternalFrequencyControl; + const randomExampleText = randomExample?.nextElementSibling?.textContent?.trim(); + const randomExamplePrefix = randomExampleText?.split('-')[0]; + const randomExampleId = randomExampleText?.split('-')[1]; + const randomExampleSuffix = randomExampleText?.split('-')[2]; - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalFrequencyControl); - expect(control).to.have.deep.property('options', [ - { value: 's', label: 'second' }, - { value: 'm', label: 'minute' }, - { value: 'h', label: 'hour' }, - { value: 'd', label: 'day' }, - ]); + expect(randomExamplePrefix).to.equal('FOO'); + expect(parseInt(randomExampleId as string)).to.be.greaterThanOrEqual(8); + expect(randomExampleId).to.have.length(4); + expect(randomExampleSuffix).to.equal('BAR'); + }); - element.edit({ app_session_time: 86400 }); - expect(control.getValue()).to.equal('1d'); + it('renders a switch control enabling custom Transaction Journal Display ID in the Custom Display ID Config section', async () => { + const element = await fixture(html``); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-enabled"]' + ); - element.edit({ app_session_time: 3600 }); - expect(control.getValue()).to.equal('1h'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalSwitchControl); + expect(control?.getValue()).to.be.false; - element.edit({ app_session_time: 60 }); - expect(control.getValue()).to.equal('1m'); + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, + }, + }, + }), + }); - element.edit({ app_session_time: 1 }); - expect(control.getValue()).to.equal('1s'); + await element.requestUpdate(); + expect(control?.getValue()).to.be.true; + control?.setValue(false); + expect(JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries.enabled) + .to.be.false; + }); - control.setValue('1d'); - expect(element).to.have.nested.property('form.app_session_time', 86400); + it('renders a text control for authcapture prefix in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); - control.setValue('1h'); - expect(element).to.have.nested.property('form.app_session_time', 3600); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-authcapture-prefix"]' + ) + ).to.not.exist; - control.setValue('1m'); - expect(element).to.have.nested.property('form.app_session_time', 60); - - control.setValue('1s'); - expect(element).to.have.nested.property('form.app_session_time', 1); - }); - - it('renders a checkbox for the "products_require_expires_property" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="products-require-expires-property"]' - ) as InternalCheckboxGroupControl; - - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: 'foo' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, + }, + }, + }), + }); - element.edit({ products_require_expires_property: false }); await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-authcapture-prefix"]' + ); - element.edit({ products_require_expires_property: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); + + control?.setValue('bar'); + expect( + JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries + .log_detail_request_types.transaction_authcapture.prefix + ).to.equal('bar'); }); - it('renders a warning when "products_require_expires_property" is enabled', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for capture prefix in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByKey(element, 'products_require_expires_property_helper_text')).to.not.exist; + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-capture-prefix"]' + ) + ).to.not.exist; - element.data = { ...element.data!, products_require_expires_property: false }; - element.edit({ products_require_expires_property: true }); - await element.requestUpdate(); + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: 'foo' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, + }, + }, + }), + }); - const control = element.renderRoot.querySelector( - '[infer="products-require-expires-property"]' - ) as InternalCheckboxGroupControl; + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-capture-prefix"]' + ); - const warning = control.nextElementSibling as HTMLDivElement; - const warningText = await getByKey(warning, 'products_require_expires_property_helper_text'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); - expect(warningText).to.exist; - expect(warningText).to.have.attribute('infer', ''); + control?.setValue('bar'); + expect( + JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries + .log_detail_request_types.transaction_capture.prefix + ).to.equal('bar'); }); - it('renders a checkbox for the "use_cart_validation" flag', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for void prefix in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="use-cart-validation"]' - ) as InternalCheckboxGroupControl; + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-void-prefix"]' + ) + ).to.not.exist; - expect(control).to.be.instanceOf(InternalCheckboxGroupControl); - expect(control).to.have.deep.property('options', [ - { label: 'option_checked', value: 'checked' }, - ]); + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: 'foo' }, + }, + }, + }), + }); - element.edit({ use_cart_validation: false }); await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal([]); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-void-prefix"]' + ); - element.edit({ use_cart_validation: true }); - await element.requestUpdate(); - expect(control?.getValue()).to.deep.equal(['checked']); - }); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); - it('renders a warning when "use_cart_validation" is enabled', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + control?.setValue('bar'); + expect( + JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries + .log_detail_request_types.transaction_void.prefix + ).to.equal('bar'); + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByKey(element, 'use_cart_validation_helper_text')).to.not.exist; + it('renders a text control for refund prefix in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); - element.data = { ...element.data!, use_cart_validation: false }; - element.edit({ use_cart_validation: true }); - await element.requestUpdate(); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-refund-prefix"]' + ) + ).to.not.exist; - const control = element.renderRoot.querySelector( - '[infer="use-cart-validation"]' - ) as InternalCheckboxGroupControl; + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: '', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: 'foo' }, + transaction_void: { prefix: '' }, + }, + }, + }), + }); - const warning = control.nextElementSibling as HTMLDivElement; - const warningText = await getByKey(warning, 'use_cart_validation_helper_text'); - const warningLink = await getByTag(warning, 'a'); + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-refund-prefix"]' + ); - expect(warningText).to.exist; - expect(warningText).to.have.attribute('infer', ''); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); - expect(warningLink).to.exist; - expect(warningLink).to.include.text( - 'HMAC Product Verification: Locking Down your Add-To-Cart Links and Forms' - ); - expect(warningLink).to.have.attribute( - 'href', - 'https://wiki.foxycart.com/v/2.0/hmac_validation' - ); + control?.setValue('bar'); + expect( + JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries + .log_detail_request_types.transaction_refund.prefix + ).to.equal('bar'); }); - it('renders a select control for checkout type', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="checkout-type"]' - ) as InternalSelectControl; + it('renders a text control for transaction separator in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalSelectControl); + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-transaction-separator"]' + ) + ).to.not.exist; - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/8', { - method: 'PATCH', - body: JSON.stringify({ - values: { - default_account: 'Default account', - default_guest: 'Default guest', + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: false, + start: '0', + length: '0', + prefix: '', + suffix: '', + transaction_journal_entries: { + enabled: true, + transaction_separator: 'foo', + log_detail_request_types: { + transaction_authcapture: { prefix: '' }, + transaction_capture: { prefix: '' }, + transaction_refund: { prefix: '' }, + transaction_void: { prefix: '' }, + }, }, }), }); - element.checkoutTypes = 'https://demo.api/hapi/property_helpers/8'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); - - expect(control).to.have.deep.property('options', [ - { value: 'default_account', label: 'Default account' }, - { value: 'default_guest', label: 'Default guest' }, - ]); - }); - - it('renders a select control for customer password hash type', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector( - '[infer="customer-password-hash-type"]' - ) as InternalSelectControl; + await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-transaction-separator"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalSelectControl); + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control?.getValue()).to.equal('foo'); + + control?.setValue('bar'); + expect( + JSON.parse(element.form.custom_display_id_config!).transaction_journal_entries + .transaction_separator + ).to.equal('bar'); + }); + + it('renders examples in the Custom Display ID Config section when Custom Transaction Journal Display ID is enabled', async () => { + const element = await fixture(html``); + + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-authcapture-example"]' + ) + ).to.not.exist; + + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-capture-example"]' + ) + ).to.not.exist; + + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-void-example"]' + ) + ).to.not.exist; + + expect( + element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [infer="custom-display-id-config-transaction-journal-entries-refund-example"]' + ) + ).to.not.exist; - await new Form.API(element).fetch('https://demo.api/hapi/property_helpers/9', { - method: 'PATCH', - body: JSON.stringify({ - values: { - concrete5: { description: 'Concrete5', config: 'PASSWORD_SALT' }, - phpass: { description: 'phpass', config: 8 }, + element.edit({ + custom_display_id_config: JSON.stringify({ + enabled: true, + start: '8', + length: '12', + prefix: 'FOO-', + suffix: '-BAR', + transaction_journal_entries: { + enabled: true, + transaction_separator: '|', + log_detail_request_types: { + transaction_authcapture: { prefix: 'AU-' }, + transaction_capture: { prefix: 'CA-' }, + transaction_refund: { prefix: 'RE-' }, + transaction_void: { prefix: 'VO-' }, + }, }, }), }); - element.customerPasswordHashTypes = 'https://demo.api/hapi/property_helpers/9'; - await waitUntil(() => !!control.options.length, '', { timeout: 5000 }); - - expect(control).to.have.deep.property('options', [ - { value: 'concrete5', label: 'Concrete5' }, - { value: 'phpass', label: 'phpass' }, - ]); - - control.setValue('concrete5'); - expect(element).to.have.nested.property('form.customer_password_hash_config', 'PASSWORD_SALT'); - - control.setValue('phpass'); - expect(element).to.have.nested.property('form.customer_password_hash_config', 8); - }); + await element.requestUpdate(); - it('renders a text or a number control for customer password hash config', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + const authcaptureExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-transaction-journal-entries-authcapture-example"]' + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ customer_password_hash_config: 'foo' }); + const captureExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-transaction-journal-entries-capture-example"]' + ); - await element.requestUpdate(); - let control = element.renderRoot.querySelector('[infer="customer-password-hash-config"]'); - expect(control).to.be.instanceOf(InternalTextControl); + const voidExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-transaction-journal-entries-void-example"]' + ); - element.edit({ customer_password_hash_config: 8 }); - await element.requestUpdate(); - control = element.renderRoot.querySelector('[infer="customer-password-hash-config"]'); + const refundExample = element.renderRoot.querySelector( + '[infer="custom-display-id-config"] [key="custom-display-id-config-transaction-journal-entries-refund-example"]' + ); - expect(control).to.be.instanceOf(InternalNumberControl); - }); + expect(authcaptureExample).to.exist; + expect(authcaptureExample).to.be.instanceOf(I18n); + expect(authcaptureExample).to.have.attribute('infer', ''); + expect(authcaptureExample?.nextElementSibling?.textContent?.trim()).to.match( + /FOO-\d{4}-BAR\|AU-\d{3}/ + ); - it('renders a password control for unified order entry password', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + expect(captureExample).to.exist; + expect(captureExample).to.be.instanceOf(I18n); + expect(captureExample).to.have.attribute('infer', ''); + expect(captureExample?.nextElementSibling?.textContent?.trim()).to.match( + /FOO-\d{4}-BAR\|CA-\d{3}/ + ); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - const control = element.renderRoot.querySelector('[infer="unified-order-entry-password"]'); + expect(voidExample).to.exist; + expect(voidExample).to.be.instanceOf(I18n); + expect(voidExample).to.have.attribute('infer', ''); + expect(voidExample?.nextElementSibling?.textContent?.trim()).to.match( + /FOO-\d{4}-BAR\|VO-\d{3}/ + ); - expect(control).to.exist; - expect(control).to.be.instanceOf(InternalPasswordControl); + expect(refundExample).to.exist; + expect(refundExample).to.be.instanceOf(I18n); + expect(refundExample).to.have.attribute('infer', ''); + expect(refundExample?.nextElementSibling?.textContent?.trim()).to.match( + /FOO-\d{4}-BAR\|RE-\d{3}/ + ); }); - it('renders a text control for sso url', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + it('renders a summary control for XML Datafeed settings', async () => { + const element = await fixture(html``); const control = element.renderRoot.querySelector( - '[infer="single-sign-on-url"]' - ) as InternalTextControl; + 'foxy-internal-summary-control[infer="xml-datafeed"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); - - element.edit({ use_single_sign_on: false, single_sign_on_url: 'https://example.com' }); - expect(control.getValue()).to.equal(''); - - element.edit({ use_single_sign_on: true, single_sign_on_url: 'https://example.com' }); - expect(control.getValue()).to.equal('https://example.com'); - - control.setValue(''); - expect(element).to.have.nested.property('form.use_single_sign_on', false); - expect(element).to.have.nested.property('form.single_sign_on_url', ''); - - control.setValue('https://example.com'); - expect(element).to.have.nested.property('form.use_single_sign_on', true); - expect(element).to.have.nested.property('form.single_sign_on_url', 'https://example.com'); }); - it('renders a text control for webhook url', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); + it('renders a switch for Use Webhook flag in the XML Datafeed section', async () => { + const element = await fixture(html``); const control = element.renderRoot.querySelector( - '[infer="webhook-url"]' - ) as InternalTextControl; + '[infer="xml-datafeed"] [infer="use-webhook"]' + ); expect(control).to.exist; - expect(control).to.be.instanceOf(InternalTextControl); - - element.edit({ use_webhook: false, webhook_url: 'https://example.com' }); - expect(control.getValue()).to.equal(''); - - element.edit({ use_webhook: true, webhook_url: 'https://example.com' }); - expect(control.getValue()).to.equal('https://example.com'); - - control.setValue(''); - expect(element).to.have.nested.property('form.use_webhook', false); - expect(element).to.have.nested.property('form.webhook_url', ''); - - control.setValue('https://example.com'); - expect(element).to.have.nested.property('form.use_webhook', true); - expect(element).to.have.nested.property('form.webhook_url', 'https://example.com'); + expect(control).to.be.instanceOf(InternalSwitchControl); }); - it('renders password controls for secret keys when "webhook_key" is a string', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + it('renders a text control for Webhook URL in the XML Datafeed section when Use Webhook is enabled', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-url"]' + ) + ).to.not.exist; - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ webhook_key: 'ABCTEST' }); + element.edit({ use_webhook: true }); await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-url"]' + ); - for (const key of ['cart_signing', 'xml_datafeed', 'api_legacy', 'sso']) { - const infer = `webhook-key-${key.replace('_', '-')}`; - const control = element.renderRoot.querySelector( - `[infer="${infer}"]` - ) as InternalPasswordControl; - - expect(control).to.be.instanceOf(InternalPasswordControl); - expect(control.getValue()).to.equal('ABCTEST'); - - control.setValue('FOOBAR'); - expect(JSON.parse(element.form.webhook_key!)).to.have.property(key, 'FOOBAR'); - - element.edit({ webhook_key: 'ABCTEST' }); - } + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalTextControl); + expect(control).to.have.attribute('layout', 'summary-item'); }); - it('renders password controls for secret keys when "webhook_key" is JSON', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - const exampleKey: Record = { - cart_signing: 'CRT_SGN_123', - xml_datafeed: 'XML_DFD_456', - api_legacy: 'API_LCY_789', - sso: 'SSO_123_456', - }; - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.edit({ webhook_key: JSON.stringify(exampleKey) }); - await element.requestUpdate(); - - for (const key of ['cart_signing', 'xml_datafeed', 'api_legacy', 'sso']) { - const infer = `webhook-key-${key.replace('_', '-')}`; - const control = element.renderRoot.querySelector( - `[infer="${infer}"]` - ) as InternalPasswordControl; - - expect(control).to.be.instanceOf(InternalPasswordControl); - expect(control.getValue()).to.equal(exampleKey[key]); - - control.setValue('FOOBAR'); - expect(JSON.parse(element.form.webhook_key!)).to.have.property(key, 'FOOBAR'); - - element.edit({ webhook_key: JSON.stringify(exampleKey) }); - } - }); + it('renders a password control for XML Datafeed key in the XML Datafeed section when Use Webhook is enabled (JSON keys)', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-key-xml-datafeed"]' + ) + ).to.not.exist; - it('renders maintenance mode switch', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); + element.edit({ + use_webhook: true, + webhook_key: JSON.stringify({ + cart_signing: 'test', + xml_datafeed: 'test', + api_legacy: 'test', + sso: 'test', + }), + }); - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - element.data = { ...element.data!, is_maintenance_mode: true }; await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-key-xml-datafeed"]' + ); - const wrapper = (await getByTestId(element, 'is-maintenance-mode'))!; - let label = (await getByKey(wrapper, 'maintenance_mode_on_explainer'))!; - const button = (await getByTag(wrapper, 'vaadin-button')) as HTMLElement; - let buttonLabel = button.querySelector('foxy-i18n[key="disable_maintenance_mode"]')!; - - expect(wrapper).to.exist; - expect(label).to.exist; - expect(label).to.have.attribute('infer', ''); - expect(button).to.exist; - expect(buttonLabel).to.exist; - expect(buttonLabel).to.have.attribute('infer', ''); - - button.click(); - await waitUntil(() => element.in('idle'), '', { timeout: 5000 }); - expect(element).to.have.nested.property('data.is_maintenance_mode', false); - - label = (await getByKey(wrapper, 'maintenance_mode_off_explainer'))!; - buttonLabel = button.querySelector('foxy-i18n[key="enable_maintenance_mode"]')!; - - expect(label).to.exist; - expect(label).to.have.attribute('infer', ''); - expect(buttonLabel).to.exist; - expect(buttonLabel).to.have.attribute('infer', ''); - - button.click(); - await waitUntil(() => element.in('idle'), '', { timeout: 5000 }); - expect(element).to.have.nested.property('data.is_maintenance_mode', true); - }); - - it('hides maintenance mode switch if targeted by hidden selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - expect(await getByTestId(element, 'is-maintenance-mode')).to.not.exist; + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalPasswordControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); + expect(control?.getValue()).to.equal('test'); + + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'test', xml_datafeed: 'foo', api_legacy: 'test', sso: 'test' }) + ); }); - it('disables maintenance mode switch if targeted by disabled selector', async () => { - const router = createRouter(); - const element = await fixture(html` - router.handleEvent(evt)} - > - - `); - - await waitUntil(() => !!element.data, '', { timeout: 5000 }); - - const wrapper = (await getByTestId(element, 'is-maintenance-mode'))!; - const button = (await getByTag(wrapper, 'vaadin-button')) as HTMLElement; - - expect(button).to.not.have.attribute('disabled'); + it('renders a password control for XML Datafeed key in the XML Datafeed section when Use Webhook is enabled (string key)', async () => { + const element = await fixture(html``); + expect( + element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-key-xml-datafeed"]' + ) + ).to.not.exist; - element.setAttribute('disabledcontrols', 'is-maintenance-mode'); + element.edit({ use_webhook: true, webhook_key: 'test' }); await element.requestUpdate(); + const control = element.renderRoot.querySelector( + '[infer="xml-datafeed"] [infer="webhook-key-xml-datafeed"]' + ); - expect(button).to.have.attribute('disabled'); + expect(control).to.exist; + expect(control).to.be.instanceOf(InternalPasswordControl); + expect(control).to.have.attribute('layout', 'summary-item'); + expect(control).to.have.attribute('show-generator'); + expect(control).to.have.deep.property('generatorOptions', { length: 32, separator: '' }); + expect(control?.getValue()).to.equal('test'); + + control?.setValue('foo'); + expect(element).to.have.nested.property( + 'form.webhook_key', + JSON.stringify({ cart_signing: 'test', xml_datafeed: 'foo', api_legacy: 'test', sso: 'test' }) + ); }); }); diff --git a/src/elements/public/StoreForm/StoreForm.ts b/src/elements/public/StoreForm/StoreForm.ts index 307d8395..cc3ebbaf 100644 --- a/src/elements/public/StoreForm/StoreForm.ts +++ b/src/elements/public/StoreForm/StoreForm.ts @@ -1,17 +1,22 @@ import type { PropertyDeclarations } from 'lit-element'; import type { Resource, Graph } from '@foxy.io/sdk/core'; -import type { Data } from './types'; import type { TemplateResult } from 'lit-html'; import type { NucleonElement } from '../NucleonElement'; import type { NucleonV8N } from '../NucleonElement/types'; import type { Item } from '../../internal/InternalEditableListControl/types'; import type { Rels } from '@foxy.io/sdk/backend'; +import type { + ParsedCustomDisplayIdConfig, + ParsedSmtpConfig, + ParsedWebhookKey, + Data, +} from './types'; + import { TranslatableMixin } from '../../../mixins/translatable'; import { ResponsiveMixin } from '../../../mixins/responsive'; import { InternalForm } from '../../internal/InternalForm/InternalForm'; import { ifDefined } from 'lit-html/directives/if-defined'; -import { classMap } from '../../../utils/class-map'; import { html } from 'lit-html'; import cloneDeep from 'lodash-es/cloneDeep'; @@ -44,37 +49,21 @@ export class StoreForm extends Base { static get v8n(): NucleonV8N { return [ ({ store_name: v }) => !!v || 'store-name:v8n_required', - ({ store_name: v }) => (v && v.length <= 50) || 'store-name:v8n_too_long', - ({ store_domain: v }) => !!v || 'store-domain:v8n_required', - ({ store_domain: v }) => (v && v.length <= 100) || 'store-domain:v8n_too_long', - ({ store_url: v }) => !!v || 'store-url:v8n_required', - ({ store_url: v }) => (v && v.length <= 300) || 'store-url:v8n_too_long', - ({ receipt_continue_url: v }) => !v || v.length <= 300 || 'receipt-continue-url:v8n_too_long', - ({ store_email: v }) => !!v || 'store-email:v8n_required', - ({ store_email: v }) => (v && v.length <= 300) || 'store-email:v8n_too_long', - ({ from_email: v }) => !v || v.length <= 100 || 'from-email:v8n_too_long', - ({ smtp_config: v }) => !v || v.length <= 1000 || 'use-smtp-config:v8n_too_long', - ({ postal_code: v }) => !!v || 'postal-code:v8n_required', - ({ postal_code: v }) => (v && v.length <= 50) || 'postal-code:v8n_too_long', - ({ region: v }) => !!v || 'region:v8n_required', - ({ region: v }) => (v && v.length <= 100) || 'region:v8n_too_long', - ({ country: v }) => !!v || 'country:v8n_required', - ({ logo_url: v }) => !v || v.length <= 200 || 'logo-url:v8n_too_long', ({ webhook_url: v, use_webhook }) => { @@ -106,7 +95,7 @@ export class StoreForm extends Base { }, ({ custom_display_id_config: v }) => { - return !v || String(v).length <= 100 || 'custom-display-id-config-enabled:v8n_too_long'; + return !v || String(v).length <= 500 || 'custom-display-id-config-enabled:v8n_too_long'; }, ]; } @@ -117,16 +106,28 @@ export class StoreForm extends Base { /** URL of the `fx:shipping_address_types` property helper resource. */ shippingAddressTypes: string | null = null; - /** URL of the `fx:store_versions` property helper resource. */ + /** + * URL of the `fx:store_versions` property helper resource. + * @deprecated All elements in this library are designed to work with store version 2.0. + */ storeVersions: string | null = null; - /** URL of the `fx:checkout_types` property helper resource. */ + /** + * URL of the `fx:checkout_types` property helper resource. + * @deprecated Checkout type is effectively controlled by the default template config. + */ checkoutTypes: string | null = null; - /** URL of the `fx:locale_codes` property helper resource. */ + /** + * URL of the `fx:locale_codes` property helper resource. + * @deprecated Default locale code is effectively controlled by the active template set. + */ localeCodes: string | null = null; - /** URL of the `fx:languages` property helper resource. */ + /** + * URL of the `fx:languages` property helper resource. + * @deprecated Default language is effectively controlled by the active template set. + */ languages: string | null = null; /** URL of the `fx:timezones` property helper resource. */ @@ -138,16 +139,54 @@ export class StoreForm extends Base { /** URL of the `fx:regions` property helper resource. */ regions: string | null = null; - private __singleCheckboxGroupOptions = [{ label: 'option_checked', value: 'checked' }]; + private readonly __currencyStyleOptions = [ + { rawLabel: '12.34', value: '101' }, + { rawLabel: 'USD 12.34', value: '001' }, + { rawLabel: '$12.34', value: '000' }, + { rawLabel: '12', value: '111' }, + { rawLabel: 'USD 12', value: '011' }, + { rawLabel: '$12', value: '010' }, + ]; + + private readonly __currencyStyleGetValue = () => { + const map: Record = { + '101': '101', + '100': '101', + '001': '001', + '000': '000', + '111': '111', + '110': '111', + '011': '011', + '010': '010', + }; + + const selectionCode = [ + this.form.hide_currency_symbol, + this.form.hide_decimal_characters, + this.form.use_international_currency_symbol, + ] + .map(v => Number(Boolean(v))) + .join(''); + + return map[selectionCode]; + }; + + private readonly __currencyStyleSetValue = (newValue: string) => { + this.edit({ + hide_currency_symbol: Boolean(Number(newValue[0])), + hide_decimal_characters: Boolean(Number(newValue[1])), + use_international_currency_symbol: Boolean(Number(newValue[2])), + }); + }; - private __appSessionTimeOptions = [ + private readonly __appSessionTimeOptions = [ { value: 's', label: 'second' }, { value: 'm', label: 'minute' }, { value: 'h', label: 'hour' }, { value: 'd', label: 'day' }, ]; - private __getStoreEmailValue = (): Item[] => { + private readonly __getStoreEmailValue = (): Item[] => { const emails = this.form.store_email ?? ''; return emails .split(',') @@ -155,32 +194,16 @@ export class StoreForm extends Base { .filter(({ value }) => value.length > 0); }; - private __setStoreEmailValue = (newValue: Item[]) => { + private readonly __setStoreEmailValue = (newValue: Item[]) => { this.edit({ store_email: newValue.map(v => v.value).join(',') }); }; - private __getBccOnReceiptEmailValue = () => { - return this.form.bcc_on_receipt_email ? ['checked'] : []; - }; - - private __setBccOnReceiptEmailValue = (newValue: string[]) => { - this.edit({ bcc_on_receipt_email: newValue.includes('checked') }); - }; - - private __getUseEmailDnsValue = () => { - return this.form.use_email_dns ? ['checked'] : []; - }; - - private __setUseEmailDnsValue = (newValue: string[]) => { - this.edit({ use_email_dns: newValue.includes('checked') }); + private readonly __getUseSmtpConfigValue = () => { + return !!this.form.smtp_config; }; - private __getUseSmtpConfigValue = () => { - return this.form.smtp_config ? ['checked'] : []; - }; - - private __setUseSmtpConfigValue = (newValue: string[]) => { - if (newValue.includes('checked')) { + private readonly __setUseSmtpConfigValue = (newValue: boolean) => { + if (newValue) { this.edit({ smtp_config: JSON.stringify({ username: '', @@ -195,23 +218,7 @@ export class StoreForm extends Base { } }; - private __getFeaturesMultishipValue = () => { - return this.form.features_multiship ? ['checked'] : []; - }; - - private __setFeaturesMultishipValue = (newValue: string[]) => { - this.edit({ features_multiship: newValue.includes('checked') }); - }; - - private __getRequireSignedShippingRatesValue = () => { - return this.form.require_signed_shipping_rates ? ['checked'] : []; - }; - - private __setRequireSignedShippingRatesValue = (newValue: string[]) => { - this.edit({ require_signed_shipping_rates: newValue.includes('checked') }); - }; - - private __getAppSessionTimeValue = () => { + private readonly __getAppSessionTimeValue = () => { const valueInSeconds = this.form.app_session_time || 43200; if (valueInSeconds % 86400 === 0) return `${valueInSeconds / 86400}d`; @@ -221,7 +228,7 @@ export class StoreForm extends Base { return `${valueInSeconds}s`; }; - private __setAppSessionTimeValue = (newValue: string) => { + private readonly __setAppSessionTimeValue = (newValue: string) => { const units = newValue[newValue.length - 1]; const count = parseInt(newValue.substring(0, newValue.length - 1)); const map: Record = { d: 86400, h: 3600, m: 60, s: 1 }; @@ -229,72 +236,105 @@ export class StoreForm extends Base { this.edit({ app_session_time: map[units] * count }); }; - private __getProductsRequireExpiresPropertyValue = () => { - return this.form.products_require_expires_property ? ['checked'] : []; + private readonly __setStoreDomainValue = (newValue: string) => { + if (newValue.endsWith('.foxycart.com')) { + const domain = newValue.substring(0, newValue.length - 13); + this.edit({ store_domain: domain, use_remote_domain: domain.includes('.') }); + } else { + this.edit({ store_domain: newValue, use_remote_domain: newValue.includes('.') }); + } }; - private __setProductsRequireExpiresPropertyValue = (newValue: string[]) => { - this.edit({ products_require_expires_property: newValue.includes('checked') }); + private readonly __smtpConfigHostGetValue = () => { + return this.__getSmtpConfig().host; }; - private __getUseCartValidationValue = () => { - return this.form.use_cart_validation ? ['checked'] : []; + private readonly __smtpConfigHostSetValue = (newValue: string) => { + this.__setSmtpConfig('host', newValue); }; - private __setUseCartValidationValue = (newValue: string[]) => { - this.edit({ use_cart_validation: newValue.includes('checked') }); + private readonly __smtpConfigPortGetValue = () => { + const port = parseInt(this.__getSmtpConfig().port); + return isNaN(port) ? undefined : port; }; - private __getSingleSignOnUrlValue = () => { - return this.form.use_single_sign_on ? this.form.single_sign_on_url ?? '' : ''; + private readonly __smtpConfigPortSetValue = (newValue: number) => { + this.__setSmtpConfig('port', String(newValue)); }; - private __setSingleSignOnUrlValue = (newValue: string) => { - this.edit({ use_single_sign_on: !!newValue, single_sign_on_url: newValue }); + private readonly __smtpConfigUsernameGetValue = () => { + return this.__getSmtpConfig().username; }; - private __getWebhookUrlValue = () => { - return this.form.use_webhook ? this.form.webhook_url ?? '' : ''; + private readonly __smtpConfigUsernameSetValue = (newValue: string) => { + this.__setSmtpConfig('username', newValue); }; - private __setWebhookUrlValue = (newValue: string) => { - this.edit({ use_webhook: !!newValue, webhook_url: newValue }); + private readonly __smtpConfigPasswordGetValue = () => { + return this.__getSmtpConfig().password; }; - private __setStoreDomainValue = (newValue: string) => { - if (newValue.endsWith('.foxycart.com')) { - const domain = newValue.substring(0, newValue.length - 13); - this.edit({ store_domain: domain, use_remote_domain: domain.includes('.') }); - } else { - this.edit({ store_domain: newValue, use_remote_domain: newValue.includes('.') }); - } + private readonly __smtpConfigPasswordSetValue = (newValue: string) => { + this.__setSmtpConfig('password', newValue); }; - renderBody(): TemplateResult { - const isUseEmailDnsWarningVisible = - this.form.use_email_dns && - !this.data?.use_email_dns && - !this.hiddenSelector.matches('use-email-dns', true); - - const isRequireSignedShippingRatesWarningVisible = - this.form.require_signed_shipping_rates && - !this.data?.require_signed_shipping_rates && - !this.hiddenSelector.matches('require-signed-shipping-rates', true); - - const isProductsRequireExpiresPropertyWarningVisible = - this.form.products_require_expires_property && - !this.data?.products_require_expires_property && - !this.hiddenSelector.matches('products-require-expires-property', true); - - const isUseCartValidationWarningVisible = - this.form.use_cart_validation && - !this.data?.use_cart_validation && - !this.hiddenSelector.matches('use-cart-validation', true); + private readonly __smtpConfigSecurityGetValue = () => { + return this.__getSmtpConfig().security; + }; + + private readonly __smtpConfigSecuritySetValue = (newValue: string) => { + this.__setSmtpConfig('security', newValue); + }; + + private readonly __smtpConfigSecurityOptions = [ + { label: 'option_ssl', value: 'ssl' }, + { label: 'option_tls', value: 'tls' }, + { label: 'option_none', value: '' }, + ]; + + private readonly __webhookKeyGeneratorOptions = { length: 32, separator: '' }; + + private readonly __webhookKeyApiLegacyGetValue = () => { + return this.__getWebhookKey().api_legacy; + }; + + private readonly __webhookKeyApiLegacySetValue = (newValue: string) => { + this.__setWebhookKey('api_legacy', newValue); + }; + private readonly __webhookKeyCartSigningGetValue = () => { + return this.__getWebhookKey().cart_signing; + }; + + private readonly __webhookKeyCartSigningSetValue = (newValue: string) => { + this.__setWebhookKey('cart_signing', newValue); + }; + + private readonly __webhookKeyXmlDatafeedGetValue = () => { + return this.__getWebhookKey().xml_datafeed; + }; + + private readonly __webhookKeyXmlDatafeedSetValue = (newValue: string) => { + this.__setWebhookKey('xml_datafeed', newValue); + }; + + private readonly __webhookKeySsoGetValue = () => { + return this.__getWebhookKey().sso; + }; + + private readonly __webhookKeySsoSetValue = (newValue: string) => { + this.__setWebhookKey('sso', newValue); + }; + + get headerSubtitleOptions(): Record { + return { context: this.data?.is_active ? 'active' : 'inactive' }; + } + + renderBody(): TemplateResult { const storeDomainHelperText = this.t( this.form.use_remote_domain && !this.data?.use_remote_domain - ? 'store-domain.custom_domain_note' - : 'store-domain.helper_text' + ? 'essentials.store-domain.custom_domain_note' + : 'essentials.store-domain.helper_text' ); const storeDomainSuffix = @@ -302,10 +342,6 @@ export class StoreForm extends Base { const customerPasswordHashTypesLoader = this.__renderLoader(1); const shippingAddressTypesLoader = this.__renderLoader(2); - const checkoutTypesLoader = this.__renderLoader(3); - const storeVersionLoader = this.__renderLoader(4); - const localeCodesLoader = this.__renderLoader(5); - const languagesLoader = this.__renderLoader(6); const timezonesLoader = this.__renderLoader(7); const countriesLoader = this.__renderLoader(8); const regionsLoader = this.__renderLoader(9); @@ -314,9 +350,6 @@ export class StoreForm extends Base { shippingAddressTypesLoader?.data?.values ?? {} ); - const storeVersion = storeVersionLoader?.data; - const localeCodes = localeCodesLoader?.data; - const languages = languagesLoader?.data; const timezones = timezonesLoader?.data?.values.timezone ?? []; const countries = Object.values(countriesLoader?.data?.values ?? {}); const regions = Object.values(regionsLoader?.data?.values ?? {}); @@ -325,26 +358,19 @@ export class StoreForm extends Base { customerPasswordHashTypesLoader?.data?.values ?? {} ); - const checkoutTypeEntries = Object.entries(checkoutTypesLoader?.data?.values ?? {}); - const localeCodeEntries = Object.entries(localeCodes?.values ?? {}); - const languageEntries = Object.entries(languages?.values ?? {}); - const customerPasswordHashTypeOptions = customerPasswordHashTypeEntries.map(v => ({ - label: v[1].description, + rawLabel: v[1].description, value: v[0], })); - const shippingAddressTypeOptions = shippingAddressTypeEntries.map(([value, label]) => ({ - label, + const shippingAddressTypeOptions = shippingAddressTypeEntries.map(([value, rawLabel]) => ({ + rawLabel, value, })); - const checkoutTypeOptions = checkoutTypeEntries.map(([value, label]) => ({ label, value })); - const localeCodeOptions = localeCodeEntries.map(([value, label]) => ({ value, label })); - const languageOptions = languageEntries.map(([value, label]) => ({ value, label })); - const timezoneOptions = timezones.map(t => ({ label: t.description, value: t.timezone })); - const countryOptions = countries.map(c => ({ label: c.default, value: c.cc2 })); - const regionOptions = regions.map(r => ({ label: r.default, value: r.code })); + const timezoneOptions = timezones.map(t => ({ rawLabel: t.description, value: t.timezone })); + const countryOptions = countries.map(c => ({ rawLabel: c.default, value: c.cc2 })); + const regionOptions = regions.map(r => ({ rawLabel: r.default, value: r.code })); let regionsUrl: string | undefined; @@ -357,157 +383,189 @@ export class StoreForm extends Base { regionsUrl = undefined; } + const customDisplayIdConfig = this.__getCustomDisplayIdConfig(); + const transactionJournalEntriesConfig = customDisplayIdConfig.transaction_journal_entries; + const logDetailRequestTypes = transactionJournalEntriesConfig.log_detail_request_types; + const displayIdExamples = this.__displayIdExamples; + const journalIdExamples = this.__journalIdExamples; + return html` ${this.renderHeader()} -
- + + + - + + - + + + + + + + + + + - - + - + ${regionOptions.length > 0 + ? html` + + + ` + : html` + + + `} + + - - - - + + + + - + + - ${isUseEmailDnsWarningVisible - ? this.__renderWarning('use_email_dns_helper_text', { - href: 'https://wiki.foxycart.com/v/1.1/emails#how_emails_are_sent_spf_dkim_dmarc_etc', - caption: 'How Emails Are Sent (SPF, DKIM, DMARC, etc.)', - }) - : ''} + + + + + + - - + - ${this.form.smtp_config && !this.hiddenSelector.matches('smtp-config', true) - ? this.__renderSmtpConfig() - : ''} + ${this.form.smtp_config + ? html` + + - - + + - ${regionOptions.length > 0 - ? html` - + + + + + + + ` - : html``} - - + : ''} + + - - - - - - - ${isRequireSignedShippingRatesWarningVisible - ? this.__renderWarning('require_signed_shipping_rates_helper_text') - : ''} - - - - - - - - ${!this.hiddenSelector.matches('currency-style', true) - ? this.__renderCurrencyStyleSelector() - : ''} - - - - ${!this.hiddenSelector.matches('custom-display-id-config', true) - ? this.__renderCustomIDSettings() - : ''} - - + + - - + + + + { > - - - - ${isProductsRequireExpiresPropertyWarningVisible - ? this.__renderWarning('products_require_expires_property_helper_text') - : ''} - - - + - ${isUseCartValidationWarningVisible - ? this.__renderWarning('use_cart_validation_helper_text', { - href: 'https://wiki.foxycart.com/v/2.0/hmac_validation', - caption: 'HMAC Product Verification: Locking Down your Add-To-Cart Links and Forms', - }) - : ''} + + - - + + + { @@ -566,61 +610,312 @@ export class StoreForm extends Base { ${typeof this.form.customer_password_hash_config === 'number' ? html` - + ` : html` - + `} + - + + + - + + + ${this.form.use_single_sign_on + ? html` + + + + + ` + : ''} + + + + + + + + + customDisplayIdConfig.enabled} + .setValue=${(newValue: boolean) => { + this.__setCustomDisplayIdConfig('enabled', newValue); + }} + > + - parseInt(customDisplayIdConfig.start || '0')} + .setValue=${(newValue: number) => { + this.__setCustomDisplayIdConfig('start', String(newValue)); + }} + > + + + parseInt(customDisplayIdConfig.length || '0')} + .setValue=${(newValue: number) => { + this.__setCustomDisplayIdConfig('length', String(newValue)); + }} + > + + + customDisplayIdConfig.prefix} + .setValue=${(newValue: string) => { + this.__setCustomDisplayIdConfig('prefix', newValue); + }} + > + + + customDisplayIdConfig.suffix} + .setValue=${(newValue: string) => { + this.__setCustomDisplayIdConfig('suffix', newValue); + }} + > + + +
+

+ +  ${displayIdExamples?.first} +

+

+ +  ${displayIdExamples?.random} +

+
+ ` + : ''} +
+ + + transactionJournalEntriesConfig.enabled} + .setValue=${(newValue: boolean) => { + this.__setTransactionJournalEntriesConfig('enabled', newValue); + }} > -
+ + + ${transactionJournalEntriesConfig.enabled + ? html` + logDetailRequestTypes.transaction_authcapture.prefix} + .setValue=${(newValue: string) => { + this.__setTransactionJournalEntriesPrefix('transaction_authcapture', newValue); + }} + > + + + logDetailRequestTypes.transaction_capture.prefix} + .setValue=${(newValue: string) => { + this.__setTransactionJournalEntriesPrefix('transaction_capture', newValue); + }} + > + + + logDetailRequestTypes.transaction_void.prefix} + .setValue=${(newValue: string) => { + this.__setTransactionJournalEntriesPrefix('transaction_void', newValue); + }} + > + + + logDetailRequestTypes.transaction_refund.prefix} + .setValue=${(newValue: string) => { + this.__setTransactionJournalEntriesPrefix('transaction_refund', newValue); + }} + > + - ${this.__renderWebhookKey()} + transactionJournalEntriesConfig.transaction_separator} + .setValue=${(newValue: string) => { + this.__setTransactionJournalEntriesConfig('transaction_separator', newValue); + }} + > + - +
+

+ + + ${journalIdExamples?.authcapture} +

+

+ + + ${journalIdExamples?.capture} +

+

+ + + ${journalIdExamples?.void} +

+

+ + + ${journalIdExamples?.refund} +

+
+ ` + : ''} + - ${this.data && !this.hiddenSelector.matches('is-maintenance-mode', true) - ? this.__renderMaintenanceModeSwitch() + + + ${this.form.use_webhook + ? html` + + + + + ` : ''} -
+
${super.renderBody()} ${customerPasswordHashTypesLoader.render(this.customerPasswordHashTypes)} ${shippingAddressTypesLoader.render(this.shippingAddressTypes)} - ${checkoutTypesLoader.render(this.checkoutTypes)} - ${storeVersionLoader.render(this.form.store_version_uri)} - ${localeCodesLoader.render(this.localeCodes)} ${timezonesLoader.render(this.timezones)} - ${countriesLoader.render(this.countries)} ${languagesLoader.render(this.languages)} + ${timezonesLoader.render(this.timezones)} ${countriesLoader.render(this.countries)} ${regionsLoader.render(regionsUrl)} `; } - private __renderWebhookKey() { - type ParsedKey = { - cart_signing: string; - xml_datafeed: string; - api_legacy: string; - sso: string; - }; + private get __displayIdExamples() { + const config = this.__getCustomDisplayIdConfig(); + const startAsInt = parseInt(config.start || '0'); + const lengthAsInt = parseInt(config.length || '0'); + const numericLength = lengthAsInt - config.prefix.length - config.suffix.length; + const randomExampleNumericId = Math.min( + startAsInt + Math.floor(Math.random() * Math.pow(10, numericLength)), + Math.pow(10, numericLength) - 1 + ); + + if (config.start && config.length && startAsInt / 10 <= numericLength) { + return { + first: `${config.prefix}${startAsInt.toString().padStart(numericLength, '0')}${ + config.suffix + }`, + random: `${config.prefix}${randomExampleNumericId.toString().padStart(numericLength, '0')}${ + config.suffix + }`, + }; + } + } - let parsedKey: ParsedKey; + private get __journalIdExamples() { + const customDisplayIdConfig = this.__getCustomDisplayIdConfig(); + const transactionJournalEntriesConfig = customDisplayIdConfig.transaction_journal_entries; + const startAsInt = parseInt(customDisplayIdConfig.start || '0'); + const lengthAsInt = parseInt(customDisplayIdConfig.length || '0'); + const numericLength = + lengthAsInt - customDisplayIdConfig.prefix.length - customDisplayIdConfig.suffix.length; + + const randomExampleNumericId = Math.min( + startAsInt + Math.floor(Math.random() * Math.pow(10, numericLength)), + Math.pow(10, numericLength) - 1 + ); + + const randomExampleId = `${customDisplayIdConfig.prefix}${randomExampleNumericId + .toString() + .padStart(numericLength, '0')}${customDisplayIdConfig.suffix}`; + + const randomNumericEntryId = Math.floor(Math.random() * 1000); + const randomEntryId = String(randomNumericEntryId).padStart(3, '0'); + + if ( + customDisplayIdConfig.start && + customDisplayIdConfig.length && + startAsInt / 10 <= numericLength + ) { + return { + authcapture: `${randomExampleId}${transactionJournalEntriesConfig.transaction_separator}${transactionJournalEntriesConfig.log_detail_request_types.transaction_authcapture.prefix}${randomEntryId}`, + capture: `${randomExampleId}${transactionJournalEntriesConfig.transaction_separator}${transactionJournalEntriesConfig.log_detail_request_types.transaction_capture.prefix}${randomEntryId}`, + refund: `${randomExampleId}${transactionJournalEntriesConfig.transaction_separator}${transactionJournalEntriesConfig.log_detail_request_types.transaction_refund.prefix}${randomEntryId}`, + void: `${randomExampleId}${transactionJournalEntriesConfig.transaction_separator}${transactionJournalEntriesConfig.log_detail_request_types.transaction_void.prefix}${randomEntryId}`, + }; + } + } + + private __getWebhookKey() { + let parsedKey: ParsedWebhookKey; try { parsedKey = JSON.parse(this.form.webhook_key ?? ''); @@ -629,136 +924,17 @@ export class StoreForm extends Base { parsedKey = { cart_signing: v, xml_datafeed: v, api_legacy: v, sso: v }; } - return Object.keys(parsedKey).map(key => { - return html` - parsedKey[key as keyof ParsedKey]} - .setValue=${(newValue: string) => { - parsedKey[key as keyof ParsedKey] = newValue; - this.edit({ webhook_key: JSON.stringify(parsedKey) }); - }} - > - - `; - }); - } - - private __renderMaintenanceModeSwitch() { - const isActive = !!this.data?.is_maintenance_mode; - const buttonKey = `${isActive ? 'disable' : 'enable'}_maintenance_mode`; - const explainerKey = `maintenance_mode_${isActive ? 'on' : 'off'}_explainer`; - - return html` -
- - - { - this.edit({ is_maintenance_mode: !this.form.is_maintenance_mode }); - this.submit(); - }} - > - - -
- `; + return parsedKey; } - private __renderCurrencyStyleSelector() { - const map: Record = { - '101': 0, - '100': 0, - '001': 1, - '000': 2, - '111': 3, - '110': 3, - '011': 4, - '010': 5, - }; - - const selectionCode = [ - this.form.hide_currency_symbol, - this.form.hide_decimal_characters, - this.form.use_international_currency_symbol, - ] - .map(v => Number(Boolean(v))) - .join(''); - - const selectionIndex = map[selectionCode]; - const isDisabled = this.disabledSelector.matches('currency-style', true); - const isReadonly = this.readonlySelector.matches('currency-style', true); - - return html` -
- ${this.renderTemplateOrSlot('currency-style:before')} - -
- - -
- ${['12.34', 'USD 12.34', '$12.34', '12', 'USD 12', '$12'].map((example, index) => { - return html` - - `; - })} -
- - -
- - ${this.renderTemplateOrSlot('currency-style:after')} -
- `; + private __setWebhookKey(key: keyof ParsedWebhookKey, value: string) { + const parsedKey = this.__getWebhookKey(); + parsedKey[key] = value; + this.edit({ webhook_key: JSON.stringify(parsedKey) }); } - private __renderCustomIDSettings() { - const defaultConfig = { + private __getCustomDisplayIdConfig() { + const defaultConfig: ParsedCustomDisplayIdConfig = { enabled: false, start: '0', length: '0', @@ -776,7 +952,7 @@ export class StoreForm extends Base { }, }; - let config: typeof defaultConfig; + let config: ParsedCustomDisplayIdConfig; try { config = JSON.parse(this.form.custom_display_id_config ?? ''); @@ -784,368 +960,67 @@ export class StoreForm extends Base { config = cloneDeep(defaultConfig); } - const startAsInt = parseInt(config.start || '0'); - const lengthAsInt = parseInt(config.length || '0'); - const numericLength = lengthAsInt - config.prefix.length - config.suffix.length; - const randomExampleNumericId = Math.min( - startAsInt + Math.floor(Math.random() * Math.pow(10, numericLength)), - Math.pow(10, numericLength) - 1 - ); - - const randomExampleId = `${config.prefix}${randomExampleNumericId - .toString() - .padStart(numericLength, '0')}${config.suffix}`; - - const randomNumericEntryId = Math.floor(Math.random() * 1000); - const randomEntryId = String(randomNumericEntryId).padStart(3, '0'); - - return html` - (config.enabled ? ['checked'] : [])} - .setValue=${(newValue: string[]) => { - config.enabled = newValue.includes('checked'); - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - .options=${this.__singleCheckboxGroupOptions} - > - - - ${config.enabled - ? html` -
- parseInt(config.start || '0')} - .setValue=${(newValue: number) => { - config.start = String(newValue); - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - + return config; + } - parseInt(config.length || '0')} - .setValue=${(newValue: number) => { - config.length = String(newValue); - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - + private __setCustomDisplayIdConfig( + key: TKey, + value: ParsedCustomDisplayIdConfig[TKey] + ) { + const currentConfig = this.__getCustomDisplayIdConfig(); + currentConfig[key] = value; + this.edit({ custom_display_id_config: JSON.stringify(currentConfig) }); + } - config.prefix} - .setValue=${(newValue: string) => { - config.prefix = newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - + private __getTransactionJournalEntriesConfig() { + return this.__getCustomDisplayIdConfig().transaction_journal_entries; + } - config.suffix} - .setValue=${(newValue: string) => { - config.suffix = newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - + private __setTransactionJournalEntriesConfig< + TKey extends keyof ParsedCustomDisplayIdConfig['transaction_journal_entries'] + >(key: TKey, value: ParsedCustomDisplayIdConfig['transaction_journal_entries'][TKey]) { + const config = this.__getCustomDisplayIdConfig().transaction_journal_entries; + config[key] = value; + this.__setCustomDisplayIdConfig('transaction_journal_entries', config); + } - ${config.start && config.length && startAsInt / 10 <= numericLength - ? html` - - - - - - - - - - - -
- - - - ${config.prefix}${startAsInt - .toString() - .padStart(numericLength, '0')}${config.suffix} -
- - - ${randomExampleId}
- ` - : ''} -
- - (config.transaction_journal_entries.enabled ? ['checked'] : [])} - .setValue=${(newValue: string[]) => { - config.transaction_journal_entries.enabled = newValue.includes('checked'); - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - .options=${this.__singleCheckboxGroupOptions} - > - - - ${config.transaction_journal_entries.enabled - ? html` -
- config.transaction_journal_entries.transaction_separator} - .setValue=${(newValue: string) => { - config.transaction_journal_entries.transaction_separator = newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - - - - config.transaction_journal_entries.log_detail_request_types - .transaction_authcapture.prefix} - .setValue=${(newValue: string) => { - config.transaction_journal_entries.log_detail_request_types.transaction_authcapture.prefix = - newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - - - - config.transaction_journal_entries.log_detail_request_types - .transaction_capture.prefix} - .setValue=${(newValue: string) => { - config.transaction_journal_entries.log_detail_request_types.transaction_capture.prefix = - newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - - - - config.transaction_journal_entries.log_detail_request_types.transaction_void - .prefix} - .setValue=${(newValue: string) => { - config.transaction_journal_entries.log_detail_request_types.transaction_void.prefix = - newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - - - - config.transaction_journal_entries.log_detail_request_types - .transaction_refund.prefix} - .setValue=${(newValue: string) => { - config.transaction_journal_entries.log_detail_request_types.transaction_refund.prefix = - newValue; - this.edit({ custom_display_id_config: JSON.stringify(config) }); - }} - > - - - ${config.start && config.length && startAsInt / 10 <= numericLength - ? html` - - - - - - - - - - - - - - - - - - - -
- - - - ${randomExampleId}${config.transaction_journal_entries - .transaction_separator}${config.transaction_journal_entries - .log_detail_request_types.transaction_authcapture - .prefix}${randomEntryId} -
- - - - ${randomExampleId}${config.transaction_journal_entries - .transaction_separator}${config.transaction_journal_entries - .log_detail_request_types.transaction_capture - .prefix}${randomEntryId} -
- - - - ${randomExampleId}${config.transaction_journal_entries - .transaction_separator}${config.transaction_journal_entries - .log_detail_request_types.transaction_void - .prefix}${randomEntryId} -
- - - - ${randomExampleId}${config.transaction_journal_entries - .transaction_separator}${config.transaction_journal_entries - .log_detail_request_types.transaction_refund - .prefix}${randomEntryId} -
- ` - : ''} -
- ` - : ''} - ` - : ''} - `; + private __setTransactionJournalEntriesPrefix( + key: keyof ParsedCustomDisplayIdConfig['transaction_journal_entries']['log_detail_request_types'], + value: string + ) { + const config = this.__getTransactionJournalEntriesConfig(); + config.log_detail_request_types[key].prefix = value; + this.__setTransactionJournalEntriesConfig( + 'log_detail_request_types', + config.log_detail_request_types + ); } - private __renderSmtpConfig() { - return html` -
- ${['host', 'port', 'username', 'password', 'security'].map(prop => { - const getValue = () => { - const config = JSON.parse(this.form.smtp_config || '{}'); - return config[prop]; - }; - - const setValue = (newValue: string) => { - const config = JSON.parse(this.form.smtp_config || '{}'); - this.edit({ - smtp_config: JSON.stringify({ - username: config.username ?? '', - password: config.password ?? '', - security: config.security ?? '', - host: config.host ?? '', - port: config.port ?? '', - [prop]: newValue, - }), - }); - }; + private __getSmtpConfig() { + let config: ParsedSmtpConfig; - const infer = `smtp-config-${prop}`; + try { + config = JSON.parse(this.form.smtp_config ?? ''); + } catch { + config = { + username: '', + password: '', + security: '', + host: '', + port: '', + }; + } - if (prop === 'password') { - return html` - - - `; - } else if (prop === 'port') { - return html` - - - `; - } else if (prop === 'security') { - return html` - - - `; - } else { - return html` - - - `; - } - })} -
- `; + return config; } - private __renderWarning(key: string, link?: { href: string; caption: string }) { - return html` -
- - - ${link - ? html` - - ${link.caption} - - - ` - : ''} -
- `; + private __setSmtpConfig( + key: TKey, + value: ParsedSmtpConfig[TKey] + ) { + const config = this.__getSmtpConfig(); + config[key] = value; + this.edit({ smtp_config: JSON.stringify(config) }); } private __renderLoader(index: number) { diff --git a/src/elements/public/StoreForm/index.ts b/src/elements/public/StoreForm/index.ts index a226a8bb..8709df07 100644 --- a/src/elements/public/StoreForm/index.ts +++ b/src/elements/public/StoreForm/index.ts @@ -1,12 +1,8 @@ -import '@vaadin/vaadin-button'; - -import '../../internal/InternalCheckboxGroupControl/index'; -import '../../internal/InternalAsyncComboBoxControl/index'; import '../../internal/InternalEditableListControl/index'; -import '../../internal/InternalRadioGroupControl/index'; import '../../internal/InternalFrequencyControl/index'; import '../../internal/InternalPasswordControl/index'; -import '../../internal/InternalIntegerControl/index'; +import '../../internal/InternalSummaryControl/index'; +import '../../internal/InternalSwitchControl/index'; import '../../internal/InternalNumberControl/index'; import '../../internal/InternalSelectControl/index'; import '../../internal/InternalTextControl/index'; diff --git a/src/elements/public/StoreForm/types.ts b/src/elements/public/StoreForm/types.ts index 84ca9fa2..95781298 100644 --- a/src/elements/public/StoreForm/types.ts +++ b/src/elements/public/StoreForm/types.ts @@ -2,3 +2,39 @@ import type { Resource } from '@foxy.io/sdk/core'; import type { Rels } from '@foxy.io/sdk/backend'; export type Data = Resource; + +// TODO add this to SDK +export type ParsedWebhookKey = { + cart_signing: string; + xml_datafeed: string; + api_legacy: string; + sso: string; +}; + +// TODO add this to SDK +export type ParsedSmtpConfig = { + username: string; + password: string; + security: string; + host: string; + port: string; +}; + +// TODO add this to SDK +export type ParsedCustomDisplayIdConfig = { + enabled: boolean; + start: string; + length: string; + prefix: string; + suffix: string; + transaction_journal_entries: { + enabled: boolean; + transaction_separator: string; + log_detail_request_types: { + transaction_authcapture: { prefix: string }; + transaction_capture: { prefix: string }; + transaction_refund: { prefix: string }; + transaction_void: { prefix: string }; + }; + }; +}; diff --git a/src/static/translations/store-form/en.json b/src/static/translations/store-form/en.json index 3d1aa5e0..af24f105 100644 --- a/src/static/translations/store-form/en.json +++ b/src/static/translations/store-form/en.json @@ -2,7 +2,8 @@ "header": { "title_existing": "Store #{{ id }}", "title_new": "New store", - "subtitle": "", + "subtitle_inactive": "Inactive – subscription required", + "subtitle_active": "Active", "copy-id": { "failed_to_copy": "Failed to copy", "click_to_copy": "Copy ID", @@ -16,301 +17,341 @@ "done": "Copied to clipboard" } }, - "maintenance_mode_on_explainer": "Maintenance mode is on. Your customers can't make purchases or use the checkout page in any way. Once you're done making changes, disable this mode to continue getting orders.", - "maintenance_mode_off_explainer": "If you're about to make changes that may disrupt the checkout process, we recommend enabling the maintenance mode first. In this mode the checkout page will be completely non-functioning and the customers will see a message asking them to come back later.", - "enable_maintenance_mode": "Enable maintenance mode", - "disable_maintenance_mode": "Disable maintenance mode", - "store-name": { - "label": "Name", - "placeholder": "My Store", - "helper_text": "The name of your store as you'd like it displayed to your customers and our system.", - "v8n_required": "Please enter the name of your store", - "v8n_too_long": "Please reduce the name of your store to 50 characters or less" - }, - "store-email": { - "label": "Emails", - "placeholder": "Enter an email and press Enter", - "helper_text": "Email addresses used for billing and communication with Foxy.", - "caption": "Submit", - "delete": "Delete", - "v8n_required": "Please enter at least one email", - "v8n_too_long": "All emails for this store must fit within 300 characters when comma-separated" - }, - "store-url": { - "label": "Website", - "placeholder": "https://my.store.example.com", - "helper_text": "The URL of your online store.", - "v8n_required": "Please enter the URL of your online store", - "v8n_too_long": "Please use a URL that is 300 characters or less" - }, - "store-domain": { - "label": "Domain", - "placeholder": "my-store.foxycart.com", - "helper_text": "Unique Foxy subdomain or a custom domain for your cart, checkout, and receipt.", - "custom_domain_note": "IMPORTANT: to use a custom domain, you must purchase an SSL certificate through Foxy. This option is only for developers who have full control of their domain settings and may take a few days to fully process.", - "v8n_required": "Please enter the domain of your store", - "v8n_too_long": "Please use a domain that is 100 characters or less" - }, - "logo-url": { - "label": "Logo", - "placeholder": "https://example.com/logo.png", - "helper_text": "URL to your store's logo that may be used in your store's templates.", - "v8n_too_long": "Please shorten this link to 200 characters of less" - }, - "timezone": { - "label": "Timezone", - "placeholder": "(GMT-08:00) Pacific Time (US and Canada)", - "helper_text": "The timezone of your store. This will impact how dates are shown to customers and within the admin." - }, - "from-email": { - "label": "FROM email", - "placeholder": "Defaults to the first store email", - "helper_text": "If you'd like us to send messages from an address other than the first one of your store emails, list it here.", - "v8n_too_long": "Please use an email that is 100 characters or less" - }, - "bcc-on-receipt-email": { - "label": "", - "helper_text": "", - "option_checked": "Send a copy of each receipt to the store email" - }, - "use-email-dns": { - "label": "", - "helper_text": "", - "option_checked": "Improve email deliverability with DNS" - }, - "use_email_dns_helper_text": "Before saving your changes, make sure the DNS records for your domain list our servers. To learn more, see our Wiki:", - "use-smtp-config": { - "label": "", + "essentials": { + "label": "Essentials", "helper_text": "", - "option_checked": "Use custom mail server", - "v8n_too_long": "This configuration must fit within 1000 characters when JSON-encoded" - }, - "smtp-config-host": { - "label": "Host", - "helper_text": "", - "placeholder": "smtp.example.com" - }, - "smtp-config-port": { - "label": "Port", - "helper_text": "", - "placeholder": "465" - }, - "smtp-config-username": { - "label": "Username", - "helper_text": "", - "placeholder": "sender@example.com" + "store-name": { + "label": "Name", + "placeholder": "My Store", + "helper_text": "", + "v8n_required": "Please enter the name of your store", + "v8n_too_long": "Please reduce the name of your store to 50 characters or less" + }, + "logo-url": { + "label": "Logo URL", + "placeholder": "https://example.com/logo.png", + "helper_text": "", + "v8n_too_long": "Please shorten this link to 200 characters of less" + }, + "store-domain": { + "label": "Domain", + "placeholder": "my-store.foxycart.com", + "helper_text": "", + "custom_domain_note": "To use a custom domain, you must purchase an SSL certificate through Foxy. This option is only for developers who have full control of their domain settings and may take a few days to fully process.", + "v8n_required": "Please enter the domain of your store", + "v8n_too_long": "Please use a domain that is 100 characters or less" + }, + "store-url": { + "label": "Website", + "placeholder": "https://my.store.example.com", + "helper_text": "", + "v8n_required": "Please enter the URL of your online store", + "v8n_too_long": "Please use a URL that is 300 characters or less" + }, + "is-maintenance-mode": { + "label": "Maintenance mode", + "helper_text": "When enabled, your customers can't make purchases or use the checkout page in any way. Once you're done making changes, disable this mode to continue getting orders.", + "checked": "On", + "unchecked": "Off" + }, + "store-email": { + "label": "Emails for billing and communication with Foxy", + "placeholder": "Enter an email and press Enter", + "helper_text": "", + "caption": "Submit", + "delete": "Delete", + "v8n_required": "Please enter at least one email", + "v8n_too_long": "All emails for this store must fit within 300 characters when comma-separated" + }, + "timezone": { + "label": "Time zone", + "placeholder": "Select", + "helper_text": "" + }, + "country": { + "label": "Country", + "placeholder": "Select", + "helper_text": "", + "v8n_required": "Please select the country of your store" + }, + "region": { + "label": "Region (state, province, etc.)", + "placeholder": "Select", + "helper_text": "", + "v8n_required": "Please select the region of your store", + "v8n_too_long": "Please shorten the region name to 50 characters of less" + }, + "postal-code": { + "label": "Postal (zip) code", + "placeholder": "012345", + "helper_text": "", + "v8n_required": "Please enter the postal code of your store", + "v8n_too_long": "Postal code can't exceed 50 characters" + }, + "currency-style": { + "label": "Currency display style", + "placeholder": "Select", + "helper_text": "" + } }, - "smtp-config-password": { - "label": "Password", + "legacy-api": { + "label": "Legacy API", "helper_text": "", - "placeholder": "Required" + "webhook-key-api-legacy": { + "label": "API key", + "placeholder": "None", + "helper_text": "" + } }, - "smtp-config-security": { - "label": "", + "emails": { + "label": "Emails", "helper_text": "", - "option_ssl": "Use SSL", - "option_tls": "Use TLS", - "option_none": "Don't encrypt" - }, - "country": { - "label": "Country", - "placeholder": "Select country...", - "helper_text": "The country your store is located in. We'll use this information to calculate shipping costs if you sell shippable items.", - "v8n_required": "Please select the country of your store" - }, - "region": { - "label": "Region", - "placeholder": "Select region...", - "helper_text": "The region, province or state your store is located in. We'll use this information to calculate shipping costs if you sell shippable items.", - "v8n_required": "Please select the region of your store", - "v8n_too_long": "Please shorten the region name to 50 characters of less" - }, - "postal-code": { - "label": "Postal code", - "placeholder": "012345", - "helper_text": "The postal code (or zip code) of the area your store is located in. We'll use this information to calculate shipping costs if you sell shippable items.", - "v8n_required": "Please enter the postal code of your store", - "v8n_too_long": "Postal code can't exceed 50 characters" - }, - "shipping-address-type": { - "label": "Rate calculation", - "placeholder": "Rate based on Company field", - "helper_text": "Used for determining the type of the customer address used when calculating shipping costs." + "from-email": { + "label": "FROM email", + "placeholder": "Defaults to the first store email", + "helper_text": "", + "v8n_too_long": "Please use an email that is 100 characters or less" + }, + "use-email-dns": { + "label": "Improve email deliverability with DNS", + "helper_text": "Before saving your changes, make sure the DNS records for your domain list our servers.", + "checked": "Yes", + "unchecked": "No" + }, + "use-smtp-config": { + "label": "Use custom mail server", + "helper_text": "If you'd like to configure your own SMTP server for sending transaction receipt emails, you can do so here.", + "checked": "Yes", + "unchecked": "No", + "v8n_too_long": "This configuration must fit within 1000 characters when JSON-encoded" + }, + "smtp-config-host": { + "label": "Host", + "helper_text": "", + "placeholder": "smtp.example.com" + }, + "smtp-config-port": { + "label": "Port", + "helper_text": "", + "placeholder": "465" + }, + "smtp-config-username": { + "label": "Username", + "helper_text": "", + "placeholder": "sender@example.com" + }, + "smtp-config-password": { + "label": "Password", + "helper_text": "", + "placeholder": "Required" + }, + "smtp-config-security": { + "label": "Encryption", + "placeholder": "Select", + "helper_text": "", + "option_ssl": "Use SSL", + "option_tls": "Use TLS", + "option_none": "Don't encrypt" + } }, - "require-signed-shipping-rates": { - "label": "", + "shipping": { + "label": "Shipping", "helper_text": "", - "option_checked": "Prevent shipping rate tampering" + "shipping-address-type": { + "label": "Rate calculation", + "placeholder": "Select", + "helper_text": "" + }, + "require-signed-shipping-rates": { + "label": "Prevent shipping rate tampering", + "helper_text": "Enabling shipping rate signing for your store ensures that the rate the customer selects is carried through and not altered in any way. If you're intending to make use of javascript snippets on your store to alter the price or label of shipping rates or add custom rates dynamically, disable this setting as it will block those rates from being applied.", + "checked": "Yes", + "unchecked": "No" + }, + "features-multiship": { + "label": "Allow multiple destinations per order", + "helper_text": "Enables multiple shipping addresses per order. For more information, see our wiki on multiship.", + "checked": "Yes", + "unchecked": "No" + } }, - "require_signed_shipping_rates_helper_text": "Enabling shipping rate signing for your store ensures that the rate the customer selects is carried through and not altered in any way. If you're intending to make use of javascript snippets on your store to alter the price or label of shipping rates or add custom rates dynamically, disable this setting as it will block those rates from being applied.", - "features-multiship": { - "label": "", + "cart": { + "label": "Cart", "helper_text": "", - "option_checked": "Allow multiple destinations per order" - }, - "language": { - "label": "Language", - "placeholder": "English", - "helper_text": "The default language for your store's cart, checkout, and receipt strings." - }, - "locale-code": { - "label": "Locale code", - "placeholder": "en_US", - "helper_text": "The locale code for your store's locale. This will be used to format strings for your store." - }, - "currency_style_label": "Currency display", - "currency_style_helper_text": "Choose how you'd like all prices and totals to appear across your store's cart, checkout, receipt and admin.", - "receipt-continue-url": { - "label": "Return URL", - "placeholder": "https://example.com/thank-you", - "helper_text": "By default, the continue button on the receipt sends the customer to the store domain after completing a purchase. Instead, you can set a specific URL here.", - "v8n_too_long": "Please use a URL that is 300 characters or less" - }, - "app-session-time": { - "label": "Clear cart after", - "helper_text": "If your store sells products which collect personal or sensitive information as product attributes, you may want to consider lowering your cart session lifespan.", - "second": "Second", - "second_plural": "Seconds", - "minute": "Minute", - "minute_plural": "Minutes", - "hour": "Hour", - "hour_plural": "Hours", - "day": "Day", - "day_plural": "Days" + "app-session-time": { + "label": "Clear cart after", + "helper_text": "If your store sells products which collect personal or sensitive information as product attributes, you may want to consider lowering your cart session lifespan.", + "select": "Select", + "second": "Second", + "second_plural": "Seconds", + "minute": "Minute", + "minute_plural": "Minutes", + "hour": "Hour", + "hour_plural": "Hours", + "day": "Day", + "day_plural": "Days" + }, + "webhook-key-cart-signing": { + "label": "HMAC encryption key", + "placeholder": "None", + "helper_text": "" + }, + "use-cart-validation": { + "label": "Prevent product link and form tampering", + "helper_text": "Enabling this option will require all cart links and forms to pass HMAC-based verification before products can be added to the cart. You will need to sign your HTML to use this feature.", + "checked": "Yes", + "unchecked": "No" + }, + "products-require-expires-property": { + "label": "My products have limited availability", + "helper_text": "All products added to the cart for your store will need to contain the expires property to ensure stale products can't be purchased.", + "checked": "Yes", + "unchecked": "No" + } }, - "products-require-expires-property": { - "label": "", + "checkout": { + "label": "Checkout", "helper_text": "", - "option_checked": "My products have limited availability" + "customer-password-hash-type": { + "label": "Password hashing method", + "placeholder": "", + "helper_text": "" + }, + "customer-password-hash-config": { + "label": "Password hashing configuration", + "placeholder": "", + "helper_text": "", + "v8n_too_long": "Please reduce your configuration settings to 100 characters or less" + }, + "unified-order-entry-password": { + "label": "Unified order entry password", + "placeholder": "None", + "helper_text": "Set a master password here if you would like to be able to check out as your customers without having to know their password.", + "v8n_required": "Please reduce your UOE password to 100 characters" + }, + "use-single-sign-on": { + "label": "Enable SSO", + "helper_text": "When enabled, Foxy will redirect customers to your SSO endpoint prior to hitting the checkout page. You can use this to validate items or to log customers in via your own auth provider.", + "checked": "Yes", + "unchecked": "No" + }, + "single-sign-on-url": { + "label": "SSO endpoint", + "placeholder": "https://example.com/sso", + "helper_text": "", + "v8n_required": "Please enter your SSO endpoint URL", + "v8n_too_long": "Please shorten this URL to 300 characters or less" + }, + "webhook-key-sso": { + "label": "SSO secret", + "placeholder": "None", + "helper_text": "" + } }, - "products_require_expires_property_helper_text": "All products added to the cart for your store will need to contain the expires property to ensure stale products can't be purchased.", - "use-cart-validation": { - "label": "", + "receipt": { + "label": "Receipt", "helper_text": "", - "option_checked": "Prevent product link and form tampering" + "receipt-continue-url": { + "label": "Return URL", + "placeholder": "https://example.com/thank-you", + "helper_text": "By default, the continue button on the receipt sends the customer to the store domain after completing a purchase. Instead, you can set a specific URL here.", + "v8n_too_long": "Please use a URL that is 300 characters or less" + }, + "bcc-on-receipt-email": { + "label": "Send a copy of each receipt to the store email", + "helper_text": "", + "checked": "Yes", + "unchecked": "No" + } }, - "use_cart_validation_helper_text": "Enabling this option will require all cart links and forms to pass HMAC-based verification before products can be added to the cart. You will need to sign your HTML to use this feature. See our docs for more info:", - "custom-display-id-config-enabled": { - "label": "", + "custom-display-id-config": { + "label": "Identifiers", "helper_text": "", - "option_checked": "Customize Transaction ID", - "v8n_too_long": "This configuration must fit within 100 characters when JSON-encoded" + "custom-display-id-config-enabled": { + "label": "Customize Transaction ID", + "helper_text": "", + "checked": "Yes", + "unchecked": "No", + "v8n_too_long": "This configuration must fit within 500 characters when JSON-encoded" + }, + "custom-display-id-config-length": { + "label": "Total length", + "placeholder": "0", + "helper_text": "" + }, + "custom-display-id-config-start": { + "label": "Counter starts at", + "placeholder": "0", + "helper_text": "" + }, + "custom-display-id-config-prefix": { + "label": "ID prefix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-suffix": { + "label": "ID suffix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-first-example": "First ID will look like this:", + "custom-display-id-config-random-example": "Random example:", + "custom-display-id-config-transaction-journal-entries-enabled": { + "label": "Customize Transaction Journal entry ID", + "helper_text": "", + "checked": "Yes", + "unchecked": "No" + }, + "custom-display-id-config-transaction-journal-entries-transaction-separator": { + "label": "Transaction ID separator", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-authcapture-prefix": { + "label": "Authorization prefix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-capture-prefix": { + "label": "Capture prefix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-void-prefix": { + "label": "Void prefix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-refund-prefix": { + "label": "Refund prefix", + "placeholder": "None", + "helper_text": "" + }, + "custom-display-id-config-transaction-journal-entries-authcapture-example": "Authorized example:", + "custom-display-id-config-transaction-journal-entries-capture-example": "Captured example:", + "custom-display-id-config-transaction-journal-entries-void-example": "Voided example:", + "custom-display-id-config-transaction-journal-entries-refund-example": "Refunded example:" }, - "custom-display-id-config-transaction-journal-entries-enabled": { - "label": "", + "xml-datafeed": { + "label": "Legacy XML datafeed", "helper_text": "", - "option_checked": "Customize Transaction Journal entry ID" - }, - "custom-display-id-config-length": { - "label": "Length", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-start": { - "label": "Start", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-prefix": { - "label": "Prefix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-suffix": { - "label": "Suffix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-first-example": "First:", - "custom-display-id-config-random-example": "Random:", - "custom-display-id-config-transaction-journal-entries-transaction-separator": { - "label": "Separator", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-authcapture-prefix": { - "label": "Authorization prefix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-capture-prefix": { - "label": "Capture prefix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-void-prefix": { - "label": "Void prefix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-transaction-journal-entries-log-detail-request-types-transaction-refund-prefix": { - "label": "Refund prefix", - "placeholder": "", - "helper_text": "" - }, - "custom-display-id-config-transaction-journal-entries-authcapture-example": "When authorized:", - "custom-display-id-config-transaction-journal-entries-capture-example": "When captured:", - "custom-display-id-config-transaction-journal-entries-void-example": "When voided:", - "custom-display-id-config-transaction-journal-entries-refund-example": "When refunded:", - "webhook-url": { - "label": "XML Datafeed", - "placeholder": "https://example.com/my-xml-datafeed", - "helper_text": "The XML datafeed is a deprecated webhook that should only be used for third-party integrations that still require it. For any new integrations where you need to trigger a webhook for each transaction on your store, we strongly recommend using the JSON webhooks.", - "v8n_too_long": "Please shorten this URL to 300 characters or less" - }, - "webhook-key-cart-signing": { - "label": "HMAC encryption key", - "placeholder": "", - "helper_text": "We'll use this key to encrypt webhook payloads as well as to sign cart links and product forms." - }, - "webhook-key-xml-datafeed": { - "label": "XML webhook encryption key", - "placeholder": "", - "helper_text": "We'll use this key to encrypt legacy XML webhook payloads." - }, - "webhook-key-api-legacy": { - "label": "Legacy API key", - "placeholder": "", - "helper_text": "API key you can use to access the legacy API." - }, - "webhook-key-sso": { - "label": "SSO secret", - "placeholder": "", - "helper_text": "We'll use this value to validate session tokens generated by your SSO setup." - }, - "single-sign-on-url": { - "label": "SSO endpoint", - "placeholder": "https://example.com/sso", - "helper_text": "When configured, Foxy will redirect customers to this URL prior to hitting the checkout page. You can use this to validate items or to log customers in via your own auth provider.", - "v8n_required": "Please enter your SSO endpoint URL", - "v8n_too_long": "Please shorten this URL to 300 characters or less" - }, - "unified-order-entry-password": { - "label": "Unified order entry password", - "placeholder": "", - "helper_text": "Set a master password here if you would like to be able to check out as your customers without having to know their password.", - "v8n_required": "Please reduce your UOE password to 100 characters" - }, - "store-version-uri": { - "label": "Version", - "placeholder": "Defaults to latest", - "helper_text": "Store version including templates, libraries, payment options and more. It's recommended to upgrade your integration to the newest version as soon as it becomes available." - }, - "checkout-type": { - "label": "New accounts", - "placeholder": "", - "helper_text": "The preferred configuration of your customer checkout experience, such as defaulting to guest checkout or requiring account creation with each checkout." - }, - "customer-password-hash-type": { - "label": "Password hashing method (advanced)", - "placeholder": "", - "helper_text": "When saving a customer to Foxy, this is the password hashing method that will be used." - }, - "customer-password-hash-config": { - "label": "Password hashing configuration (advanced)", - "placeholder": "", - "helper_text": "Configuration settings for the selected hashing method.", - "v8n_too_long": "Please reduce your configuration settings to 100 characters or less" + "use-webhook": { + "label": "Enable datafeed", + "helper_text": "This is a deprecated webhook that should only be used for third-party integrations that still require it. For any new integrations where you need to trigger a webhook for each transaction on your store, we strongly recommend using the JSON webhooks.", + "checked": "Yes", + "unchecked": "No" + }, + "webhook-url": { + "label": "Webhook URL", + "placeholder": "https://example.com/my-xml-datafeed", + "helper_text": "", + "v8n_too_long": "Please shorten this URL to 300 characters or less" + }, + "webhook-key-xml-datafeed": { + "label": "Encryption key", + "placeholder": "None", + "helper_text": "" + } }, "timestamps": { "date_created": "Created on", From 4299789962b968c26e825d5bcf3712b85eb6ac1c Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 3 Sep 2024 13:25:22 -0300 Subject: [PATCH 12/15] internal: adjust summary item styles --- .../InternalAsyncListControl.ts | 4 ++-- .../InternalPasswordControl/vaadinStyles.ts | 2 +- .../InternalSummaryControl.ts | 13 ++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts index 7fc8e7c5..13a36f6c 100644 --- a/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts +++ b/src/elements/internal/InternalAsyncListControl/InternalAsyncListControl.ts @@ -307,8 +307,8 @@ export class InternalAsyncListControl extends InternalEditableControl { > `} -
- +
+ ${this.label && this.label !== 'label' ? this.label : ''} diff --git a/src/elements/internal/InternalPasswordControl/vaadinStyles.ts b/src/elements/internal/InternalPasswordControl/vaadinStyles.ts index 02799bb4..5646c2da 100644 --- a/src/elements/internal/InternalPasswordControl/vaadinStyles.ts +++ b/src/elements/internal/InternalPasswordControl/vaadinStyles.ts @@ -10,7 +10,7 @@ registerStyles( :host([theme~='summary-item']) .vaadin-text-field-container { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: auto auto; grid-template-rows: repeat(3, min-content); gap: 0 var(--lumo-space-m); } diff --git a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts index 8944aaa8..da418beb 100644 --- a/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts +++ b/src/elements/internal/InternalSummaryControl/InternalSummaryControl.ts @@ -10,7 +10,7 @@ export class InternalSummaryControl extends InternalEditableControl { ...super.styles, css` ::slotted(*) { - background-color: var(--lumo-base-color); + background-color: var(--lumo-contrast-5pct); padding: calc(0.625em + (var(--lumo-border-radius) / 4) - 1px); } `, @@ -23,14 +23,13 @@ export class InternalSummaryControl extends InternalEditableControl { renderControl(): TemplateResult { return html` -

${this.label}

-
+
+

${this.label}

+

${this.helperText}

+
+
-

${this.helperText}

`; } } From 337a9414a1ac3049e795fed095d0beb772ba3c26 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 3 Sep 2024 13:27:20 -0300 Subject: [PATCH 13/15] internal(text-control): add missing prefix/suffix to summary item layout --- .../InternalTextControl.test.ts | 18 ++++++++++++++++++ .../InternalTextControl/InternalTextControl.ts | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/src/elements/internal/InternalTextControl/InternalTextControl.test.ts b/src/elements/internal/InternalTextControl/InternalTextControl.test.ts index ee4318e5..a106239f 100644 --- a/src/elements/internal/InternalTextControl/InternalTextControl.test.ts +++ b/src/elements/internal/InternalTextControl/InternalTextControl.test.ts @@ -349,4 +349,22 @@ describe('InternalTextControl', () => { submitMethod.restore(); }); + + it('renders prefix text in summary item layout', async () => { + const control = await fixture(html` + + + `); + + expect(control.renderRoot).to.include.text('Test Prefix'); + }); + + it('renders suffix text in summary item layout', async () => { + const control = await fixture(html` + + + `); + + expect(control.renderRoot).to.include.text('Test Suffix'); + }); }); diff --git a/src/elements/internal/InternalTextControl/InternalTextControl.ts b/src/elements/internal/InternalTextControl/InternalTextControl.ts index ccd3e2b7..6d4f9cb5 100644 --- a/src/elements/internal/InternalTextControl/InternalTextControl.ts +++ b/src/elements/internal/InternalTextControl/InternalTextControl.ts @@ -102,6 +102,8 @@ export class InternalTextControl extends InternalEditableControl {
+ ${this.prefix ? html`
${this.prefix}
` : ''} + + ${this.suffix ? html`
${this.suffix}
` : ''} +