@byline/admin 2.5.2 → 2.6.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.
Files changed (260) hide show
  1. package/dist/fields/array/array-field.d.ts +14 -0
  2. package/dist/fields/array/array-field.js +177 -0
  3. package/dist/fields/array/array-field.module.js +11 -0
  4. package/dist/fields/array/array-field_module.css +32 -0
  5. package/dist/fields/blocks/blocks-field.d.ts +13 -0
  6. package/dist/fields/blocks/blocks-field.js +245 -0
  7. package/dist/fields/blocks/blocks-field.module.js +26 -0
  8. package/dist/fields/blocks/blocks-field_module.css +107 -0
  9. package/dist/fields/checkbox/checkbox-field.d.ts +16 -0
  10. package/dist/fields/checkbox/checkbox-field.js +28 -0
  11. package/dist/fields/checkbox/checkbox-field.module.js +6 -0
  12. package/dist/fields/checkbox/checkbox-field_module.css +4 -0
  13. package/dist/fields/column-formatter.d.ts +20 -0
  14. package/dist/fields/column-formatter.js +15 -0
  15. package/dist/fields/date-time-formatter.d.ts +16 -0
  16. package/dist/fields/date-time-formatter.js +8 -0
  17. package/dist/fields/datetime/datetime-field.d.ts +16 -0
  18. package/dist/fields/datetime/datetime-field.js +37 -0
  19. package/dist/fields/datetime/datetime-field.module.js +5 -0
  20. package/dist/fields/datetime/datetime-field_module.css +4 -0
  21. package/dist/fields/draggable-context-menu.d.ts +6 -0
  22. package/dist/fields/draggable-context-menu.js +85 -0
  23. package/dist/fields/draggable-context-menu.module.js +15 -0
  24. package/dist/fields/draggable-context-menu_module.css +91 -0
  25. package/dist/fields/field-helpers.d.ts +26 -0
  26. package/dist/fields/field-helpers.js +50 -0
  27. package/dist/fields/field-renderer.d.ts +37 -0
  28. package/dist/fields/field-renderer.js +206 -0
  29. package/dist/fields/field-renderer.module.js +8 -0
  30. package/dist/fields/field-renderer_module.css +11 -0
  31. package/dist/fields/field-services-context.d.ts +16 -0
  32. package/dist/fields/field-services-context.js +13 -0
  33. package/dist/fields/field-services-types.d.ts +63 -0
  34. package/dist/fields/field-services-types.js +1 -0
  35. package/dist/fields/file/file-field.d.ts +19 -0
  36. package/dist/fields/file/file-field.js +225 -0
  37. package/dist/fields/file/file-field.module.js +18 -0
  38. package/dist/fields/file/file-field_module.css +131 -0
  39. package/dist/fields/file/file-upload-field.d.ts +21 -0
  40. package/dist/fields/file/file-upload-field.js +130 -0
  41. package/dist/fields/file/file-upload-field.module.js +15 -0
  42. package/dist/fields/file/file-upload-field_module.css +74 -0
  43. package/dist/fields/group/group-field.d.ts +15 -0
  44. package/dist/fields/group/group-field.js +59 -0
  45. package/dist/fields/group/group-field.module.js +9 -0
  46. package/dist/fields/group/group-field_module.css +27 -0
  47. package/dist/fields/image/image-field.d.ts +19 -0
  48. package/dist/fields/image/image-field.js +241 -0
  49. package/dist/fields/image/image-field.module.js +22 -0
  50. package/dist/fields/image/image-field_module.css +121 -0
  51. package/dist/fields/image/image-upload-field.d.ts +21 -0
  52. package/dist/fields/image/image-upload-field.js +190 -0
  53. package/dist/fields/image/image-upload-field.module.js +19 -0
  54. package/dist/fields/image/image-upload-field_module.css +92 -0
  55. package/dist/fields/local-date-time.d.ts +27 -0
  56. package/dist/fields/local-date-time.js +49 -0
  57. package/dist/fields/locale-badge.d.ts +18 -0
  58. package/dist/fields/locale-badge.js +10 -0
  59. package/dist/fields/locale-badge.module.js +5 -0
  60. package/dist/fields/locale-badge_module.css +27 -0
  61. package/dist/fields/numerical/numerical-field.d.ts +18 -0
  62. package/dist/fields/numerical/numerical-field.js +74 -0
  63. package/dist/fields/relation/relation-display.d.ts +40 -0
  64. package/dist/fields/relation/relation-display.js +58 -0
  65. package/dist/fields/relation/relation-display.module.js +9 -0
  66. package/dist/fields/relation/relation-display_module.css +21 -0
  67. package/dist/fields/relation/relation-field.d.ts +18 -0
  68. package/dist/fields/relation/relation-field.js +138 -0
  69. package/dist/fields/relation/relation-field.module.js +13 -0
  70. package/dist/fields/relation/relation-field_module.css +62 -0
  71. package/dist/fields/relation/relation-picker.d.ts +49 -0
  72. package/dist/fields/relation/relation-picker.js +236 -0
  73. package/dist/fields/relation/relation-picker.module.js +26 -0
  74. package/dist/fields/relation/relation-picker_module.css +124 -0
  75. package/dist/fields/relation/relation-summary.d.ts +31 -0
  76. package/dist/fields/relation/relation-summary.js +50 -0
  77. package/dist/fields/relation/relation-summary.module.js +11 -0
  78. package/dist/fields/relation/relation-summary_module.css +37 -0
  79. package/dist/fields/select/select-field.d.ts +16 -0
  80. package/dist/fields/select/select-field.js +50 -0
  81. package/dist/fields/select/select-field.module.js +5 -0
  82. package/dist/fields/select/select-field_module.css +4 -0
  83. package/dist/fields/sortable-item.d.ts +15 -0
  84. package/dist/fields/sortable-item.js +81 -0
  85. package/dist/fields/sortable-item.module.js +22 -0
  86. package/dist/fields/sortable-item_module.css +124 -0
  87. package/dist/fields/text/text-field.d.ts +20 -0
  88. package/dist/fields/text/text-field.js +104 -0
  89. package/dist/fields/text/text-field.module.js +6 -0
  90. package/dist/fields/text/text-field_module.css +5 -0
  91. package/dist/fields/text-area/text-area-field.d.ts +20 -0
  92. package/dist/fields/text-area/text-area-field.js +105 -0
  93. package/dist/fields/text-area/text-area-field.module.js +6 -0
  94. package/dist/fields/text-area/text-area-field_module.css +5 -0
  95. package/dist/fields/use-field-change-handler.d.ts +23 -0
  96. package/dist/fields/use-field-change-handler.js +52 -0
  97. package/dist/forms/document-actions.d.ts +48 -0
  98. package/dist/forms/document-actions.js +475 -0
  99. package/dist/forms/document-actions.module.js +34 -0
  100. package/dist/forms/document-actions_module.css +118 -0
  101. package/dist/forms/form-context.d.ts +89 -0
  102. package/dist/forms/form-context.js +466 -0
  103. package/dist/forms/form-renderer.d.ts +98 -0
  104. package/dist/forms/form-renderer.js +597 -0
  105. package/dist/forms/form-renderer.module.js +46 -0
  106. package/dist/forms/form-renderer_module.css +245 -0
  107. package/dist/forms/navigation-guard.d.ts +54 -0
  108. package/dist/forms/navigation-guard.js +22 -0
  109. package/dist/forms/path-widget.d.ts +36 -0
  110. package/dist/forms/path-widget.js +116 -0
  111. package/dist/forms/path-widget.module.js +8 -0
  112. package/dist/forms/path-widget_module.css +29 -0
  113. package/dist/forms/upload-executor.d.ts +57 -0
  114. package/dist/forms/upload-executor.js +94 -0
  115. package/dist/lib/translate-validation-error.d.ts +36 -0
  116. package/dist/lib/translate-validation-error.js +11 -0
  117. package/dist/modules/admin-account/commands.d.ts +2 -1
  118. package/dist/modules/admin-account/commands.js +13 -2
  119. package/dist/modules/admin-account/components/change-password.js +45 -36
  120. package/dist/modules/admin-account/components/container.js +185 -134
  121. package/dist/modules/admin-account/components/preferences.d.ts +8 -0
  122. package/dist/modules/admin-account/components/preferences.js +152 -0
  123. package/dist/modules/admin-account/components/preferences.module.js +11 -0
  124. package/dist/modules/admin-account/components/preferences_module.css +41 -0
  125. package/dist/modules/admin-account/components/update.js +50 -31
  126. package/dist/modules/admin-account/index.d.ts +3 -3
  127. package/dist/modules/admin-account/index.js +2 -2
  128. package/dist/modules/admin-account/schemas.d.ts +4 -0
  129. package/dist/modules/admin-account/schemas.js +4 -1
  130. package/dist/modules/admin-account/service.d.ts +1 -0
  131. package/dist/modules/admin-account/service.js +8 -0
  132. package/dist/modules/admin-permissions/components/inspector.js +31 -41
  133. package/dist/modules/admin-roles/components/create.js +43 -26
  134. package/dist/modules/admin-roles/components/permissions.js +26 -35
  135. package/dist/modules/admin-roles/components/update.js +26 -16
  136. package/dist/modules/admin-users/components/create.js +60 -40
  137. package/dist/modules/admin-users/components/roles.js +9 -15
  138. package/dist/modules/admin-users/components/set-password.js +30 -31
  139. package/dist/modules/admin-users/components/update.js +58 -39
  140. package/dist/modules/admin-users/dto.js +1 -0
  141. package/dist/modules/admin-users/repository.d.ts +17 -0
  142. package/dist/modules/admin-users/schemas.d.ts +4 -0
  143. package/dist/modules/admin-users/schemas.js +6 -2
  144. package/dist/modules/auth/components/sign-in-form.js +10 -8
  145. package/dist/presentation/group.d.ts +27 -0
  146. package/dist/presentation/group.js +14 -0
  147. package/dist/presentation/group.module.js +6 -0
  148. package/dist/presentation/group_module.css +19 -0
  149. package/dist/presentation/row.d.ts +25 -0
  150. package/dist/presentation/row.js +8 -0
  151. package/dist/presentation/row.module.js +5 -0
  152. package/dist/presentation/row_module.css +18 -0
  153. package/dist/presentation/tabs.d.ts +25 -0
  154. package/dist/presentation/tabs.js +39 -0
  155. package/dist/presentation/tabs.module.js +10 -0
  156. package/dist/presentation/tabs_module.css +68 -0
  157. package/dist/react.d.ts +66 -0
  158. package/dist/react.js +36 -0
  159. package/dist/services/admin-services-types.d.ts +16 -0
  160. package/dist/widgets/diff-viewer/diff-modal.d.ts +22 -0
  161. package/dist/widgets/diff-viewer/diff-modal.js +149 -0
  162. package/dist/widgets/diff-viewer/diff-modal.module.js +14 -0
  163. package/dist/widgets/diff-viewer/diff-modal_module.css +56 -0
  164. package/dist/widgets/status-badge/status-badge.d.ts +25 -0
  165. package/dist/widgets/status-badge/status-badge.js +37 -0
  166. package/dist/widgets/status-badge/status-badge.module.js +7 -0
  167. package/dist/widgets/status-badge/status-badge_module.css +20 -0
  168. package/package.json +14 -4
  169. package/src/fields/array/array-field.module.css +48 -0
  170. package/src/fields/array/array-field.tsx +267 -0
  171. package/src/fields/blocks/blocks-field.module.css +148 -0
  172. package/src/fields/blocks/blocks-field.tsx +323 -0
  173. package/src/fields/checkbox/checkbox-field.module.css +4 -0
  174. package/src/fields/checkbox/checkbox-field.tsx +54 -0
  175. package/src/fields/column-formatter.tsx +31 -0
  176. package/src/fields/date-time-formatter.tsx +22 -0
  177. package/src/fields/datetime/datetime-field.module.css +13 -0
  178. package/src/fields/datetime/datetime-field.tsx +54 -0
  179. package/src/fields/draggable-context-menu.module.css +127 -0
  180. package/src/fields/draggable-context-menu.tsx +87 -0
  181. package/src/fields/field-helpers.ts +69 -0
  182. package/src/fields/field-renderer.module.css +22 -0
  183. package/src/fields/field-renderer.tsx +288 -0
  184. package/src/fields/field-services-context.tsx +35 -0
  185. package/src/fields/field-services-types.ts +68 -0
  186. package/src/fields/file/file-field.module.css +153 -0
  187. package/src/fields/file/file-field.tsx +286 -0
  188. package/src/fields/file/file-upload-field.module.css +101 -0
  189. package/src/fields/file/file-upload-field.tsx +187 -0
  190. package/src/fields/group/group-field.module.css +43 -0
  191. package/src/fields/group/group-field.tsx +84 -0
  192. package/src/fields/image/image-field.module.css +155 -0
  193. package/src/fields/image/image-field.tsx +306 -0
  194. package/src/fields/image/image-upload-field.module.css +123 -0
  195. package/src/fields/image/image-upload-field.tsx +276 -0
  196. package/src/fields/local-date-time.tsx +88 -0
  197. package/src/fields/locale-badge.module.css +37 -0
  198. package/src/fields/locale-badge.tsx +32 -0
  199. package/src/fields/numerical/numerical-field.tsx +114 -0
  200. package/src/fields/relation/relation-display.module.css +36 -0
  201. package/src/fields/relation/relation-display.tsx +130 -0
  202. package/src/fields/relation/relation-field.module.css +83 -0
  203. package/src/fields/relation/relation-field.tsx +211 -0
  204. package/src/fields/relation/relation-picker.module.css +168 -0
  205. package/src/fields/relation/relation-picker.tsx +326 -0
  206. package/src/fields/relation/relation-summary.module.css +55 -0
  207. package/src/fields/relation/relation-summary.tsx +123 -0
  208. package/src/fields/select/select-field.module.css +13 -0
  209. package/src/fields/select/select-field.tsx +61 -0
  210. package/src/fields/sortable-item.module.css +167 -0
  211. package/src/fields/sortable-item.tsx +106 -0
  212. package/src/fields/text/text-field.module.css +13 -0
  213. package/src/fields/text/text-field.tsx +146 -0
  214. package/src/fields/text-area/text-area-field.module.css +13 -0
  215. package/src/fields/text-area/text-area-field.tsx +147 -0
  216. package/src/fields/use-field-change-handler.ts +112 -0
  217. package/src/forms/document-actions.module.css +160 -0
  218. package/src/forms/document-actions.tsx +482 -0
  219. package/src/forms/form-context.tsx +704 -0
  220. package/src/forms/form-renderer.module.css +321 -0
  221. package/src/forms/form-renderer.tsx +891 -0
  222. package/src/forms/navigation-guard.tsx +98 -0
  223. package/src/forms/path-widget.module.css +41 -0
  224. package/src/forms/path-widget.test.tsx +217 -0
  225. package/src/forms/path-widget.tsx +183 -0
  226. package/src/forms/upload-executor.ts +192 -0
  227. package/src/lib/translate-validation-error.ts +56 -0
  228. package/src/modules/admin-account/commands.ts +13 -0
  229. package/src/modules/admin-account/components/change-password.tsx +46 -31
  230. package/src/modules/admin-account/components/container.tsx +83 -38
  231. package/src/modules/admin-account/components/preferences.module.css +60 -0
  232. package/src/modules/admin-account/components/preferences.tsx +203 -0
  233. package/src/modules/admin-account/components/update.tsx +53 -27
  234. package/src/modules/admin-account/index.ts +3 -0
  235. package/src/modules/admin-account/schemas.ts +13 -0
  236. package/src/modules/admin-account/service.ts +12 -0
  237. package/src/modules/admin-permissions/components/inspector.tsx +22 -14
  238. package/src/modules/admin-roles/components/create.tsx +51 -23
  239. package/src/modules/admin-roles/components/permissions.tsx +25 -21
  240. package/src/modules/admin-roles/components/update.tsx +37 -19
  241. package/src/modules/admin-users/components/create.tsx +63 -34
  242. package/src/modules/admin-users/components/roles.tsx +9 -8
  243. package/src/modules/admin-users/components/set-password.tsx +34 -28
  244. package/src/modules/admin-users/components/update.tsx +58 -36
  245. package/src/modules/admin-users/dto.ts +1 -0
  246. package/src/modules/admin-users/repository.ts +17 -0
  247. package/src/modules/admin-users/schemas.ts +12 -0
  248. package/src/modules/auth/components/sign-in-form.tsx +14 -8
  249. package/src/presentation/group.module.css +41 -0
  250. package/src/presentation/group.tsx +40 -0
  251. package/src/presentation/row.module.css +32 -0
  252. package/src/presentation/row.tsx +33 -0
  253. package/src/presentation/tabs.module.css +107 -0
  254. package/src/presentation/tabs.tsx +84 -0
  255. package/src/react.ts +84 -0
  256. package/src/services/admin-services-types.ts +18 -0
  257. package/src/widgets/diff-viewer/diff-modal.module.css +79 -0
  258. package/src/widgets/diff-viewer/diff-modal.tsx +186 -0
  259. package/src/widgets/status-badge/status-badge.module.css +31 -0
  260. package/src/widgets/status-badge/status-badge.tsx +71 -0
