@shopbite-de/storefront 1.18.5 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yaml +48 -48
- package/app/components/Category/Listing.vue +41 -95
- package/app/components/Product/Configurator.vue +4 -9
- package/app/components/Product/CrossSelling.vue +7 -50
- package/app/components/Product/Detail.vue +7 -119
- package/app/composables/useCategory.ts +6 -34
- package/app/composables/useCategoryListing.ts +131 -0
- package/app/composables/useProductConfigurator.ts +12 -52
- package/app/composables/useProductCrossSelling.ts +45 -0
- package/app/composables/useProductDetail.ts +62 -0
- package/app/pages/bestellung/bestaetigen.vue +9 -0
- package/app/pages/bestellung/warenkorb.vue +9 -0
- package/app/pages/bestellung/zahlung-versand.vue +9 -0
- package/nuxt.config.ts +9 -4
- package/package.json +1 -1
- package/server/api/category/[categoryId].get.ts +26 -0
- package/server/api/listing/[categoryId].get.ts +107 -0
- package/server/api/product/[productId]/cross-selling.get.ts +31 -0
- package/server/api/product/[productId].get.ts +68 -0
- package/server/api/product/variant.get.ts +99 -0
- package/test/nuxt/useProductConfigurator.test.ts +14 -17
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { components } from "~~/api-types/storeApiTypes";
|
|
2
|
+
|
|
3
|
+
type Schemas = components["schemas"];
|
|
4
|
+
|
|
5
|
+
const MAX_OPTION_IDS = 20;
|
|
6
|
+
const MAX_OPTION_ID_LENGTH = 64;
|
|
7
|
+
|
|
8
|
+
function parseOptionIds(raw: unknown): string[] {
|
|
9
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
10
|
+
const seen = new Set<string>();
|
|
11
|
+
for (const v of arr) {
|
|
12
|
+
if (
|
|
13
|
+
typeof v !== "string" ||
|
|
14
|
+
v.trim() === "" ||
|
|
15
|
+
v.length > MAX_OPTION_ID_LENGTH
|
|
16
|
+
)
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(v);
|
|
19
|
+
if (seen.size >= MAX_OPTION_IDS) break;
|
|
20
|
+
}
|
|
21
|
+
return [...seen];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default defineCachedEventHandler(
|
|
25
|
+
async (event): Promise<Schemas["Product"] | null> => {
|
|
26
|
+
const { parentId, optionIds: rawOptionIds } = getQuery(event);
|
|
27
|
+
|
|
28
|
+
if (typeof parentId !== "string" || parentId.trim() === "") {
|
|
29
|
+
throw createError({
|
|
30
|
+
statusCode: 400,
|
|
31
|
+
message: "Missing or invalid parentId",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const optionIds = parseOptionIds(rawOptionIds);
|
|
36
|
+
if (optionIds.length === 0) {
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 400,
|
|
39
|
+
message: "At least one optionId is required",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { endpoint, accessToken } = useRuntimeConfig().public.shopware;
|
|
44
|
+
|
|
45
|
+
const filter: Schemas["Filters"] = [
|
|
46
|
+
{ type: "equals", field: "parentId", value: parentId },
|
|
47
|
+
...optionIds.map(
|
|
48
|
+
(id) =>
|
|
49
|
+
({
|
|
50
|
+
type: "equals",
|
|
51
|
+
field: "optionIds",
|
|
52
|
+
value: id,
|
|
53
|
+
}) as Schemas["EqualsFilter"],
|
|
54
|
+
),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const response = await $fetch<{ elements?: Schemas["Product"][] }>(
|
|
58
|
+
`${endpoint}/product`,
|
|
59
|
+
{
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "sw-access-key": accessToken },
|
|
62
|
+
body: {
|
|
63
|
+
filter,
|
|
64
|
+
limit: 1,
|
|
65
|
+
includes: {
|
|
66
|
+
product: [
|
|
67
|
+
"id",
|
|
68
|
+
"name",
|
|
69
|
+
"description",
|
|
70
|
+
"translated",
|
|
71
|
+
"productNumber",
|
|
72
|
+
"options",
|
|
73
|
+
"properties",
|
|
74
|
+
"calculatedPrice",
|
|
75
|
+
],
|
|
76
|
+
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
77
|
+
property: ["id", "name", "translated", "options"],
|
|
78
|
+
property_group_option: ["id", "name", "translated", "group"],
|
|
79
|
+
},
|
|
80
|
+
associations: {
|
|
81
|
+
options: { associations: { group: {} } },
|
|
82
|
+
properties: { associations: { group: {} } },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return response.elements?.[0] ?? null;
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
maxAge: useRuntimeConfig().public.shopBite.cacheTtl.variant,
|
|
92
|
+
name: "variant",
|
|
93
|
+
getKey: (event) => {
|
|
94
|
+
const { parentId, optionIds } = getQuery(event);
|
|
95
|
+
const ids = parseOptionIds(optionIds).sort();
|
|
96
|
+
return `variant-${parentId}-${ids.join("|")}`;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
);
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { useProductConfigurator } from "../../app/composables/useProductConfigurator";
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
|
|
4
|
+
const { mockFetch, mockConfigurator, mockProduct } = vi.hoisted(() => ({
|
|
5
|
+
mockFetch: vi.fn(),
|
|
6
6
|
mockConfigurator: { value: [] },
|
|
7
7
|
mockProduct: { value: { id: "p1", optionIds: [], options: [] } },
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
vi.stubGlobal("$fetch", mockFetch);
|
|
11
|
+
|
|
11
12
|
vi.mock("@shopware/composables", () => ({
|
|
12
|
-
useShopwareContext: () => ({
|
|
13
|
-
apiClient: {
|
|
14
|
-
invoke: mockInvoke,
|
|
15
|
-
},
|
|
16
|
-
}),
|
|
17
13
|
useProductConfigurator: () => ({
|
|
18
14
|
handleChange: vi.fn(),
|
|
19
15
|
}),
|
|
@@ -57,24 +53,25 @@ describe("useProductConfigurator", () => {
|
|
|
57
53
|
mockProduct.value = {
|
|
58
54
|
parentId: "parent-1",
|
|
59
55
|
} as unknown as typeof mockProduct.value;
|
|
60
|
-
|
|
61
|
-
data: {
|
|
62
|
-
elements: [{ id: "variant-1" }],
|
|
63
|
-
},
|
|
64
|
-
});
|
|
56
|
+
mockFetch.mockResolvedValue({ id: "variant-1" });
|
|
65
57
|
|
|
66
58
|
const { findVariantForSelectedOptions } = useProductConfigurator();
|
|
67
59
|
const result = await findVariantForSelectedOptions({ Size: "o1" });
|
|
68
60
|
|
|
69
|
-
expect(
|
|
70
|
-
"
|
|
71
|
-
expect.
|
|
61
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
62
|
+
"/api/product/variant",
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
query: {
|
|
65
|
+
parentId: "parent-1",
|
|
66
|
+
optionIds: ["o1"],
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
72
69
|
);
|
|
73
70
|
expect(result).toEqual({ id: "variant-1" });
|
|
74
71
|
});
|
|
75
72
|
|
|
76
73
|
it("should return undefined on error in findVariantForSelectedOptions", async () => {
|
|
77
|
-
|
|
74
|
+
mockFetch.mockRejectedValue(new Error("API Error"));
|
|
78
75
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
79
76
|
|
|
80
77
|
const { findVariantForSelectedOptions } = useProductConfigurator();
|