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"