Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5fa1b50
feat(cms-base-layer): apply new styling for product listing
mkucmus Sep 25, 2025
035b556
Merge remote-tracking branch 'origin/main' into feat/product-listing-ui
mkucmus Sep 25, 2025
5d05817
fix: types
mkucmus Sep 25, 2025
ba7b9ab
Merge branch 'main' into feat/product-listing-ui
mkucmus Sep 26, 2025
64d2506
chore: cleanup
mkucmus Sep 29, 2025
2a6ca6e
Merge branch 'feat/product-listing-ui' of https://github.com/shopware…
mkucmus Sep 29, 2025
8b26482
fix: add missing theme colors
mkucmus Sep 29, 2025
5fd0f52
chore: lint
mkucmus Sep 29, 2025
12516c5
fix: replace old stuff
mkucmus Sep 29, 2025
e848d7d
fix: changes after CR
mkucmus Oct 1, 2025
290a29e
fix: hydration mismatch
mkucmus Oct 1, 2025
9ff8df4
feat: category sidebar navigation
mkucmus Oct 1, 2025
a279ce0
fix: apply meteor chevron icon instead of carbon
mkucmus Oct 2, 2025
05577ec
fix: replace i-carbon icons with meteor
mkucmus Oct 2, 2025
ef0eb16
fix: missing update
mkucmus Oct 2, 2025
2332e64
refactor(composables): new structure of product listing
mkucmus Oct 3, 2025
34ed2ac
fix: helper types mismatch
mkucmus Oct 3, 2025
c357182
chore: cleanup
mkucmus Oct 6, 2025
0598f2d
fix: CR fixes
mkucmus Oct 6, 2025
2cf87ed
fix: types in shipping free
mkucmus Oct 6, 2025
a2f4aef
fix: apply one chevron icon also for other filter types
mkucmus Oct 6, 2025
570c875
fix: remove figma unused parts of html
mkucmus Oct 6, 2025
ee19cd6
feat: chevron icon
mkucmus Oct 6, 2025
ca5e3f5
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 7, 2025
c3c0de7
feat: add DragType for price slider
mkucmus Oct 7, 2025
21a82bb
refactor: product listing card, colors, price
mkucmus Oct 7, 2025
6a8f63b
feat: use v-model instead props
mkucmus Oct 7, 2025
e158516
fix(template): typo in class name
mkucmus Oct 8, 2025
f1e0521
chore: use specific class for sale
mkucmus Oct 8, 2025
b8d2021
feat: expand transition for other filter types
mkucmus Oct 8, 2025
397e666
fix: change breakpoint for listing
mkucmus Oct 8, 2025
e80cdb9
Merge branch 'feat/product-listing-ui' into refactor/product-listing
mkucmus Oct 8, 2025
11c6aec
chore: cleanup
mkucmus Oct 16, 2025
0cebe0c
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 16, 2025
db7cc42
fix: pnpm lock
mkucmus Oct 16, 2025
9aa8a79
fix: add peer dep rule
mkucmus Oct 16, 2025
bb42b9c
Merge branch 'feat/product-listing-ui' of https://github.com/shopware…
mkucmus Oct 16, 2025
5aacc8c
chore: pnpm lock update
mkucmus Oct 16, 2025
4c3be4d
fix: multiselect for filters
mkucmus Oct 16, 2025
b695220
fix: multiselect and sorting alignment
mkucmus Oct 17, 2025
6fc31b6
fix(vue-starter-template): add missing translation for home
mkucmus Oct 17, 2025
3a987ce
Merge branch 'feat/product-listing-ui' into refactor/product-listing
mkucmus Oct 17, 2025
abcfd07
refactor: remove button inside the other button that causes hydration…
mkucmus Oct 17, 2025
d03813c
Merge branch 'feat/product-listing-ui' into refactor/product-listing
mkucmus Oct 20, 2025
ae81774
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 20, 2025
3df70ce
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 20, 2025
3bff298
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 20, 2025
792a34c
fix: pagination condition
mkucmus Oct 20, 2025
39faafb
fix: spacing and alignment
mkucmus Oct 20, 2025
5181c62
fix: multiselect
mkucmus Oct 20, 2025
cb650a1
fix: switch button alignment
mkucmus Oct 22, 2025
cfad4e6
feat: move pagination into separate component
mkucmus Oct 23, 2025
b69e58c
Merge branch 'main' into feat/product-listing-ui
mkucmus Oct 23, 2025
e131f92
chore: lock update after conflicts
mkucmus Oct 23, 2025
2467606
Merge remote-tracking branch 'origin/main' into feat/product-listing-ui
mkucmus Oct 23, 2025
a0af246
chore: after solving the conflicts
mkucmus Oct 23, 2025
1669915
fix: revert emits from child components for filter options
mkucmus Oct 24, 2025
55e1497
feat: extract FilterChips and sorting dropdown
mkucmus Oct 24, 2025
a315563
refactor: dry
mkucmus Oct 24, 2025
d2e3ca0
chore: add dev dep
mkucmus Oct 27, 2025
593b333
chore: vercel deployment issue
mkucmus Oct 27, 2025
b4eb919
Merge remote-tracking branch 'origin/main' into feat/product-listing-ui
mkucmus Oct 27, 2025
13d1497
chore: revert peerDependencyRules
mkucmus Oct 27, 2025
9276144
fix: typecheck
mkucmus Oct 27, 2025
0c10108
fix: changes after CR
mkucmus Oct 27, 2025
9dfa370
Merge remote-tracking branch 'origin/main' into feat/product-listing-ui
mkucmus Oct 27, 2025
3d54dc8
chore: pnpm-lock update
mkucmus Oct 27, 2025
b41eac7
fix: copilots review
mkucmus Oct 27, 2025
2f8c08f
fix: changes after CR
mkucmus Oct 27, 2025
0a94182
Merge remote-tracking branch 'origin/main' into feat/product-listing-ui
mkucmus Oct 28, 2025
ac881b8
Merge branch 'main' into refactor/product-listing
mkucmus Oct 29, 2025
f0670ba
chore: pnpm-lock update
mkucmus Oct 29, 2025
08ba4b7
Merge branch 'feat/product-listing-ui' into refactor/product-listing
mkucmus Oct 29, 2025
969143c
Merge remote-tracking branch 'origin/main' into refactor/product-listing
mkucmus Oct 29, 2025
e666e0d
feat: changeset
mkucmus Oct 29, 2025
f796455
fix: incompatibility issues
mkucmus Oct 29, 2025
726e863
fix: tests
mkucmus Oct 29, 2025
a6fe5b3
fix: remove redundant watchers
mkucmus Oct 30, 2025
d1a1594
fix: changes after CR
mkucmus Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions .changeset/better-views-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
"@shopware/composables": minor
---

