@dashadmin/dash-admin 1.3.44 → 1.3.45
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/serviceAccount/ApiKeyRevealDialog.js +110 -0
- package/dist/components/serviceAccount/ServiceAccountStatusSwitch.js +94 -0
- package/dist/providers/i18n/en.js +75 -0
- package/dist/providers/i18n/es.js +75 -0
- package/dist/providers/i18n/languages.js +3 -1
- package/dist/providers/i18n/mergeTranslations.js +18 -0
- package/dist/templates/ResourceTemplateCreate.js +5 -2
- package/package.json +4 -1
- package/src/components/serviceAccount/ApiKeyRevealDialog.tsx +129 -0
- package/src/components/serviceAccount/ServiceAccountStatusSwitch.tsx +99 -0
- package/src/providers/i18n/en.ts +80 -0
- package/src/providers/i18n/es.ts +80 -0
- package/src/providers/i18n/languages.tsx +3 -0
- package/src/providers/i18n/mergeTranslations.ts +53 -0
- package/src/templates/ResourceTemplateCreate.tsx +17 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
var __async = (__this, __arguments, generator) => {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
var fulfilled = (value) => {
|
|
4
|
+
try {
|
|
5
|
+
step(generator.next(value));
|
|
6
|
+
} catch (e) {
|
|
7
|
+
reject(e);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var rejected = (value) => {
|
|
11
|
+
try {
|
|
12
|
+
step(generator.throw(value));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
reject(e);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
18
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
import React, { useState } from "react";
|
|
23
|
+
import Alert from "@mui/material/Alert";
|
|
24
|
+
import Box from "@mui/material/Box";
|
|
25
|
+
import IconButton from "@mui/material/IconButton";
|
|
26
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
27
|
+
import Typography from "@mui/material/Typography";
|
|
28
|
+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
29
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
30
|
+
import { useI18nBridge } from "../../contexts/I18nBridgeContext";
|
|
31
|
+
const ApiKeyRevealContent = ({ token, name }) => {
|
|
32
|
+
const { i18nProvider, locale } = useI18nBridge();
|
|
33
|
+
const [copied, setCopied] = useState(false);
|
|
34
|
+
const translate = React.useCallback(
|
|
35
|
+
(key, options) => {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
if (i18nProvider == null ? void 0 : i18nProvider.translate) {
|
|
38
|
+
try {
|
|
39
|
+
return i18nProvider.translate(key, options);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return (_a = options == null ? void 0 : options._) != null ? _a : key;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (_b = options == null ? void 0 : options._) != null ? _b : key;
|
|
45
|
+
},
|
|
46
|
+
// locale is a dependency so the text re-renders on a language switch.
|
|
47
|
+
[i18nProvider, locale]
|
|
48
|
+
);
|
|
49
|
+
const handleCopy = () => __async(null, null, function* () {
|
|
50
|
+
try {
|
|
51
|
+
yield navigator.clipboard.writeText(token);
|
|
52
|
+
setCopied(true);
|
|
53
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
setCopied(false);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: translate("serviceAccount.reveal.warning", {
|
|
60
|
+
_: "This is the only time this key will be shown. It cannot be recovered later \u2014 if you lose it, you will have to issue a new one."
|
|
61
|
+
}) }),
|
|
62
|
+
name ? /* @__PURE__ */ jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [
|
|
63
|
+
translate("serviceAccount.reveal.forKey", { _: "Key" }),
|
|
64
|
+
": ",
|
|
65
|
+
/* @__PURE__ */ jsx("strong", { children: name })
|
|
66
|
+
] }) : null,
|
|
67
|
+
/* @__PURE__ */ jsxs(
|
|
68
|
+
Box,
|
|
69
|
+
{
|
|
70
|
+
sx: {
|
|
71
|
+
display: "flex",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
gap: 1,
|
|
74
|
+
p: 1.5,
|
|
75
|
+
borderRadius: 1,
|
|
76
|
+
border: (theme) => `1px solid ${theme.palette.divider}`,
|
|
77
|
+
bgcolor: (theme) => theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)"
|
|
78
|
+
},
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
Box,
|
|
82
|
+
{
|
|
83
|
+
component: "code",
|
|
84
|
+
sx: {
|
|
85
|
+
flex: 1,
|
|
86
|
+
fontFamily: "monospace",
|
|
87
|
+
fontSize: "0.875rem",
|
|
88
|
+
wordBreak: "break-all",
|
|
89
|
+
userSelect: "all"
|
|
90
|
+
},
|
|
91
|
+
children: token
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
Tooltip,
|
|
96
|
+
{
|
|
97
|
+
title: copied ? translate("serviceAccount.reveal.copied", { _: "Copied" }) : translate("serviceAccount.reveal.copy", { _: "Copy" }),
|
|
98
|
+
children: /* @__PURE__ */ jsx(IconButton, { onClick: handleCopy, size: "small", "aria-label": "copy api key", children: copied ? /* @__PURE__ */ jsx(CheckIcon, { color: "success", fontSize: "small" }) : /* @__PURE__ */ jsx(ContentCopyIcon, { fontSize: "small" }) })
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
] });
|
|
105
|
+
};
|
|
106
|
+
var ApiKeyRevealDialog_default = ApiKeyRevealContent;
|
|
107
|
+
export {
|
|
108
|
+
ApiKeyRevealContent,
|
|
109
|
+
ApiKeyRevealDialog_default as default
|
|
110
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
var __async = (__this, __arguments, generator) => {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
var fulfilled = (value) => {
|
|
4
|
+
try {
|
|
5
|
+
step(generator.next(value));
|
|
6
|
+
} catch (e) {
|
|
7
|
+
reject(e);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var rejected = (value) => {
|
|
11
|
+
try {
|
|
12
|
+
step(generator.throw(value));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
reject(e);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
18
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
import React, { useState } from "react";
|
|
23
|
+
import { useRecordContext, useNotify, useRefresh } from "react-admin";
|
|
24
|
+
import { useAxios } from "dash-axios-hook";
|
|
25
|
+
import Box from "@mui/material/Box";
|
|
26
|
+
import Switch from "@mui/material/Switch";
|
|
27
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
28
|
+
import CircularProgress from "@mui/material/CircularProgress";
|
|
29
|
+
import { useI18nBridge } from "../../contexts/I18nBridgeContext";
|
|
30
|
+
const ServiceAccountStatusSwitch = ({
|
|
31
|
+
resourceConfig
|
|
32
|
+
}) => {
|
|
33
|
+
const record = useRecordContext();
|
|
34
|
+
const notify = useNotify();
|
|
35
|
+
const refresh = useRefresh();
|
|
36
|
+
const axios = useAxios();
|
|
37
|
+
const { i18nProvider } = useI18nBridge();
|
|
38
|
+
const [loading, setLoading] = useState(false);
|
|
39
|
+
const translate = React.useCallback(
|
|
40
|
+
(key, fallback) => {
|
|
41
|
+
var _a;
|
|
42
|
+
try {
|
|
43
|
+
return (_a = i18nProvider == null ? void 0 : i18nProvider.translate(key, { _: fallback })) != null ? _a : fallback;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[i18nProvider]
|
|
49
|
+
);
|
|
50
|
+
if (!record) return null;
|
|
51
|
+
const isActive = record.status === "active";
|
|
52
|
+
const handleToggle = () => __async(null, null, function* () {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const action = isActive ? "disable" : "enable";
|
|
56
|
+
yield axios.post(`${resourceConfig.model}/${record.id}/${action}`);
|
|
57
|
+
notify(
|
|
58
|
+
isActive ? translate("serviceAccount.notify.disabled", "API key disabled") : translate("serviceAccount.notify.enabled", "API key enabled"),
|
|
59
|
+
{ type: "success" }
|
|
60
|
+
);
|
|
61
|
+
refresh();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("ServiceAccountStatusSwitch: toggle failed", error);
|
|
64
|
+
notify(
|
|
65
|
+
translate("serviceAccount.notify.toggle_error", "Could not change the key status"),
|
|
66
|
+
{ type: "error" }
|
|
67
|
+
);
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1, my: 1 }, children: [
|
|
73
|
+
/* @__PURE__ */ jsx(
|
|
74
|
+
FormControlLabel,
|
|
75
|
+
{
|
|
76
|
+
control: /* @__PURE__ */ jsx(
|
|
77
|
+
Switch,
|
|
78
|
+
{
|
|
79
|
+
checked: isActive,
|
|
80
|
+
onChange: handleToggle,
|
|
81
|
+
disabled: loading,
|
|
82
|
+
color: "success"
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
label: isActive ? translate("serviceAccount.status.active", "Active") : translate("serviceAccount.status.disabled", "Disabled")
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
loading ? /* @__PURE__ */ jsx(CircularProgress, { size: 16 }) : null
|
|
89
|
+
] });
|
|
90
|
+
};
|
|
91
|
+
var ServiceAccountStatusSwitch_default = ServiceAccountStatusSwitch;
|
|
92
|
+
export {
|
|
93
|
+
ServiceAccountStatusSwitch_default as default
|
|
94
|
+
};
|
|
@@ -302,8 +302,55 @@ const dashEnglish = {
|
|
|
302
302
|
auto_reset_notice: "This screen will automatically reset in 30 seconds",
|
|
303
303
|
loading: "Loading..."
|
|
304
304
|
},
|
|
305
|
+
// Service account API keys
|
|
306
|
+
serviceAccount: {
|
|
307
|
+
reveal: {
|
|
308
|
+
title: "Copy your API key now",
|
|
309
|
+
warning: "This is the only time this key will be shown. It cannot be recovered later \u2014 if you lose it, you will have to issue a new one.",
|
|
310
|
+
forKey: "Key",
|
|
311
|
+
copy: "Copy",
|
|
312
|
+
copied: "Copied",
|
|
313
|
+
dismiss: "I have saved it"
|
|
314
|
+
},
|
|
315
|
+
status: {
|
|
316
|
+
active: "Active",
|
|
317
|
+
disabled: "Disabled"
|
|
318
|
+
},
|
|
319
|
+
expiration: {
|
|
320
|
+
"1": "1 day",
|
|
321
|
+
"7": "7 days",
|
|
322
|
+
"30": "30 days",
|
|
323
|
+
never: "Never"
|
|
324
|
+
},
|
|
325
|
+
notify: {
|
|
326
|
+
enabled: "API key enabled",
|
|
327
|
+
disabled: "API key disabled",
|
|
328
|
+
toggle_error: "Could not change the key status"
|
|
329
|
+
},
|
|
330
|
+
action: {
|
|
331
|
+
enable: "Enable",
|
|
332
|
+
disable: "Disable"
|
|
333
|
+
},
|
|
334
|
+
// Shown when a tenancy-level role is selected: such a key reaches every
|
|
335
|
+
// tenant in the account, so the tenant field stops being a boundary.
|
|
336
|
+
tenancy_role_warning: "This role grants access to every tenant in the account. The selected tenant is informational, not a security boundary."
|
|
337
|
+
},
|
|
305
338
|
// Resource labels for admin panel
|
|
306
339
|
resource: {
|
|
340
|
+
tenancy: {
|
|
341
|
+
serviceAccounts: {
|
|
342
|
+
label: "Service Accounts",
|
|
343
|
+
name: "Name",
|
|
344
|
+
tenant: "Tenant",
|
|
345
|
+
roles: "Roles",
|
|
346
|
+
expiration: "Expiration",
|
|
347
|
+
status: "Status",
|
|
348
|
+
token: "Key",
|
|
349
|
+
expires_at: "Expires",
|
|
350
|
+
last_used_at: "Last used",
|
|
351
|
+
created_at: "Created"
|
|
352
|
+
}
|
|
353
|
+
},
|
|
307
354
|
// Groups
|
|
308
355
|
groups: {
|
|
309
356
|
system_resources: "System Resources",
|
|
@@ -506,6 +553,34 @@ const dashEnglish = {
|
|
|
506
553
|
all_products: "All Products",
|
|
507
554
|
has_modifiers: "Customizable",
|
|
508
555
|
no_more_products: "No more products"
|
|
556
|
+
},
|
|
557
|
+
// JSON Color Selector - image-based theme extraction
|
|
558
|
+
colorSelector: {
|
|
559
|
+
imageExtractor: {
|
|
560
|
+
title_ai: "AI Theme Generator",
|
|
561
|
+
title_local: "Theme from Image",
|
|
562
|
+
upload_prompt: "Upload an Image to Extract Colors",
|
|
563
|
+
upload_hint: "Select an image file and we'll automatically extract a color palette",
|
|
564
|
+
upload_button: "Choose Image",
|
|
565
|
+
drop_hint: "or drag and drop it here",
|
|
566
|
+
invalid_file_type: "Please select a valid image file",
|
|
567
|
+
file_too_large: "File size must be less than %{limit}MB",
|
|
568
|
+
extracting: "Extracting colors from image...",
|
|
569
|
+
extracted_colors: "Extracted Colors (%{count} colors found)",
|
|
570
|
+
dominant_label: "Dominant",
|
|
571
|
+
uploaded_image_alt: "Uploaded image",
|
|
572
|
+
no_palette_error: "Please extract colors from an image first",
|
|
573
|
+
theme_generation_failed: "Failed to generate theme",
|
|
574
|
+
theme_description_label: "Theme Description (Optional)",
|
|
575
|
+
theme_description_placeholder: "Optional: Describe the theme style you want (e.g., 'modern and minimalist', 'warm and cozy', 'professional corporate')",
|
|
576
|
+
apply_colors_button: "Apply Colors",
|
|
577
|
+
generate_button: "Generate AI Theme",
|
|
578
|
+
generating_button: "Generating...",
|
|
579
|
+
apply_colors_help_label: "Apply Colors:",
|
|
580
|
+
apply_colors_help_desc: "Maps the extracted palette to your base theme colors locally \u2014 no AI involved",
|
|
581
|
+
generate_help_label: "Generate AI Theme:",
|
|
582
|
+
generate_help_desc: "Uses AI to create a complete color scheme based on extracted colors and your description"
|
|
583
|
+
}
|
|
509
584
|
}
|
|
510
585
|
};
|
|
511
586
|
var en_default = __spreadValues(__spreadValues({}, raEnglish), dashEnglish);
|
|
@@ -309,8 +309,55 @@ const dashSpanish = {
|
|
|
309
309
|
auto_reset_notice: "Esta pantalla se reiniciar\xE1 autom\xE1ticamente en 30 segundos",
|
|
310
310
|
loading: "Cargando..."
|
|
311
311
|
},
|
|
312
|
+
// Service account API keys
|
|
313
|
+
serviceAccount: {
|
|
314
|
+
reveal: {
|
|
315
|
+
title: "Copia tu API Key ahora",
|
|
316
|
+
warning: "Esta es la \xFAnica vez que se mostrar\xE1 esta clave. No se puede recuperar m\xE1s adelante \u2014 si la pierdes, tendr\xE1s que emitir una nueva.",
|
|
317
|
+
forKey: "Clave",
|
|
318
|
+
copy: "Copiar",
|
|
319
|
+
copied: "Copiado",
|
|
320
|
+
dismiss: "La he guardado"
|
|
321
|
+
},
|
|
322
|
+
status: {
|
|
323
|
+
active: "Activa",
|
|
324
|
+
disabled: "Deshabilitada"
|
|
325
|
+
},
|
|
326
|
+
expiration: {
|
|
327
|
+
"1": "1 d\xEDa",
|
|
328
|
+
"7": "7 d\xEDas",
|
|
329
|
+
"30": "30 d\xEDas",
|
|
330
|
+
never: "Nunca"
|
|
331
|
+
},
|
|
332
|
+
notify: {
|
|
333
|
+
enabled: "API Key habilitada",
|
|
334
|
+
disabled: "API Key deshabilitada",
|
|
335
|
+
toggle_error: "No se pudo cambiar el estado de la API Key"
|
|
336
|
+
},
|
|
337
|
+
action: {
|
|
338
|
+
enable: "Habilitar",
|
|
339
|
+
disable: "Deshabilitar"
|
|
340
|
+
},
|
|
341
|
+
// Shown when a tenancy-level role is selected: such a key reaches every
|
|
342
|
+
// tenant in the account, so the tenant field stops being a boundary.
|
|
343
|
+
tenancy_role_warning: "Este rol otorga acceso a todos los locales de la cuenta. El local seleccionado es informativo, no un l\xEDmite de seguridad."
|
|
344
|
+
},
|
|
312
345
|
// Resource labels for admin panel
|
|
313
346
|
resource: {
|
|
347
|
+
tenancy: {
|
|
348
|
+
serviceAccounts: {
|
|
349
|
+
label: "Cuentas de Servicio",
|
|
350
|
+
name: "Nombre",
|
|
351
|
+
tenant: "Local",
|
|
352
|
+
roles: "Roles",
|
|
353
|
+
expiration: "Expiraci\xF3n",
|
|
354
|
+
status: "Estado",
|
|
355
|
+
token: "Clave",
|
|
356
|
+
expires_at: "Expira",
|
|
357
|
+
last_used_at: "\xDAltimo uso",
|
|
358
|
+
created_at: "Creado"
|
|
359
|
+
}
|
|
360
|
+
},
|
|
314
361
|
// Groups
|
|
315
362
|
groups: {
|
|
316
363
|
system_resources: "Recursos de Sistema",
|
|
@@ -512,6 +559,34 @@ const dashSpanish = {
|
|
|
512
559
|
all_products: "Todos los Productos",
|
|
513
560
|
has_modifiers: "Personalizable",
|
|
514
561
|
no_more_products: "No hay m\xE1s productos"
|
|
562
|
+
},
|
|
563
|
+
// JSON Color Selector - extracción de tema desde imagen
|
|
564
|
+
colorSelector: {
|
|
565
|
+
imageExtractor: {
|
|
566
|
+
title_ai: "Generador de Tema con IA",
|
|
567
|
+
title_local: "Tema desde Imagen",
|
|
568
|
+
upload_prompt: "Sube una Imagen para Extraer Colores",
|
|
569
|
+
upload_hint: "Selecciona un archivo de imagen y extraeremos autom\xE1ticamente una paleta de colores",
|
|
570
|
+
upload_button: "Elegir Imagen",
|
|
571
|
+
drop_hint: "o arr\xE1strala aqu\xED",
|
|
572
|
+
invalid_file_type: "Por favor selecciona un archivo de imagen v\xE1lido",
|
|
573
|
+
file_too_large: "El archivo debe pesar menos de %{limit}MB",
|
|
574
|
+
extracting: "Extrayendo colores de la imagen...",
|
|
575
|
+
extracted_colors: "Colores Extra\xEDdos (%{count} colores encontrados)",
|
|
576
|
+
dominant_label: "Dominante",
|
|
577
|
+
uploaded_image_alt: "Imagen subida",
|
|
578
|
+
no_palette_error: "Primero extrae los colores de una imagen",
|
|
579
|
+
theme_generation_failed: "No se pudo generar el tema",
|
|
580
|
+
theme_description_label: "Descripci\xF3n del Tema (Opcional)",
|
|
581
|
+
theme_description_placeholder: "Opcional: Describe el estilo de tema que deseas (ej: 'moderno y minimalista', 'c\xE1lido y acogedor', 'corporativo profesional')",
|
|
582
|
+
apply_colors_button: "Aplicar Colores",
|
|
583
|
+
generate_button: "Generar Tema con IA",
|
|
584
|
+
generating_button: "Generando...",
|
|
585
|
+
apply_colors_help_label: "Aplicar Colores:",
|
|
586
|
+
apply_colors_help_desc: "Aplica la paleta extra\xEDda a los colores base de tu tema localmente \u2014 sin usar IA",
|
|
587
|
+
generate_help_label: "Generar Tema con IA:",
|
|
588
|
+
generate_help_desc: "Usa IA para crear un esquema de color completo basado en los colores extra\xEDdos y tu descripci\xF3n"
|
|
589
|
+
}
|
|
515
590
|
}
|
|
516
591
|
};
|
|
517
592
|
var es_default = __spreadValues(__spreadValues({}, raSpanish), dashSpanish);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
|
+
const mergeTranslations = (...sources) => {
|
|
3
|
+
const result = {};
|
|
4
|
+
for (const source of sources) {
|
|
5
|
+
if (!isPlainObject(source)) continue;
|
|
6
|
+
for (const key of Object.keys(source)) {
|
|
7
|
+
const incoming = source[key];
|
|
8
|
+
const existing = result[key];
|
|
9
|
+
result[key] = isPlainObject(existing) && isPlainObject(incoming) ? mergeTranslations(existing, incoming) : incoming;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
};
|
|
14
|
+
var mergeTranslations_default = mergeTranslations;
|
|
15
|
+
export {
|
|
16
|
+
mergeTranslations_default as default,
|
|
17
|
+
mergeTranslations
|
|
18
|
+
};
|
|
@@ -40,15 +40,18 @@ const ResourceTemplateCreate = (props) => {
|
|
|
40
40
|
return `/${path}`.replace(/\/+/g, "/");
|
|
41
41
|
};
|
|
42
42
|
const onCreate = (data) => {
|
|
43
|
+
var _a, _b;
|
|
43
44
|
const translatedLabel = translate(resourceConfig.label, { _: resourceConfig.label });
|
|
44
45
|
if (_showNotifyAfterSubmit) {
|
|
45
46
|
notify("dash.resource.created", { type: "success" });
|
|
46
47
|
}
|
|
47
48
|
if (_showDialogAfterSubmit) {
|
|
49
|
+
const successOverride = (_a = resourceConfig == null ? void 0 : resourceConfig.createSuccessDialog) == null ? void 0 : _a.call(resourceConfig, data);
|
|
50
|
+
const overrideTitle = typeof (successOverride == null ? void 0 : successOverride.title) === "string" ? translate(successOverride.title, { _: successOverride.title }) : successOverride == null ? void 0 : successOverride.title;
|
|
48
51
|
dialog({
|
|
49
52
|
variant: "info",
|
|
50
|
-
title: translate("dash.resource.created"),
|
|
51
|
-
content: translate("dash.resource.created_message", { label: translatedLabel }),
|
|
53
|
+
title: overrideTitle != null ? overrideTitle : translate("dash.resource.created"),
|
|
54
|
+
content: (_b = successOverride == null ? void 0 : successOverride.content) != null ? _b : translate("dash.resource.created_message", { label: translatedLabel }),
|
|
52
55
|
onConfirm: () => {
|
|
53
56
|
switch (resourceConfig == null ? void 0 : resourceConfig.redirectAfterCreate) {
|
|
54
57
|
case false:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashadmin/dash-admin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.45",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@capacitor/core": "^7.4.2",
|
|
29
29
|
"@capacitor/push-notifications": "^7.0.1",
|
|
30
|
+
"antd": "latest",
|
|
31
|
+
"@tanstack/react-query": "latest",
|
|
32
|
+
"@tanstack/react-query-persist-client": "latest",
|
|
30
33
|
"@emotion/react": "latest",
|
|
31
34
|
"@emotion/styled": "latest",
|
|
32
35
|
"@mui/material": "^7.3.10",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import Alert from "@mui/material/Alert";
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import IconButton from "@mui/material/IconButton";
|
|
5
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
6
|
+
import Typography from "@mui/material/Typography";
|
|
7
|
+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
8
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
9
|
+
import { useI18nBridge } from "../../contexts/I18nBridgeContext";
|
|
10
|
+
|
|
11
|
+
export interface IApiKeyRevealContentProps {
|
|
12
|
+
/** The plaintext token, shown exactly once. */
|
|
13
|
+
token: string;
|
|
14
|
+
/** Name of the key, for context. */
|
|
15
|
+
name?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One-time reveal of a newly issued API key.
|
|
20
|
+
*
|
|
21
|
+
* The plaintext credential is never persisted in recoverable form - only a
|
|
22
|
+
* short display prefix is stored - so this is the single moment the operator
|
|
23
|
+
* can copy it.
|
|
24
|
+
*
|
|
25
|
+
* Designed to be passed as the `content` of the framework dialog via a
|
|
26
|
+
* resource's `createSuccessDialog`, so it inherits the app's dialog chrome
|
|
27
|
+
* rather than introducing a second dialog system:
|
|
28
|
+
*
|
|
29
|
+
* createSuccessDialog: (data) => data?.plain_text_token
|
|
30
|
+
* ? { title: '...', content: <ApiKeyRevealContent token={data.plain_text_token} name={data.name} /> }
|
|
31
|
+
* : null,
|
|
32
|
+
*/
|
|
33
|
+
export const ApiKeyRevealContent: React.FC<IApiKeyRevealContentProps> = ({ token, name }) => {
|
|
34
|
+
// DialogServiceProvider is mounted ABOVE <Admin> in DASHAppProviders, so
|
|
35
|
+
// dialog content renders outside React Admin's i18n context and
|
|
36
|
+
// useTranslate() would silently fall through to the English defaults.
|
|
37
|
+
// The bridge is the supported way to translate from out there - same
|
|
38
|
+
// pattern as AppMaterialMenu.
|
|
39
|
+
const { i18nProvider, locale } = useI18nBridge();
|
|
40
|
+
const [copied, setCopied] = useState(false);
|
|
41
|
+
|
|
42
|
+
const translate = React.useCallback(
|
|
43
|
+
(key: string, options?: any) => {
|
|
44
|
+
if (i18nProvider?.translate) {
|
|
45
|
+
try {
|
|
46
|
+
return i18nProvider.translate(key, options);
|
|
47
|
+
} catch {
|
|
48
|
+
return options?._ ?? key;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return options?._ ?? key;
|
|
52
|
+
},
|
|
53
|
+
// locale is a dependency so the text re-renders on a language switch.
|
|
54
|
+
[i18nProvider, locale]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleCopy = async () => {
|
|
58
|
+
try {
|
|
59
|
+
await navigator.clipboard.writeText(token);
|
|
60
|
+
setCopied(true);
|
|
61
|
+
setTimeout(() => setCopied(false), 2000);
|
|
62
|
+
} catch {
|
|
63
|
+
// Clipboard API unavailable (insecure context or denied permission).
|
|
64
|
+
// The value stays selectable so the operator can still copy it by
|
|
65
|
+
// hand - never leave them without a path to the secret.
|
|
66
|
+
setCopied(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Box>
|
|
72
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
73
|
+
{translate("serviceAccount.reveal.warning", {
|
|
74
|
+
_: "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.",
|
|
75
|
+
})}
|
|
76
|
+
</Alert>
|
|
77
|
+
|
|
78
|
+
{name ? (
|
|
79
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
80
|
+
{translate("serviceAccount.reveal.forKey", { _: "Key" })}: <strong>{name}</strong>
|
|
81
|
+
</Typography>
|
|
82
|
+
) : null}
|
|
83
|
+
|
|
84
|
+
<Box
|
|
85
|
+
sx={{
|
|
86
|
+
display: "flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
gap: 1,
|
|
89
|
+
p: 1.5,
|
|
90
|
+
borderRadius: 1,
|
|
91
|
+
border: (theme) => `1px solid ${theme.palette.divider}`,
|
|
92
|
+
bgcolor: (theme) =>
|
|
93
|
+
theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Box
|
|
97
|
+
component="code"
|
|
98
|
+
sx={{
|
|
99
|
+
flex: 1,
|
|
100
|
+
fontFamily: "monospace",
|
|
101
|
+
fontSize: "0.875rem",
|
|
102
|
+
wordBreak: "break-all",
|
|
103
|
+
userSelect: "all",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{token}
|
|
107
|
+
</Box>
|
|
108
|
+
|
|
109
|
+
<Tooltip
|
|
110
|
+
title={
|
|
111
|
+
copied
|
|
112
|
+
? translate("serviceAccount.reveal.copied", { _: "Copied" })
|
|
113
|
+
: translate("serviceAccount.reveal.copy", { _: "Copy" })
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<IconButton onClick={handleCopy} size="small" aria-label="copy api key">
|
|
117
|
+
{copied ? (
|
|
118
|
+
<CheckIcon color="success" fontSize="small" />
|
|
119
|
+
) : (
|
|
120
|
+
<ContentCopyIcon fontSize="small" />
|
|
121
|
+
)}
|
|
122
|
+
</IconButton>
|
|
123
|
+
</Tooltip>
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default ApiKeyRevealContent;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useRecordContext, useNotify, useRefresh } from "react-admin";
|
|
3
|
+
import { useAxios } from "dash-axios-hook";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Switch from "@mui/material/Switch";
|
|
6
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
7
|
+
import CircularProgress from "@mui/material/CircularProgress";
|
|
8
|
+
import { IDashAutoAdminCustomFieldComponent } from "dash-auto-admin";
|
|
9
|
+
import { useI18nBridge } from "../../contexts/I18nBridgeContext";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Enable/disable switch for a service account API key.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT a form field. Status is changed through the dedicated
|
|
15
|
+
* enable/disable endpoints rather than the update payload, for two reasons:
|
|
16
|
+
*
|
|
17
|
+
* - The update contract rejects everything except role_ids, so submitting a
|
|
18
|
+
* status would 422.
|
|
19
|
+
* - Disabling must not rotate the credential. The endpoints flip a flag and
|
|
20
|
+
* leave the token untouched, so re-enabling restores the same key rather
|
|
21
|
+
* than forcing every integration to redeploy a new secret.
|
|
22
|
+
*
|
|
23
|
+
* The switch therefore acts immediately and refreshes, instead of waiting for
|
|
24
|
+
* a form save that would never carry it.
|
|
25
|
+
*/
|
|
26
|
+
const ServiceAccountStatusSwitch: React.FC<IDashAutoAdminCustomFieldComponent> = ({
|
|
27
|
+
resourceConfig,
|
|
28
|
+
}) => {
|
|
29
|
+
const record = useRecordContext();
|
|
30
|
+
const notify = useNotify();
|
|
31
|
+
const refresh = useRefresh();
|
|
32
|
+
const axios = useAxios();
|
|
33
|
+
const { i18nProvider } = useI18nBridge();
|
|
34
|
+
const [loading, setLoading] = useState(false);
|
|
35
|
+
|
|
36
|
+
const translate = React.useCallback(
|
|
37
|
+
(key: string, fallback: string) => {
|
|
38
|
+
try {
|
|
39
|
+
return i18nProvider?.translate(key, { _: fallback }) ?? fallback;
|
|
40
|
+
} catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[i18nProvider]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!record) return null;
|
|
48
|
+
|
|
49
|
+
const isActive = record.status === "active";
|
|
50
|
+
|
|
51
|
+
const handleToggle = async () => {
|
|
52
|
+
setLoading(true);
|
|
53
|
+
try {
|
|
54
|
+
const action = isActive ? "disable" : "enable";
|
|
55
|
+
|
|
56
|
+
await axios.post(`${resourceConfig.model}/${record.id}/${action}`);
|
|
57
|
+
|
|
58
|
+
notify(
|
|
59
|
+
isActive
|
|
60
|
+
? translate("serviceAccount.notify.disabled", "API key disabled")
|
|
61
|
+
: translate("serviceAccount.notify.enabled", "API key enabled"),
|
|
62
|
+
{ type: "success" }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
refresh();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("ServiceAccountStatusSwitch: toggle failed", error);
|
|
68
|
+
notify(
|
|
69
|
+
translate("serviceAccount.notify.toggle_error", "Could not change the key status"),
|
|
70
|
+
{ type: "error" }
|
|
71
|
+
);
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, my: 1 }}>
|
|
79
|
+
<FormControlLabel
|
|
80
|
+
control={
|
|
81
|
+
<Switch
|
|
82
|
+
checked={isActive}
|
|
83
|
+
onChange={handleToggle}
|
|
84
|
+
disabled={loading}
|
|
85
|
+
color="success"
|
|
86
|
+
/>
|
|
87
|
+
}
|
|
88
|
+
label={
|
|
89
|
+
isActive
|
|
90
|
+
? translate("serviceAccount.status.active", "Active")
|
|
91
|
+
: translate("serviceAccount.status.disabled", "Disabled")
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
{loading ? <CircularProgress size={16} /> : null}
|
|
95
|
+
</Box>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default ServiceAccountStatusSwitch;
|
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,37 @@ 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
|
+
},
|
|
528
608
|
};
|
|
529
609
|
|
|
530
610
|
export default {
|
package/src/providers/i18n/es.ts
CHANGED
|
@@ -318,8 +318,57 @@ const dashSpanish = {
|
|
|
318
318
|
auto_reset_notice: 'Esta pantalla se reiniciará automáticamente en 30 segundos',
|
|
319
319
|
loading: 'Cargando...',
|
|
320
320
|
},
|
|
321
|
+
// Service account API keys
|
|
322
|
+
serviceAccount: {
|
|
323
|
+
reveal: {
|
|
324
|
+
title: 'Copia tu API Key ahora',
|
|
325
|
+
warning:
|
|
326
|
+
'Esta es la única vez que se mostrará esta clave. No se puede recuperar más adelante — si la pierdes, tendrás que emitir una nueva.',
|
|
327
|
+
forKey: 'Clave',
|
|
328
|
+
copy: 'Copiar',
|
|
329
|
+
copied: 'Copiado',
|
|
330
|
+
dismiss: 'La he guardado',
|
|
331
|
+
},
|
|
332
|
+
status: {
|
|
333
|
+
active: 'Activa',
|
|
334
|
+
disabled: 'Deshabilitada',
|
|
335
|
+
},
|
|
336
|
+
expiration: {
|
|
337
|
+
'1': '1 día',
|
|
338
|
+
'7': '7 días',
|
|
339
|
+
'30': '30 días',
|
|
340
|
+
never: 'Nunca',
|
|
341
|
+
},
|
|
342
|
+
notify: {
|
|
343
|
+
enabled: 'API Key habilitada',
|
|
344
|
+
disabled: 'API Key deshabilitada',
|
|
345
|
+
toggle_error: 'No se pudo cambiar el estado de la API Key',
|
|
346
|
+
},
|
|
347
|
+
action: {
|
|
348
|
+
enable: 'Habilitar',
|
|
349
|
+
disable: 'Deshabilitar',
|
|
350
|
+
},
|
|
351
|
+
// Shown when a tenancy-level role is selected: such a key reaches every
|
|
352
|
+
// tenant in the account, so the tenant field stops being a boundary.
|
|
353
|
+
tenancy_role_warning:
|
|
354
|
+
'Este rol otorga acceso a todos los locales de la cuenta. El local seleccionado es informativo, no un límite de seguridad.',
|
|
355
|
+
},
|
|
321
356
|
// Resource labels for admin panel
|
|
322
357
|
resource: {
|
|
358
|
+
tenancy: {
|
|
359
|
+
serviceAccounts: {
|
|
360
|
+
label: 'Cuentas de Servicio',
|
|
361
|
+
name: 'Nombre',
|
|
362
|
+
tenant: 'Local',
|
|
363
|
+
roles: 'Roles',
|
|
364
|
+
expiration: 'Expiración',
|
|
365
|
+
status: 'Estado',
|
|
366
|
+
token: 'Clave',
|
|
367
|
+
expires_at: 'Expira',
|
|
368
|
+
last_used_at: 'Último uso',
|
|
369
|
+
created_at: 'Creado',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
323
372
|
// Groups
|
|
324
373
|
groups: {
|
|
325
374
|
system_resources: 'Recursos de Sistema',
|
|
@@ -531,6 +580,37 @@ const dashSpanish = {
|
|
|
531
580
|
has_modifiers: 'Personalizable',
|
|
532
581
|
no_more_products: 'No hay más productos',
|
|
533
582
|
},
|
|
583
|
+
// JSON Color Selector - extracción de tema desde imagen
|
|
584
|
+
colorSelector: {
|
|
585
|
+
imageExtractor: {
|
|
586
|
+
title_ai: 'Generador de Tema con IA',
|
|
587
|
+
title_local: 'Tema desde Imagen',
|
|
588
|
+
upload_prompt: 'Sube una Imagen para Extraer Colores',
|
|
589
|
+
upload_hint: 'Selecciona un archivo de imagen y extraeremos automáticamente una paleta de colores',
|
|
590
|
+
upload_button: 'Elegir Imagen',
|
|
591
|
+
drop_hint: 'o arrástrala aquí',
|
|
592
|
+
invalid_file_type: 'Por favor selecciona un archivo de imagen válido',
|
|
593
|
+
file_too_large: 'El archivo debe pesar menos de %{limit}MB',
|
|
594
|
+
extracting: 'Extrayendo colores de la imagen...',
|
|
595
|
+
extracted_colors: 'Colores Extraídos (%{count} colores encontrados)',
|
|
596
|
+
dominant_label: 'Dominante',
|
|
597
|
+
uploaded_image_alt: 'Imagen subida',
|
|
598
|
+
no_palette_error: 'Primero extrae los colores de una imagen',
|
|
599
|
+
theme_generation_failed: 'No se pudo generar el tema',
|
|
600
|
+
theme_description_label: 'Descripción del Tema (Opcional)',
|
|
601
|
+
theme_description_placeholder:
|
|
602
|
+
"Opcional: Describe el estilo de tema que deseas (ej: 'moderno y minimalista', 'cálido y acogedor', 'corporativo profesional')",
|
|
603
|
+
apply_colors_button: 'Aplicar Colores',
|
|
604
|
+
generate_button: 'Generar Tema con IA',
|
|
605
|
+
generating_button: 'Generando...',
|
|
606
|
+
apply_colors_help_label: 'Aplicar Colores:',
|
|
607
|
+
apply_colors_help_desc:
|
|
608
|
+
'Aplica la paleta extraída a los colores base de tu tema localmente — sin usar IA',
|
|
609
|
+
generate_help_label: 'Generar Tema con IA:',
|
|
610
|
+
generate_help_desc:
|
|
611
|
+
'Usa IA para crear un esquema de color completo basado en los colores extraídos y tu descripción',
|
|
612
|
+
},
|
|
613
|
+
},
|
|
534
614
|
};
|
|
535
615
|
|
|
536
616
|
export default {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge translation message trees.
|
|
3
|
+
*
|
|
4
|
+
* Apps compose their messages as `{ ...dashAdminEs, ...customSpanish }`. That
|
|
5
|
+
* spread is shallow, so any top-level key defined on BOTH sides is replaced
|
|
6
|
+
* wholesale by the app's version - every core key underneath it is silently
|
|
7
|
+
* dropped. Apps worked around this by hand-merging one extra level for
|
|
8
|
+
* `resource`, but that only moved the problem down a level: core's
|
|
9
|
+
* `resource.tenancy.*` is still erased by an app that defines any
|
|
10
|
+
* `resource.tenancy` key of its own.
|
|
11
|
+
*
|
|
12
|
+
* This merges recursively instead, so core can ship translations for a subtree
|
|
13
|
+
* an app also contributes to. App values still win on leaf collisions.
|
|
14
|
+
*
|
|
15
|
+
* Arrays are replaced, not concatenated - a translation array is a complete
|
|
16
|
+
* value (e.g. a list of month names), never something to append to.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* mergeTranslations(dashAdminEs, customSpanish)
|
|
20
|
+
* // core: resource.tenancy.serviceAccounts.label
|
|
21
|
+
* // app: resource.tenancy.tenants.label
|
|
22
|
+
* // -> both survive
|
|
23
|
+
*/
|
|
24
|
+
export type TranslationMessages = Record<string, any>;
|
|
25
|
+
|
|
26
|
+
const isPlainObject = (value: unknown): value is TranslationMessages =>
|
|
27
|
+
typeof value === 'object' &&
|
|
28
|
+
value !== null &&
|
|
29
|
+
!Array.isArray(value);
|
|
30
|
+
|
|
31
|
+
export const mergeTranslations = (
|
|
32
|
+
...sources: (TranslationMessages | undefined | null)[]
|
|
33
|
+
): TranslationMessages => {
|
|
34
|
+
const result: TranslationMessages = {};
|
|
35
|
+
|
|
36
|
+
for (const source of sources) {
|
|
37
|
+
if (!isPlainObject(source)) continue;
|
|
38
|
+
|
|
39
|
+
for (const key of Object.keys(source)) {
|
|
40
|
+
const incoming = source[key];
|
|
41
|
+
const existing = result[key];
|
|
42
|
+
|
|
43
|
+
result[key] =
|
|
44
|
+
isPlainObject(existing) && isPlainObject(incoming)
|
|
45
|
+
? mergeTranslations(existing, incoming)
|
|
46
|
+
: incoming;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default mergeTranslations;
|
|
@@ -45,10 +45,25 @@ export const ResourceTemplateCreate: FC<IResourceTemplate> = (props) => {
|
|
|
45
45
|
notify('dash.resource.created', { type: 'success' });
|
|
46
46
|
}
|
|
47
47
|
if (_showDialogAfterSubmit) {
|
|
48
|
+
// A resource may override the success dialog to surface values the
|
|
49
|
+
// API returns exactly once (e.g. a generated secret that is never
|
|
50
|
+
// recoverable afterwards).
|
|
51
|
+
const successOverride = resourceConfig?.createSuccessDialog?.(data);
|
|
52
|
+
|
|
53
|
+
// A string title is treated as a translation key (with itself as
|
|
54
|
+
// fallback), matching how labels and menu titles behave elsewhere.
|
|
55
|
+
// JSX titles are rendered as-is.
|
|
56
|
+
const overrideTitle =
|
|
57
|
+
typeof successOverride?.title === 'string'
|
|
58
|
+
? translate(successOverride.title, { _: successOverride.title })
|
|
59
|
+
: successOverride?.title;
|
|
60
|
+
|
|
48
61
|
dialog({
|
|
49
62
|
variant: 'info',
|
|
50
|
-
title: translate('dash.resource.created'),
|
|
51
|
-
content:
|
|
63
|
+
title: overrideTitle ?? translate('dash.resource.created'),
|
|
64
|
+
content:
|
|
65
|
+
successOverride?.content ??
|
|
66
|
+
translate('dash.resource.created_message', { label: translatedLabel }),
|
|
52
67
|
onConfirm: () => {
|
|
53
68
|
switch (resourceConfig?.redirectAfterCreate) {
|
|
54
69
|
case false:
|