@teamnovu/kit-shopware-composables 0.0.1
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/.env +2 -0
- package/api-gen.config.json +5 -0
- package/api-types/storeApiSchema.json +12187 -0
- package/api-types/storeApiSchema.localhost.http +1043 -0
- package/api-types/storeApiSchema.overrides.json +1 -0
- package/api-types/storeApiTypes.d.ts +5909 -0
- package/docker/boot_end.sh +12 -0
- package/docker/docker-compose.yaml +22 -0
- package/docker/types.sh +14 -0
- package/eslint.config.mjs +84 -0
- package/package.json +49 -0
- package/src/context/useContextOptions.ts +16 -0
- package/src/context/useContextUpdate.ts +16 -0
- package/src/general/useSeoUrl.ts +18 -0
- package/src/index.ts +24 -0
- package/src/inject.ts +16 -0
- package/src/keys.ts +42 -0
- package/src/products/useCategoryQueryOptions.ts +25 -0
- package/src/products/useProductListingQueryOptions.test.ts +24 -0
- package/src/products/useProductListingQueryOptions.ts +28 -0
- package/src/products/useProductPrice.ts +136 -0
- package/src/products/useProductQueryOptions.ts +28 -0
- package/src/products/useProductVariantForOptions.ts +21 -0
- package/src/types/query.ts +18 -0
- package/src/usePagination.ts +72 -0
- package/src/util/url.ts +8 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +39 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
export COMPOSER_HOME=/var/www
|
|
4
|
+
|
|
5
|
+
bin/console database:migrate --all
|
|
6
|
+
bin/console cache:clear
|
|
7
|
+
composer require teamnovu/shopware-headless-plugin
|
|
8
|
+
bin/console plugin:refresh
|
|
9
|
+
bin/console plugin:install --activate NovuShopwareHeadlessPlugin
|
|
10
|
+
bin/console cache:clear
|
|
11
|
+
|
|
12
|
+
echo "Custom Setup Done :)"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: shopware-for-kit
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
shopware-for-kit:
|
|
5
|
+
image: dockware/dev:6.7.0.0-rc2
|
|
6
|
+
container_name: shopware-for-kit
|
|
7
|
+
ports:
|
|
8
|
+
- "8500:80"
|
|
9
|
+
volumes:
|
|
10
|
+
- "db:/var/lib/mysql"
|
|
11
|
+
- "./boot_end.sh:/var/www/boot_end.sh"
|
|
12
|
+
- "./custom:/var/www/html/custom"
|
|
13
|
+
networks:
|
|
14
|
+
- web
|
|
15
|
+
|
|
16
|
+
volumes:
|
|
17
|
+
db:
|
|
18
|
+
driver: local
|
|
19
|
+
|
|
20
|
+
networks:
|
|
21
|
+
web:
|
|
22
|
+
external: false
|
package/docker/types.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
cd docker && docker compose up &
|
|
3
|
+
|
|
4
|
+
# This currently works but it goes on with the script before all plugins are installed.
|
|
5
|
+
# thus the first time this runs we may have to run the script twice.
|
|
6
|
+
until curl --output /dev/null --silent --head --fail http://localhost:8500; do
|
|
7
|
+
sleep 3
|
|
8
|
+
done
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
pnpm dlx @shopware/api-gen loadSchema --apiType=store
|
|
12
|
+
pnpm dlx @shopware/api-gen generate --apiType=store
|
|
13
|
+
|
|
14
|
+
cd docker && docker compose down
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import pluginStylistic from '@stylistic/eslint-plugin'
|
|
3
|
+
import tseslint from 'typescript-eslint'
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
js.configs.recommended,
|
|
7
|
+
tseslint.configs.recommended,
|
|
8
|
+
pluginStylistic.configs['recommended'],
|
|
9
|
+
{
|
|
10
|
+
rules: {
|
|
11
|
+
// stylistic
|
|
12
|
+
// ********************
|
|
13
|
+
'@stylistic/newline-per-chained-call': [
|
|
14
|
+
'error',
|
|
15
|
+
{ ignoreChainWithDepth: 2 },
|
|
16
|
+
],
|
|
17
|
+
'@stylistic/nonblock-statement-body-position': 'error',
|
|
18
|
+
'@stylistic/one-var-declaration-per-line': 'error',
|
|
19
|
+
'@stylistic/operator-linebreak': [
|
|
20
|
+
'error',
|
|
21
|
+
'before',
|
|
22
|
+
{ overrides: { '=': 'none' } },
|
|
23
|
+
],
|
|
24
|
+
'@stylistic/function-paren-newline': ['error', 'multiline-arguments'],
|
|
25
|
+
'@stylistic/dot-location': ['error', 'property'],
|
|
26
|
+
'@stylistic/no-mixed-operators': ['error', { allowSamePrecedence: true }],
|
|
27
|
+
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
|
28
|
+
'@stylistic/array-bracket-newline': ['error', { multiline: true }],
|
|
29
|
+
'@stylistic/array-element-newline': [
|
|
30
|
+
'error',
|
|
31
|
+
{
|
|
32
|
+
multiline: true,
|
|
33
|
+
consistent: true,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
'@stylistic/max-len': [
|
|
37
|
+
'warn',
|
|
38
|
+
{
|
|
39
|
+
code: 150,
|
|
40
|
+
tabWidth: 2,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
'@stylistic/object-curly-newline': [
|
|
44
|
+
'error',
|
|
45
|
+
{
|
|
46
|
+
multiline: true,
|
|
47
|
+
consistent: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
'@stylistic/object-property-newline': [
|
|
51
|
+
'error',
|
|
52
|
+
{ allowAllPropertiesOnSameLine: false },
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
// common
|
|
56
|
+
// ********************
|
|
57
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
58
|
+
'prefer-destructuring': [
|
|
59
|
+
'error',
|
|
60
|
+
{
|
|
61
|
+
array: false,
|
|
62
|
+
object: true,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
'no-unused-vars': 'off',
|
|
66
|
+
'eqeqeq': ['error', 'smart'],
|
|
67
|
+
'no-undef': 'off', // done by typescript
|
|
68
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
69
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
70
|
+
'@typescript-eslint/no-unused-vars': [
|
|
71
|
+
'error',
|
|
72
|
+
{
|
|
73
|
+
args: 'all',
|
|
74
|
+
argsIgnorePattern: '^_',
|
|
75
|
+
caughtErrors: 'all',
|
|
76
|
+
caughtErrorsIgnorePattern: '^_',
|
|
77
|
+
destructuredArrayIgnorePattern: '^_',
|
|
78
|
+
varsIgnorePattern: '^_',
|
|
79
|
+
ignoreRestSiblings: true,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
)
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teamnovu/kit-shopware-composables",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A collection of composables for the Shopware API",
|
|
5
|
+
"main": "dist/index.mjs",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@tanstack/vue-query": "^5.0.0",
|
|
16
|
+
"@teamnovu/kit-shopware-api-client": "workspace:*",
|
|
17
|
+
"vue": "^3.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"watch": "NODE_ENV=development vite build --watch",
|
|
22
|
+
"gen-types": "bash ./docker/types.sh",
|
|
23
|
+
"lint": "eslint --fix --ignore-pattern 'dist/**' ."
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+ssh://git@github.com:teamnovu/kit.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [],
|
|
30
|
+
"author": "Elias Bernhaut",
|
|
31
|
+
"homepage": "https://kit.novu.ch/packages/shopware-composables/",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"vite": "^6.2.5"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^9.25.0",
|
|
37
|
+
"@shopware/api-gen": "^1.3.0",
|
|
38
|
+
"@shopware/helpers": "^1.4.0",
|
|
39
|
+
"@stylistic/eslint-plugin": "^4.2.0",
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
|
41
|
+
"@typescript-eslint/parser": "^8.30.1",
|
|
42
|
+
"@vue/test-utils": "^2.4.6",
|
|
43
|
+
"eslint": "^9.25.0",
|
|
44
|
+
"eslint-import-resolver-typescript": "^4.3.3",
|
|
45
|
+
"eslint-plugin-import": "^2.31.0",
|
|
46
|
+
"typescript-eslint": "^8.30.1",
|
|
47
|
+
"vitest": "^3.1.2"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import { queryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import { useShopwareQueryClient } from '../inject'
|
|
4
|
+
import { contextKeys } from '../keys'
|
|
5
|
+
import type { OperationKey } from '../types/query'
|
|
6
|
+
|
|
7
|
+
const readContextOperation = 'readContext get /context' satisfies OperationKey
|
|
8
|
+
|
|
9
|
+
export function useContextOptions<Operations extends operations>() {
|
|
10
|
+
const client = useShopwareQueryClient<Operations>()
|
|
11
|
+
|
|
12
|
+
return queryOptions({
|
|
13
|
+
queryKey: contextKeys.all(),
|
|
14
|
+
queryFn: () => client.query(readContextOperation),
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import { useMutation } from '@tanstack/vue-query'
|
|
3
|
+
import { useShopwareQueryClient } from '../inject'
|
|
4
|
+
import type { OperationKey } from '../types/query'
|
|
5
|
+
|
|
6
|
+
const updateContextOperation = 'updateContext patch /context' satisfies OperationKey
|
|
7
|
+
|
|
8
|
+
export function useContextUpdate<Operations extends operations>() {
|
|
9
|
+
const client = useShopwareQueryClient<Operations>()
|
|
10
|
+
|
|
11
|
+
return useMutation({
|
|
12
|
+
mutationFn: async () => {
|
|
13
|
+
return client.query(updateContextOperation)
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GenericRecord } from '#store-types'
|
|
2
|
+
import type { MaybeRef } from 'vue'
|
|
3
|
+
import { computed, unref } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface SeoUrlEntity {
|
|
6
|
+
extensions?: {
|
|
7
|
+
novuSeoUrls?: GenericRecord
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getSeoUrl = <T extends SeoUrlEntity>(entity: T, languageId: string) => {
|
|
12
|
+
const urls = entity.extensions?.novuSeoUrls as Record<string, string>
|
|
13
|
+
return urls?.[languageId] ?? ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useSeoUrl = <T extends SeoUrlEntity>(entity: MaybeRef<T>, languageId: MaybeRef<string>) => {
|
|
17
|
+
return computed(() => getSeoUrl(unref(entity), unref(languageId)))
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Schemas as BaseSchemas, operations } from '#store-types'
|
|
2
|
+
import { useContextOptions } from './context/useContextOptions'
|
|
3
|
+
import { useContextUpdate } from './context/useContextUpdate'
|
|
4
|
+
import { useCategoryQueryOptions } from './products/useCategoryQueryOptions'
|
|
5
|
+
import { useProductListingQueryOptions } from './products/useProductListingQueryOptions'
|
|
6
|
+
import { useProductQueryOptions } from './products/useProductQueryOptions'
|
|
7
|
+
import { useProductVariantForOptions } from './products/useProductVariantForOptions'
|
|
8
|
+
|
|
9
|
+
export * from './general/useSeoUrl'
|
|
10
|
+
export * from './inject'
|
|
11
|
+
export * from './keys'
|
|
12
|
+
export * from './products/useProductPrice'
|
|
13
|
+
export * from './products/useProductVariantForOptions'
|
|
14
|
+
export * from './usePagination'
|
|
15
|
+
export * from './util/url'
|
|
16
|
+
|
|
17
|
+
export default class ShopwareComposables<Operations extends operations, Schemas extends BaseSchemas> {
|
|
18
|
+
useProductListingQueryOptions = useProductListingQueryOptions<Operations>
|
|
19
|
+
useCategoryQueryOptions = useCategoryQueryOptions<Operations>
|
|
20
|
+
useContextOptions = useContextOptions<Operations>
|
|
21
|
+
useContextUpdate = useContextUpdate<Operations>
|
|
22
|
+
useProductQueryOptions = useProductQueryOptions<Operations>
|
|
23
|
+
useProductVariantForOptions = useProductVariantForOptions<Schemas>
|
|
24
|
+
}
|
package/src/inject.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import type { ShopwareClient } from '@teamnovu/kit-shopware-api-client'
|
|
3
|
+
import type { InjectionKey } from 'vue'
|
|
4
|
+
import { inject } from 'vue'
|
|
5
|
+
|
|
6
|
+
export const shopwareClientKey = Symbol('shopwareClient') as InjectionKey<ShopwareClient<never>>
|
|
7
|
+
|
|
8
|
+
export function useShopwareQueryClient<Operations>() {
|
|
9
|
+
const client = inject(shopwareClientKey)
|
|
10
|
+
|
|
11
|
+
if (!client) {
|
|
12
|
+
throw new Error('Shopware client not provided!')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return client as ShopwareClient<operations & Operations>
|
|
16
|
+
}
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { MaybeRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const contextKeys = {
|
|
4
|
+
all: () => ['context'] as const,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const categoryKeys = {
|
|
8
|
+
all: () => ['category'] as const,
|
|
9
|
+
lists: () => [...categoryKeys.all(), 'list'] as const,
|
|
10
|
+
list: (body: MaybeRef<unknown>) =>
|
|
11
|
+
[
|
|
12
|
+
...categoryKeys.all(),
|
|
13
|
+
'list',
|
|
14
|
+
{
|
|
15
|
+
body,
|
|
16
|
+
},
|
|
17
|
+
] as const,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const productKeys = {
|
|
21
|
+
all: () => ['product'] as const,
|
|
22
|
+
lists: () => [...productKeys.all(), 'list'] as const,
|
|
23
|
+
list: (url: MaybeRef<string>, body: MaybeRef<unknown>) =>
|
|
24
|
+
[
|
|
25
|
+
...productKeys.all(),
|
|
26
|
+
'list',
|
|
27
|
+
{
|
|
28
|
+
url,
|
|
29
|
+
body,
|
|
30
|
+
},
|
|
31
|
+
] as const,
|
|
32
|
+
details: () => [...productKeys.all(), 'detail'] as const,
|
|
33
|
+
detail: (url: MaybeRef<string>, body: MaybeRef<unknown>) =>
|
|
34
|
+
[
|
|
35
|
+
...productKeys.all(),
|
|
36
|
+
'detail',
|
|
37
|
+
{
|
|
38
|
+
url,
|
|
39
|
+
body,
|
|
40
|
+
},
|
|
41
|
+
] as const,
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import { queryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import type { MaybeRef } from 'vue'
|
|
4
|
+
import { unref } from 'vue'
|
|
5
|
+
import { useShopwareQueryClient } from '../inject'
|
|
6
|
+
import { categoryKeys } from '../keys'
|
|
7
|
+
import type { OperationBody, OperationKey } from '../types/query'
|
|
8
|
+
|
|
9
|
+
const readCategoryListOperation = 'readCategoryList post /category' satisfies OperationKey
|
|
10
|
+
|
|
11
|
+
export function useCategoryQueryOptions<Operations extends operations>(
|
|
12
|
+
body?: MaybeRef<OperationBody<Operations, typeof readCategoryListOperation>>,
|
|
13
|
+
) {
|
|
14
|
+
const client = useShopwareQueryClient<Operations>()
|
|
15
|
+
const queryKey = categoryKeys.list(body)
|
|
16
|
+
|
|
17
|
+
return queryOptions({
|
|
18
|
+
queryKey,
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
return client.query(readCategoryListOperation, {
|
|
21
|
+
body: unref(body),
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useQuery, VueQueryPlugin } from '@tanstack/vue-query'
|
|
2
|
+
import { ShopwareClient } from '@teamnovu/kit-shopware-api-client'
|
|
3
|
+
import { createApp } from 'vue'
|
|
4
|
+
import { shopwareClientKey } from '../inject'
|
|
5
|
+
import { useProductListingQueryOptions } from './useProductListingQueryOptions'
|
|
6
|
+
|
|
7
|
+
test('useProducts receives products', async () => {
|
|
8
|
+
const app = createApp({ template: '' })
|
|
9
|
+
|
|
10
|
+
app.provide(shopwareClientKey, new ShopwareClient({
|
|
11
|
+
apiKey: import.meta.env.VITE_SHOPWARE_ACCESS_KEY,
|
|
12
|
+
baseURL: import.meta.env.VITE_SHOPWARE_URL,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
app.use(VueQueryPlugin)
|
|
16
|
+
|
|
17
|
+
await app.runWithContext(async () => {
|
|
18
|
+
const { data, suspense } = useQuery(useProductListingQueryOptions('/Food/Bakery-products/'))
|
|
19
|
+
|
|
20
|
+
await suspense()
|
|
21
|
+
|
|
22
|
+
expect(data).toBeTruthy()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import { queryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import type { MaybeRef } from 'vue'
|
|
4
|
+
import { unref } from 'vue'
|
|
5
|
+
import { useShopwareQueryClient } from '../inject'
|
|
6
|
+
import { productKeys } from '../keys'
|
|
7
|
+
import type { OperationBody, OperationKey } from '../types/query'
|
|
8
|
+
import { relativizeSeoUrl } from '../util/url'
|
|
9
|
+
|
|
10
|
+
const readListingOperation = 'readCompactProductListing post /novu/headless/product-listing/{seoUrl}' satisfies OperationKey
|
|
11
|
+
|
|
12
|
+
export function useProductListingQueryOptions<Operations extends operations>(
|
|
13
|
+
seoUrl: MaybeRef<string>,
|
|
14
|
+
body?: MaybeRef<OperationBody<Operations, typeof readListingOperation>>,
|
|
15
|
+
) {
|
|
16
|
+
const client = useShopwareQueryClient<Operations>()
|
|
17
|
+
const queryKey = productKeys.list(seoUrl, body)
|
|
18
|
+
|
|
19
|
+
return queryOptions({
|
|
20
|
+
queryKey,
|
|
21
|
+
queryFn: async () => {
|
|
22
|
+
return client.query(readListingOperation, {
|
|
23
|
+
params: { seoUrl: relativizeSeoUrl(unref(seoUrl)) },
|
|
24
|
+
body: unref(body),
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copied from:
|
|
3
|
+
* https://github.com/shopware/frontends/blob/main/packages/composables/src/useProductPrice/useProductPrice.ts#L7
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Schemas } from '#store-types'
|
|
7
|
+
import type { TierPrice } from '@shopware/helpers'
|
|
8
|
+
import { getProductTierPrices } from '@shopware/helpers'
|
|
9
|
+
import type { ComputedRef, Ref } from 'vue'
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
|
|
12
|
+
export type UseProductPriceReturn = {
|
|
13
|
+
/**
|
|
14
|
+
* Whole calculated price object
|
|
15
|
+
*/
|
|
16
|
+
price: ComputedRef<Schemas['CalculatedPrice'] | undefined>
|
|
17
|
+
/**
|
|
18
|
+
* Calculated price value for one selling unit
|
|
19
|
+
*/
|
|
20
|
+
totalPrice: ComputedRef<number | undefined>
|
|
21
|
+
/**
|
|
22
|
+
* Current unit price value
|
|
23
|
+
*/
|
|
24
|
+
unitPrice: ComputedRef<number | undefined>
|
|
25
|
+
/**
|
|
26
|
+
* Can be used if isListPrice is set to true
|
|
27
|
+
*/
|
|
28
|
+
referencePrice: ComputedRef<
|
|
29
|
+
Schemas['CalculatedPrice']['referencePrice'] | undefined
|
|
30
|
+
>
|
|
31
|
+
/**
|
|
32
|
+
* determines if `price` contains the minimum tier price
|
|
33
|
+
*/
|
|
34
|
+
displayFrom: ComputedRef<boolean>
|
|
35
|
+
/**
|
|
36
|
+
* cheapest price value for a variant if exists
|
|
37
|
+
*/
|
|
38
|
+
displayFromVariants: ComputedRef<number | false | undefined>
|
|
39
|
+
/**
|
|
40
|
+
* array of TierPrice object
|
|
41
|
+
*/
|
|
42
|
+
tierPrices: ComputedRef<TierPrice[]>
|
|
43
|
+
/**
|
|
44
|
+
* determines whether a discount price is set
|
|
45
|
+
*/
|
|
46
|
+
isListPrice: ComputedRef<boolean>
|
|
47
|
+
/**
|
|
48
|
+
* price for products with regulation price
|
|
49
|
+
*/
|
|
50
|
+
regulationPrice: ComputedRef<number | undefined>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The purpose of the `useProductPrice` function is to abstract the logic to expose most useful helpers for price displaying.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
* @category Product
|
|
58
|
+
*/
|
|
59
|
+
export function useProductPrice(
|
|
60
|
+
product: Ref<Schemas['Product'] | undefined>,
|
|
61
|
+
): UseProductPriceReturn {
|
|
62
|
+
const _cheapest: ComputedRef<
|
|
63
|
+
Schemas['Product']['calculatedCheapestPrice'] | undefined
|
|
64
|
+
> = computed(() => product.value?.calculatedCheapestPrice)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* calculatedPrices are used for product with tier prices
|
|
68
|
+
*/
|
|
69
|
+
const _real: ComputedRef<Schemas['CalculatedPrice'] | undefined> = computed(
|
|
70
|
+
() =>
|
|
71
|
+
(product.value?.calculatedPrices?.length ?? 0) > 0
|
|
72
|
+
? product.value?.calculatedPrices?.[0]
|
|
73
|
+
: product.value?.calculatedPrice,
|
|
74
|
+
)
|
|
75
|
+
const referencePrice: ComputedRef<
|
|
76
|
+
Schemas['CalculatedPrice']['referencePrice'] | undefined
|
|
77
|
+
> = computed(() => _real?.value?.referencePrice)
|
|
78
|
+
|
|
79
|
+
const displayFrom: ComputedRef<boolean> = computed(() => {
|
|
80
|
+
return (product.value?.calculatedPrices?.length ?? 0) > 1
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const displayFromVariants: ComputedRef<number | false | undefined> = computed(
|
|
84
|
+
() => {
|
|
85
|
+
return (
|
|
86
|
+
!!product.value?.parentId
|
|
87
|
+
&& product.value?.calculatedCheapestPrice?.hasRange
|
|
88
|
+
&& _real?.value?.unitPrice !== _cheapest?.value?.unitPrice
|
|
89
|
+
&& _cheapest?.value?.unitPrice
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const _price: ComputedRef<Schemas['CalculatedPrice'] | undefined> = computed(
|
|
95
|
+
() => {
|
|
96
|
+
if (displayFrom.value && getProductTierPrices(product.value).length > 1) {
|
|
97
|
+
return product.value?.calculatedPrices?.reduce((previous, current) => {
|
|
98
|
+
return current.unitPrice < previous.unitPrice ? current : previous
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
return _real.value
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const unitPrice: ComputedRef<number | undefined> = computed(
|
|
106
|
+
() => _price.value?.unitPrice,
|
|
107
|
+
)
|
|
108
|
+
const totalPrice: ComputedRef<number | undefined> = computed(
|
|
109
|
+
() => _price.value?.totalPrice,
|
|
110
|
+
)
|
|
111
|
+
const price: ComputedRef<Schemas['CalculatedPrice'] | undefined> = computed(
|
|
112
|
+
() => _price.value,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const isListPrice: ComputedRef<boolean> = computed(() => {
|
|
116
|
+
return !!_price.value?.listPrice?.percentage
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const regulationPrice: ComputedRef<number | undefined> = computed(
|
|
120
|
+
() => product.value?.calculatedPrice?.regulationPrice?.price,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const tierPrices = computed(() => getProductTierPrices(product.value))
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
price,
|
|
127
|
+
totalPrice,
|
|
128
|
+
unitPrice,
|
|
129
|
+
displayFromVariants,
|
|
130
|
+
displayFrom,
|
|
131
|
+
tierPrices,
|
|
132
|
+
referencePrice,
|
|
133
|
+
isListPrice,
|
|
134
|
+
regulationPrice,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import { queryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import type { MaybeRef } from 'vue'
|
|
4
|
+
import { unref } from 'vue'
|
|
5
|
+
import { useShopwareQueryClient } from '../inject'
|
|
6
|
+
import { productKeys } from '../keys'
|
|
7
|
+
import type { OperationBody, OperationKey } from '../types/query'
|
|
8
|
+
import { relativizeSeoUrl } from '../util/url'
|
|
9
|
+
|
|
10
|
+
const readCustomProductDetailOperation = 'readCustomProductDetail post /novu/headless/product/{seoUrl}' satisfies OperationKey
|
|
11
|
+
|
|
12
|
+
export function useProductQueryOptions<Operations extends operations>(
|
|
13
|
+
seoUrl: MaybeRef<string>,
|
|
14
|
+
body?: MaybeRef<OperationBody<Operations, typeof readCustomProductDetailOperation>>,
|
|
15
|
+
) {
|
|
16
|
+
const client = useShopwareQueryClient<Operations>()
|
|
17
|
+
const queryKey = productKeys.detail(seoUrl, body)
|
|
18
|
+
|
|
19
|
+
return queryOptions({
|
|
20
|
+
queryKey,
|
|
21
|
+
queryFn: async () => {
|
|
22
|
+
return client.query(readCustomProductDetailOperation, {
|
|
23
|
+
params: { seoUrl: relativizeSeoUrl(unref(seoUrl)) },
|
|
24
|
+
body: unref(body),
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Schemas } from '#store-types'
|
|
2
|
+
import type { MaybeRef } from 'vue'
|
|
3
|
+
import { computed, unref } from 'vue'
|
|
4
|
+
|
|
5
|
+
type DetailProduct<S extends Schemas> = S['CustomProductDetailResponse']['product']
|
|
6
|
+
|
|
7
|
+
export function getProductVariantForOptions<S extends Schemas>(product: DetailProduct<S>, optionIds: string[]) {
|
|
8
|
+
const variants = product.extensions?.variants
|
|
9
|
+
|
|
10
|
+
if (optionIds.length === 0) {
|
|
11
|
+
// if no options are selected, return the first variant without options and
|
|
12
|
+
// if there is none, return the first variant
|
|
13
|
+
return variants?.find(v => v.optionIds?.length === 0) ?? variants?.[0]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return variants?.find(v => v.optionIds?.every(optId => optionIds.includes(optId)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useProductVariantForOptions<S extends Schemas>(product: MaybeRef<DetailProduct<S>>, optionIds: MaybeRef<string[]>) {
|
|
20
|
+
return computed(() => getProductVariantForOptions(unref(product), unref(optionIds)))
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { operations } from '#store-types'
|
|
2
|
+
import type { QueryKey, UseQueryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import type { OperationProp } from '@teamnovu/kit-shopware-api-client'
|
|
4
|
+
|
|
5
|
+
export type OperationKey = keyof operations
|
|
6
|
+
export type OperationBody<Operations extends operations, K extends OperationKey> =
|
|
7
|
+
OperationProp<Operations, K, 'body'>
|
|
8
|
+
export type OperationResponse<Operations extends operations, K extends OperationKey> =
|
|
9
|
+
OperationProp<Operations, K, 'response'>
|
|
10
|
+
|
|
11
|
+
export type Options<Operations extends operations, K extends OperationKey, QK extends QueryKey = QueryKey> =
|
|
12
|
+
UseQueryOptions<
|
|
13
|
+
OperationResponse<Operations, K>,
|
|
14
|
+
Error,
|
|
15
|
+
OperationResponse<Operations, K>,
|
|
16
|
+
OperationResponse<Operations, K>,
|
|
17
|
+
QK
|
|
18
|
+
>
|