diff --git a/.github/workflows/pretter.yml b/.github/workflows/prettier.yml
similarity index 100%
rename from .github/workflows/pretter.yml
rename to .github/workflows/prettier.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11cebc192..d851b7394 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,62 @@
# Changelog
-[Unreleased changes](https://github.com/rapidez/core/compare/1.0.0...master)
+[Unreleased changes](https://github.com/rapidez/core/compare/1.4.1...master)
+## [1.4.1](https://github.com/rapidez/core/releases/tag/1.4.1) - 2023-11-24
+
+### Fixed
+
+- Swatches fetch loop bug (#385)
+- Improve slider performance (#384)
+- Throw exception when the store is not found (#386)
+
+## [1.4.0](https://github.com/rapidez/core/releases/tag/1.4.0) - 2023-11-22
+
+### Added
+
+- Support for image disabled option (#381)
+- Position per category support (#382)
+
+### Fixed
+
+- Clear cart storage when not found (#383)
+
+## [1.3.0](https://github.com/rapidez/core/releases/tag/1.3.0) - 2023-11-14
+
+### Added
+
+- Added special price and priceValidUntil to microdata (#380)
+
+### Fixed
+
+- Support MSP way of saving method_title (#379)
+
+## [1.2.0](https://github.com/rapidez/core/releases/tag/1.2.0) - 2023-11-07
+
+### Added
+
+- Implemented customer address prefix, suffix, vat_id and fax (#373)
+
+### Fixed
+
+- Use new config path (#375)
+
+## [1.1.0](https://github.com/rapidez/core/releases/tag/1.1.0) - 2023-11-03
+
+### Changed
+
+- Indexer config with visibility option (#369)
+
+## [1.0.1](https://github.com/rapidez/core/releases/tag/1.0.1) - 2023-11-01
+
+### Fixed
+
+- Avoid error in console (#365)
+- Set mutating to false in a finally (#366)
+- Remove old button component (#367)
+- Use dragOnClick and disable keyboard for the price filter (#368)
+- Use data_get to allow both stdClass & arrays to be used (#370)
+- Update prices when super attribute option is changed (#371)
+
## [1.0.0](https://github.com/rapidez/core/releases/tag/1.0.0) - 2023-10-19
### Added
@@ -38,8 +94,7 @@
- Wrap raw query parts (#352)
- Make slidesTotal a computed property (#346)
- Passive listeners and key instead of keyCode in product image component (#350)
-- Translatable cart title (7a6c533)
-- Translatable checkout title (2a68e8b)
+- Translatable cart + checkout title (7a6c533, 2a68e8b)
## [0.96.1](https://github.com/rapidez/core/releases/tag/0.96.1) - 2023-10-19
diff --git a/config/rapidez/frontend.php b/config/rapidez/frontend.php
index 8cf3bec80..2b17bb48d 100644
--- a/config/rapidez/frontend.php
+++ b/config/rapidez/frontend.php
@@ -1,9 +1,6 @@
false,
-
// The variables which should be exposed to the frontend.
'exposed' => [
'store',
@@ -14,7 +11,7 @@
'notifications',
'checkout_steps',
'flushable_localstorage_keys',
- 'customer_fields_show',
+ 'show_customer_address_fields',
],
// The checkout steps which are used to name the steps
diff --git a/config/rapidez/indexer.php b/config/rapidez/indexer.php
new file mode 100644
index 000000000..517991e66
--- /dev/null
+++ b/config/rapidez/indexer.php
@@ -0,0 +1,22 @@
+ [2, 3, 4],
+
+ // Additional searchable attributes with the search weight.
+ 'searchable' => [
+ // 'attribute' => 4.0,
+ ],
+
+ // From Magento only "Yes/No, Dropdown, Multiple Select and Price" attribute types
+ // can be configured as filter. If you'd like to have a filter for an attribute
+ // with, for example, the type of "Text", you can specify the attribute_code here.
+ 'additional_filters' => [
+ // eav_attribute attribute_code. e.g. brand
+ ],
+];
diff --git a/config/rapidez/models.php b/config/rapidez/models.php
index aed1d04ac..4686a1867 100644
--- a/config/rapidez/models.php
+++ b/config/rapidez/models.php
@@ -13,6 +13,7 @@
'option_swatch' => Rapidez\Core\Models\OptionSwatch::class,
'option_value' => Rapidez\Core\Models\OptionValue::class,
'product_image' => Rapidez\Core\Models\ProductImage::class,
+ 'product_image_value' => Rapidez\Core\Models\ProductImageValue::class,
'product_view' => Rapidez\Core\Models\ProductView::class,
'product_option' => Rapidez\Core\Models\ProductOption::class,
'product_option_title' => Rapidez\Core\Models\ProductOptionTitle::class,
@@ -32,4 +33,5 @@
'sales_order_item' => Rapidez\Core\Models\SalesOrderItem::class,
'sales_order_payment' => Rapidez\Core\Models\SalesOrderPayment::class,
'search_query' => Rapidez\Core\Models\SearchQuery::class,
+ 'search_synonym' => Rapidez\Core\Models\SearchSynonym::class,
];
diff --git a/config/rapidez/system.php b/config/rapidez/system.php
index 631d2f485..56fbd04e4 100644
--- a/config/rapidez/system.php
+++ b/config/rapidez/system.php
@@ -22,15 +22,6 @@
// With this token you can run commands from an url.
'admin_token' => env('RAPIDEZ_TOKEN', env('APP_KEY')),
- // Additional searchable attributes with the search weight.
- 'searchable' => [
- // 'attribute' => 4.0,
- ],
-
- // From Magento only "Yes/No, Dropdown, Multiple Select and Price" attribute types
- // can be configured as filter. If you'd like to have a filter for an attribute
- // with, for example, the type of "Text", you can specify the attribute_code here.
- 'additional_filters' => [
- // eav_attribute attribute_code. e.g. brand
- ],
+ // Should the stock qty be exposed and indexed within Elasticsearch?
+ 'expose_stock' => false,
];
diff --git a/package.json b/package.json
index 2099c9a32..d8feb0d67 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
},
"devDependencies": {
"@appbaseio/reactivesearch-vue": "^1.33.13",
- "@hotwired/turbo": "^7.2.4",
+ "@hotwired/turbo": "^8.0.0-beta.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@vitejs/plugin-vue2": "^2.2.0",
diff --git a/resources/js/components/Checkout/Checkout.vue b/resources/js/components/Checkout/Checkout.vue
index ac0f736a5..b9bd8eb91 100644
--- a/resources/js/components/Checkout/Checkout.vue
+++ b/resources/js/components/Checkout/Checkout.vue
@@ -188,7 +188,9 @@ export default {
}
const optionalFields = Object.keys(
- Object.fromEntries(Object.entries(window.config.customer_fields_show).filter(([key, value]) => !value || value === 'opt')),
+ Object.fromEntries(
+ Object.entries(window.config.show_customer_address_fields).filter(([key, value]) => !value || value === 'opt'),
+ ),
)
Object.entries(this.checkout.shipping_address).forEach(([key, val]) => {
if (!val && !['region_id', 'customer_address_id', 'same_as_billing'].concat(optionalFields).includes(key)) {
diff --git a/resources/js/components/Elements/Slider.vue b/resources/js/components/Elements/Slider.vue
index 1739a9c85..f8f50f94d 100644
--- a/resources/js/components/Elements/Slider.vue
+++ b/resources/js/components/Elements/Slider.vue
@@ -44,7 +44,6 @@ export default {
mounted: false,
hover: false,
direction: 1,
- chunk: '',
pause: () => {},
resume: () => {},
}
@@ -52,9 +51,12 @@ export default {
mounted() {
this.initSlider()
useEventListener(this.slider, 'scroll', useThrottleFn(this.scroll, 150, true, true), { passive: true })
+ if (this.loop) {
+ useEventListener(this.slider, 'scrollend', this.scrollend, { passive: true })
+ }
this.$nextTick(() => {
if (this.loop) {
- this.chunk = this.slider.cloneNode(true)
+ this.initLoop()
}
this.slider.dispatchEvent(new CustomEvent('scroll'))
this.mounted = true
@@ -66,6 +68,29 @@ export default {
initSlider() {
this.slider = this.$scopedSlots.default()[0].context.$refs[this.reference]
},
+ initLoop() {
+ if (!this.loop) {
+ return
+ }
+
+ const slides = Array.from(this.slides)
+ if (!slides.length) {
+ return
+ }
+ let firstChild = this.slider.firstChild
+
+ for (let slide of slides) {
+ let startClone = this.slider.insertBefore(slide.cloneNode(true), firstChild)
+ startClone.dataset.clone = true
+ startClone.dataset.position = 'start'
+
+ let endClone = this.slider.appendChild(slide.cloneNode(true))
+ endClone.dataset.clone = true
+ endClone.dataset.position = 'end'
+ }
+
+ this.slider.dispatchEvent(new CustomEvent('scrollend'))
+ },
initAutoPlay() {
if (!this.autoplay) {
return
@@ -85,12 +110,20 @@ export default {
this.showLeft = this.loop || this.position
this.showRight = this.loop || this.slider.offsetWidth + this.position < this.slider.scrollWidth - 1
},
+ scrollend(event) {
+ let scrollPosition = this.vertical ? event.target.scrollTop : event.target.scrollLeft
+ if (scrollPosition < this.sliderStart) {
+ this.slider.scrollTo({ [this.vertical ? 'top' : 'left']: scrollPosition + this.sliderStart, behavior: 'instant' })
+ } else if (scrollPosition > this.sliderEnd) {
+ this.slider.scrollTo({ [this.vertical ? 'top' : 'left']: scrollPosition - this.sliderStart, behavior: 'instant' })
+ }
+ },
autoScroll() {
if (this.slidesTotal == 1) {
return
}
let next = this.currentSlide + this.direction
- if (next >= this.slidesTotal || next < 0) {
+ if ((next >= this.slidesTotal && !this.loop) || next < 0) {
if (this.bounce) {
this.direction = -this.direction
next = this.currentSlide + this.direction
@@ -101,21 +134,11 @@ export default {
this.navigate(next)
},
navigate(index) {
+ index = this.loop ? index + this.slides.length : index
+
this.vertical
? this.slider.scrollTo(0, this.slider.children[index]?.offsetTop)
- : this.slider.scrollTo(this.slider.children[0]?.offsetWidth * index, 0)
- },
- handleLoop() {
- if (this.currentSlide + 1 === this.slidesTotal - 1) {
- Array.from(this.chunk.children).forEach((child) => {
- this.slider.appendChild(child.cloneNode(true))
- })
- }
- if (this.currentSlide < 1) {
- Array.from(this.chunk.children).forEach((child) => {
- this.slider.insertBefore(child.cloneNode(true), this.slider.firstChild)
- })
- }
+ : this.slider.scrollTo(this.slider.children[index]?.offsetLeft, 0)
},
},
watch: {
@@ -128,9 +151,6 @@ export default {
},
currentSlide() {
this.initSlider()
- if (this.loop) {
- this.handleLoop()
- }
},
},
computed: {
@@ -138,8 +158,7 @@ export default {
if (!this.mounted) {
return 0
}
-
- return Math.round(this.position / this.childSpan)
+ return Math.round(this.position / this.childSpan) % this.slides.length
},
slidesVisible() {
if (!this.mounted) {
@@ -169,7 +188,22 @@ export default {
return 0
}
- return (this.slider.children?.length ?? 1) - this.slidesVisible + 1
+ return (this.slides?.length ?? 1) - (this.loop ? 0 : this.slidesVisible - 1)
+ },
+ slides() {
+ return this.slider.querySelectorAll(':scope > :not([data-clone=true])')
+ },
+ sliderStart() {
+ return this.vertical ? this.slides[0].offsetTop : this.slides[0].offsetLeft
+ },
+ sliderEnd() {
+ let lastChild = this.slides[this.slides.length - 1]
+
+ if (this.vertical) {
+ return lastChild.offsetTop + lastChild.offsetHeight
+ }
+
+ return lastChild.offsetLeft + lastChild.offsetWidth
},
},
}
diff --git a/resources/js/components/Product/AddToCart.vue b/resources/js/components/Product/AddToCart.vue
index a570bc403..4aea39f1e 100644
--- a/resources/js/components/Product/AddToCart.vue
+++ b/resources/js/components/Product/AddToCart.vue
@@ -363,6 +363,12 @@ export default {
},
deep: true,
},
+ options: {
+ handler() {
+ this.calculatePrices()
+ },
+ deep: true,
+ },
},
}
diff --git a/resources/js/stores/useSwatches.js b/resources/js/stores/useSwatches.js
index 0165cd962..e69915883 100644
--- a/resources/js/stores/useSwatches.js
+++ b/resources/js/stores/useSwatches.js
@@ -2,6 +2,7 @@ import { computedAsync, useLocalStorage } from '@vueuse/core'
export const swatchesStorage = useLocalStorage('swatches', {})
let isRefreshing = false
+let hasFetched = false
export const refresh = async function () {
if (isRefreshing) {
@@ -25,6 +26,7 @@ export const refresh = async function () {
return false
}
+ hasFetched = true
swatchesStorage.value = response.data
return true
@@ -32,11 +34,12 @@ export const refresh = async function () {
export const clear = async function () {
swatchesStorage.value = {}
+ hasFetched = false
}
export const swatches = computedAsync(
async () => {
- if (Object.keys(swatchesStorage.value).length === 0) {
+ if (!hasFetched && Object.keys(swatchesStorage.value).length === 0) {
await refresh()
}
diff --git a/resources/views/checkout/partials/address.blade.php b/resources/views/checkout/partials/address.blade.php
index 217e70b6c..3dc64bf11 100644
--- a/resources/views/checkout/partials/address.blade.php
+++ b/resources/views/checkout/partials/address.blade.php
@@ -16,6 +16,25 @@
+ @if(Rapidez::config('customer/address/prefix_show', '') && strlen(Rapidez::config('customer/address/prefix_options', '')))
+
+
+ @if(Rapidez::config('customer/address/prefix_show', '') === 'opt')
+
+ @endif
+ @foreach(explode(';', Rapidez::config('customer/address/prefix_options', '')) as $prefix)
+
+ @endforeach
+
+
+ @endif
+ @if(Rapidez::config('customer/address/suffix_show', '') && strlen(Rapidez::config('customer/address/suffix_options', '')))
+
+
+ @if(Rapidez::config('customer/address/suffix_show', '') === 'opt')
+
+ @endif
+ @foreach(explode(';', Rapidez::config('customer/address/suffix_options', '')) as $suffix)
+
+ @endforeach
+
+
+ @endif
@endif
- @if(Rapidez::config('customer/address/company_show', 'opt'))
+ @if(Rapidez::config('customer/address/fax_show', false))
+
+
+ @endif
+ @if(Rapidez::config('customer/address/company_show', 'opt'))
+
@lang('Payment method')
-
@{{ method.additional_information.method_title }}
+
@{{ method.additional_information.method_title || method.additional_information.raw_details_info.method_title }}
diff --git a/resources/views/components/listing.blade.php b/resources/views/components/listing.blade.php
index 51900ebe7..74f8b9300 100644
--- a/resources/views/components/listing.blade.php
+++ b/resources/views/components/listing.blade.php
@@ -10,7 +10,7 @@
@endisset
+
{{ $before ?? '' }}
@if ($slot->isEmpty())
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php
index c42cd1477..99f867276 100644
--- a/resources/views/errors/404.blade.php
+++ b/resources/views/errors/404.blade.php
@@ -6,6 +6,14 @@
@section('title', $page->meta_title ?: $page->title)
@section('description', $page->meta_description)
+@push('head')
+
+@endpush
+
@section('content')
{{ $page->content_heading }}
diff --git a/resources/views/layouts/partials/header/autocomplete.blade.php b/resources/views/layouts/partials/header/autocomplete.blade.php
index 8ca7f4480..ce997ff25 100644
--- a/resources/views/layouts/partials/header/autocomplete.blade.php
+++ b/resources/views/layouts/partials/header/autocomplete.blade.php
@@ -12,6 +12,7 @@ class="{{ $inputClasses }}"
v-if="$root.loadAutocomplete"
v-cloak
:additionals="{ categories: ['name^3', 'meta_description^1'] }"
+ class="w-full"
>
-
+
+ @if($product->special_to_date && $product->special_to_date > now()->toDateTimeString())
+
+ @endif
diff --git a/src/Commands/ElasticsearchIndexCommand.php b/src/Commands/ElasticsearchIndexCommand.php
index 8ac4893d2..6cd4907f4 100644
--- a/src/Commands/ElasticsearchIndexCommand.php
+++ b/src/Commands/ElasticsearchIndexCommand.php
@@ -11,6 +11,10 @@ abstract class ElasticsearchIndexCommand extends Command
{
public ElasticsearchIndexer $indexer;
+ public array $mapping = [];
+ public array $settings = [];
+ public array $synonymsFor = [];
+
public function __construct(ElasticsearchIndexer $indexer)
{
parent::__construct();
@@ -35,7 +39,7 @@ public function indexStore(Store|array $store, string $indexName, callable|itera
$this->line('Indexing `' . $indexName . '` for store ' . $storeName);
try {
- $this->prepareIndexerWithStore($store, $indexName);
+ $this->prepareIndexerWithStore($store, $indexName, $this->mapping, $this->settings, $this->synonymsFor);
$this->indexer->index($this->dataFrom($items), $dataFilter, $id);
$this->indexer->finish();
} catch (Exception $e) {
@@ -45,10 +49,10 @@ public function indexStore(Store|array $store, string $indexName, callable|itera
}
}
- public function prepareIndexerWithStore(Store|array $store, string $indexName, array $mapping = [], array $settings = []): void
+ public function prepareIndexerWithStore(Store|array $store, string $indexName, array $mapping = [], array $settings = [], array $synonymsFor = []): void
{
Rapidez::setStore($store);
- $this->indexer->prepare(config('rapidez.es_prefix') . '_' . $indexName . '_' . $store['store_id'], $mapping, $settings);
+ $this->indexer->prepare(config('rapidez.es_prefix') . '_' . $indexName . '_' . $store['store_id'], $mapping, $settings, $synonymsFor);
}
public function dataFrom(callable|iterable $items)
diff --git a/src/Commands/ElasticsearchIndexer.php b/src/Commands/ElasticsearchIndexer.php
index b86acbdc4..52bace8ce 100644
--- a/src/Commands/ElasticsearchIndexer.php
+++ b/src/Commands/ElasticsearchIndexer.php
@@ -60,7 +60,7 @@ public function indexItem(object $item, callable|array|null $dataFilter, callabl
$currentId = is_callable($id)
? $id($item)
- : $item[$id];
+ : data_get($item, $id);
if (is_null($currentId)) {
return;
@@ -69,8 +69,23 @@ public function indexItem(object $item, callable|array|null $dataFilter, callabl
IndexJob::dispatch($this->index, $currentId, $currentValues);
}
- public function prepare(string $indexName, array $mapping = [], array $settings = []): void
+ public function prepare(string $indexName, array $mapping = [], array $settings = [], array $synonymsFor = []): void
{
+ if (count($synonymsFor)) {
+ $synonyms = config('rapidez.models.search_synonym')::whereIn('store_id', [0, config('rapidez.store')])
+ ->get()
+ ->map(fn ($synonym) => $synonym->synonyms)
+ ->toArray();
+
+ data_set($settings, 'index.analysis.filter.synonym', ['type' => 'synonym', 'synonyms' => $synonyms]);
+ data_set($settings, 'index.analysis.analyzer.synonym', ['tokenizer' => 'whitespace', 'filter' => ['synonym']]);
+
+ foreach ($synonymsFor as $property) {
+ data_set($mapping, 'properties.' . $property . '.type', 'text');
+ data_set($mapping, 'properties.' . $property . '.analyzer', 'synonym');
+ }
+ }
+
$this->createAlias($indexName);
$this->createIndex($this->index, $mapping, $settings);
}
diff --git a/src/Commands/IndexCategoriesCommand.php b/src/Commands/IndexCategoriesCommand.php
index 0f57e9938..6bdaa4e51 100644
--- a/src/Commands/IndexCategoriesCommand.php
+++ b/src/Commands/IndexCategoriesCommand.php
@@ -13,6 +13,8 @@ class IndexCategoriesCommand extends ElasticsearchIndexCommand
public function handle(): int
{
+ $this->synonymsFor = ['name'];
+
$this->indexStores(
stores: Rapidez::getStores($this->argument('store')),
indexName: 'categories',
diff --git a/src/Commands/IndexProductsCommand.php b/src/Commands/IndexProductsCommand.php
index 875ef5601..7d2adeb36 100644
--- a/src/Commands/IndexProductsCommand.php
+++ b/src/Commands/IndexProductsCommand.php
@@ -9,6 +9,7 @@
use Rapidez\Core\Events\IndexBeforeEvent;
use Rapidez\Core\Facades\Rapidez;
use Rapidez\Core\Models\Category;
+use Rapidez\Core\Models\CategoryProduct;
use TorMorten\Eventy\Facades\Eventy;
class IndexProductsCommand extends ElasticsearchIndexCommand
@@ -41,10 +42,16 @@ public function handle()
'type' => 'flattened',
],
],
- ]), Eventy::filter('index.product.settings', []));
-
+ ]), Eventy::filter('index.product.settings', []), ['name']);
try {
+ $maxPositions = CategoryProduct::query()
+ ->selectRaw('GREATEST(MAX(position), 0) as position')
+ ->addSelect('category_id')
+ ->groupBy('category_id')
+ ->pluck('position', 'category_id');
+
$productQuery = $productModel::selectOnlyIndexable()
+ ->with('categoryProducts')
->withEventyGlobalScopes('index.product.scopes')
->withExists('options AS has_options');
@@ -56,10 +63,11 @@ public function handle()
->pluck('name', 'entity_id');
$showOutOfStock = (bool) Rapidez::config('cataloginventory/options/show_out_of_stock', 0);
+ $indexVisibility = config('rapidez.indexer.visibility');
- $productQuery->chunk($this->chunkSize, function ($products) use ($store, $bar, $categories, $showOutOfStock) {
- $this->indexer->index($products, function ($product) use ($store, $categories, $showOutOfStock) {
- if ($product->visibility === 1) {
+ $productQuery->chunk($this->chunkSize, function ($products) use ($store, $bar, $categories, $showOutOfStock, $indexVisibility, $maxPositions) {
+ $this->indexer->index($products, function ($product) use ($store, $categories, $showOutOfStock, $indexVisibility, $maxPositions) {
+ if (! in_array($product->visibility, $indexVisibility)) {
return;
}
@@ -77,6 +85,11 @@ public function handle()
$data = $this->withCategories($data, $categories);
+ $data['positions'] = $product->categoryProducts
+ ->pluck('position', 'category_id')
+ // Turn all positions positive
+ ->mapWithKeys(fn ($position, $category_id) => [$category_id => $maxPositions[$category_id] - $position]);
+
return Eventy::filter('index.product.data', $data, $product);
});
diff --git a/src/Http/ViewComposers/ConfigComposer.php b/src/Http/ViewComposers/ConfigComposer.php
index 06397b2b0..ad881c200 100644
--- a/src/Http/ViewComposers/ConfigComposer.php
+++ b/src/Http/ViewComposers/ConfigComposer.php
@@ -38,19 +38,21 @@ public function compose(View $view)
Config::set('frontend.show_swatches', (bool) Rapidez::config('catalog/frontend/show_swatches_in_product_list'));
Config::set('frontend.translations', __('rapidez::frontend'));
Config::set('frontend.recaptcha', Rapidez::config('recaptcha_frontend/type_recaptcha_v3/public_key', null, true));
- Config::set('frontend.searchable', array_merge($searchableAttributes, config('rapidez.searchable')));
- Config::set('frontend.customer_fields_show', $this->getCustomerFields());
+ Config::set('frontend.searchable', array_merge($searchableAttributes, config('rapidez.indexer.searchable')));
+ Config::set('frontend.show_customer_address_fields', $this->getCustomerAddressFields());
Config::set('frontend.grid_per_page', Rapidez::config('catalog/frontend/grid_per_page', 12));
Config::set('frontend.grid_per_page_values', explode(',', Rapidez::config('catalog/frontend/grid_per_page_values', '12,24,36')));
Config::set('frontend.queries.cart', view('rapidez::cart.queries.cart')->renderOneliner());
}
- public function getCustomerFields()
+ public function getCustomerAddressFields()
{
return [
+ 'prefix' => strlen(Rapidez::config('customer/address/prefix_options', '')) ? Rapidez::config('customer/address/prefix_show', 'opt') : 'opt',
'firstname' => 'req',
'middlename' => Rapidez::config('customer/address/middlename_show', 0) ? 'opt' : false,
'lastname' => 'req',
+ 'suffix' => strlen(Rapidez::config('customer/address/suffix_options', '')) ? Rapidez::config('customer/address/suffix_show', 'opt') : 'opt',
'postcode' => 'req',
'housenumber' => Rapidez::config('customer/address/street_lines', 3) >= 2 ? 'req' : false,
'addition' => Rapidez::config('customer/address/street_lines', 3) >= 3 ? 'opt' : false,
@@ -59,6 +61,8 @@ public function getCustomerFields()
'country_id' => 'req',
'telephone' => Rapidez::config('customer/address/telephone_show', 'req'),
'company' => Rapidez::config('customer/address/company_show', 'opt'),
+ 'vat_id' => Rapidez::config('customer/address/taxvat_show', 'opt'),
+ 'fax' => Rapidez::config('customer/address/fax_show', 'opt'),
];
}
}
diff --git a/src/Models/Attribute.php b/src/Models/Attribute.php
index 9318ddf94..be8d82bfe 100644
--- a/src/Models/Attribute.php
+++ b/src/Models/Attribute.php
@@ -21,7 +21,7 @@ protected static function booting()
protected function filter(): CastsAttribute
{
return CastsAttribute::make(
- get: fn ($value) => $value || in_array($this->code, config('rapidez.additional_filters')),
+ get: fn ($value) => $value || in_array($this->code, config('rapidez.indexer.additional_filters')),
)->shouldCache();
}
diff --git a/src/Models/Product.php b/src/Models/Product.php
index a4fc372bd..626d99e1c 100644
--- a/src/Models/Product.php
+++ b/src/Models/Product.php
@@ -108,6 +108,15 @@ public function options(): HasMany
);
}
+ public function categoryProducts(): HasMany
+ {
+ return $this
+ ->hasMany(
+ config('rapidez.models.category_product'),
+ 'product_id',
+ );
+ }
+
public function rewrites(): HasMany
{
return $this
diff --git a/src/Models/ProductImage.php b/src/Models/ProductImage.php
index 018fb6262..fce7c075e 100644
--- a/src/Models/ProductImage.php
+++ b/src/Models/ProductImage.php
@@ -2,9 +2,30 @@
namespace Rapidez\Core\Models;
+use Illuminate\Contracts\Database\Eloquent\Builder;
+
class ProductImage extends Model
{
protected $table = 'catalog_product_entity_media_gallery';
protected $primaryKey = 'value_id';
+
+ protected static function booted(): void
+ {
+ static::addGlobalScope(
+ 'enabled',
+ fn (Builder $query) => $query
+ ->where($query->qualifyColumn('disabled'), 0)
+ ->whereHas(
+ 'productImageValue',
+ fn ($query) => $query
+ ->where($query->qualifyColumn('disabled'), 0)
+ )
+ );
+ }
+
+ public function productImageValue()
+ {
+ return $this->belongsTo(config('rapidez.models.product_image_value'), 'value_id', 'value_id');
+ }
}
diff --git a/src/Models/ProductImageValue.php b/src/Models/ProductImageValue.php
new file mode 100644
index 000000000..0f936a3c2
--- /dev/null
+++ b/src/Models/ProductImageValue.php
@@ -0,0 +1,24 @@
+belongsTo(config('rapidez.models.product_image'), 'value_id');
+ }
+}
diff --git a/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php
new file mode 100644
index 000000000..8251a0c2a
--- /dev/null
+++ b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php
@@ -0,0 +1,41 @@
+where($query->qualifyColumn($this->storeIdColumn), 0);
+ }
+
+ return $query
+ // Pre-filter results to be default and current store only.
+ ->whereIn($query->qualifyColumn($this->storeIdColumn), [0, config('rapidez.store')])
+ // Remove values from the default store where values for the current store exist.
+ ->where(fn ($query) => $query
+ // Remove values where we already have values in the current store.
+ ->whereNotIn($query->qualifyColumn($this->uniquePerStoreKey), fn ($query) => $query
+ ->select($model->qualifyColumn($this->uniquePerStoreKey))
+ ->from($model->getTable())
+ ->whereColumn($model->qualifyColumn($this->uniquePerStoreKey), $model->qualifyColumn($this->uniquePerStoreKey))
+ ->where($model->qualifyColumn($this->storeIdColumn), config('rapidez.store'))
+ )
+ // Unless the value IS the current store.
+ ->orWhere($query->qualifyColumn($this->storeIdColumn), config('rapidez.store'))
+ );
+ }
+}
diff --git a/src/Models/Scopes/Product/WithProductChildrenScope.php b/src/Models/Scopes/Product/WithProductChildrenScope.php
index 6e1df885f..95eef4746 100644
--- a/src/Models/Scopes/Product/WithProductChildrenScope.php
+++ b/src/Models/Scopes/Product/WithProductChildrenScope.php
@@ -25,7 +25,7 @@ public function apply(Builder $builder, Model $model)
$superAttributesSelect .= '"' . $superAttribute . '", ' . $grammar->wrap('children.' . $superAttribute) . ',';
}
- $stockQty = config('rapidez.frontend.expose_stock') ? '"qty", children_stock.qty,' : '';
+ $stockQty = config('rapidez.system.expose_stock') ? '"qty", children_stock.qty,' : '';
$builder
->selectRaw('JSON_REMOVE(JSON_OBJECTAGG(IFNULL(children.entity_id, "null__"), JSON_OBJECT(
diff --git a/src/Models/Scopes/Product/WithProductGroupedScope.php b/src/Models/Scopes/Product/WithProductGroupedScope.php
index 7a3e5b139..58f716c7c 100644
--- a/src/Models/Scopes/Product/WithProductGroupedScope.php
+++ b/src/Models/Scopes/Product/WithProductGroupedScope.php
@@ -11,7 +11,7 @@ class WithProductGroupedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
- $stockQty = config('rapidez.frontend.expose_stock') ? '"qty", grouped_stock.qty,' : '';
+ $stockQty = config('rapidez.system.expose_stock') ? '"qty", grouped_stock.qty,' : '';
$builder
->selectRaw('JSON_REMOVE(JSON_OBJECTAGG(IFNULL(grouped.entity_id, "null__"), JSON_OBJECT(
diff --git a/src/Models/Scopes/Product/WithProductStockScope.php b/src/Models/Scopes/Product/WithProductStockScope.php
index 1074f6133..b147cde58 100644
--- a/src/Models/Scopes/Product/WithProductStockScope.php
+++ b/src/Models/Scopes/Product/WithProductStockScope.php
@@ -10,7 +10,7 @@ class WithProductStockScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
- if (config('rapidez.frontend.expose_stock')) {
+ if (config('rapidez.system.expose_stock')) {
$builder->selectRaw('ANY_VALUE(cataloginventory_stock_item.qty) AS qty');
}
diff --git a/src/Models/SearchSynonym.php b/src/Models/SearchSynonym.php
new file mode 100644
index 000000000..35efb5997
--- /dev/null
+++ b/src/Models/SearchSynonym.php
@@ -0,0 +1,17 @@
+getStores($store));
+ $stores = $this->getStores($store);
+
+ throw_if(
+ empty($stores),
+ StoreNotFoundException::class,
+ 'Store not found.'
+ );
+
+ return Arr::first($stores);
}
public function setStore(Store|array|callable|int|string $store): void
diff --git a/src/RapidezServiceProvider.php b/src/RapidezServiceProvider.php
index c65c5753f..d9e6e8e74 100644
--- a/src/RapidezServiceProvider.php
+++ b/src/RapidezServiceProvider.php
@@ -41,6 +41,7 @@ class RapidezServiceProvider extends ServiceProvider
protected $configFiles = [
'frontend',
'healthcheck',
+ 'indexer',
'jwt',
'models',
'routing',
diff --git a/tailwind.config.js b/tailwind.config.js
index 3936ef1a4..24c331dd4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -8,8 +8,8 @@ module.exports = {
'./vendor/rapidez/**/*.css',
'./vendor/rapidez/**/*.vue',
- './config/rapidez.php',
- './vendor/rapidez/core/config/rapidez.php',
+ './config/rapidez/frontend.php',
+ './vendor/rapidez/core/config/rapidez/frontend.php',
'./vendor/rapidez/menu/config/menu.php',
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/tailwind.blade.php',
],
diff --git a/yarn.lock b/yarn.lock
index 4de33064b..d0614b920 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -269,10 +269,12 @@
fast-deep-equal "^3.1.3"
supercluster "^7.1.3"
-"@hotwired/turbo@^7.2.4":
- version "7.2.4"
- resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.4.tgz#0d35541be32cfae3b4f78c6ab9138f5b21f28a21"
- integrity sha512-c3xlOroHp/cCZHDOuLp6uzQYEbvXBUVaal0puXoGJ9M8L/KHwZ3hQozD4dVeSN9msHWLxxtmPT1TlCN7gFhj4w==
+"@hotwired/turbo@^8.0.0-beta.1":
+ version "8.0.0-beta.1"
+ resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.0-beta.1.tgz#35a7086c4c959445db059ea4a9b9af85cdafa616"
+ integrity sha512-g66YmO/Oa+EThB3KkNDhrM9mFnNyRn6tqgwGiBHh4Vf+d3XjCznuWSG2o2e2cO/RlddVRtCwvBSMuG6sYQWX+g==
+ dependencies:
+ idiomorph "https://github.com/basecamp/idiomorph#rollout-build"
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.3"
@@ -939,6 +941,10 @@ hotkeys-js@^3.8.7:
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.10.1.tgz#0c67e72298f235c9200e421ab112d156dc81356a"
integrity sha512-mshqjgTqx8ee0qryHvRgZaZDxTwxam/2yTQmQlqAWS3+twnq1jsY9Yng9zB7lWq6WRrjTbTOc7knNwccXQiAjQ==
+"idiomorph@https://github.com/basecamp/idiomorph#rollout-build":
+ version "0.0.8"
+ resolved "https://github.com/basecamp/idiomorph#e906820368e4c9c52489a3336b8c3826b1bf6de5"
+
import-fresh@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"