Refactor product listing composables into modular, context-based architecture with improved SSR support.

## Breaking Changes

### 1. Pagination Parameter Change

**Changed:** The pagination parameter has been standardized from `page` to `p`:

```typescript
// Before
changeCurrentPage(2); // Sends: { body: { page: 2 } }

// After
changeCurrentPage(2); // Sends: { body: { p: 2 } }
```

**Impact:** If you're directly using the API client or inspecting search parameters, update to use `p` instead of `page`.

### 2. Error Handling - Context Required

**Changed:** `useCategoryListing()` now **throws an error** if called without context:

```typescript
// Before - Would fail silently or return null
const listing = useCategoryListing();

// After - Throws clear error
const listing = useCategoryListing();
// Error: "[useCategoryListing] Please call `createCategoryListingContext`
// on the appropriate parent component"
```

**Impact:** You **must** call `createCategoryListingContext(initialListing)` before using `useCategoryListing()`.

### 3. Loading State Split

**Changed:** Loading states are now separated:

```typescript
// Before - Single loading ref
const { loading } = useListing();

// After - Two separate refs
const { loading, loadingMore } = useListing();
// loading - for search operations
// loadingMore - for pagination/load more operations
```

**Impact:** If you're checking loading state for pagination, also check `loadingMore`.

### 4. Context-Based Architecture (Required for Category Pages)

**New Requirement:** Category pages must create listing context in parent component:

```ts
// Required in CmsPage.vue or similar
import { createCategoryListingContext } from "@shopware/composables";

const initialListing = getProductListingFromCmsPage(props.content);
if (initialListing) {
createCategoryListingContext(initialListing);
}
```

**Impact:** Without this, child components using `useCategoryListing()` will throw errors.

### 5. Composable Structure (Internal Change)

The `useListing` composable has been refactored into smaller, focused composables:

- `useListingCore` - Core listing state management
- `useProductListingFilters` - Filter state and operations
- `useProductListingPagination` - Pagination logic
- `useProductListingProducts` - Product data handling
- `useProductListingSorting` - Sorting functionality

**The public API of `useListing` remains the same**, but internal implementation has changed significantly.

### 6. SSR Improvements

