From 85b75b0aea3ce2ea81bf4e81d2e001528772de73 Mon Sep 17 00:00:00 2001 From: Sofia Matulis Date: Mon, 6 May 2024 16:07:19 -0400 Subject: [PATCH] [Fix] QOL Cart race conditions (#3395) --- assets/bulk-add.js | 41 ++++ assets/quick-order-list.css | 4 + assets/quick-order-list.js | 218 +++++-------------- layout/theme.liquid | 1 + sections/featured-collection.liquid | 2 +- sections/main-collection-product-grid.liquid | 2 +- sections/main-product.liquid | 5 +- sections/quick-order-list.liquid | 2 +- snippets/quick-order-list-row.liquid | 4 +- snippets/quick-order-list.liquid | 2 +- 10 files changed, 112 insertions(+), 169 deletions(-) create mode 100644 assets/bulk-add.js diff --git a/assets/bulk-add.js b/assets/bulk-add.js new file mode 100644 index 00000000000..0821122f3b8 --- /dev/null +++ b/assets/bulk-add.js @@ -0,0 +1,41 @@ +class BulkAdd extends HTMLElement { + constructor() { + super(); + this.queue = [] + this.requestStarted = false; + this.ids = [] + } + + startQueue(id, quantity) { + this.queue.push({id, quantity}) + const interval = setInterval(() => { + if (this.queue.length > 0) { + if (!this.requestStarted) { + this.sendRequest(this.queue) + } + } else { + clearInterval(interval) + } + }, 250) + } + + + sendRequest(queue) { + this.requestStarted = true; + const items = {} + const ids = [] + queue.forEach((queueItem) => { + items[parseInt(queueItem.id)] = queueItem.quantity; + ids.push(queueItem.id) + }); + this.queue = this.queue.filter(queueElement => !queue.includes(queueElement)); + const quickOrderList = this.closest('quick-order-list'); + quickOrderList.updateMultipleQty(items, ids) + } +} + +if (!customElements.get('bulk-add')) { + customElements.define('bulk-add', BulkAdd) +}; + + diff --git a/assets/quick-order-list.css b/assets/quick-order-list.css index 78ec7c5f81c..f05c41fd13a 100644 --- a/assets/quick-order-list.css +++ b/assets/quick-order-list.css @@ -309,6 +309,10 @@ quick-order-list-remove-button .icon-remove { left: 2rem; top: 1.2rem; } + + .variant-remove-total--empty .loading__spinner { + top: -1rem; + } } quick-order-list-remove-button:hover .icon-remove { diff --git a/assets/quick-order-list.js b/assets/quick-order-list.js index bc4d6d0843f..45c51cac6e2 100644 --- a/assets/quick-order-list.js +++ b/assets/quick-order-list.js @@ -1,13 +1,12 @@ if (!customElements.get('quick-order-list-remove-button')) { customElements.define( 'quick-order-list-remove-button', - class QuickOrderListRemoveButton extends HTMLElement { + class QuickOrderListRemoveButton extends BulkAdd { constructor() { super(); this.addEventListener('click', (event) => { event.preventDefault(); - const quickOrderList = this.closest('quick-order-list'); - quickOrderList.updateQuantity(this.dataset.index, 0); + this.startQueue(this.dataset.index, 0); }); } } @@ -69,15 +68,10 @@ if (!customElements.get('quick-order-list-remove-all-button')) { if (!customElements.get('quick-order-list')) { customElements.define( 'quick-order-list', - class QuickOrderList extends HTMLElement { + class QuickOrderList extends BulkAdd { constructor() { super(); this.cart = document.querySelector('cart-drawer'); - this.actions = { - add: 'ADD', - update: 'UPDATE', - }; - this.quickOrderListId = `quick-order-list-${this.dataset.productId}`; this.defineInputsAndQuickOrderTable(); @@ -145,14 +139,13 @@ if (!customElements.get('quick-order-list')) { const inputValue = parseInt(event.target.value); const cartQuantity = parseInt(event.target.dataset.cartQuantity); const index = event.target.dataset.index; - const name = document.activeElement.getAttribute('name'); const quantity = inputValue - cartQuantity; this.cleanErrorMessageOnType(event); if (inputValue == 0) { - this.updateQuantity(index, inputValue, name, this.actions.update); + this.startQueue(index, inputValue); } else { - this.validateQuantity(event, name, index, inputValue, cartQuantity, quantity); + this.validateQuantity(event, index, inputValue, cartQuantity, quantity); } } @@ -163,7 +156,7 @@ if (!customElements.get('quick-order-list')) { }); } - validateQuantity(event, name, index, inputValue, cartQuantity, quantity) { + validateQuantity(event, index, inputValue, cartQuantity, quantity) { if (inputValue < event.target.dataset.min) { this.setValidity( event, @@ -178,9 +171,9 @@ if (!customElements.get('quick-order-list')) { event.target.setCustomValidity(''); event.target.reportValidity(); if (cartQuantity > 0) { - this.updateQuantity(index, inputValue, name, this.actions.update); + this.startQueue(index, inputValue); } else { - this.updateQuantity(index, quantity, name, this.actions.add); + this.startQueue(index, quantity); } } } @@ -262,57 +255,52 @@ if (!customElements.get('quick-order-list')) { this.querySelectorAll('quantity-input').forEach((qty) => { const debouncedOnChange = debounce((event) => { this.onChange(event); - }, ON_CHANGE_DEBOUNCE_TIMER); + }, 100); qty.addEventListener('change', debouncedOnChange.bind(this)); }); } - addDebounce(id) { - const element = this.querySelector(`#Variant-${id} quantity-input`); - const debouncedOnChange = debounce((event) => { - this.onChange(event); - }, ON_CHANGE_DEBOUNCE_TIMER); - element.addEventListener('change', debouncedOnChange.bind(this)); - } - - renderSections(parsedState, id) { - this.getSectionsToRender().forEach((section) => { - const sectionElement = document.getElementById(section.id); - if ( - sectionElement && - sectionElement.parentElement && - sectionElement.parentElement.classList.contains('drawer') - ) { - parsedState.items.length > 0 - ? sectionElement.parentElement.classList.remove('is-empty') - : sectionElement.parentElement.classList.add('is-empty'); - setTimeout(() => { - document.querySelector('#CartDrawer-Overlay').addEventListener('click', this.cart.close.bind(this.cart)); + renderSections(parsedState, ids) { + if (ids) { + this.ids.push(ids) + } + const intersection = this.queue.filter(element => ids.includes(element.id)); + if (intersection.length === 0) { + this.getSectionsToRender().forEach((section) => { + const sectionElement = document.getElementById(section.id); + if ( + sectionElement && + sectionElement.parentElement && + sectionElement.parentElement.classList.contains('drawer') + ) { + parsedState.items.length > 0 + ? sectionElement.parentElement.classList.remove('is-empty') + : sectionElement.parentElement.classList.add('is-empty'); + setTimeout(() => { + document.querySelector('#CartDrawer-Overlay').addEventListener('click', this.cart.close.bind(this.cart)); + }); + } + const elementToReplace = + sectionElement && sectionElement.querySelector(section.selector) + ? sectionElement.querySelector(section.selector) + : sectionElement; + if (elementToReplace) { + if (section.selector === `#${this.quickOrderListId} .js-contents` && this.ids.length > 0) { + this.ids.flat().forEach((i) => { + elementToReplace.querySelector(`#Variant-${i}`).innerHTML = + this.getSectionInnerHTML(parsedState.sections[section.section], `#Variant-${i}`); + }); + } else { + elementToReplace.innerHTML = this.getSectionInnerHTML( + parsedState.sections[section.section], + section.selector + ); + } + } }); - } - const elementToReplace = - sectionElement && sectionElement.querySelector(section.selector) - ? sectionElement.querySelector(section.selector) - : sectionElement; - if (elementToReplace) { - if (section.selector === `#${this.quickOrderListId} .js-contents` && id !== undefined) { - elementToReplace.querySelector(`#Variant-${id}`).innerHTML = this.getSectionInnerHTML( - parsedState.sections[section.section], - `#Variant-${id}` - ); - } else { - elementToReplace.innerHTML = this.getSectionInnerHTML( - parsedState.sections[section.section], - section.selector - ); - } - } - }); - this.defineInputsAndQuickOrderTable(); - if (id) { - this.addDebounce(id); - } else { + this.defineInputsAndQuickOrderTable(); this.addMultipleDebounce(); + this.ids = [] } } @@ -406,8 +394,8 @@ if (!customElements.get('quick-order-list')) { } } - updateMultipleQty(items) { - this.querySelector('.variant-remove-total .loading__spinner').classList.remove('hidden'); + updateMultipleQty(items, ids) { + this.querySelector('.variant-remove-total .loading__spinner')?.classList.remove('hidden'); const body = JSON.stringify({ updates: items, @@ -424,13 +412,13 @@ if (!customElements.get('quick-order-list')) { }) .then((state) => { const parsedState = JSON.parse(state); - this.renderSections(parsedState); - }) - .catch(() => { + this.renderSections(parsedState, ids); + }).catch(() => { this.setErrorMessage(window.cartStrings.error); }) .finally(() => { - this.querySelector('.variant-remove-total .loading__spinner').classList.add('hidden'); + this.querySelector('.variant-remove-total .loading__spinner')?.classList.add('hidden'); + this.requestStarted = false; }); } @@ -442,98 +430,6 @@ if (!customElements.get('quick-order-list')) { } } - updateQuantity(id, quantity, name, action) { - this.toggleLoading(id, true); - this.cleanErrors(); - - let routeUrl = routes.cart_change_url; - let body = JSON.stringify({ - quantity, - id, - sections: this.getSectionsToRender().map((section) => section.section), - sections_url: this.getSectionsUrl(), - }); - let fetchConfigType; - if (action === this.actions.add) { - fetchConfigType = 'javascript'; - routeUrl = routes.cart_add_url; - body = JSON.stringify({ - items: [ - { - quantity: parseInt(quantity), - id: parseInt(id), - }, - ], - sections: this.getSectionsToRender().map((section) => section.section), - sections_url: this.getSectionsUrl(), - }); - } - - this.updateMessage(); - this.setErrorMessage(); - - fetch(`${routeUrl}`, { ...fetchConfig(fetchConfigType), ...{ body } }) - .then((response) => { - return response.text(); - }) - .then((state) => { - const parsedState = JSON.parse(state); - const quantityElement = document.getElementById(`Quantity-${id}`); - const items = document.querySelectorAll('.variant-item'); - - if (parsedState.description || parsedState.errors) { - const variantItem = document.querySelector( - `[id^="Variant-${id}"] .variant-item__totals.small-hide .loading__spinner` - ); - variantItem.classList.add('loading__spinner--error'); - this.resetQuantityInput(id, quantityElement); - if (parsedState.errors) { - this.updateLiveRegions(id, parsedState.errors); - } else { - this.updateLiveRegions(id, parsedState.description); - } - return; - } - - this.classList.toggle('is-empty', parsedState.item_count === 0); - - this.renderSections(parsedState, id); - - let hasError = false; - - const currentItem = parsedState.items.find((item) => item.variant_id === parseInt(id)); - const updatedValue = currentItem ? currentItem.quantity : undefined; - if (updatedValue && updatedValue !== quantity) { - this.updateError(updatedValue, id); - hasError = true; - } - - publish(PUB_SUB_EVENTS.cartUpdate, { source: this.quickOrderListId, cartData: parsedState }); - - if (hasError) { - this.updateMessage(); - } else if (action === this.actions.add) { - this.updateMessage(parseInt(quantity)); - } else if (action === this.actions.update) { - this.updateMessage(parseInt(quantity - quantityElement.dataset.cartQuantity)); - } else { - this.updateMessage(-parseInt(quantityElement.dataset.cartQuantity)); - } - }) - .catch((error) => { - this.querySelectorAll('.loading__spinner').forEach((overlay) => overlay.classList.add('hidden')); - this.resetQuantityInput(id); - console.error(error); - this.setErrorMessage(window.cartStrings.error); - }) - .finally(() => { - this.toggleLoading(id); - if (this.lastKey && this.lastElement === id) { - this.querySelector(`#Variant-${id} input`).select(); - } - }); - } - resetQuantityInput(id, quantityElement) { const input = quantityElement ?? document.getElementById(`Quantity-${id}`); input.value = input.getAttribute('value'); @@ -592,9 +488,9 @@ if (!customElements.get('quick-order-list')) { this.updateLiveRegions(id, message); } - cleanErrors() { - this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden')); - this.querySelectorAll(`.variant-item__error-text`).forEach((error) => (error.innerHTML = '')); + cleanErrors(id) { + // this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden')); + // this.querySelectorAll(`.variant-item__error-text`).forEach((error) => error.innerHTML = ''); } updateLiveRegions(id, message) { diff --git a/layout/theme.liquid b/layout/theme.liquid index b4bd68d9adc..963bb22944c 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -28,6 +28,7 @@ {% render 'meta-tags' %} + diff --git a/sections/featured-collection.liquid b/sections/featured-collection.liquid index 65b7a3720f1..6d79f837426 100644 --- a/sections/featured-collection.liquid +++ b/sections/featured-collection.liquid @@ -19,9 +19,9 @@ {%- if section.settings.quick_add == 'bulk' -%} - + {%- endif -%} {%- style -%} diff --git a/sections/main-collection-product-grid.liquid b/sections/main-collection-product-grid.liquid index 267be57df13..edefa4f52b7 100644 --- a/sections/main-collection-product-grid.liquid +++ b/sections/main-collection-product-grid.liquid @@ -17,9 +17,9 @@ {%- if section.settings.quick_add == 'bulk' -%} - + {%- endif -%} {%- style -%} diff --git a/sections/main-product.liquid b/sections/main-product.liquid index 3983a67003c..e57d05f1b72 100644 --- a/sections/main-product.liquid +++ b/sections/main-product.liquid @@ -9,6 +9,7 @@ {{ 'component-slider.css' | asset_url | stylesheet_tag }} {{ 'component-rating.css' | asset_url | stylesheet_tag }} {{ 'component-deferred-media.css' | asset_url | stylesheet_tag }} + {% unless product.has_only_default_variant %} {{ 'component-product-variant-picker.css' | asset_url | stylesheet_tag }} {{ 'component-swatch-input.css' | asset_url | stylesheet_tag }} @@ -560,9 +561,9 @@ -%}
  • {%- if block.settings.enable_quick_add -%} - {% assign quick_add = "standard" %} + {% assign quick_add = 'standard' %} {%- else -%} - {% assign quick_add = "none" %} + {% assign quick_add = 'none' %} {%- endif -%} {% render 'card-product', card_product: product, diff --git a/sections/quick-order-list.liquid b/sections/quick-order-list.liquid index 540f19766e6..65aa270a5ce 100644 --- a/sections/quick-order-list.liquid +++ b/sections/quick-order-list.liquid @@ -2,9 +2,9 @@ {{ 'quick-order-list.css' | asset_url | stylesheet_tag }} {{ 'quantity-popover.css' | asset_url | stylesheet_tag }} - + {%- style -%} .section-{{ section.id }}-padding { diff --git a/snippets/quick-order-list-row.liquid b/snippets/quick-order-list-row.liquid index 9e3ed0b9c42..4dbe38a3590 100644 --- a/snippets/quick-order-list-row.liquid +++ b/snippets/quick-order-list-row.liquid @@ -284,7 +284,7 @@ {% render 'quantity-input', variant: variant, min: 0 %} {%- endif -%} - {%- if cart_qty > 0 -%} + {% if cart_qty > 0 %} - {%- endif -%} + {% endif %} {%- if has_popover -%}