@dashadmin/dash-admin 1.3.44 → 1.3.46

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 (53) hide show
  1. package/dist/components/i18n/BridgedLocalesMenuButton.js +1 -2
  2. package/dist/components/menu/DarkToggleMode.js +1 -1
  3. package/dist/components/serviceAccount/ApiKeyRevealDialog.js +110 -0
  4. package/dist/components/serviceAccount/ServiceAccountStatusSwitch.js +94 -0
  5. package/dist/components/subscription/PlanPricesSettings.js +4 -2
  6. package/dist/components/tenancy/GlobalTenantWrapper.js +64 -0
  7. package/dist/components/tenancy/TenantSwitcher.js +389 -0
  8. package/dist/contexts/com/components/EchoNotificationsCenter.js +40 -0
  9. package/dist/default-theme/menu/AppMenuComponents/expanded/SidebarItem.js +1 -0
  10. package/dist/index.js +60 -40
  11. package/dist/pages/Profile.js +32 -38
  12. package/dist/providers/i18n/en.js +108 -0
  13. package/dist/providers/i18n/es.js +108 -0
  14. package/dist/providers/i18n/languages.js +3 -1
  15. package/dist/providers/i18n/mergeTranslations.js +18 -0
  16. package/dist/resources/User/components/AvatarUploadField.js +61 -0
  17. package/dist/resources/User/components/PasswordFields.js +67 -0
  18. package/dist/resources/User/getProfileMenu.js +36 -0
  19. package/dist/resources/User/profileResource.js +33 -0
  20. package/dist/resources/User/userLayout.js +58 -0
  21. package/dist/resources/User/userResource.js +90 -0
  22. package/dist/resources/User/userSchema.js +206 -0
  23. package/dist/resources/User/usersFilters.js +22 -0
  24. package/dist/systemResources.js +30 -19
  25. package/dist/templates/ResourceTemplateCreate.js +5 -2
  26. package/package.json +4 -1
  27. package/src/components/i18n/BridgedLocalesMenuButton.tsx +1 -2
  28. package/src/components/menu/DarkToggleMode.tsx +1 -1
  29. package/src/components/serviceAccount/ApiKeyRevealDialog.tsx +129 -0
  30. package/src/components/serviceAccount/ServiceAccountStatusSwitch.tsx +99 -0
  31. package/src/components/subscription/PlanPricesSettings.tsx +2 -2
  32. package/src/components/tenancy/GlobalTenantWrapper.tsx +84 -0
  33. package/src/components/tenancy/TenantSwitcher.tsx +485 -0
  34. package/src/contexts/com/components/EchoNotificationsCenter.tsx +65 -0
  35. package/src/default-theme/menu/AppMaterialMenu.tsx +1 -1
  36. package/src/default-theme/menu/AppMenuComponents/expanded/SidebarItem.tsx +1 -0
  37. package/src/default-theme/menu/SidebarActions.tsx +1 -1
  38. package/src/index.tsx +15 -0
  39. package/src/pages/Profile.tsx +69 -107
  40. package/src/providers/i18n/en.ts +113 -0
  41. package/src/providers/i18n/es.ts +113 -0
  42. package/src/providers/i18n/languages.tsx +3 -0
  43. package/src/providers/i18n/mergeTranslations.ts +53 -0
  44. package/src/resources/User/components/AvatarUploadField.tsx +57 -0
  45. package/src/resources/User/components/PasswordFields.tsx +73 -0
  46. package/src/resources/User/getProfileMenu.ts +57 -0
  47. package/src/resources/User/profileResource.tsx +51 -0
  48. package/src/resources/User/userLayout.tsx +62 -0
  49. package/src/resources/User/userResource.tsx +111 -0
  50. package/src/resources/User/userSchema.tsx +213 -0
  51. package/src/resources/User/usersFilters.tsx +21 -0
  52. package/src/systemResources.tsx +31 -20
  53. package/src/templates/ResourceTemplateCreate.tsx +17 -2
