@graphcommerce/storyblok-ui 10.1.0-canary.21 → 10.1.0-canary.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @graphcommerce/storyblok-ui
2
2
 
3
+ ## 10.1.0-canary.23
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2634](https://github.com/graphcommerce-org/graphcommerce/pull/2634) [`e1a7e85`](https://github.com/graphcommerce-org/graphcommerce/commit/e1a7e856669b792b5c7464a7585868b4c8107622) - Sync Storyblok content with the Visual Editor's selected language ([@bramvanderholst](https://github.com/bramvanderholst))
8
+
9
+ ## 10.1.0-canary.22
10
+
3
11
  ## 10.1.0-canary.21
4
12
 
5
13
  ## 10.1.0-canary.20
package/Config.graphqls CHANGED
@@ -30,3 +30,16 @@ extend input GraphCommerceConfig {
30
30
  """
31
31
  storyblok: StoryblokConfig!
32
32
  }
33
+
34
+ extend input GraphCommerceStorefrontConfig {
35
+ """
36
+ Storyblok language code to request for this storefront. Maps the storefront's
37
+ GraphCommerce locale to a Storyblok language version. When unset, the storefront's
38
+ GraphCommerce locale prefix is used as the language code, unless it matches the
39
+ default locale (in which case no `language` param is sent and Storyblok serves
40
+ its space-default language). Set this when the GraphCommerce default locale
41
+ differs from the Storyblok default language (e.g. GraphCommerce default `nl_NL`
42
+ while the Storyblok space default stays English).
43
+ """
44
+ storyblokLocale: String
45
+ }
package/lib/fetch.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ApolloClient } from '@graphcommerce/graphql'
2
+ import { storefrontConfig } from '@graphcommerce/next-ui'
2
3
  import {
3
4
  getStoryblokApi,
4
5
  type ISbStoriesParams,
@@ -18,6 +19,14 @@ export type FetchStoryOpts = {
18
19
  * into full story objects in the response (e.g. `row_button_link_list.links`).
19
20
  */
20
21
  resolveRelations?: string | string[]
22
+ /**
23
+ * Direct Storyblok language override. When set, takes precedence over the
24
+ * storefront-config based `storyblokLocale` and the locale-prefix fallback.
25
+ * Empty string means "no `language` param" — Storyblok serves its space
26
+ * default. Used by `useStoryblokState` to refetch in the editor's selected
27
+ * language.
28
+ */
29
+ language?: string
21
30
  }
22
31
  export type FetchStoriesParams = ISbStoriesParams & FetchStoryOpts
23
32
 
@@ -36,6 +45,18 @@ export const sbParams = (opts: FetchStoryOpts = {}) => {
36
45
  const lang = langPrefix(opts.locale)
37
46
  const defaultLang = langPrefix(opts.defaultLocale)
38
47
  const isDefault = !lang || lang === defaultLang
48
+ // Per-storefront override — lets projects map a GraphCommerce locale to a
49
+ // Storyblok language explicitly. Needed when the GC default locale differs
50
+ // from the Storyblok space's default language (e.g. GC default `nl_NL` while
51
+ // Storyblok stays English-default). When unset, fall back to the legacy
52
+ // locale-prefix heuristic.
53
+ const explicitLanguage = storefrontConfig(opts.locale)?.storyblokLocale
54
+ // Direct override has highest priority. Empty string = no `language` param
55
+ // (Storyblok serves its default language).
56
+ const language =
57
+ opts.language !== undefined
58
+ ? opts.language || undefined
59
+ : (explicitLanguage ?? (!isDefault ? lang : undefined))
39
60
 
40
61
  const resolveRelations = Array.isArray(opts.resolveRelations)
41
62
  ? opts.resolveRelations.join(',')
@@ -49,7 +70,7 @@ export const sbParams = (opts: FetchStoryOpts = {}) => {
49
70
  resolve_links: 'story' as const,
50
71
  ...(resolveRelations && { resolve_relations: resolveRelations }),
51
72
  ...(isDev && { cv: Date.now() }),
52
- ...(!isDefault && { language: lang }),
73
+ ...(language && { language }),
53
74
  }
54
75
  }
55
76
 
@@ -5,6 +5,7 @@ import {
5
5
  type SbBlokData,
6
6
  } from '@storyblok/react'
7
7
  import { useEffect, useRef, useState } from 'react'
8
+ import { fetchStory } from './fetch'
8
9
  import { resolveStoryblokProducts } from './resolveProducts'
9
10
 
10
11
  export type UseStoryblokStateOptions = {
@@ -14,6 +15,14 @@ export type UseStoryblokStateOptions = {
14
15
  * fully resolved by `fetchStory` in `getStaticProps`.
15
16
  */
16
17
  skip?: boolean
18
+ /**
19
+ * Storyblok language code the Visual Editor is currently showing. When
20
+ * different from `initialStory`'s language, the hook refetches the story
21
+ * in this language on mount so the editor preview matches the editor's
22
+ * sidebar selection regardless of which GraphCommerce storefront the page
23
+ * renders in. Empty string means "Storyblok-default language".
24
+ */
25
+ editorLanguage?: string
17
26
  }
18
27
 
19
28
  /**
@@ -22,18 +31,52 @@ export type UseStoryblokStateOptions = {
22
31
  * `magento_category_id` fields are changed.
23
32
  *
24
33
  * The generic `T` lets the caller narrow the returned `content` to an auto-generated Storyblok
25
- * content type. The runtime check guards against feeding in a story whose content isn't a `page`.
34
+ * content type.
26
35
  */
27
36
  export function useStoryblokState<T = SbBlokData>(
28
37
  initialStory: ISbStoryData | null,
29
38
  options: UseStoryblokStateOptions = {},
30
39
  ): ISbStoryData<T> | null {
31
- const { skip = false } = options
40
+ const { skip = false, editorLanguage } = options
32
41
  const story = useStoryblokStateBase(initialStory, { resolveLinks: 'story' })
33
42
  const client = useApolloClient()
34
43
  const [resolvedStory, setResolvedStory] = useState(story)
35
44
  const prevStoryRef = useRef(story)
36
45
 
46
+ // Editor language refetch: Storyblok's bridge doesn't proactively push
47
+ // content on language switches (only on field edits), and preview cookies
48
+ // can't reach getStaticProps from third-party iframes. So when the editor's
49
+ // language differs from the initial SSR fetch we refetch client-side once.
50
+ useEffect(() => {
51
+ if (skip || editorLanguage === undefined || !initialStory?.full_slug) return
52
+ const target = editorLanguage || 'default'
53
+ const current = initialStory.lang ?? 'default'
54
+ if (target === current) return
55
+
56
+ // Storyblok's `full_slug` is prefixed with the language folder (e.g.
57
+ // `nl/home`). Strip it so the refetch targets the canonical story
58
+ // regardless of which language we're switching to.
59
+ const lang = initialStory.lang
60
+ const baseSlug =
61
+ lang && lang !== 'default' && initialStory.full_slug.startsWith(`${lang}/`)
62
+ ? initialStory.full_slug.slice(lang.length + 1)
63
+ : initialStory.full_slug
64
+
65
+ let cancelled = false
66
+ fetchStory(baseSlug, { preview: true, language: editorLanguage }, client).then(
67
+ (result) => {
68
+ if (cancelled) return
69
+ const fetched = result.data?.story
70
+ if (!fetched) return
71
+ prevStoryRef.current = fetched
72
+ setResolvedStory(fetched as typeof story)
73
+ },
74
+ )
75
+ return () => {
76
+ cancelled = true
77
+ }
78
+ }, [skip, editorLanguage, initialStory, client])
79
+
37
80
  useEffect(() => {
38
81
  if (skip) return
39
82
  if (prevStoryRef.current === story) return
@@ -53,16 +96,5 @@ export function useStoryblokState<T = SbBlokData>(
53
96
  }, [skip, story, client])
54
97
 
55
98
  const finalStory = skip ? initialStory : resolvedStory
56
-
57
- if (!finalStory) return null
58
- if (finalStory.content?.component === 'page') {
59
- return finalStory as ISbStoryData<T>
60
- }
61
-
62
- if (process.env.NODE_ENV === 'development') {
63
- throw new Error(
64
- `useStoryblokState: expected story content of type 'page' but got '${finalStory.content?.component}' for slug '${finalStory.full_slug}'. Use the upstream @storyblok/react useStoryblokState for non-page content.`,
65
- )
66
- }
67
- return null
99
+ return (finalStory as ISbStoryData<T> | null) ?? null
68
100
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/storyblok-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "10.1.0-canary.21",
5
+ "version": "10.1.0-canary.23",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,13 +12,13 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.21",
16
- "@graphcommerce/graphql": "^10.1.0-canary.21",
17
- "@graphcommerce/image": "^10.1.0-canary.21",
18
- "@graphcommerce/magento-product": "^10.1.0-canary.21",
19
- "@graphcommerce/next-ui": "^10.1.0-canary.21",
20
- "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.21",
21
- "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.21",
15
+ "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.23",
16
+ "@graphcommerce/graphql": "^10.1.0-canary.23",
17
+ "@graphcommerce/image": "^10.1.0-canary.23",
18
+ "@graphcommerce/magento-product": "^10.1.0-canary.23",
19
+ "@graphcommerce/next-ui": "^10.1.0-canary.23",
20
+ "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.23",
21
+ "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.23",
22
22
  "@mui/material": "^7.0.0",
23
23
  "@storyblok/react": "^6.0.0",
24
24
  "react": "^19.2.0",