@byline/admin 6.6.0 → 6.7.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.
@@ -14,6 +14,12 @@ export interface AvailableLocalesWidgetProps {
14
14
  * surface supplies it (Slice 6) — in which case every row renders neutral.
15
15
  */
16
16
  availableVersionLocales: readonly string[];
17
+ /**
18
+ * The document's source language. Its row explains that the source stays
19
+ * available while published whether or not it is checked; the checkbox only
20
+ * controls whether it is advertised.
21
+ */
22
+ sourceLocale?: string;
17
23
  }
18
24
  /**
19
25
  * System-managed `availableLocales` widget — the editorial "advertise these
@@ -27,4 +33,4 @@ export interface AvailableLocalesWidgetProps {
27
33
  * Stable override handles: `.byline-form-available-locales`,
28
34
  * `.byline-form-available-locales-list`.
29
35
  */
30
- export declare const AvailableLocalesWidget: ({ disabled: mutationsBlocked, contentLocales, availableVersionLocales, }: AvailableLocalesWidgetProps) => import("react").JSX.Element | null;
36
+ export declare const AvailableLocalesWidget: ({ disabled: mutationsBlocked, contentLocales, availableVersionLocales, sourceLocale, }: AvailableLocalesWidgetProps) => import("react").JSX.Element | null;
@@ -2,12 +2,12 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useMemo } from "react";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
- import { Checkbox, Label } from "@byline/ui/react";
5
+ import { Checkbox, HelpText, Label } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
7
  import { reconcileLocaleState } from "./available-locales-reconcile.js";
8
8
  import available_locales_widget_module from "./available-locales-widget.module.js";
9
9
  import { useFormContext, useSystemAvailableLocales } from "./form-context.js";
10
- const AvailableLocalesWidget = ({ disabled: mutationsBlocked = false, contentLocales, availableVersionLocales })=>{
10
+ const AvailableLocalesWidget = ({ disabled: mutationsBlocked = false, contentLocales, availableVersionLocales, sourceLocale })=>{
11
11
  const { t } = useTranslation('byline-admin');
12
12
  const { setSystemAvailableLocales } = useFormContext();
13
13
  const advertised = useSystemAvailableLocales();
@@ -55,10 +55,15 @@ const AvailableLocalesWidget = ({ disabled: mutationsBlocked = false, contentLoc
55
55
  intent: intent,
56
56
  checked: checked,
57
57
  disabled: mutationsBlocked || disabled,
58
- onCheckedChange: (value)=>toggle(code, true === value)
58
+ onCheckedChange: (value)=>toggle(code, true === value),
59
+ helpText: code === sourceLocale ? t('availableLocalesWidget.sourceRowHint') : void 0
59
60
  }, code);
60
61
  })
61
62
  }),
