@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.
- package/dist/components/i18n/BridgedLocalesMenuButton.js +1 -2
- package/dist/components/menu/DarkToggleMode.js +1 -1
- package/dist/components/serviceAccount/ApiKeyRevealDialog.js +110 -0
- package/dist/components/serviceAccount/ServiceAccountStatusSwitch.js +94 -0
- package/dist/components/subscription/PlanPricesSettings.js +4 -2
- package/dist/components/tenancy/GlobalTenantWrapper.js +64 -0
- package/dist/components/tenancy/TenantSwitcher.js +389 -0
- package/dist/contexts/com/components/EchoNotificationsCenter.js +40 -0
- package/dist/default-theme/menu/AppMenuComponents/expanded/SidebarItem.js +1 -0
- package/dist/index.js +60 -40
- package/dist/pages/Profile.js +32 -38
- package/dist/providers/i18n/en.js +108 -0
- package/dist/providers/i18n/es.js +108 -0
- package/dist/providers/i18n/languages.js +3 -1
- package/dist/providers/i18n/mergeTranslations.js +18 -0
- package/dist/resources/User/components/AvatarUploadField.js +61 -0
- package/dist/resources/User/components/PasswordFields.js +67 -0
- package/dist/resources/User/getProfileMenu.js +36 -0
- package/dist/resources/User/profileResource.js +33 -0
- package/dist/resources/User/userLayout.js +58 -0
- package/dist/resources/User/userResource.js +90 -0
- package/dist/resources/User/userSchema.js +206 -0
- package/dist/resources/User/usersFilters.js +22 -0
- package/dist/systemResources.js +30 -19
- package/dist/templates/ResourceTemplateCreate.js +5 -2
- package/package.json +4 -1
- package/src/components/i18n/BridgedLocalesMenuButton.tsx +1 -2
- package/src/components/menu/DarkToggleMode.tsx +1 -1
- package/src/components/serviceAccount/ApiKeyRevealDialog.tsx +129 -0
- package/src/components/serviceAccount/ServiceAccountStatusSwitch.tsx +99 -0
- package/src/components/subscription/PlanPricesSettings.tsx +2 -2
- package/src/components/tenancy/GlobalTenantWrapper.tsx +84 -0
- package/src/components/tenancy/TenantSwitcher.tsx +485 -0
- package/src/contexts/com/components/EchoNotificationsCenter.tsx +65 -0
- package/src/default-theme/menu/AppMaterialMenu.tsx +1 -1
- package/src/default-theme/menu/AppMenuComponents/expanded/SidebarItem.tsx +1 -0
- package/src/default-theme/menu/SidebarActions.tsx +1 -1
- package/src/index.tsx +15 -0
- package/src/pages/Profile.tsx +69 -107
- package/src/providers/i18n/en.ts +113 -0
- package/src/providers/i18n/es.ts +113 -0
- package/src/providers/i18n/languages.tsx +3 -0
- package/src/providers/i18n/mergeTranslations.ts +53 -0
- package/src/resources/User/components/AvatarUploadField.tsx +57 -0
- package/src/resources/User/components/PasswordFields.tsx +73 -0
- package/src/resources/User/getProfileMenu.ts +57 -0
- package/src/resources/User/profileResource.tsx +51 -0
- package/src/resources/User/userLayout.tsx +62 -0
- package/src/resources/User/userResource.tsx +111 -0
- package/src/resources/User/userSchema.tsx +213 -0
- package/src/resources/User/usersFilters.tsx +21 -0
- package/src/systemResources.tsx +31 -20
- 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
|
|
85
|
+
<SidebarItem level={level} navExpanded={navExpanded} navSize={navSize} item={item} key={index} />
|
|
86
86
|
);
|
|
87
87
|
})}
|
|
88
88
|
</List>
|
|
@@ -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
|
package/src/pages/Profile.tsx
CHANGED
|
@@ -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
|
-
|
|
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 '
|
|
20
|
-
import SingleImageUploader from '
|
|
18
|
+
import { useAxios } from 'dash-axios-hook';
|
|
19
|
+
import { SingleImageUploader } from 'dash-components';
|
|
21
20
|
import { useAuthContext } from '../contexts/auth';
|
|
22
|
-
import UserPreferences from
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
62
|
-
errors.password_confirmation =
|
|
67
|
+
errors.password = required;
|
|
68
|
+
errors.password_confirmation = required;
|
|
63
69
|
}
|
|
64
70
|
if (values.password_confirmation !== values.password) {
|
|
65
|
-
errors.password =
|
|
66
|
-
errors.password_confirmation =
|
|
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('
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
194
|
-
{
|
|
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
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
)}
|
package/src/providers/i18n/en.ts
CHANGED
|
@@ -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 {
|