diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 2a927030b..6df702862 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -6,7 +6,7 @@ runs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 20 + node-version-file: .nvmrc - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index 659affd9e..f633ea3f7 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ ai-prompt.md chat-history.md scratchpad.md .ai-history/ + +# Hummingbird QA seed: per-shop record of what --undo must restore +docs/qa/.seed-state.json diff --git a/.nvmrc b/.nvmrc index 70f6554c7..166aae115 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.19.4 +v24.21.0 diff --git a/CONTEXT.md b/CONTEXT.md index 79a8025ef..233c13b7a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -177,3 +177,9 @@ When asked to write or modify code, you MUST follow these rules: 9. **Keep it modular (SRP):** Separate logic into cohesive components. 10. **Smarty variables:** Ensure proper escaping for Smarty variables (e.g., `{$variable|escape:'html':'UTF-8'}`). +11. **QA checklist sync:** `docs/qa/testing-checklist.md` lists what to test in + the theme, and every list in it is derived from a file in the repository. + When you change hook assignments in `config/theme.yml`, add or remove a + module override under `modules/`, add a page or partial under `templates/`, + or change the breakpoints, you MUST update that file in the same change. Its + section 9 maps each list back to its source. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f3fe2dc9..ffe8bc446 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Follow the [README.md](README.md) for detailed setup and build instructions. [Set up your Git for contributing](https://build.prestashop-project.org/howtos/misc/set-up-your-git-for-contributing/) 2. Use an editor that supports .editorconfig.
[Download an EditorConfig plugin](http://editorconfig.org/#download). -3. Make sure you have [Node.js v20](https://nodejs.org/en/download/) installed. +3. Make sure you have [Node.js v24](https://nodejs.org/en/download/) installed. 4. If using AI tools to write code, make sure your agent has read the [`CONTEXT.md`](README.md#🤖-ai-assisted-development) file. diff --git a/README.md b/README.md index 8459f860f..5530d2d5c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![CI](https://github.com/PrestaShop/hummingbird/actions/workflows/lint.yml/badge.svg) ![Docker Ready](https://img.shields.io/badge/docker-ready-blue.svg) -![Node.js v20](https://img.shields.io/badge/node.js-20.x-blue.svg) +![Node.js v24](https://img.shields.io/badge/node.js-24.x-blue.svg) ![License](https://img.shields.io/badge/license-AFL%203.0-lightgray.svg) Hummingbird is a modern theme for PrestaShop. See the @@ -15,7 +15,7 @@ supported by each branch. | ----------- | ---------- | ------ | | `develop` | `~10.0.0` | Next major | | `2.x` | `~9.2.0` | Maintained | -| `2.1.0` | `~9.2.0` | Released | +| `2.1.x` | `~9.2.0` | Released | | `2.0.0` | `~9.1.0` | Released | > [!NOTE] @@ -33,8 +33,8 @@ supported by each branch. To work on Hummingbird, you'll need: -- Node.js **v20.x**. -- npm **v8**. +- Node.js **v24.x**. +- npm **v11**. ## 📑 Table of Contents diff --git a/config/theme.yml b/config/theme.yml index 2d6c3ade4..ebaa8a3a5 100644 --- a/config/theme.yml +++ b/config/theme.yml @@ -1,6 +1,6 @@ name: hummingbird display_name: Hummingbird -version: 2.1.0 +version: 2.1.2 author: name: "PrestaShop Team and Contributors" email: "pub@prestashop.com" @@ -219,6 +219,11 @@ global_settings: width: 2000 height: 400 scope: [categories] + manufacturer_product_page: + width: 98 + height: 98 + scope: [manufacturers] + image_fitment: bound theme_settings: rtl_generation: false diff --git a/docs/qa/testing-checklist.md b/docs/qa/testing-checklist.md new file mode 100644 index 000000000..a189de62f --- /dev/null +++ b/docs/qa/testing-checklist.md @@ -0,0 +1,520 @@ +# Hummingbird QA checklist + +Full test pass on the Hummingbird theme, from environment setup to sign-off. Run it before a release, after a dependency bump, or when a change touches shared layout, JS or SCSS. + +Hummingbird is a presentation layer. Bugs in business logic, database queries or controller behaviour belong to [PrestaShop/PrestaShop](https://github.com/PrestaShop/PrestaShop). Bugs in a module's own markup or logic belong to that module's repository; a theme-side override is a shim, not a fix. + +For QA scoped to one pull request, use the `prestashop-pr-qa` skill instead. + +This file describes the theme as it is today and goes stale on its own. Update it in the same pull request as the change whenever the theme evolves. Section 9 lists what it is derived from. + +Items marked **(config)** change a shop-wide setting. Note the current value first, and restore it before moving on, or every item after it is testing a different shop. + +This folder is not just this file. `seed.php` brings a demo shop up to the state the list below assumes, `config.php` reads and writes one setting through the API the back office itself calls, and `bem-inventory.mjs` prints the theme's real class names. Section 1.5 says how to use them. Start there: most of what looks unanswerable on a fresh demo install is a shop that has not been prepared. + +Items marked **(human)** cannot be settled by a machine. A tool can measure them and show the number and the picture, and several here say what it can measure, but the verdict is a person's: judging a colour over a photograph, whether a focus ring is visible enough, whether anything was lost at 300% zoom, or what a screen reader actually announces. A pass that reports them green without a person having looked is not a pass, it is a gap with a tick in it. + +## 1. Environment + +### 1.1 Build the theme + +Requirements and commands are in the README: [Develop on Hummingbird](../../README.md#-develop-on-hummingbird). + +- [ ] Dependencies installed and assets built, no errors and no new warnings +- [ ] Linters and Prettier clean +- [ ] `npm test` green + +### 1.2 Run PrestaShop + +Two options. Pick the first one to test the working copy, the second one to test the released theme against a core checkout. + +#### Option A: docker from this repo + +Follow [Run Hummingbird with Docker](../../README.md#-run-hummingbird-with-docker) in the README. The compose files mount the repository root on `themes/hummingbird`, so the install runs the working copy. + +- [ ] Front office and back office both respond. The addresses depend on how the shop is run: the compose files in this repository happen to serve port 8887 with the back office on `/admin-dev`, but any other installation will differ, so use the addresses of the shop under test + +#### Option B: a PrestaShop/PrestaShop checkout + +`composer install` in the core repository pulls `prestashop/hummingbird` at the constraint set in its `composer.json`, so you get a released tag, not the branch under test. `composer/installers` puts it in `themes/hummingbird`. + +Hummingbird is the default theme of a fresh core install: the core ships `PS_FF_DEFAULT_THEME=hummingbird` in `.env`, and `Theme::DEFAULT_THEME` falls back to the same value. Nothing to switch on in the back office. + +To test local changes rather than the released theme, replace `themes/hummingbird` with a symlink to this working copy, or declare a composer path repository pointing at it. That is setup, not a test, so it carries no tick box. + +- [ ] Confirm which tag composer resolved: `composer show prestashop/hummingbird` + +#### Both options + +- [ ] Theme version in Design > Theme & Logo matches the version under test, and the PrestaShop version is within the compatibility range declared in `config/theme.yml` +- [ ] Demo catalogue installed, native modules enabled and up to date + +Record the environment before starting. Section 8 refers back to it, and a defect without it is not reproducible. + +| Field | Value | +| --- | --- | +| Front office URL | | +| Back office URL | | +| Install method | Option A or Option B | +| Theme version | | +| PrestaShop version | | +| Date of the pass | | + +### 1.3 Test matrix + +Run the pass twice on Chrome, once per profile. Use two Chrome profiles, or a separate incognito window, so the two sessions and carts do not collide. + +| Profile | Customer group | Viewport | +| --- | --- | --- | +| Standard B2C | Visitor, then registered customer, prices incl. tax | Desktop and mobile | +| B2B | B2B group, prices excl. tax | Desktop and mobile | + +Cover at least once: + +- [ ] Safari, desktop and iOS **(human)**: WebKit can be driven for the desktop half; iOS needs the real device +- [ ] A second language (`ps_languageselector`, translations, date and price formats) +- [ ] A second currency (`ps_currencyselector`) +- [ ] Multistore, if the change touches header, footer or URLs **(config)** **(human)** + +### 1.4 Cache + +In Advanced Parameters > Performance: + +- [ ] CCC (Combine, Compress and Cache): all options disabled, so the pass exercises the theme's own assets rather than a merged bundle +- [ ] Smarty: Force compilation set to Yes, Cache set to No (`PS_DEV_MODE: 1` in `docker-compose-prestashop.yml` already forces compilation; the Flashlight compose file does not set it) +- [ ] Cache cleared, and cleared again after each configuration change +- [ ] Browser cache disabled in DevTools, or a hard reload after `npm run build` + +See [Troubleshooting](../../README.md#-troubleshooting) in the README if assets still do not update. + +### 1.5 Prepare the shop + +A fresh demo install cannot answer this list. It has no linked accessory, so the You might also like block never renders; no paid order, so best sellers and cross-selling have nothing to rank; one currency, no cart rule, nothing out of stock, no product whose minimum quantity is above one, and every product on the same carriers, so no cart can be made to split. None of that is a defect of the theme, and a pass that reports it as one has spent its time describing the shop. + +`seed.php` makes the narrowest thing each of those points needs, through PrestaShop's own model classes so search indexes, image types and friendly URLs stay consistent. It is idempotent, it names everything `QA` so a human can tell it apart a month later, and it reports what it did. + +```bash +# the shop root is the working directory, not where the file lives +php docs/qa/seed.php --status # what is missing, changes nothing +php docs/qa/seed.php --apply # create it +php docs/qa/seed.php --undo # put back everything that can be put back +``` + +Run `--status` before starting. What it lists missing is the difference between answering the checklist and explaining why you could not. + +It creates, each for a named point: a linked accessory; a cart rule; a second currency; an additional description on a category; a **B2B customer group showing prices excluding tax, with category and module access copied from the default group**; a validated order; an out-of-stock product; a product whose minimum quantity is above one; a product that answers 410; a carrier restricted so a cart splits into several shipments; an image customisation field and a second customisable product; and a category whose subcategories have mixed thumbnails. + +The B2B group is worth its own sentence. A group created by hand gets no category or module access, and PrestaShop grants both per group: every category then answers 403 and the header comes out empty, which reads exactly like the theme collapsing. The script copies the access from the default customer group. Section 1.3's B2B profile does not work without this. + +`--undo` restores the settings it changed and deletes the records it created. One thing it cannot undo is the order it moved to Payment accepted, and it says so at the end of the run rather than leaving it implicit. + +Two more things live here: + +* `config.php --get NAME` / `--set NAME=value` / `--unset NAME` reads and writes one setting through the Configuration API the back office calls, printing the value read back rather than the one asked for. Section 4 is twenty-four settings to flip and restore; use this for the flip and the browser for the front office half. A pass that cannot put a setting back is worse than one that never changed it. +* `bem-inventory.mjs` prints the class names that actually reach the browser, read out of the templates and the stylesheets. Read it before writing a selector: the pager is a ` + + `); + + const form = document.querySelector('#f') as HTMLFormElement; + const button = document.querySelector('button') as HTMLButtonElement; + button.click(); + + expect(form.classList.contains('was-validated')).toBe(true); + }); + + it('binds a link marked as the submit control', () => { + // WHY: CONTEXT.md documents data-ps-action="form-validation-submit" for buttons and links, + // and a link is not a listed form control, so this is what form.elements would miss. + setUp(` +
+ + Send +
+ `); + + const form = document.querySelector('#f') as HTMLFormElement; + const link = document.querySelector('a') as HTMLAnchorElement; + link.click(); + + expect(form.classList.contains('was-validated')).toBe(true); + }); + + it('does not throw when the form has no submit button', () => { + expect(() => setUp('
')).not.toThrow(); + }); +}); diff --git a/src/js/modules/productcomments.ts b/src/js/modules/productcomments.ts index c39de09ad..96ff3fb58 100644 --- a/src/js/modules/productcomments.ts +++ b/src/js/modules/productcomments.ts @@ -47,6 +47,16 @@ interface UsefulnessResponse { error?: string; } +interface ProductListReviewData { + id_product: number; + comments_nb: string; + average_grade: number | null; +} + +interface ProductListReviewsResponse { + products: ProductListReviewData[]; +} + interface ReportResponse { success: boolean; error?: string; @@ -808,13 +818,15 @@ class ProductListReviews { private static async loadProductListReviews(): Promise { const {productListReviews} = ProductCommentsElements; - if (!productListReviews) return; + if (productListReviews.length === 0) return; + // The same product can appear in several widgets, and the ratings endpoint + // fails on a duplicated id list. const productIds: Array = []; productListReviews.forEach((review) => { const productId = parseInt(review.getAttribute('data-id') || '0', 10); - if (productId > 0) { + if (productId > 0 && productIds.indexOf(productId) === -1) { productIds.push(productId); } }); @@ -822,61 +834,108 @@ class ProductListReviews { if (productIds.length === 0) return; try { - const productListReview = document.querySelector(SELECTORS.PRODUCT_LIST_REVIEW); - const url = productListReview?.getAttribute('data-url'); + const url = productListReviews[0].getAttribute('data-url'); if (!url) return; - const response = await fetch(`${url}?id_products[]=${productIds.join('&id_products[]=')}`); + // The endpoint url can already carry a query string, so the ids cannot be + // appended with a literal `?`. + const endpoint = new URL(url, window.location.href); + productIds.forEach((productId) => { + endpoint.searchParams.append('id_products[]', productId.toString()); + }); - if (response.status === 200) { - const data = await response.json(); - this.updateProductListReviews(data); + const response = await fetch(endpoint.toString()); + + if (!response.ok) { + console.warn(`Product list reviews request failed with status ${response.status}`); + + return; } + + // The endpoint answers 200 with an empty body when it has nothing to return. + const payload = await response.text(); + + if (payload.trim() === '') return; + + const data = JSON.parse(payload) as ProductListReviewsResponse; + + if (!Array.isArray(data?.products)) { + console.warn('Unexpected product list reviews payload:', payload.slice(0, 200)); + + return; + } + + this.updateProductListReviews(data); } catch (error) { console.error('Error loading product list reviews:', error); } } - private static updateProductListReviews( - data: { products: Array<{ id_product: number; comments_nb: string; average_grade: number | null }> }, - ): void { + private static updateProductListReviews(data: ProductListReviewsResponse): void { const {productListReviews} = ProductCommentsElements; + const reviewsByProductId: Record = {}; + + data.products.forEach((product) => { + reviewsByProductId[product.id_product] = product; + }); productListReviews.forEach((review) => { const productId = parseInt(review.getAttribute('data-id') || '0', 10); - const productData = data.products.find((p) => p.id_product === productId); + const productData = reviewsByProductId[productId]; + const commentsNb = productData ? parseInt(String(productData.comments_nb), 10) : 0; + const averageGrade = productData ? productData.average_grade : null; - if (productData && productData.comments_nb !== '0' && productData.average_grade !== null) { - this.updateSingleProductReview(review as HTMLElement, { - grade: Math.round(productData.average_grade), - comments_nb: parseInt(productData.comments_nb, 10), - }); - review.classList.add('d-flex'); + // Reset, otherwise a product whose reviews were deleted keeps its old grade. + if (!commentsNb || averageGrade === null) { + this.clearProductListReview(review as HTMLElement); + + return; } + + this.updateSingleProductReview(review as HTMLElement, { + grade: Math.round(averageGrade), + comments_nb: commentsNb, + }); + review.classList.add('d-flex'); }); } + private static clearProductListReview(reviewElement: HTMLElement): void { + reviewElement.classList.remove('d-flex'); + + const starsContainer = reviewElement.querySelector(SELECTORS.GRADE_STARS); + + if (starsContainer) { + ProductCommentsRating.renderStars(starsContainer, 0); + } + + const productListCommentsNumber = ProductCommentsElements.getProductListCommentsNumber(reviewElement); + const productListGradeNumber = ProductCommentsElements.getProductListGradeNumber(reviewElement); + + if (productListCommentsNumber) productListCommentsNumber.textContent = '0'; + if (productListGradeNumber) productListGradeNumber.textContent = '0'; + } + private static updateSingleProductReview( reviewElement: HTMLElement, data: { grade: number; comments_nb: number }, ): void { const starsContainer = reviewElement.querySelector(SELECTORS.GRADE_STARS); if (starsContainer) { - this.updateStarsWithRating(starsContainer, data.grade); + ProductCommentsRating.renderStars(starsContainer, data.grade); } const productListCommentsNumber = ProductCommentsElements.getProductListCommentsNumber(reviewElement); - const productListGradeNUmber = ProductCommentsElements.getProductListGradeNumber(reviewElement); + const productListGradeNumber = ProductCommentsElements.getProductListGradeNumber(reviewElement); - if (productListCommentsNumber && productListGradeNUmber) { + if (productListCommentsNumber) { productListCommentsNumber.textContent = data.comments_nb.toString(); - productListGradeNUmber.textContent = data.grade.toString(); } - } - private static updateStarsWithRating(container: Element, grade: number): void { - ProductCommentsRating.renderStars(container, grade); + if (productListGradeNumber) { + productListGradeNumber.textContent = data.grade.toString(); + } } } diff --git a/src/scss/bootstrap/components/_breadcrumb.scss b/src/scss/bootstrap/components/_breadcrumb.scss index ef219f455..2339ef65e 100644 --- a/src/scss/bootstrap/components/_breadcrumb.scss +++ b/src/scss/bootstrap/components/_breadcrumb.scss @@ -11,10 +11,6 @@ $component-name: breadcrumb; overflow-x: scroll; scrollbar-width: none; - &[data-depth="1"] { - display: none; - } - &-item { padding: 0; white-space: nowrap; diff --git a/src/scss/prestashop/components/customer/_customer-account-menu.scss b/src/scss/prestashop/components/customer/_customer-account-menu.scss index 0b9c1ed30..ac8a616ea 100644 --- a/src/scss/prestashop/components/customer/_customer-account-menu.scss +++ b/src/scss/prestashop/components/customer/_customer-account-menu.scss @@ -90,8 +90,13 @@ $component-name: account-menu; } &--sidebar { + position: sticky; + top: var(--scroll-padding-top); + z-index: $zindex-sticky; display: flex; flex-direction: column; + max-height: calc(100vh - var(--scroll-padding-top)); + overflow-y: auto; } } } diff --git a/src/scss/prestashop/pages/_index.scss b/src/scss/prestashop/pages/_index.scss index d71c6ef66..6194be21c 100644 --- a/src/scss/prestashop/pages/_index.scss +++ b/src/scss/prestashop/pages/_index.scss @@ -9,6 +9,5 @@ @import "login"; @import "order-details"; @import "order-returns"; -@import "page-not-found"; @import "pages"; @import "product"; diff --git a/src/scss/prestashop/pages/_page-not-found.scss b/src/scss/prestashop/pages/_page-not-found.scss deleted file mode 100644 index d6b1de65b..000000000 --- a/src/scss/prestashop/pages/_page-not-found.scss +++ /dev/null @@ -1,9 +0,0 @@ -/** - * For the full copyright and license information, please view the - * LICENSE.md file that was distributed with this source code. - */ -#pagenotfound { - .wrapper { - padding-top: 3rem; - } -} diff --git a/templates/catalog/_partials/product-details.tpl b/templates/catalog/_partials/product-details.tpl index 983c3f127..13274721d 100644 --- a/templates/catalog/_partials/product-details.tpl +++ b/templates/catalog/_partials/product-details.tpl @@ -25,14 +25,14 @@
- {if isset($product_manufacturer.image.bySize.small_default.url)} + {if isset($product_manufacturer.image.bySize.manufacturer_product_page.url)} - diff --git a/templates/catalog/_partials/product-prices.tpl b/templates/catalog/_partials/product-prices.tpl index c58e61003..942bbf3ca 100644 --- a/templates/catalog/_partials/product-prices.tpl +++ b/templates/catalog/_partials/product-prices.tpl @@ -55,36 +55,40 @@ {/if} {/block} -
- - {if !$configuration.taxes_enabled} - {l s='No tax' d='Shop.Theme.Catalog'} - {elseif $configuration.display_taxes_label} - {$product.labels.tax_long} - {/if} - - {hook h='displayProductPriceBlock' product=$product type="price"} - {hook h='displayProductPriceBlock' product=$product type="after_price"} - + {capture name='product_price_hooks'}{hook h='displayProductPriceBlock' product=$product type="price"}{hook h='displayProductPriceBlock' product=$product type="after_price"}{/capture} + {if $configuration.display_taxes_label || $product.ecotax.amount > 0 || $smarty.capture.product_price_hooks|trim} +
+ + {if $configuration.display_taxes_label} + {if $configuration.taxes_enabled} + {$product.labels.tax_long} + {else} + {l s='No tax' d='Shop.Theme.Catalog'} + {/if} + {/if} - {* Separator *} - {if $configuration.display_taxes_label && $product.ecotax.amount > 0} - {/if} + {$smarty.capture.product_price_hooks nofilter} + - {block name='product_ecotax'} - {if $product.ecotax.amount> 0} - - {l s='Including %amount% for ecotax' d='Shop.Theme.Catalog' sprintf=['%amount%' => $product.ecotax.value]} - {if $product.has_discount} - {l s='(not impacted by the discount)' d='Shop.Theme.Catalog'} - {/if} - - {/if} - {/block} -
+ {* Separator *} + {if $configuration.display_taxes_label && $product.ecotax.amount > 0} - {/if} + + {block name='product_ecotax'} + {if $product.ecotax.amount> 0} + + {l s='Including %amount% for ecotax' d='Shop.Theme.Catalog' sprintf=['%amount%' => $product.ecotax.value]} + {if $product.has_discount} + {l s='(not impacted by the discount)' d='Shop.Theme.Catalog'} + {/if} + + {/if} + {/block} +
+ {/if} {block name='product_without_taxes'} {if $priceDisplay == 0 && $configuration.is_b2b} - {l s='%price% tax excluded' d='Shop.Theme.Catalog' sprintf=['%price%' => $product.price_tax_exc]} + {l s='%price% tax excluded' d='Shop.Theme.Catalog' sprintf=['%price%' => $product.price_tax_excluded]} {/if} {/block}
diff --git a/templates/checkout/_partials/address-selector-block.tpl b/templates/checkout/_partials/address-selector-block.tpl index 75e3bfeb8..9ecaad96d 100644 --- a/templates/checkout/_partials/address-selector-block.tpl +++ b/templates/checkout/_partials/address-selector-block.tpl @@ -15,6 +15,7 @@ class="form-check-input" name="{$name}" value="{$address.id}" + {if isset($form_id)}form="{$form_id}"{/if} {if $address.id == $selected}checked{/if} aria-label="{l s='Select address: %addressAlias%' sprintf=['%addressAlias%' => $address.alias|lower] d='Shop.Theme.Actions'}" aria-describedby="address-{$address.id}" @@ -60,7 +61,7 @@ {if $interactive}
- +
{/if} {/block} diff --git a/templates/checkout/_partials/checkout-step-buttons.tpl b/templates/checkout/_partials/checkout-step-buttons.tpl index 97b591c47..13e8b7b65 100644 --- a/templates/checkout/_partials/checkout-step-buttons.tpl +++ b/templates/checkout/_partials/checkout-step-buttons.tpl @@ -11,7 +11,7 @@ {/if} {if isset($next_step) && (!isset($show_next_button) || $show_next_button)} - diff --git a/templates/checkout/_partials/steps/addresses.tpl b/templates/checkout/_partials/steps/addresses.tpl index 15f151751..436be29b0 100644 --- a/templates/checkout/_partials/steps/addresses.tpl +++ b/templates/checkout/_partials/steps/addresses.tpl @@ -5,110 +5,118 @@ {extends file='checkout/_partials/steps/checkout-step.tpl'} {block name='step_content'} + {$addresses_form_id = 'checkout-addresses-form'}
+ {* WHY: this step also renders full address
elements (checkout/_partials/address-form.tpl). + Wrapping those in this form produced nested tags: the HTML parser drops the inner start + tag and lets the inner
close THIS form instead, which orphaned the address selector and + the Continue button that follow it. This form therefore stays empty and its controls are + attached explicitly through the form attribute. *}
- {if $use_same_address} -

- {if $cart.is_virtual} - {l s='The selected address will be used as your personal address (for invoice).' d='Shop.Theme.Checkout'} - {else} - {l s='The selected address will be used both as your personal address (for invoice) and as your delivery address.' d='Shop.Theme.Checkout'} - {/if} -

+ >
+ {if $use_same_address} +

+ {if $cart.is_virtual} + {l s='The selected address will be used as your personal address (for invoice).' d='Shop.Theme.Checkout'} + {else} + {l s='The selected address will be used both as your personal address (for invoice) and as your delivery address.' d='Shop.Theme.Checkout'} + {/if} +

+ {else} +

{l s='Shipping Address' d='Shop.Theme.Checkout'}

+ {/if} + + {if $show_delivery_address_form} +
+ {render file = 'checkout/_partials/address-form.tpl' + ui = $address_form + use_same_address = $use_same_address + type = "delivery" + form_has_continue_button = $form_has_continue_button + } +
+ {elseif $customer.addresses|count> 0} +
+ {include file = 'checkout/_partials/address-selector-block.tpl' + addresses = $customer.addresses + name = "id_address_delivery" + selected = $id_address_delivery + type = "delivery" + form_id = $addresses_form_id + interactive = !$show_delivery_address_form and !$show_invoice_address_form + } +
+ + {if isset($delivery_address_error)} +

{$delivery_address_error.exception}

{else} -

{l s='Shipping Address' d='Shop.Theme.Checkout'}

+ {/if} - {if $show_delivery_address_form} -
+ + + {if $use_same_address && !$cart.is_virtual} + + {l s='Billing address differs from shipping address' d='Shop.Theme.Checkout'} + + {/if} + {/if} + + {if !$use_same_address} +

{l s='Your Invoice Address' d='Shop.Theme.Checkout'}

+ + {if $show_invoice_address_form} +
{render file = 'checkout/_partials/address-form.tpl' ui = $address_form use_same_address = $use_same_address - type = "delivery" + type = "invoice" form_has_continue_button = $form_has_continue_button }
- {elseif $customer.addresses|count> 0} -
+ {else} +
{include file = 'checkout/_partials/address-selector-block.tpl' addresses = $customer.addresses - name = "id_address_delivery" - selected = $id_address_delivery - type = "delivery" + name = "id_address_invoice" + selected = $id_address_invoice + type = "invoice" + form_id = $addresses_form_id interactive = !$show_delivery_address_form and !$show_invoice_address_form }
- {if isset($delivery_address_error)} -

{$delivery_address_error.exception}

+ {if isset($invoice_address_error)} +

{$invoice_address_error.exception}

{else} - + {/if} - - - {if $use_same_address && !$cart.is_virtual} - - {l s='Billing address differs from shipping address' d='Shop.Theme.Checkout'} - - {/if} + + + {l s='Add new address' d='Shop.Theme.Actions'} + {/if} + {/if} - {if !$use_same_address} -

{l s='Your Invoice Address' d='Shop.Theme.Checkout'}

- - {if $show_invoice_address_form} -
- {render file = 'checkout/_partials/address-form.tpl' - ui = $address_form - use_same_address = $use_same_address - type = "invoice" - form_has_continue_button = $form_has_continue_button - } -
- {else} -
- {include file = 'checkout/_partials/address-selector-block.tpl' - addresses = $customer.addresses - name = "id_address_invoice" - selected = $id_address_invoice - type = "invoice" - interactive = !$show_delivery_address_form and !$show_invoice_address_form - } -
- - {if isset($invoice_address_error)} -

{$invoice_address_error.exception}

- {else} - - {/if} +
+ {include file='checkout/_partials/checkout-step-buttons.tpl' show_next_button=false} - - - {l s='Add new address' d='Shop.Theme.Actions'} - - {/if} + {if !$form_has_continue_button} + {include file='checkout/_partials/checkout-step-buttons.tpl' show_back_button=false submit_name='confirm-addresses' submit_value='1' form_id=$addresses_form_id} + {/if} - -
- {include file='checkout/_partials/checkout-step-buttons.tpl' show_next_button=false} - - {if !$form_has_continue_button} - {include file='checkout/_partials/checkout-step-buttons.tpl' show_back_button=false submit_name='confirm-addresses' submit_value='1'} - - {/if} -
- +
{capture name="address_selector_bottom"}{hook h='displayAddressSelectorBottom'}{/capture} {if $smarty.capture.address_selector_bottom} diff --git a/templates/cms/stores.tpl b/templates/cms/stores.tpl index 10974d9b5..6b961f606 100644 --- a/templates/cms/stores.tpl +++ b/templates/cms/stores.tpl @@ -58,9 +58,9 @@ {if $store.note || $store.phone || $store.fax || $store.email}
- +
@@ -72,19 +72,31 @@ @@ -95,7 +107,7 @@
-
diff --git a/templates/customer/_partials/account-transformation-form.tpl b/templates/customer/_partials/account-transformation-form.tpl index 7339417e6..5ffddac85 100644 --- a/templates/customer/_partials/account-transformation-form.tpl +++ b/templates/customer/_partials/account-transformation-form.tpl @@ -15,6 +15,13 @@
+ {* WHY: the guest tracking form is posted, so the order lookup is no longer in the URL this form posts back + to, and the controller finds nothing to transform without it. Order confirmation includes this partial + without these parameters and resolves the order from the cart instead. *} + {if isset($order_reference) && isset($guest_email)} + + + {/if}
{/foreach} {/if} + {hook h='displayOrderDetailProductLine' id_order=$product.id_order id_order_detail=$product.id_order_detail id_product=$product.id_product} diff --git a/templates/customer/_partials/order-detail-return.tpl b/templates/customer/_partials/order-detail-return.tpl index 9dc1d6032..45db5c10c 100644 --- a/templates/customer/_partials/order-detail-return.tpl +++ b/templates/customer/_partials/order-detail-return.tpl @@ -128,6 +128,7 @@
{/foreach} {/if} + {hook h='displayOrderDetailProductLine' id_order=$product.id_order id_order_detail=$product.id_order_detail id_product=$product.id_product} diff --git a/templates/customer/guest-login.tpl b/templates/customer/guest-login.tpl index 5fc30640f..6b74212ad 100644 --- a/templates/customer/guest-login.tpl +++ b/templates/customer/guest-login.tpl @@ -11,7 +11,11 @@ {/block} {block name='page_content'} - + {* Submitted with POST so the shopper's email address does not end up in the URL, where it reaches + analytics as a page path, the browser history, the access log and any referrer. The controller reads + the fields with Tools::getValue(), which takes POST as well, and links that already carry the + parameters in a query string keep working. *} +

{l s='To track your order, please enter the following information:' d='Shop.Theme.Customeraccount'}

diff --git a/templates/customer/guest-tracking.tpl b/templates/customer/guest-tracking.tpl index f05c3bac6..404f59f33 100644 --- a/templates/customer/guest-tracking.tpl +++ b/templates/customer/guest-tracking.tpl @@ -19,7 +19,7 @@ {if !$registered_customer_exists} {block name='page_content' append} {block name='account_transformation_form'} - {include file='customer/_partials/account-transformation-form.tpl'} + {include file='customer/_partials/account-transformation-form.tpl' order_reference=$order.details.reference guest_email=$guest_email} {/block} {/block} {/if} diff --git a/templates/errors/404.tpl b/templates/errors/404.tpl index 28fdb04eb..9524b886c 100644 --- a/templates/errors/404.tpl +++ b/templates/errors/404.tpl @@ -4,8 +4,6 @@ *} {extends file='page.tpl'} -{block name='breadcrumb'}{/block} - {block name='container_class'}container container--limited-md text-center{/block} {block name='page_header_container'} diff --git a/templates/errors/410.tpl b/templates/errors/410.tpl index 7fda1449e..5dc54de73 100644 --- a/templates/errors/410.tpl +++ b/templates/errors/410.tpl @@ -4,8 +4,6 @@ *} {extends file='page.tpl'} -{block name='breadcrumb'}{/block} - {block name='container_class'}container container--limited-md text-center{/block} {block name='page_header_container'}