63
+ /*#__PURE__*/ jsx(HelpText, {
64
+ className: "byline-form-available-locales-hint",
65
+ text: t('availableLocalesWidget.hint')
66
+ }),
62
67
  /*#__PURE__*/ jsx("span", {
63
68
  id: "available-locales-description",
64
69
  className: clsx('byline-form-available-locales-sr-only', available_locales_widget_module["sr-only"]),
@@ -34,8 +34,9 @@ type FieldUploadingListener = (uploading: boolean) => void;
34
34
  * Why the form is dirty, partitioned by write semantics — drives the single
35
35
  * Save button. `content` mints a new version (normal workflow). `direct-write`
36
36
  * is an immediate, non-versioned write of the document-grain system fields
37
- * (path / advertised locales) that does NOT reset workflow status. `both` does
38
- * each through its own write path. See docs/08-internationalization/index.md.
37
+ * (path / advertised locales) that does NOT reset workflow status. `both`
38
+ * saves the two together: one request, one guarded transaction, so both commit
39
+ * or neither does. See docs/08-internationalization/03-content-locales.md.
39
40
  */
40
41
  export type DirtyReason = 'none' | 'content' | 'direct-write' | 'both';
41
42
  export interface DirtyBreakdown {
@@ -10,7 +10,11 @@ export declare const UnsavedChangesModal: ({ onClose }: {
10
10
  * Confirms an immediate, non-versioned write of the document-grain system
11
11
  * fields (path / advertised locales) — which does NOT reset workflow status.
12
12
  * When content is also dirty, the copy reassures that content edits still
13
- * follow the normal revision + publish workflow. See docs/08-internationalization/index.md.
13
+ * follow the normal revision + publish workflow, and, for a locale change,
14
+ * explains that locale and content changes commit together (one guarded
15
+ * transaction, so a failed save applies neither), that an already-published
16
+ * checked translation becomes public once saved, and that a draft-only
17
+ * translation stays withheld until it is published. See docs/08-internationalization/index.md.
14
18
  */
15
19
  export declare const SystemFieldsConfirmModal: ({ contentDirty, pathDirty, availableLocalesDirty, onCancel, onConfirm, }: {
16
20
  contentDirty: boolean;
@@ -81,6 +81,10 @@ const SystemFieldsConfirmModal = ({ contentDirty, pathDirty, availableLocalesDir
81
81
  className: clsx('byline-form-system-fields-content-note', 'm-0 mt-2'),
82
82
  children: t('forms.systemFieldsConfirm.contentNote')
83
83
  }),
84
+ contentDirty && availableLocalesDirty && /*#__PURE__*/ jsx("p", {
85
+ className: clsx('byline-form-system-fields-locales-note', 'm-0 mt-2'),
86
+ children: t('forms.systemFieldsConfirm.localesContentNote')
87
+ }),
84
88
  /*#__PURE__*/ jsx("p", {
85
89
  className: "m-0 mt-2",
86
90
  style: contentDirty ? {
@@ -236,7 +236,8 @@ const FormSidebarWidgets = ({ disabled, mode, initialData, collectionPath, defau
236
236
  advertiseLocales && /*#__PURE__*/ jsx(AvailableLocalesWidget, {
237
237
  disabled: disabled,
238
238
  contentLocales: contentLocales ?? [],
239
- availableVersionLocales: initialData?._availableVersionLocales ?? []
239
+ availableVersionLocales: initialData?._availableVersionLocales ?? [],
240
+ sourceLocale: initialData?.sourceLocale ?? defaultLocale
240
241
  })
241
242
  ]
242
243
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "6.6.0",
5
+ "version": "6.7.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -183,12 +183,12 @@
183
183
  "react-diff-viewer-continued": "^4.4.0",
184
184
  "uuid": "^14.0.2",
185
185
  "zod": "^4.6.5",
186
- "@byline/analytics": "6.6.0",
187
- "@byline/i18n": "6.6.0",
188
- "@byline/core": "6.6.0",
189
- "@byline/auth": "6.6.0",
190
- "@byline/analytics-agent": "6.6.0",
191
- "@byline/ui": "6.6.0"
186
+ "@byline/analytics-agent": "6.7.0",
187
+ "@byline/core": "6.7.0",
188
+ "@byline/i18n": "6.7.0",
189
+ "@byline/analytics": "6.7.0",
190
+ "@byline/ui": "6.7.0",
191
+ "@byline/auth": "6.7.0"
192
192
  },
193
193
  "peerDependencies": {
194
194
  "react": "^19.0.0",
@@ -11,7 +11,7 @@
11
11
  import { useCallback, useMemo } from 'react'
12
12
 
13
13
  import { useTranslation } from '@byline/i18n/react'
14
- import { Checkbox, Label } from '@byline/ui/react'
14
+ import { Checkbox, HelpText, Label } from '@byline/ui/react'
15
15
  import cx from 'clsx'
16
16
 
17
17
  import { reconcileLocaleState } from './available-locales-reconcile'
@@ -36,6 +36,12 @@ export interface AvailableLocalesWidgetProps {
36
36
  * surface supplies it (Slice 6) — in which case every row renders neutral.
37
37
  */
38
38
  availableVersionLocales: readonly string[]
39
+ /**
40
+ * The document's source language. Its row explains that the source stays
41
+ * available while published whether or not it is checked; the checkbox only
42
+ * controls whether it is advertised.
43
+ */
44
+ sourceLocale?: string
39
45
  }
40
46
 
41
47
  /**
@@ -54,6 +60,7 @@ export const AvailableLocalesWidget = ({
54
60
  disabled: mutationsBlocked = false,
55
61
  contentLocales,
56
62
  availableVersionLocales,
63
+ sourceLocale,
57
64
  }: AvailableLocalesWidgetProps) => {
58
65
  const { t } = useTranslation('byline-admin')
59
66
  const { setSystemAvailableLocales } = useFormContext()
@@ -107,10 +114,17 @@ export const AvailableLocalesWidget = ({
107
114
  checked={checked}
108
115
  disabled={mutationsBlocked || disabled}
109
116
  onCheckedChange={(value) => toggle(code, value === true)}
117
+ helpText={
118
+ code === sourceLocale ? t('availableLocalesWidget.sourceRowHint') : undefined
119
+ }
110
120
  />
111
121
  )
112
122
  })}
113
123
  </div>
124
+ <HelpText
125
+ className="byline-form-available-locales-hint"
126
+ text={t('availableLocalesWidget.hint')}
127
+ />
114
128
  <span
115
129
  id="available-locales-description"
116
130
  className={cx('byline-form-available-locales-sr-only', styles['sr-only'])}
@@ -73,8 +73,9 @@ const sameLocaleSet = (a: string[], b: string[]): boolean => {
73
73
  * Why the form is dirty, partitioned by write semantics — drives the single
74
74
  * Save button. `content` mints a new version (normal workflow). `direct-write`
75
75
  * is an immediate, non-versioned write of the document-grain system fields
76
- * (path / advertised locales) that does NOT reset workflow status. `both` does
77
- * each through its own write path. See docs/08-internationalization/index.md.
76
+ * (path / advertised locales) that does NOT reset workflow status. `both`
77
+ * saves the two together: one request, one guarded transaction, so both commit
78
+ * or neither does. See docs/08-internationalization/03-content-locales.md.
78
79
  */
79
80
  export type DirtyReason = 'none' | 'content' | 'direct-write' | 'both'
80
81
 
@@ -54,7 +54,11 @@ export const UnsavedChangesModal = ({ onClose }: { onClose: () => void }) => {
54
54
  * Confirms an immediate, non-versioned write of the document-grain system
55
55
  * fields (path / advertised locales) — which does NOT reset workflow status.
56
56
  * When content is also dirty, the copy reassures that content edits still
57
- * follow the normal revision + publish workflow. See docs/08-internationalization/index.md.
57
+ * follow the normal revision + publish workflow, and, for a locale change,
58
+ * explains that locale and content changes commit together (one guarded
59
+ * transaction, so a failed save applies neither), that an already-published
60
+ * checked translation becomes public once saved, and that a draft-only
61
+ * translation stays withheld until it is published. See docs/08-internationalization/index.md.
58
62
  */
59
63
  export const SystemFieldsConfirmModal = ({
60
64
  contentDirty,
@@ -92,6 +96,13 @@ export const SystemFieldsConfirmModal = ({
92
96
  {t('forms.systemFieldsConfirm.contentNote')}
93
97
  </p>
94
98
  )}
99
+ {/* Locale and content changes commit together. Say what the locale
100
+ change does to public delivery once the save succeeds. */}
101
+ {contentDirty && availableLocalesDirty && (
102
+ <p className={cx('byline-form-system-fields-locales-note', 'm-0 mt-2')}>
103
+ {t('forms.systemFieldsConfirm.localesContentNote')}
104
+ </p>
105
+ )}
95
106
  <p
96
107
  className="m-0 mt-2"
97
108
  style={
@@ -437,6 +437,7 @@ export const FormSidebarWidgets = ({
437
437
  availableVersionLocales={
438
438
  (initialData?._availableVersionLocales as string[] | undefined) ?? []
439
439
  }
440
+ sourceLocale={(initialData?.sourceLocale as string | undefined) ?? defaultLocale}
440
441
  />
441
442
  )}
442
443
  </>
@@ -0,0 +1,180 @@
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
+ /**
10
+ * The advertised-locales widget and the immediate-save confirmation explain
11
+ * what a checkbox does to public delivery. Rendered with the real English
12
+ * admin bundle, so a renamed or deleted key fails here rather than showing a
13
+ * raw key to an editor.
14
+ */
15
+
16
+ import { act, type ReactNode } from 'react'
17
+
18
+ import { adminTranslations } from '@byline/i18n/admin'
19
+ import { I18nProvider } from '@byline/i18n/react'
20
+ import { createRoot, type Root } from 'react-dom/client'
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
22
+
23
+ vi.mock('@byline/ui/react', () => {
24
+ const Modal = ({ children }: { children?: ReactNode }) => <div>{children}</div>
25
+ const Part = ({ children }: { children?: ReactNode }) => <div>{children}</div>
26
+ Modal.Container = Part
27
+ Modal.Header = Part
28
+ Modal.Content = Part
29
+ Modal.Actions = Part
30
+ return {
31
+ Label: ({ label }: { label?: string }) => <span>{label}</span>,
32
+ HelpText: ({ text }: { text: string }) => <p data-testid="group-hint">{text}</p>,
33
+ Checkbox: ({ id, label, helpText }: { id: string; label: string; helpText?: string }) => (
34
+ <div data-testid={id}>
35
+ <span>{label}</span>
36
+ {helpText ? <span data-testid={`${id}-hint`}>{helpText}</span> : null}
37
+ </div>
38
+ ),
39
+ Modal,
40
+ Button: ({ children }: { children?: ReactNode }) => <button type="button">{children}</button>,
41
+ IconButton: ({ children }: { children?: ReactNode }) => (
42
+ <button type="button">{children}</button>
43
+ ),
44
+ CloseIcon: () => null,
45
+ }
46
+ })
47
+
48
+ vi.mock('./form-context', () => ({
49
+ useFormContext: () => ({ setSystemAvailableLocales: vi.fn() }),
50
+ useSystemAvailableLocales: () => ['en'],
51
+ }))
52
+
53
+ import { AvailableLocalesWidget } from './available-locales-widget'
54
+ import { SystemFieldsConfirmModal } from './form-modals'
55
+
56
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
57
+
58
+ const en: Record<string, string> = adminTranslations({ locales: ['en'] }).en?.['byline-admin'] ?? {}
59
+
60
+ describe('locale visibility copy', () => {
61
+ let container: HTMLDivElement
62
+ let root: Root
63
+
64
+ beforeEach(() => {
65
+ container = document.createElement('div')
66
+ document.body.appendChild(container)
67
+ root = createRoot(container)
68
+ })
69
+
70
+ afterEach(() => {
71
+ act(() => {
72
+ root.unmount()
73
+ })
74
+ container.remove()
75
+ })
76
+
77
+ const render = (node: ReactNode) => {
78
+ act(() => {
79
+ root.render(
80
+ <I18nProvider
81
+ bundle={adminTranslations({ locales: ['en'] })}
82
+ activeLocale="en"
83
+ defaultLocale="en"
84
+ localeDefinitions={[{ code: 'en', nativeName: 'English' }]}
85
+ >
86
+ {node}
87
+ </I18nProvider>
88
+ )
89
+ })
90
+ }
91
+
92
+ const text = (testId: string) =>
93
+ container.querySelector(`[data-testid="${testId}"]`)?.textContent ?? null
94
+
95
+ it('explains source availability on the source row only, and delivery for the group', () => {
96
+ render(
97
+ <AvailableLocalesWidget
98
+ contentLocales={[
99
+ { code: 'en', label: 'English' },
100
+ { code: 'es', label: 'Español' },
101
+ ]}
102
+ availableVersionLocales={['en', 'es']}
103
+ sourceLocale="en"
104
+ />
105
+ )
106
+
107
+ expect(text('available-locale-en-hint')).toBe(en['availableLocalesWidget.sourceRowHint'])
108
+ expect(text('available-locale-en-hint')).toContain('available while published')
109
+ expect(text('available-locale-es-hint')).toBeNull()
110
+ expect(text('group-hint')).toBe(en['availableLocalesWidget.hint'])
111
+ expect(text('group-hint')).toContain('unpublished draft')
112
+ expect(container.textContent).toContain('whether or not it is checked')
113
+ })
114
+
115
+ it('moves the source note with the document source language', () => {
116
+ render(
117
+ <AvailableLocalesWidget
118
+ contentLocales={[
119
+ { code: 'en', label: 'English' },
120
+ { code: 'es', label: 'Español' },
121
+ ]}
122
+ availableVersionLocales={['en', 'es']}
123
+ sourceLocale="es"
124
+ />
125
+ )
126
+
127
+ expect(text('available-locale-es-hint')).toBe(en['availableLocalesWidget.sourceRowHint'])
128
+ expect(text('available-locale-en-hint')).toBeNull()
129
+ })
130
+
131
+ it('explains immediate public delivery for a locale-only save', () => {
132
+ render(
133
+ <SystemFieldsConfirmModal
134
+ contentDirty={false}
135
+ pathDirty={false}
136
+ availableLocalesDirty={true}
137
+ onCancel={() => {}}
138
+ onConfirm={() => {}}
139
+ />
140
+ )
141
+
142
+ expect(container.textContent).toContain(en['forms.systemFieldsConfirm.bulletLocales'])
143
+ expect(container.textContent).toContain('appears on the public site straight away')
144
+ expect(container.textContent).not.toContain(en['forms.systemFieldsConfirm.localesContentNote'])
145
+ })
146
+
147
+ it('explains the save order when locale and content changes are saved together', () => {
148
+ render(
149
+ <SystemFieldsConfirmModal
150
+ contentDirty={true}
151
+ pathDirty={false}
152
+ availableLocalesDirty={true}
153
+ onCancel={() => {}}
154
+ onConfirm={() => {}}
155
+ />
156
+ )
157
+
158
+ const note = en['forms.systemFieldsConfirm.localesContentNote']
159
+ expect(container.textContent).toContain(note)
160
+ // The combined save is one guarded commit: a failed save applies neither.
161
+ expect(note).toContain('saved together')
162
+ expect(note).toContain('neither is applied')
163
+ expect(note).toContain('stays hidden until it is published')
164
+ })
165
+
166
+ it('adds no locale note to a combined path and content save', () => {
167
+ render(
168
+ <SystemFieldsConfirmModal
169
+ contentDirty={true}
170
+ pathDirty={true}
171
+ availableLocalesDirty={false}
172
+ onCancel={() => {}}
173
+ onConfirm={() => {}}
174
+ />
175
+ )
176
+
177
+ expect(container.textContent).not.toContain(en['forms.systemFieldsConfirm.localesContentNote'])
178
+ expect(container.textContent).not.toContain(en['forms.systemFieldsConfirm.bulletLocales'])
179
+ })
180
+ })