Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vue-demo): add dynamic breadcrumbs #1369

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .changeset/funny-hotels-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@shopware-pwa/composables-next": minor
---

Added `getCategoryBreadcrumbs` method for fetching backend breadcrumbs in `useBreadcrumbs` composable
patzick marked this conversation as resolved.
Show resolved Hide resolved
Added `buildDynamicBreadcrumbs` method for building breadcrumbs structure
Added `pushBreadcrumb` method to push single breadcrumb at the top of the breadcrumbs list
Added `associations` option to the `search` method in `useProductSearch` composable
5 changes: 5 additions & 0 deletions .changeset/grumpy-news-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/helpers-next": minor
---

Added `getCmsBreadcrumbs` helper for building CMS breadcrumbs
5 changes: 5 additions & 0 deletions .changeset/rude-students-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vue-demo-store": minor
---

Added dynamic breadcrumbs
17 changes: 11 additions & 6 deletions apps/docs/src/getting-started/page-elements/breadcrumbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ nav:
In this chapter you will learn how to

- Build breadcrumbs for static page
- How breadcrumbs are built for CMS pages
- Build dynamic breadcrumbs for category/product page

### Quick reference

- [useBreadcrumbs](/packages/composables.html#usebreadcrumbs) is a composable used for a breadcrumbs management with sharable state
- [getCategoryBreadcrumbs](/packages/helpers.html#getcategorybreadcrumbs) is a helper used for converting `Category` to the `Breadcrumb` object
- [getCmsBreadcrumbs](/packages/helpers.html#getcmsbreadcrumbs) is a helper used for building breadcrumbs for `Landing Pages`

## Building breadcrumbs for a static page

Expand All @@ -36,12 +37,16 @@ useBreadcrumbs([
]);
```

## Building breadcrumbs for CMS pages
## Building breadcrumbs for a category/product page

:::warning
Currently Shopware 6 API returns breadcrumbs without links.
It means that breadcrumbs for a product and category page, are just a plain text.
:::
```ts
// props.navigationId is a page id

const { buildDynamicBreadcrumbs } = useBreadcrumbs();
buildDynamicBreadcrumbs(props.navigationId);
```

## Building breadcrumbs for CMS pages - without additional request

Each CMS page contains the `Category` with `breadcrumb` array, which contains a list of names, like:

Expand Down
6 changes: 5 additions & 1 deletion examples/api-client-tutorial/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"recommendations": ["astro-build.astro-vscode", "StackBlitz.tutorialkit", "unifiedjs.vscode-mdx"],
"recommendations": [
"astro-build.astro-vscode",
"StackBlitz.tutorialkit",
"unifiedjs.vscode-mdx"
],
"unwantedRecommendations": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
<body>
<div class="center">
<h1 class="center">Live example</h1>
<pre id="result"><button id="loadContext">Load context via API Client</button></pre>
<pre
id="result"
><button id="loadContext">Load context via API Client</button></pre>
</div>
<script type="module" src="/main.ts"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
<body>
<div class="center">
<h1 class="center">Live example</h1>
<pre id="result"><button id="loadContext">Click to load context via API Client</button></pre>
<pre
id="result"
><button id="loadContext">Click to load context via API Client</button></pre>
</div>
<script type="module" src="/main.ts"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
<body>
<div class="center">
<h1 class="center">Live example</h1>
<pre id="result"><button id="loadContext">Load context via API Client</button></pre>
<pre
id="result"
><button id="loadContext">Load context via API Client</button></pre>
</div>
<script type="module" src="/main.ts"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21009,4 +21009,4 @@
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21009,4 +21009,4 @@
}
}
]
}
}
12 changes: 4 additions & 8 deletions examples/api-client-tutorial/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@
"baseUrl": "./",
"jsxImportSource": "solid-js",
"paths": {
"@*": [
"src/*"
]
},
"@*": ["src/*"]
}
},
"exclude": [
"dist"
]
}
"exclude": ["dist"]
}
63 changes: 62 additions & 1 deletion packages/composables/src/useBreadcrumbs/useBreadcrumbs.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, beforeEach, vi } from "vitest";
import { useBreadcrumbs } from "./useBreadcrumbs";
import { useSetup } from "../_test";
import type { operations } from "#shopware";

describe("useBreadcrumbs", () => {
const consoleErrorSpy = vi.spyOn(console, "error");

beforeEach(() => {
consoleErrorSpy.mockImplementation(() => {});
});

it("should add breadcrumbs", async () => {
const { vm } = useSetup(() =>
useBreadcrumbs([
Expand All @@ -29,4 +36,58 @@ describe("useBreadcrumbs", () => {
vm.clearBreadcrumbs();
expect(vm.breadcrumbs.length).toBe(0);
});

it("should invoke getCategoryBreadcrumbs", async () => {
const { vm, injections } = useSetup(() =>
useBreadcrumbs([
{
name: "Test",
path: "/",
},
]),
);
injections.apiClient.invoke.mockResolvedValue({ data: {} });
vm.getCategoryBreadcrumbs("123");

expect(injections.apiClient.invoke).toHaveBeenCalledWith(
expect.stringContaining("readBreadcrumb"),
expect.objectContaining({
pathParams: {
id: "123",
},
}),
);
});

it("should invoke buildDynamicBreadcrumbs", async () => {
const { vm, injections } = useSetup(() => useBreadcrumbs());
injections.apiClient.invoke.mockResolvedValue({
data: {
breadcrumbs: [
{
path: "test",
},
],
},
});
await vm.buildDynamicBreadcrumbs({
breadcrumbs: [{ path: "test" }],
} as unknown as operations["readBreadcrumb get /breadcrumb/{id}"]["response"]);

expect(vm.breadcrumbs[0].path).toBe("/test");
});

it("should push breadcrumb", async () => {
const { vm } = useSetup(() => useBreadcrumbs());
vm.pushBreadcrumb({
name: "Test",
path: "/",
});
expect(vm.breadcrumbs.length).toBe(1);
vm.pushBreadcrumb({
name: "Test",
path: "/",
});
expect(vm.breadcrumbs.length).toBe(2);
});
});
74 changes: 66 additions & 8 deletions packages/composables/src/useBreadcrumbs/useBreadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { computed } from "vue";
import type { ComputedRef } from "vue";
import { useContext } from "#imports";
import { useContext, useShopwareContext } from "#imports";
import type { operations, Schemas } from "#shopware";

/**
* @internal
*/
export type Breadcrumb = {
name: string;
path?: string;
};
export type Breadcrumb =
| {
name: string;
path?: string;
}
| Schemas["Breadcrumb"];

/**
* @public
Expand All @@ -22,20 +25,43 @@ export type UseBreadcrumbsReturn = {
* List of breadcrumbs
*/
breadcrumbs: ComputedRef<Breadcrumb[]>;
/**
* Get category breadcrumbs from the API
*
* @param {string} categoryId
* @returns
*/
getCategoryBreadcrumbs: (
categoryId: string,
) => Promise<operations["readBreadcrumb get /breadcrumb/{id}"]["response"]>;
/**
* Build breadcrumbs dynamically for a category by fetching them from the API
*
* @param {operations["readBreadcrumb get /breadcrumb/{id}"]["response"]} breadcrumbs
*/
buildDynamicBreadcrumbs(
breadcrumbs: operations["readBreadcrumb get /breadcrumb/{id}"]["response"],
): Promise<void>;
/**
* Add a breadcrumb to the breadcrumbs list
*
* @param {Breadcrumb} breadcrumb
*/
pushBreadcrumb(breadcrumb: Breadcrumb): void;
};

/**
* Composable for breadcrumbs management.
* Read the [guide](https://frontends.shopware.com//getting-started/page-elements/breadcrumbs.html#building-breadcrumbs-for-cms-pages).
*
* It's recommended to use [getCategoryBreadcrumbs](https://frontends.shopware.com/packages/helpers.html#getcategorybreadcrumbs) for category breadcrumbs.
* Read the [guide](https://frontends.shopware.com/getting-started/page-elements/breadcrumbs.html#building-breadcrumbs-for-cms-pages).
*
* @public
* @category CMS (Shopping Experiences)
*/
export function useBreadcrumbs(
newBreadcrumbs?: Breadcrumb[],
): UseBreadcrumbsReturn {
const { apiClient } = useShopwareContext();

// Store for breadcrumbs
const _breadcrumbs = useContext<Breadcrumb[]>("swBreadcrumb", {
replace: newBreadcrumbs,
Expand All @@ -48,8 +74,40 @@ export function useBreadcrumbs(
_breadcrumbs.value = [];
};

const getCategoryBreadcrumbs = async (categoryId: string) => {
const response = await apiClient.invoke(
"readBreadcrumb get /breadcrumb/{id}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this endpoint is not only to load category breadcrumbs, I'm not sure about exposing it here, what is the designed flow here looks like?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint is only for the category breadcrumbs. For the product entity we will also receive, category breadcrumbs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the endpoint is for both and takes more parameters
image

I just find getCategoryBreadcrumbs misleading as I would search for other types of breadcrumbs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is - I would not keep this endpoint in the composable, it does not interfere with the logic and everyone can just use apiClient method for that and pass the data through buildDynamicBreadcrumbs

{
pathParams: {
id: categoryId,
},
},
);
return response.data;
};

const pushBreadcrumb = (breadcrumb: Breadcrumb) => {
if (_breadcrumbs.value) _breadcrumbs.value.push(breadcrumb);
else _breadcrumbs.value = [breadcrumb];
};

const buildDynamicBreadcrumbs = async (
breadcrumbs: operations["readBreadcrumb get /breadcrumb/{id}"]["response"],
) => {
_breadcrumbs.value = breadcrumbs.breadcrumbs.map((breadcrumb) => {
// Adjust path to be compatible with the router
return {
...breadcrumb,
path: `/${breadcrumb.path}`,
};
});
};

return {
clearBreadcrumbs,
breadcrumbs: computed(() => _breadcrumbs.value),
getCategoryBreadcrumbs,
buildDynamicBreadcrumbs,
pushBreadcrumb,
};
}
2 changes: 2 additions & 0 deletions packages/composables/src/useProductSearch/useProductSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defu } from "defu";
type UseProductSearchReturnOptions = {
withCmsAssociations?: boolean;
criteria?: Partial<Schemas["Criteria"]>;
associations?: Partial<Schemas["Association"]>;
};

export type UseProductSearchReturn = {
Expand Down Expand Up @@ -36,6 +37,7 @@ export function useProductSearch(): UseProductSearchReturn {
const associations = defu(
options?.withCmsAssociations ? cmsAssociations : {},
options?.criteria,
{ associations: options?.associations ?? {} },
);
const result = await apiClient.invoke(
"readProductDetail post /product/{productId}",
Expand Down
14 changes: 14 additions & 0 deletions packages/helpers/src/cms/getCmsBreadcrumbs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { getCmsBreadcrumbs } from "./getCmsBreadcrumbs";

describe("getCmsBreadcrumbs", () => {
it("should return translated name", () => {
expect(
getCmsBreadcrumbs({
translated: {
name: "translated name",
},
}),
).toEqual([{ name: "translated name" }]);
});
});
19 changes: 19 additions & 0 deletions packages/helpers/src/cms/getCmsBreadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Build the breadcrumbs for the CMS page
*
* @param page
* @returns
*/
export function getCmsBreadcrumbs<
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about this helper - it's not creating breadcrumbs, just displaying entity translated property, when would that be useful for the users?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using it to build CMS page breadcrumbs, where we always have 1 level of breadcrumbs

T extends {
translated: {
name: string;
};
},
>(page: T): { name: string }[] {
return [
{
name: page.translated.name,
},
];
}
1 change: 1 addition & 0 deletions packages/helpers/src/cms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from "./buildUrlPrefix";
export * from "./layoutClasses";
export * from "./isMaintenanceMode";
export * from "./getCmsTranslate";
export * from "./getCmsBreadcrumbs";

/**
* Returns the main page object depending of the type of the CMS page.
Expand Down
1 change: 1 addition & 0 deletions packages/helpers/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe("helpers - test global API", () => {
"getCategoryImageUrl": [Function],
"getCategoryRoute": [Function],
"getCategoryUrl": [Function],
"getCmsBreadcrumbs": [Function],
"getCmsEntityObject": [Function],
"getCmsLayoutConfiguration": [Function],
"getCmsTranslate": [Function],
Expand Down
Loading
Loading