@byline/admin 6.2.0 → 6.2.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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { act } from 'react'
10
+
11
+ import { defineAdminConfig } from '@byline/core'
12
+ import { adminTranslations } from '@byline/i18n/admin'
13
+ import { I18nProvider } from '@byline/i18n/react'
14
+ import { createRoot, type Root } from 'react-dom/client'
15
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
16
+
17
+ import { BylineFieldServicesProvider } from '../fields/field-services-context'
18
+ import { FormRenderer } from './form-renderer'
19
+
20
+ const globalWithAct = globalThis as any
21
+ globalWithAct.IS_REACT_ACT_ENVIRONMENT = true
22
+
23
+ defineAdminConfig({
24
+ i18n: {
25
+ admin: { defaultLocale: 'en', locales: ['en'] },
26
+ content: { defaultLocale: 'en', locales: ['en', 'fr'] },
27
+ },
28
+ collections: [
29
+ {
30
+ path: 'pages',
31
+ labels: { singular: 'Page', plural: 'Pages' },
32
+ fields: [{ name: 'title', label: 'Title', type: 'text' }],
33
+ },
34
+ ],
35
+ slugifier: (value: string) => value.toLowerCase().trim().replace(/\s+/g, '-'),
36
+ })
37
+
38
+ const fields = [
39
+ { name: 'title', label: 'Title', type: 'text' as const },
40
+ { name: 'seoTitle', label: 'SEO title', type: 'text' as const },
41
+ ]
42
+
43
+ const adminConfig = {
44
+ layout: { main: ['content'] },
45
+ tabSets: [
46
+ {
47
+ name: 'content',
48
+ tabs: [
49
+ { name: 'general', label: 'General', fields: ['title'] },
50
+ { name: 'seo', label: 'SEO', fields: ['seoTitle'] },
51
+ ],
52
+ },
53
+ ],
54
+ }
55
+
56
+ let container: HTMLDivElement
57
+ let root: Root
58
+
59
+ beforeEach(() => {
60
+ container = document.createElement('div')
61
+ document.body.appendChild(container)
62
+ root = createRoot(container)
63
+ })
64
+
65
+ afterEach(() => {
66
+ act(() => {
67
+ root.unmount()
68
+ })
69
+ container.remove()
70
+ })
71
+
72
+ const mount = (locale: string, versionId: string) => {
73
+ act(() => {
74
+ root.render(
75
+ <I18nProvider
76
+ bundle={adminTranslations({ locales: ['en'] })}
77
+ activeLocale="en"
78
+ defaultLocale="en"
79
+ localeDefinitions={[{ code: 'en', nativeName: 'English' }]}
80
+ >
81
+ <BylineFieldServicesProvider
82
+ services={
83
+ {
84
+ getCollectionDocuments: async () => ({ docs: [], total: 0 }),
85
+ uploadField: async () => ({}),
86
+ } as any
87
+ }
88
+ >
89
+ <FormRenderer
90
+ mode="edit"
91
+ fields={fields as any}
92
+ initialLocale={locale}
93
+ initialData={{
94
+ id: 'doc1',
95
+ versionId,
96
+ fields: { title: locale, seoTitle: `${locale} SEO` },
97
+ }}
98
+ adminConfig={adminConfig as any}
99
+ collectionPath="pages"
100
+ onSubmit={() => {}}
101
+ onCancel={() => {}}
102
+ />
103
+ </BylineFieldServicesProvider>
104
+ </I18nProvider>
105
+ )
106
+ })
107
+ }
108
+
109
+ const selectedTabLabel = () =>
110
+ container.querySelector('[role="tab"][aria-selected="true"]')?.textContent
111
+
112
+ describe('FormRenderer tab persistence across keyed remounts', () => {
113
+ // FormProvider is keyed on `${initialLocale}-${versionId}`, so changing either
114
+ // remounts the whole subtree. Tab selection is lifted above that key on
115
+ // purpose; this asserts the wiring, which a controlled-prop test on
116
+ // FormLayout alone cannot reach.
117
+ //
118
+ // Adopted from the reviewer's Checkpoint 2 regression fixture.
119
+ it('retains the selected tab through locale and version remounts', () => {
120
+ mount('en', 'v1')
121
+
122
+ const seoTab = [...container.querySelectorAll('[role="tab"]')].find(
123
+ (t) => t.textContent === 'SEO'
124
+ )
125
+ if (seoTab == null) throw new Error('SEO tab not found')
126
+ act(() => {
127
+ ;(seoTab as HTMLElement).click()
128
+ })
129
+ const first = container.querySelector<HTMLInputElement>('input[name="seoTitle"]')
130
+ expect(first).not.toBeNull()
131
+
132
+ mount('fr', 'v1') // locale change remounts FormProvider via its key
133
+ const french = container.querySelector<HTMLInputElement>('input[name="seoTitle"]')
134
+ expect(french).not.toBeNull()
135
+ expect(french).not.toBe(first)
136
+ expect(french?.value).toBe('fr SEO')
137
+ expect(selectedTabLabel()).toBe('SEO')
138
+
139
+ mount('fr', 'v2') // version change remounts it again
140
+ const versioned = container.querySelector<HTMLInputElement>('input[name="seoTitle"]')
141
+ expect(versioned).not.toBeNull()
142
+ expect(versioned).not.toBe(french)
143
+ expect(selectedTabLabel()).toBe('SEO')
144
+ })
145
+ })
@@ -304,6 +304,24 @@
304
304
  }
305
305
  }
306
306
 
307
+ /* One column at every width. The split layout above becomes a two-column grid
308
+ at >=60rem; a stacked form must not inherit that, or it reserves an empty
309
+ second track and narrows its only content column. */
310
+ .layout-stacked,
311
+ :global(.byline-form-layout-stacked) {
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: var(--spacing-12);
315
+ padding-top: var(--spacing-32);
316
+ }
317
+
318
+ @media (min-width: 60rem) {
319
+ .layout-stacked,
320
+ :global(.byline-form-layout-stacked) {
321
+ gap: var(--spacing-32);
322
+ }
323
+ }
324
+
307
325
  .content,
308
326
  :global(.byline-form-content) {
309
327
  display: flex;