- **Removed `<ClientOnly>` wrappers** from filter components
- Initial listing data is now provided via context during SSR
- Components can access listing data synchronously on server and client

**Impact:** No hydration mismatches, but ensure context is created during SSR.

## Migration Guide

### Before (prop drilling)

```vue
<template>
<SwProductListingFilters
:listing="listing"
@filter-change="handleFilterChange"
/>
</template>

<script setup>
const listing = useListing();
</script>
```

### After (context-based)

```vue
<!-- In CmsPage.vue or layout -->
<script setup>
import { createCategoryListingContext } from "@shopware/composables";

// Create context once at top level
if (initialListing) {
createCategoryListingContext(initialListing);
}
</script>

<!-- In any child component -->
<template>
<SwProductListingFilters @filter-change="handleFilterChange" />
</template>

<script setup>
// Access listing from context
const listing = useCategoryListing();
</script>
```

### Using the New Composables

If you need fine-grained control, use the individual composables:

```ts
import {
useProductListingFilters,
useProductListingPagination,
useProductListingSorting,
useProductListingProducts,
} from "@shopware/composables";

// Access specific functionality
const { getInitialFilters, getCurrentFilters } = useProductListingFilters();
const { currentPage, totalPages, loadMore } = useProductListingPagination();
const { getSortingOrders, changeCurrentSortingOrder } = useProductListingSorting();
const { getElements, search } = useProductListingProducts();
```

## New Features

### Helper Functions

Added `getProductListingFromCmsPage` helper to extract listing data from CMS pages:

```ts
import { getProductListingFromCmsPage } from "@shopware/helpers";

const listing = getProductListingFromCmsPage(cmsPage);
```

### Utility Functions

New internal utilities for listing operations (exported from `utils.ts`):
- Improved type safety with `ListingType` and `ShortcutFilterParam` types
- Better merge strategies for listing criteria

## What Changed

### File Structure
- Split monolithic `useListing.ts` (~500 lines) into focused modules
- Each module handles a single responsibility
- Better tree-shaking and code organization

### Performance
- Reduced prop drilling
- More efficient reactive updates
- Better separation of concerns

### Developer Experience
- Easier to test individual features
- Clearer API boundaries
- Better TypeScript inference
- Simplified component templates
94 changes: 34 additions & 60 deletions packages/cms-base-layer/app/components/SwProductListingFilters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
import { useCategoryListing } from "#imports";
import type { Schemas, operations } from "#shopware";