@@ -0,0 +1,65 @@
1
+ /**
2
+ * EchoNotificationsCenter
3
+ *
4
+ * Wires LaravelEchoContext's real-time "urgency-alert" events into
5
+ * dash-components' generic NotificationsCenter (bell button + menu + toast
6
+ * stack + draggable dialogs). This replaces what used to be an ~500-line
7
+ * NotificationsCenter.tsx hand-copied into every domain app — kitchntabs-app,
8
+ * kitchntabs-system, kitchntabs-web, vanexa-app, vanexa-system, vanexa-web all
9
+ * had byte-identical copies of this exact wiring. Each of those apps now just
10
+ * renders <EchoNotificationsCenter /> instead of maintaining its own copy.
11
+ *
12
+ * kitchntabs-mall intentionally keeps its own NotificationsCenter — it also
13
+ * handles mall-specific "mall_order_status_update" events with event
14
+ * deduplication and translated messages, which is real business logic, not
15
+ * duplicated boilerplate. Folding it into this shared component would need a
16
+ * proper event-transformer API, not a copy/paste consolidation.
17
+ */
18
+ import React, { useContext, useEffect } from 'react';
19
+ import { NotificationsCenter, useNotificationsCenter, type NotificationsCenterLabels } from 'dash-components';
20
+ import LaravelEchoContext, { ILaravelEchoContext } from '../LaravelEchoContext';
21
+
22
+ // Matches the hardcoded Spanish strings every duplicated app copy used to
23
+ // render (dash-components' defaults are English).
24
+ const spanishLabels: NotificationsCenterLabels = {
25
+ title: 'Notificaciones',
26
+ markAll: 'Marcar todas',
27
+ noNotifications: 'No hay notificaciones',
28
+ close: 'Cerrar',
29
+ acknowledge: 'Entendido',
30
+ customer: 'Cliente:',
31
+ store: 'Tienda:',
32
+ received: 'Recibido:',
33
+ now: 'Ahora',
34
+ minutesAgo: 'm',
35
+ hoursAgo: 'h',
36
+ daysAgo: 'd',
37
+ };
38
+
39
+ const EchoNotificationsCenter: React.FC = () => {
40
+ const { lastEvent } = useContext<ILaravelEchoContext>(LaravelEchoContext);
41
+ const hook = useNotificationsCenter({ labels: spanishLabels });
42
+
43
+ useEffect(() => {
44
+ if (lastEvent && lastEvent.type === 'urgency-alert') {
45
+ const notification: any = lastEvent?.notificationPayload;
46
+ hook.addNotification({
47
+ title: notification?.title || 'Alerta',
48
+ message: notification?.message || '',
49
+ customer: notification?.customer_info?.name
50
+ ? `${notification.customer_info.name} - Mesa ${notification.customer_info.table}`
51
+ : '',
52
+ store: notification?.store_info?.name || '',
53
+ data: notification,
54
+ }, { showDialog: true, showToast: true, toastSeverity: 'warning' });
55
+ }
56
+ // Only re-run when a new event arrives, matching the original per-app
57
+ // implementations — addNotification's own setState calls are all
58
+ // functional updates, so a stale hook reference here is harmless.
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [lastEvent]);
61
+
62
+ return <NotificationsCenter notificationsHook={hook} />;
63
+ };
64
+
65
+ export default EchoNotificationsCenter;
@@ -82,7 +82,7 @@ const GenerateItems: React.FC<{ items: IMenuItem[]; navExpanded: boolean, navSiz
82
82
  />
83
83
  ) : (
84
84
 
85
- <SidebarItem level={level} navExpanded={navExpanded} navSize={navSize} item={item} key={index} />
85
+ <SidebarItem level={level} navExpanded={navExpanded} navSize={navSize} item={item} key={index} />
86
86
  );
87
87
  })}
88
88
  </List>