@@ -27,15 +27,18 @@
27
27
  import type React from 'react'
28
28
  import { useState } from 'react'
29
29
 
30
- import { Button, CloseIcon, Drawer, EditIcon, IconButton, LocalDateTime } from '@byline/ui/react'
30
+ import { LocalDateTime } from '@byline/admin/react'
31
+ import { useTranslation } from '@byline/i18n/react'
32
+ import { Button, CloseIcon, Drawer, EditIcon, IconButton } from '@byline/ui/react'
31
33
  import cx from 'classnames'
32
34
 
33
35
  import { ChangeAccountPassword } from './change-password.js'
34
36
  import styles from './container.module.css'
37
+ import { Preferences } from './preferences.js'
35
38
  import { UpdateAccount } from './update.js'
36
39
  import type { AccountResponse } from '../index.js'
37
40
 
38
- type ComponentKey = 'update' | 'change_password' | 'empty'
41
+ type ComponentKey = 'update' | 'change_password' | 'preferences' | 'empty'
39
42
 
40
43
  interface PanelProps {
41
44
  account: AccountResponse
@@ -43,20 +46,22 @@ interface PanelProps {
43
46
  onSuccess?: (account: AccountResponse) => void
44
47
  }
45
48
 
46
- const panels: Record<ComponentKey, { title: string; component: React.ComponentType<PanelProps> }> =
47
- {
48
- update: { title: 'Profile', component: UpdateAccount },
49
- change_password: { title: 'Change Password', component: ChangeAccountPassword },
50
- empty: { title: '', component: () => null },
51
- }
49
+ const panelComponents: Record<ComponentKey, React.ComponentType<PanelProps>> = {
50
+ update: UpdateAccount,
51
+ change_password: ChangeAccountPassword,
52
+ preferences: Preferences,
53
+ empty: () => null,
54
+ }
52
55
 
53
56
  function ContainerSection({
54
57
  title,
55
58
  onEdit,
59
+ editAriaLabel,
56
60
  children,
57
61
  }: {
58
62
  title: string
59
63
  onEdit?: () => void
64
+ editAriaLabel?: string
60
65
  children: React.ReactNode
61
66
  }) {
62
67
  return (
@@ -64,7 +69,7 @@ function ContainerSection({
64
69
  <div className={cx('byline-account-section-head', styles['section-head'])}>
65
70
  <h2>{title}</h2>
66
71
  {onEdit ? (
67
- <IconButton variant="text" onClick={onEdit} aria-label={`Edit ${title}`}>
72
+ <IconButton variant="text" onClick={onEdit} aria-label={editAriaLabel ?? title}>
68
73
  <EditIcon width="20px" height="20px" />
69
74
  </IconButton>
70
75
  ) : null}
@@ -79,6 +84,7 @@ interface AccountSelfContainerProps {
79
84
  }
80
85
 
81
86
  export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
87
+ const { t } = useTranslation('byline-admin')
82
88
  const [currentAccount, setCurrentAccount] = useState<AccountResponse>(account)
83
89
  const [current, setCurrent] = useState<ComponentKey>('empty')
84
90
  const [isDrawerOpen, setIsDrawerOpen] = useState(false)
@@ -95,82 +101,120 @@ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
95
101
  setCurrentAccount(updated)
96
102
  }
97
103
 
98
- const Panel = panels[current].component
104
+ const Panel = panelComponents[current]
105
+ const panelTitles: Record<ComponentKey, string> = {
106
+ update: t('account.sections.profile'),
107
+ change_password: t('account.sections.password'),
108
+ preferences: t('account.sections.preferences'),
109
+ empty: '',
110
+ }
111
+ const editAriaFor = (section: string) => t('account.editAriaLabel', { section })
99
112
 
100
113
  return (
101
114
  <>
102
115
  <div className={cx('byline-account-grid', styles.grid)}>
103
116
  <div className={cx('byline-account-column', styles.column)}>
104
- <ContainerSection title="Profile" onEdit={openDrawer('update')}>
117
+ <ContainerSection
118
+ title={t('account.sections.profile')}
119
+ onEdit={openDrawer('update')}
120
+ editAriaLabel={editAriaFor(t('account.sections.profile'))}
121
+ >
105
122
  <p className={cx('byline-account-line', styles.line)}>
106
- <span className="muted">Email:</span> {currentAccount.email}
123
+ <span className="muted">{t('account.profile.emailColon')}</span>{' '}
124
+ {currentAccount.email}
107
125
  </p>
108
126
  <p className={cx('byline-account-line', styles.line)}>
109
- <span className="muted">Given name:</span>{' '}
127
+ <span className="muted">{t('account.profile.givenName')}</span>{' '}
110
128
  {currentAccount.given_name ?? (
111
129
  <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
112
- Not set
130
+ {t('common.notSet')}
113
131
  </span>
114
132
  )}
115
133
  </p>
116
134
  <p className={cx('byline-account-line', styles.line)}>
117
- <span className="muted">Family name:</span>{' '}
135
+ <span className="muted">{t('account.profile.familyName')}</span>{' '}
118
136
  {currentAccount.family_name ?? (
119
137
  <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
120
- Not set
138
+ {t('common.notSet')}
121
139
  </span>
122
140
  )}
123
141
  </p>
124
142
  <p className={cx('byline-account-cta-line', styles['cta-line'])}>
125
- <span className="muted">Username:</span>{' '}
143
+ <span className="muted">{t('account.profile.username')}</span>{' '}
126
144
  {currentAccount.username ?? (
127
145
  <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
128
- Not set
146
+ {t('common.notSet')}
129
147
  </span>
130
148
  )}
131
149
  </p>
132
150
  <Button size="sm" onClick={openDrawer('update')}>
133
- Edit Profile
151
+ {t('account.profile.editButton')}
134
152
  </Button>
135
153
  <div className={cx('muted', 'byline-account-meta', styles.meta)}>
136
154
  <p>
137
- <span className="font-bold">Created:&nbsp;</span>
155
+ <span className="font-bold">{t('account.profile.created')}&nbsp;</span>
138
156
  <LocalDateTime value={currentAccount.created_at} />
139
157
  </p>
140
158
  <p>
141
- <span className="font-bold">Updated:&nbsp;</span>
159
+ <span className="font-bold">{t('account.profile.updated')}&nbsp;</span>
142
160
  <LocalDateTime value={currentAccount.updated_at} />
143
161
  </p>
144
162
  <p className={cx('byline-account-line', styles.line)}>
145
- <span className="font-bold">Last login:&nbsp;</span>
146
- <LocalDateTime value={currentAccount.last_login} fallback="Never" />
163
+ <span className="font-bold">{t('account.profile.lastLogin')}&nbsp;</span>
164
+ <LocalDateTime value={currentAccount.last_login} fallback={t('common.never')} />
147
165
  </p>
148
166
  </div>
149
167
  </ContainerSection>
168
+
169
+ <ContainerSection
170
+ title={t('account.sections.preferences')}
171
+ onEdit={openDrawer('preferences')}
172
+ editAriaLabel={editAriaFor(t('account.sections.preferences'))}
173
+ >
174
+ <p className={cx('byline-account-line', styles.line)}>
175
+ <span className="muted">{t('account.preferences.interfaceLanguage')}</span>{' '}
176
+ {currentAccount.preferred_locale ?? (
177
+ <span className={cx('muted', 'byline-account-not-set', styles['not-set'])}>
178
+ {t('language.useBrowserDefault')}
179
+ </span>
180
+ )}
181
+ </p>
182
+ <p className={cx('byline-account-cta-line', styles['cta-line'])}>
183
+ <Button size="sm" onClick={openDrawer('preferences')}>
184
+ {t('account.preferences.editButton')}
185
+ </Button>
186
+ </p>
187
+ <p className={cx('muted', 'byline-account-status-help', styles['status-help'])}>
188
+ {t('account.preferences.help')}
189
+ </p>
190
+ </ContainerSection>
150
191
  </div>
151
192
 
152
193
  <div className={cx('byline-account-column', styles.column)}>
153
- <ContainerSection title="Password" onEdit={openDrawer('change_password')}>
194
+ <ContainerSection
195
+ title={t('account.sections.password')}
196
+ onEdit={openDrawer('change_password')}
197
+ editAriaLabel={editAriaFor(t('account.sections.password'))}
198
+ >
154
199
  <p className={cx('byline-account-cta-line', styles['cta-line'])}>
155
- Change the password used to sign in to the admin. You'll need to enter your current
156
- password to confirm the change.
200
+ {t('account.password.intro')}
157
201
  </p>
158
202
  <Button size="sm" onClick={openDrawer('change_password')}>
159
- Change Password
203
+ {t('account.password.editButton')}
160
204
  </Button>
161
205
  </ContainerSection>
162
206
 
163
- <ContainerSection title="Account Status">
207
+ <ContainerSection title={t('account.sections.status')}>
164
208
  <p className={cx('byline-account-line', styles.line)}>
165
- <span className="muted">Super admin:</span>{' '}
166
- {currentAccount.is_super_admin ? 'Yes' : 'No'}
209
+ <span className="muted">{t('account.status.superAdmin')}</span>{' '}
210
+ {currentAccount.is_super_admin ? t('common.boolean.yes') : t('common.boolean.no')}
167
211
  </p>
168
212
  <p className={cx('byline-account-line', styles.line)}>
169
- <span className="muted">Email verified:</span>{' '}
170
- {currentAccount.is_email_verified ? 'Yes' : 'No'}
213
+ <span className="muted">{t('account.status.emailVerified')}</span>{' '}
214
+ {currentAccount.is_email_verified ? t('common.boolean.yes') : t('common.boolean.no')}
171
215
  </p>
172
216
  <p className={cx('byline-account-line', styles.line)}>
173
- <span className="muted">Status:</span>{' '}
217
+ <span className="muted">{t('account.status.status')}</span>{' '}
174
218
  <span
175
219
  className={
176
220
  currentAccount.is_enabled
@@ -178,12 +222,13 @@ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
178
222
  : cx('byline-account-status-off', styles['status-off'])
179
223
  }
180
224
  >
181
- {currentAccount.is_enabled ? 'Enabled' : 'Disabled'}
225
+ {currentAccount.is_enabled
226
+ ? t('account.status.enabled')
227
+ : t('account.status.disabled')}
182
228
  </span>
183
229
  </p>
184
230
  <p className={cx('muted', 'byline-account-status-help', styles['status-help'])}>
185
- These flags are managed by an admin with the appropriate permissions and are not
186
- self-editable.
231
+ {t('account.status.help')}
187
232
  </p>
188
233
  </ContainerSection>
189
234
  </div>
@@ -210,12 +255,12 @@ export function AccountSelfContainer({ account }: AccountSelfContainerProps) {
210
255
  >
211
256
  no action
212
257
  </button>
213
- <IconButton aria-label="Close" size="sm" onClick={closeDrawer}>
258
+ <IconButton aria-label={t('common.actions.close')} size="sm" onClick={closeDrawer}>
214
259
  <CloseIcon width="14px" height="14px" svgClassName="white-icon stroke-white" />
215
260
  </IconButton>
216
261
  </Drawer.TopActions>
217
262
  <Drawer.Header>
218
- <h2>{panels[current].title}</h2>
263
+ <h2>{panelTitles[current]}</h2>
219
264
  </Drawer.Header>
220
265
  <Drawer.Content>
221
266
  <div className={cx('byline-account-drawer-scroll', styles['drawer-scroll'])}>
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Preferences — self-service account-preferences form (drawer body).
3
+ *
4
+ * Override handles:
5
+ * .byline-account-preferences-wrap — outer container
6
+ * .byline-account-preferences-form — vertical-stack form element
7
+ * .byline-account-preferences-row — label + control row (per field)
8
+ * .byline-account-preferences-label — text label above the control
9
+ * .byline-account-preferences-help — muted help-text line
10
+ * .byline-account-preferences-actions — Cancel/Save row
11
+ * .byline-account-preferences-action — buttons in the actions row
12
+ */
13
+
14
+ .wrap,
15
+ :global(.byline-account-preferences-wrap) {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--spacing-8);
19
+ padding: var(--spacing-4);
20
+ margin-top: var(--spacing-4);
21
+ }
22
+
23
+ .form,
24
+ :global(.byline-account-preferences-form) {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--spacing-16);
28
+ padding-top: var(--spacing-8);
29
+ }
30
+
31
+ .row,
32
+ :global(.byline-account-preferences-row) {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: var(--spacing-4);
36
+ }
37
+
38
+ .label,
39
+ :global(.byline-account-preferences-label) {
40
+ font-weight: 500;
41
+ }
42
+
43
+ .help,
44
+ :global(.byline-account-preferences-help) {
45
+ font-size: 0.875rem;
46
+ }
47
+
48
+ .actions,
49
+ :global(.byline-account-preferences-actions) {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: flex-end;
53
+ gap: var(--spacing-8);
54
+ margin-top: var(--spacing-16);
55
+ }
56
+
57
+ .action,
58
+ :global(.byline-account-preferences-action) {
59
+ min-width: 4rem;
60
+ }
@@ -0,0 +1,203 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * This Source Code is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ *
8
+ * Copyright (c) Infonomic Company Limited
9
+ */
10
+
11
+ /**
12
+ * Self-service Preferences form.
13
+ *
14
+ * General-purpose container for the signed-in admin's UI preferences.
15
+ * Today the only setting is the interface locale; future preferences
16
+ * (theme, default landing page, density, etc.) land here as additional
17
+ * `<form.Field>` blocks rather than spinning up a new drawer per
18
+ * setting.
19
+ *
20
+ * Locale write surface mirrors the chrome-bar `<LanguageMenu>` — both
21
+ * route through `setInterfaceLocale` (the service) →
22
+ * `setInterfaceLocaleFn` (the host server fn) → the shared
23
+ * cookie + `admin_users.preferred_locale` writes. The dropdown carries
24
+ * an explicit "Use browser default" entry that maps to `null`
25
+ * server-side, clearing the column and re-engaging the detection
26
+ * cascade (cookie → Accept-Language → defaultLocale). The sentinel
27
+ * value `__auto__` is the wire form for that option; the service
28
+ * signature uses `string | null`.
29
+ *
30
+ * On save, the freshened `AccountResponse` returned by the server fn
31
+ * is lifted into the parent container so the read-only Preferences
32
+ * section re-renders without a route reload. Mirrors how
33
+ * `UpdateAccount` threads its result through `onSuccess`.
34
+ */
35
+
36
+ import { useMemo, useState } from 'react'
37
+ import { revalidateLogic, useForm } from '@tanstack/react-form-start'
38
+
39
+ import { getClientConfig } from '@byline/core'
40
+ import { Alert, Button, LoaderEllipsis, Select } from '@byline/ui/react'
41
+ import cx from 'classnames'
42
+ import { z } from 'zod'
43
+
44
+ import { useBylineAdminServices } from '../../../services/admin-services-context.js'
45
+ import styles from './preferences.module.css'
46
+ import type { AccountResponse } from '../index.js'
47
+
48
+ /**
49
+ * Sentinel for the "Use browser default" entry in the locale select.
50
+ * The widget's value type is `string`; we map this sentinel to `null`
51
+ * at submit time. Prefixed with double-underscore to make collision
52
+ * with a real BCP 47 tag structurally impossible (no real locale code
53
+ * contains underscores).
54
+ */
55
+ const AUTO_VALUE = '__auto__'
56
+
57
+ const preferencesSchema = z.object({
58
+ // Validation against the configured locale set happens server-side
59
+ // (it's the single source of truth). The form schema just enforces
60
+ // non-emptiness — `AUTO_VALUE` is a valid selection.
61
+ locale: z.string().min(1, { message: 'Select a language' }),
62
+ })
63
+
64
+ type PreferencesValues = z.infer<typeof preferencesSchema>
65
+
66
+ function defaultsFrom(account: AccountResponse): PreferencesValues {
67
+ return { locale: account.preferred_locale ?? AUTO_VALUE }
68
+ }
69
+
70
+ interface PreferencesProps {
71
+ account: AccountResponse
72
+ onClose?: () => void
73
+ onSuccess?: (account: AccountResponse) => void
74
+ }
75
+
76
+ export function Preferences({ account, onClose, onSuccess }: PreferencesProps) {
77
+ const { setInterfaceLocale } = useBylineAdminServices()
78
+ const [formError, setFormError] = useState<string | null>(null)
79
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
80
+
81
+ // Build the dropdown's items from the host's configured interface
82
+ // locales. `localeDefinitions` carries host-authored display names
83
+ // ("Français" vs CLDR's lowercase "français"); when it's not set,
84
+ // fall back to the raw code so the form still functions.
85
+ const localeItems = useMemo(() => {
86
+ const { i18n } = getClientConfig()
87
+ const definitionByCode = new Map(
88
+ (i18n.interface.localeDefinitions ?? []).map((d) => [d.code, d.nativeName])
89
+ )
90
+ const items = i18n.interface.locales.map((code) => ({
91
+ value: code,
92
+ label: definitionByCode.get(code) ?? code,
93
+ }))
94
+ return [{ value: AUTO_VALUE, label: 'Use browser default' }, ...items]
95
+ }, [])
96
+
97
+ const form = useForm({
98
+ defaultValues: defaultsFrom(account),
99
+ validationLogic: revalidateLogic({
100
+ mode: 'blur',
101
+ modeAfterSubmission: 'change',
102
+ }),
103
+ validators: {
104
+ onDynamic: preferencesSchema,
105
+ },
106
+ onSubmit: async ({ value }) => {
107
+ setFormError(null)
108
+ setSuccessMessage(null)
109
+ const nextLocale = value.locale === AUTO_VALUE ? null : value.locale
110
+ if (nextLocale === (account.preferred_locale ?? null)) {
111
+ setSuccessMessage('No changes to save.')
112
+ return
113
+ }
114
+ try {
115
+ const result = await setInterfaceLocale({ data: { locale: nextLocale } })
116
+ // Pre-auth path returns `account: null`; the form is only
117
+ // reachable behind an admin session, so `account` should be
118
+ // populated. Treat absence as a defensive no-op.
119
+ if (result.account != null) {
120
+ setSuccessMessage('Saved.')
121
+ onSuccess?.(result.account)
122
+ } else {
123
+ setSuccessMessage('Saved.')
124
+ }
125
+ } catch {
126
+ setFormError('Could not save changes. Please try again.')
127
+ }
128
+ },
129
+ })
130
+
131
+ return (
132
+ <div className={cx('byline-account-preferences-wrap', styles.wrap)}>
133
+ <form
134
+ noValidate
135
+ onSubmit={(event) => {
136
+ event.preventDefault()
137
+ event.stopPropagation()
138
+ void form.handleSubmit()
139
+ }}
140
+ className={cx('byline-account-preferences-form', styles.form)}
141
+ >
142
+ {formError ? <Alert intent="danger">{formError}</Alert> : null}
143
+ {successMessage ? <Alert intent="success">{successMessage}</Alert> : null}
144
+
145
+ <form.Field name="locale">
146
+ {(field) => (
147
+ <div className={cx('byline-account-preferences-row', styles.row)}>
148
+ <label
149
+ htmlFor="preferences-locale-select"
150
+ className={cx('byline-account-preferences-label', styles.label)}
151
+ >
152
+ Interface language
153
+ </label>
154
+ <Select<string>
155
+ id="preferences-locale-select"
156
+ size="sm"
157
+ ariaLabel="Interface language"
158
+ value={field.state.value}
159
+ items={localeItems}
160
+ onValueChange={(value) => {
161
+ if (value != null) field.handleChange(value)
162
+ }}
163
+ />
164
+ <p className={cx('muted', 'byline-account-preferences-help', styles.help)}>
165
+ Choose "Use browser default" to let your browser's language settings decide.
166
+ </p>
167
+ </div>
168
+ )}
169
+ </form.Field>
170
+
171
+ <div className={cx('byline-account-preferences-actions', styles.actions)}>
172
+ <Button
173
+ type="button"
174
+ intent="secondary"
175
+ size="sm"
176
+ onClick={onClose}
177
+ className={cx('byline-account-preferences-action', styles.action)}
178
+ >
179
+ {successMessage ? 'Close' : 'Cancel'}
180
+ </Button>
181
+ <form.Subscribe
182
+ selector={(state) => ({
183
+ canSubmit: state.canSubmit,
184
+ isSubmitting: state.isSubmitting,
185
+ })}
186
+ >
187
+ {({ canSubmit, isSubmitting }) => (
188
+ <Button
189
+ size="sm"
190
+ intent="primary"
191
+ type="submit"
192
+ disabled={!canSubmit || isSubmitting}
193
+ className={cx('byline-account-preferences-action', styles.action)}
194
+ >
195
+ {isSubmitting === true ? <LoaderEllipsis size={42} /> : 'Save'}
196
+ </Button>
197
+ )}
198
+ </form.Subscribe>
199
+ </div>
200
+ </form>
201
+ </div>
202
+ )
203
+ }