const props = defineProps<{
defineProps<{
content: CmsElementProductListing | CmsElementSidebarFilter;
listingType?: string;
}>();
Expand Down Expand Up @@ -48,7 +48,6 @@ const router = useRouter();

const {
changeCurrentSortingOrder,
getCurrentFilters,
getCurrentSortingOrder,
getInitialFilters,
getSortingOrders,
Expand Down Expand Up @@ -233,10 +232,6 @@ async function invokeCleanFilters() {
}
}

const isDefaultSidebarFilter =
props.content.type === "sidebar-filter" &&
props.content.config?.boxLayout?.value === "standard";

const handleSortChange = (sortKey: string) => {
currentSortingOrder.value = sortKey;
};
Expand Down Expand Up @@ -269,63 +264,42 @@ const handleRemoveFilterChip = async (chip: {
@remove="handleRemoveFilterChip"
/>

<ClientOnly>
<template #fallback>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<div
class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant">
<div class="h-6 w-24 bg-surface-surface-container rounded animate-pulse"></div>
<div class="h-10 w-20 bg-surface-surface-container rounded animate-pulse"></div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<div v-for="i in 5" :key="i" class="w-full">
<div class="self-stretch py-3 border-b border-outline-outline-variant flex justify-between items-center">
<div class="h-6 w-32 bg-surface-surface-container rounded animate-pulse"></div>
<div class="h-6 w-6 bg-surface-surface-container rounded animate-pulse"></div>
</div>
</div>
</div>
</div>
</template>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<div
class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant">
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
{{ translations.listing.filters }}
</div>
<SwSortDropdown
:sort-options="getSortingOrders ?? []"
:current-sort="getCurrentSortingOrder ?? ''"
:label="translations.listing.sort"
@sort-change="handleSortChange"
/>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<div
class="flex flex-row items-center justify-between w-full mb-4 py-3 border-b border-outline-outline-variant">
<div class="flex-1 text-surface-on-surface text-base font-bold leading-normal">
{{ translations.listing.filters }}
</div>
<SwSortDropdown
:sort-options="getSortingOrders ?? []"
:current-sort="getCurrentSortingOrder ?? ''"
:label="translations.listing.sort"
@sort-change="handleSortChange"
/>
</div>
</ClientOnly>
</div>
<!-- Filters List -->
<ClientOnly>
<div v-if="!getInitialFilters.length"
class="self-stretch flex flex-col justify-start items-start gap-4 animate-pulse">
<div v-for="i in 3" :key="i" class="w-full h-12 bg-surface-surface-container rounded"></div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4" v-else>
<SwProductListingFilter v-for="filter in getInitialFilters" :key="filter.id"
:filter="filter"
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
:selected-properties="sidebarSelectedFilters.properties"
:selected-min-price="sidebarSelectedFilters['min-price']"
:selected-max-price="sidebarSelectedFilters['max-price']"
:selected-rating="sidebarSelectedFilters.rating"
:selected-shipping-free="sidebarSelectedFilters['shipping-free']"
@filter-change="handleFilterChange"
class="w-full" />
<div v-if="showResetFiltersButton" class="w-full">
<SwBaseButton variant="primary" size="medium" block @click="invokeCleanFilters" type="button">
{{ translations.listing.resetFilters }}
<span class="w-6 h-6 i-carbon-close-filled inline-block align-middle ml-2"></span>
</SwBaseButton>
</div>
<div v-if="!getInitialFilters.length"
class="self-stretch flex flex-col justify-start items-start gap-4 animate-pulse">
<div v-for="i in 3" :key="i" class="w-full h-12 bg-surface-surface-container rounded"></div>
</div>
<div class="self-stretch flex flex-col justify-start items-start gap-4" v-else>
<SwProductListingFilter v-for="filter in getInitialFilters" :key="filter.id"
:filter="filter"
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
:selected-properties="sidebarSelectedFilters.properties"
:selected-min-price="sidebarSelectedFilters['min-price']"
:selected-max-price="sidebarSelectedFilters['max-price']"
:selected-rating="sidebarSelectedFilters.rating"
:selected-shipping-free="sidebarSelectedFilters['shipping-free']"
@filter-change="handleFilterChange"
class="w-full" />
<div v-if="showResetFiltersButton" class="w-full">
<SwBaseButton variant="primary" size="medium" block @click="invokeCleanFilters" type="button">
{{ translations.listing.resetFilters }}
<span class="w-6 h-6 i-carbon-close-filled inline-block align-middle ml-2"></span>
</SwBaseButton>
</div>
</ClientOnly>
</div>
</div>
</template>
21 changes: 18 additions & 3 deletions packages/cms-base-layer/app/components/public/cms/CmsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import {
getBackgroundImageUrl,
getCmsLayoutConfiguration,
getProductListingFromCmsPage,
} from "@shopware/helpers";
import { pascalCase } from "scule";
import { computed, h, resolveComponent } from "vue";
import { computed, h, resolveComponent, watchEffect } from "vue";
import { createCategoryListingContext, useNavigationContext } from "#imports";
import type { Schemas } from "#shopware";

Expand All @@ -13,10 +14,24 @@ const props = defineProps<{
}>();

const { routeName } = useNavigationContext();
if (routeName.value === "frontend.navigation.page") {
createCategoryListingContext();

// Function to initialize or update listing context
function updateListingContext(content: Schemas["CmsPage"]) {
if (routeName.value === "frontend.navigation.page") {
const initialListing =
getProductListingFromCmsPage<Schemas["ProductListingResult"]>(content);

if (initialListing) {
createCategoryListingContext(initialListing);
}
}
}

// Watch for content changes and update context
watchEffect(() => {
updateListingContext(props.content);
});

const cmsSections = computed<Schemas["CmsSection"][]>(() => {
return props.content?.sections || [];
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,10 @@ let translations: Translations = {
};
translations = defu(useCmsTranslations(), translations) as Translations;

const {
changeCurrentPage,
getCurrentPage,
getElements,
getTotalPagesCount,
loading,
setInitialListing,
} = useCategoryListing();
// Use granular composables for better separation of concerns
const listing = useCategoryListing();
const { getElements, loading } = listing;
const { changeCurrentPage, getCurrentPage, getTotalPagesCount } = listing;
const route = useRoute();
const router = useRouter();
const limit = ref(
Expand Down Expand Up @@ -141,10 +137,6 @@ const compareRouteQueryWithInitialListing = async () => {
}
};

setInitialListing(
props?.content?.data?.listing as Schemas["ProductListingResult"],
);

compareRouteQueryWithInitialListing();
</script>

Expand Down
Loading
Loading