@@ -63,6 +63,7 @@ const SidebarItem = ({
63
63
 
64
64
  return (
65
65
  <ListItemButton
66
+ className={className}
66
67
  selected={isCurrent}
67
68
  onClick={(e) => {
68
69
 
@@ -71,7 +71,7 @@ const SidebarActions: React.FC<SidebarActionsProps> = ({
71
71
  <HeaderToolBar />
72
72
  ) : (
73
73
  // Default mode: Render all actions
74
- <>
74
+ <>
75
75
  {HeaderToolBar && <HeaderToolBar />}
76
76
  {authContext?.authenticated ? <BridgedLocalesMenuButton /> : <LangSwitcher />}
77
77
  <DarkToggleMode />
package/src/index.tsx CHANGED
@@ -32,6 +32,7 @@ export { default as CacheInvalidatorListenerComponent } from './utils/cache/Cach
32
32
  export { AuthContext } from './contexts/auth';
33
33
  export { NotificationComponent } from './contexts/com/components/NotificationRenderer'
34
34
  export { NotificationWrapper } from './contexts/com/components/NotificationsWidget'
35
+ export { default as EchoNotificationsCenter } from './contexts/com/components/EchoNotificationsCenter'
35
36
 
36
37
  export { default as useLaravelEcho } from './contexts/com/useLaravelEcho';
37
38
  export { default as WSMessagesManager } from './hooks/notifications/WSMessagesManager';
@@ -84,7 +85,21 @@ export { default as useFormPersistence } from "./hooks/useFormPersistance";
84
85
  export { default as AvatarComponent } from './components/user/AvatarComponent';
85
86
  export { default as TenantAvatarComponent } from './components/user/TenantAvatarComponent';
86
87
  export { default as UserPreferences } from './components/user/UserPreferences';
88
+ export { default as Profile } from './pages/Profile';
87
89
  export { default as LanguageSwitcher } from './components/misc/LanguageSwitcher';
90
+ export { default as LangSwitcher } from './components/i18n/LangSwitcher';
91
+ export {
92
+ default as TenantSwitcher,
93
+ dispatchTenantSwitchEvent,
94
+ getActiveTenantId,
95
+ isTenantImpersonationEnabled,
96
+ type TenantSwitcherProps,
97
+ type TenantSwitchEventDetail,
98
+ } from './components/tenancy/TenantSwitcher';
99
+ export {
100
+ default as GlobalTenantWrapper,
101
+ type IGlobalTenantWrapper,
102
+ } from './components/tenancy/GlobalTenantWrapper';
88
103
  export { default as DarkModeSwitcher } from './components/misc/DarkModeSwitcher';
89
104
 
90
105
  // Navigation Components
@@ -1,5 +1,4 @@
1
1
  import { FC, useEffect, useState } from 'react';
2
- //import { Button as LoadingButton } from "@mui/material";
3
2
 
4
3
  import { Button, Card, CardContent, CardHeader, Grid, Typography } from '@mui/material';
5
4
 
@@ -8,7 +7,7 @@ import {
8
7
  useRefresh,
9
8
  Loading,
10
9
  useNotify,
11
- useGetIdentity,
10
+ useTranslate,
12
11
  TextInput,
13
12
  } from 'react-admin';
14
13
 
@@ -16,54 +15,61 @@ import { useDispatch } from 'react-redux';
16
15
  import { DASH_REDUX_ACTIONS } from 'dash-admin-state';
17
16
 
18
17
  import React from 'react';
19
- import useAxios from '../hooks/axios';
20
- import SingleImageUploader from './SingleImageUploader';
18
+ import { useAxios } from 'dash-axios-hook';
19
+ import { SingleImageUploader } from 'dash-components';
21
20
  import { useAuthContext } from '../contexts/auth';
22
- import UserPreferences from "../components/user/UserPreferences";
21
+ import UserPreferences from '../components/user/UserPreferences';
23
22
 
24
23
  const Profile: FC = (_props) => {
25
24
  const [currTab, setCurrTab] = useState('auth/update/info');
26
25
  const [isLoading, setIsLoading] = useState(false);
27
- //const { identity, isLoading: identityLoading } = useGetIdentity();
28
-
29
- const { user, fetchAuth } = useAuthContext(); // Add fetchCompleteAuth here
26
+
27
+ const { user, fetchAuth } = useAuthContext();
30
28
  const [avatar, setAvatar] = useState({ rawFile: null, urlFile: '' });
31
29
  const notify = useNotify();
32
-
33
- const { axios } = useAxios();
30
+ const translate = useTranslate();
31
+
32
+ const axios = useAxios();
34
33
  const refresh = useRefresh();
35
34
 
35
+ const { showPreferences = false } = _props;
36
+
36
37
  const dispatch = useDispatch();
37
38
 
38
39
  useEffect(() => {
39
40
  dispatch(
40
41
  DASH_REDUX_ACTIONS.updatePage({
41
- title: 'Perfil',
42
+ title: translate('profile.title', { _: 'Perfil' }),
42
43
  //icon:
43
- subTitle: 'Configuración de perfil',
44
+ subTitle: translate('profile.subtitle', { _: 'Configuración de perfil' }),
44
45
  }),
45
46
  );
46
- }, []);
47
+ // translate is included so the page header re-translates on locale change
48
+ // (react-admin returns a new translate reference when the locale changes)
49
+ }, [translate]);
47
50
 
48
51
  const validateUserCreation = (values) => {
49
52
  const errors: any = {};
50
53
  if (currTab === 'auth/update/password') {
54
+ const required = translate('profile.validation.required', { _: 'El campo es requerido' });
55
+ const mismatch = translate('profile.validation.password_mismatch', { _: 'Las contraseñas no coinciden' });
56
+
51
57
  if (
52
58
  (values.password || values.password_confirmation) &&
53
59
  !values.current_password
54
60
  ) {
55
- errors.current_password = 'El campo es requerido';
61
+ errors.current_password = required;
56
62
  }
57
63
  if (
58
64
  values.current_password &&
59
65
  (!values.password || !values.password_confirmation)
60
66
  ) {
61
- errors.password = 'El campo es requerido';
62
- errors.password_confirmation = 'El campo es requerido';
67
+ errors.password = required;
68
+ errors.password_confirmation = required;
63
69
  }
64
70
  if (values.password_confirmation !== values.password) {
65
- errors.password = 'Las contraseñas no coinciden';
66
- errors.password_confirmation = 'Las contraseñas no coinciden';
71
+ errors.password = mismatch;
72
+ errors.password_confirmation = mismatch;
67
73
  }
68
74
  }
69
75
 
@@ -72,7 +78,7 @@ const Profile: FC = (_props) => {
72
78
 
73
79
  const handleSubmit = async (values) => {
74
80
  setIsLoading(true);
75
-
81
+
76
82
  const formData = new FormData();
77
83
 
78
84
  const passwords = {
@@ -96,31 +102,27 @@ const Profile: FC = (_props) => {
96
102
  } else {
97
103
  await axios.put(currTab, passwords);
98
104
  }
99
-
100
-
101
-
105
+
102
106
  // Refresh the auth context with updated user data
103
107
  try {
104
-
105
108
  await fetchAuth();
106
109
  console.log('Auth context refreshed after profile update');
107
110
  } catch (authError) {
108
- console.error('Faileds to refresh auth context:', authError);
111
+ console.error('Failed to refresh auth context:', authError);
109
112
  // Don't show error to user as the main operation succeeded
110
113
  }
111
114
 
112
- notify('Usuario actualizado correctamente', { type:'success' });
115
+ notify(translate('profile.notify.success', { _: 'Usuario actualizado correctamente' }), { type: 'success' });
113
116
  refresh();
114
117
  } catch (error: any) {
115
118
 
116
-
117
119
  // Handle backend error messages gracefully
118
120
  let mainMessage = error?.response?.data?.message || error?.body?.message || '';
119
121
  let fieldErrors = error?.response?.data?.errors || error?.body?.errors || error;
120
122
  if (mainMessage) {
121
- notify(`Error al actualizar el usuario, ${mainMessage}`, { type: 'error' });
123
+ notify(translate('profile.notify.error_with_message', { message: mainMessage, _: `Error al actualizar el usuario, ${mainMessage}` }), { type: 'error' });
122
124
  } else {
123
- notify('Error al actualizar el usuario', { type: 'error' });
125
+ notify(translate('profile.notify.error', { _: 'Error al actualizar el usuario' }), { type: 'error' });
124
126
  }
125
127
  if (fieldErrors && typeof fieldErrors === 'object') {
126
128
  const errors: any = {};
@@ -133,6 +135,8 @@ const Profile: FC = (_props) => {
133
135
  }
134
136
  } finally {
135
137
  setIsLoading(false);
138
+ // TechDebt: the avatar update doesn't reflect immediately after the update, this is a quick fix to reload the page and show the new avatar, ideally we should update the auth context with the new avatar url and avoid reloading the page
139
+ window.location.reload();
136
140
  }
137
141
  };
138
142
 
@@ -146,17 +150,6 @@ const Profile: FC = (_props) => {
146
150
 
147
151
  return (
148
152
  <div className='dash-profile'>
149
-
150
- {/*Page Header
151
- <Row>
152
- <Col xs={24} md={12}>
153
- <div className="dash-mb-2">
154
- <h2 className="dash-page-title">Perfil</h2>
155
- </div>
156
- </Col>
157
- </Row>
158
- */}
159
-
160
153
 
161
154
  {user ? (
162
155
  <Form
@@ -165,16 +158,16 @@ const Profile: FC = (_props) => {
165
158
  defaultValues={{ name: user?.name, email: user?.email, lastname: user?.lastname }}
166
159
  className='dash-form'
167
160
  >
168
-
161
+
169
162
  <div className='dash-card-content dash-module dash-card-profile'>
170
-
171
- <Grid
172
- container
163
+
164
+ <Grid
165
+ container
173
166
  spacing={{ xs: 2, md: 3 }}
174
167
  sx={{ minHeight: '400px' }}
175
168
  >
176
169
  {/* Profile Image Section */}
177
- <Grid
170
+ <Grid
178
171
  size={{ xs: 12, md: 4 }}
179
172
  display="flex"
180
173
  justifyContent="center"
@@ -190,51 +183,18 @@ const Profile: FC = (_props) => {
190
183
  width: '100%',
191
184
  }}
192
185
  >
193
- {/*<div className='dash-profile-img'>
194
- {identity.image_url || avatar.urlFile ? (
195
- <img src={avatar.urlFile || identity.image_url} alt='' />
196
- ) : (
197
- <div className='dash-profile-icon'>
198
- <Icon.User size={'90px'} color={'#fff'} />
199
- </div>
200
- )}
201
- <IconButton
202
- aria-label='upload picture'
203
- component='label'
204
- className='dash-profile-edit'
205
- >
206
- <input
207
- hidden
208
- accept='image/*'
209
- type='file'
210
- onChange={(e) =>
211
- setAvatar({
212
- rawFile: e.target.files[0],
213
- urlFile: URL.createObjectURL(e.target.files[0]),
214
- })
215
- }
216
- />
217
- <Icon.PenTool
218
- size={'22px'}
219
- color={'#000'}
220
- style={{ opacity: '0.6' }}
221
- />
222
- </IconButton>
223
- </div>*/}
224
-
225
- {"<!-- F -->"}
226
- <SingleImageUploader
227
- currentUrl={user?.image_path}
186
+ <SingleImageUploader
187
+ currentUrl={user?.image_path}
228
188
  onChange={(file: File) => {
229
189
  setAvatar({
230
190
  rawFile: file,
231
191
  urlFile: URL.createObjectURL(file),
232
192
  });
233
- }}
234
- classNamePrefix='dash-profile'
193
+ }}
194
+ classNamePrefix='dash-profile'
235
195
  />
236
- <Typography
237
- className='dash-profile-name'
196
+ <Typography
197
+ className='dash-profile-name'
238
198
  variant='h2'
239
199
  sx={{ mt: 2, textAlign: 'center' }}
240
200
  >
@@ -257,16 +217,16 @@ const Profile: FC = (_props) => {
257
217
 
258
218
  {/* User Info Section */}
259
219
  <Grid size={{ xs: 12, md: 4 }}>
260
- <CardHeader
261
- sx={{ padding: '16px 25px 0' }}
262
- title='Info'
220
+ <CardHeader
221
+ sx={{ padding: '16px 25px 0' }}
222
+ title={translate('profile.info.title', { _: 'Info' })}
263
223
  />
264
224
  <CardContent sx={{ padding: '25px' }}>
265
225
  <Grid container spacing={2}>
266
226
  <Grid size={12}>
267
227
  <TextInput
268
228
  source='name'
269
- label='Nombre'
229
+ label={translate('profile.info.name', { _: 'Nombre' })}
270
230
  variant='outlined'
271
231
  fullWidth
272
232
  />
@@ -274,7 +234,7 @@ const Profile: FC = (_props) => {
274
234
  <Grid size={12}>
275
235
  <TextInput
276
236
  source='lastname'
277
- label='Apellido'
237
+ label={translate('profile.info.lastname', { _: 'Apellido' })}
278
238
  variant='outlined'
279
239
  fullWidth
280
240
  />
@@ -282,7 +242,7 @@ const Profile: FC = (_props) => {
282
242
  <Grid size={12}>
283
243
  <TextInput
284
244
  source='email'
285
- label='Email'
245
+ label={translate('profile.info.email', { _: 'Email' })}
286
246
  variant='outlined'
287
247
  fullWidth
288
248
  />
@@ -303,7 +263,9 @@ const Profile: FC = (_props) => {
303
263
  }
304
264
  }}
305
265
  >
306
- {isLoading && currTab === 'auth/update/info' ? 'Guardando...' : 'Guardar'}
266
+ {isLoading && currTab === 'auth/update/info'
267
+ ? translate('profile.info.saving', { _: 'Guardando...' })
268
+ : translate('profile.info.save', { _: 'Guardar' })}
307
269
  </Button>
308
270
  </Grid>
309
271
  </Grid>
@@ -314,7 +276,7 @@ const Profile: FC = (_props) => {
314
276
  <Grid size={{ xs: 12, md: 4 }}>
315
277
  <CardHeader
316
278
  sx={{ padding: '16px 25px 0' }}
317
- title='Contraseñas'
279
+ title={translate('profile.password.title', { _: 'Contraseñas' })}
318
280
  />
319
281
  <CardContent sx={{ padding: '25px' }}>
320
282
  <Grid container spacing={2}>
@@ -322,7 +284,7 @@ const Profile: FC = (_props) => {
322
284
  <TextInput
323
285
  type='password'
324
286
  source='current_password'
325
- label='Contraseña actual'
287
+ label={translate('profile.password.current', { _: 'Contraseña actual' })}
326
288
  variant='outlined'
327
289
  fullWidth
328
290
  />
@@ -331,14 +293,14 @@ const Profile: FC = (_props) => {
331
293
  <TextInput
332
294
  type='password'
333
295
  source='password'
334
- label='Nueva contraseña'
296
+ label={translate('profile.password.new', { _: 'Nueva contraseña' })}
335
297
  variant='outlined'
336
298
  fullWidth
337
299
  />
338
300
  </Grid>
339
301
  <Grid size={12}>
340
302
  <TextInput
341
- label='Repita nueva contraseña'
303
+ label={translate('profile.password.confirm', { _: 'Repita nueva contraseña' })}
342
304
  type='password'
343
305
  source='password_confirmation'
344
306
  variant='outlined'
@@ -361,7 +323,9 @@ const Profile: FC = (_props) => {
361
323
  }
362
324
  }}
363
325
  >
364
- {isLoading && currTab === 'auth/update/password' ? 'Guardando Contraseñas...' : 'Guardar Contraseñas'}
326
+ {isLoading && currTab === 'auth/update/password'
327
+ ? translate('profile.password.saving', { _: 'Guardando Contraseñas...' })
328
+ : translate('profile.password.save', { _: 'Guardar Contraseñas' })}
365
329
  </Button>
366
330
  </Grid>
367
331
  </Grid>
@@ -369,21 +333,19 @@ const Profile: FC = (_props) => {
369
333
  </Grid>
370
334
 
371
335
  {/* User Preferences Section */}
372
- <Grid size={{ xs: 12
373
- }}>
374
- <CardHeader
375
- sx={{ padding: '16px 25px 0' }}
376
- title='Preferencias'
377
- />
378
- <CardContent sx={{ padding: '25px' }}>
379
- <UserPreferences />
380
- </CardContent>
381
- </Grid>
336
+ {showPreferences && (
337
+ <Grid size={{ xs: 12 }}>
338
+ <CardContent sx={{ padding: '25px' }}>
339
+ <UserPreferences />
340
+ </CardContent>
341
+ </Grid>
342
+ )}
343
+
382
344
  </Grid>
383
345
  </div>
384
-
346
+
385
347
  </Form>
386
-
348
+
387
349
  ) : (
388
350
  <Loading />
389
351
  )}
@@ -311,8 +311,57 @@ const dashEnglish = {
311
311
  auto_reset_notice: 'This screen will automatically reset in 30 seconds',
312
312
  loading: 'Loading...',
313
313
  },
314
+ // Service account API keys
315
+ serviceAccount: {
316
+ reveal: {
317
+ title: 'Copy your API key now',
318
+ warning:
319
+ 'This is the only time this key will be shown. It cannot be recovered later — if you lose it, you will have to issue a new one.',
320
+ forKey: 'Key',
321
+ copy: 'Copy',
322
+ copied: 'Copied',
323
+ dismiss: 'I have saved it',
324
+ },
325
+ status: {
326
+ active: 'Active',
327
+ disabled: 'Disabled',
328
+ },
329
+ expiration: {
330
+ '1': '1 day',
331
+ '7': '7 days',
332
+ '30': '30 days',
333
+ never: 'Never',
334
+ },
335
+ notify: {
336
+ enabled: 'API key enabled',
337
+ disabled: 'API key disabled',
338
+ toggle_error: 'Could not change the key status',
339
+ },
340
+ action: {
341
+ enable: 'Enable',
342
+ disable: 'Disable',
343
+ },
344
+ // Shown when a tenancy-level role is selected: such a key reaches every
345
+ // tenant in the account, so the tenant field stops being a boundary.
346
+ tenancy_role_warning:
347
+ 'This role grants access to every tenant in the account. The selected tenant is informational, not a security boundary.',
348
+ },
314
349
  // Resource labels for admin panel
315
350
  resource: {
351
+ tenancy: {
352
+ serviceAccounts: {
353
+ label: 'Service Accounts',
354
+ name: 'Name',
355
+ tenant: 'Tenant',
356
+ roles: 'Roles',
357
+ expiration: 'Expiration',
358
+ status: 'Status',
359
+ token: 'Key',
360
+ expires_at: 'Expires',
361
+ last_used_at: 'Last used',
362
+ created_at: 'Created',
363
+ },
364
+ },
316
365
  // Groups
317
366
  groups: {
318
367
  system_resources: 'System Resources',
@@ -525,6 +574,70 @@ const dashEnglish = {
525
574
  has_modifiers: 'Customizable',
526
575
  no_more_products: 'No more products',
527
576
  },
577
+ // JSON Color Selector - image-based theme extraction
578
+ colorSelector: {
579
+ imageExtractor: {
580
+ title_ai: 'AI Theme Generator',
581
+ title_local: 'Theme from Image',
582
+ upload_prompt: 'Upload an Image to Extract Colors',
583
+ upload_hint: "Select an image file and we'll automatically extract a color palette",
584
+ upload_button: 'Choose Image',
585
+ drop_hint: 'or drag and drop it here',
586
+ invalid_file_type: 'Please select a valid image file',
587
+ file_too_large: 'File size must be less than %{limit}MB',
588
+ extracting: 'Extracting colors from image...',
589
+ extracted_colors: 'Extracted Colors (%{count} colors found)',
590
+ dominant_label: 'Dominant',
591
+ uploaded_image_alt: 'Uploaded image',
592
+ no_palette_error: 'Please extract colors from an image first',
593
+ theme_generation_failed: 'Failed to generate theme',
594
+ theme_description_label: 'Theme Description (Optional)',
595
+ theme_description_placeholder:
596
+ "Optional: Describe the theme style you want (e.g., 'modern and minimalist', 'warm and cozy', 'professional corporate')",
597
+ apply_colors_button: 'Apply Colors',
598
+ generate_button: 'Generate AI Theme',
599
+ generating_button: 'Generating...',
600
+ apply_colors_help_label: 'Apply Colors:',
601
+ apply_colors_help_desc:
602
+ 'Maps the extracted palette to your base theme colors locally — no AI involved',
603
+ generate_help_label: 'Generate AI Theme:',
604
+ generate_help_desc:
605
+ 'Uses AI to create a complete color scheme based on extracted colors and your description',
606
+ },
607
+ },
608
+ // Profile page
609
+ profile: {
610
+ title: 'Profile',
611
+ subtitle: 'Profile settings',
612
+ info: {
613
+ title: 'Info',
614
+ name: 'Name',
615
+ lastname: 'Last name',
616
+ email: 'Email',
617
+ save: 'Save',
618
+ saving: 'Saving...',
619
+ },
620
+ password: {
621
+ title: 'Passwords',
622
+ current: 'Current password',
623
+ new: 'New password',
624
+ confirm: 'Confirm new password',
625
+ save: 'Save Passwords',
626
+ saving: 'Saving Passwords...',
627
+ },
628
+ preferences: {
629
+ title: 'Preferences',
630
+ },
631
+ validation: {
632
+ required: 'This field is required',
633
+ password_mismatch: 'Passwords do not match',
634
+ },
635
+ notify: {
636
+ success: 'User updated successfully',
637
+ error: 'Error updating user',
638
+ error_with_message: 'Error updating user, %{message}',
639
+ },
640
+ },
528
641
  };
529
642
 
530
643
  export default {