data-primals-engine 1.4.3 → 1.5.1
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/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
|
@@ -33,8 +33,10 @@ const formatDate = (value, type, t) => {
|
|
|
33
33
|
|
|
34
34
|
const FlexDataRenderer = ({ value, fieldDefinition,data }) => {
|
|
35
35
|
const { t, i18n } = useTranslation();
|
|
36
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
36
37
|
const {models} = useModelContext()
|
|
37
38
|
const {me} = useAuthContext()
|
|
39
|
+
|
|
38
40
|
// 'translatedLabel' is added by getFieldDefinitionByPath
|
|
39
41
|
const field = fieldDefinition;
|
|
40
42
|
const translatedLabel = t('model_'+field.name, field.name);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useModelContext } from './contexts/ModelContext.jsx';
|
|
3
|
+
import FlexDataRenderer from './FlexDataRenderer.jsx';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders a flexible data card, typically generated by the AI assistant.
|
|
8
|
+
* It displays a set of fields for a single data record.
|
|
9
|
+
* @param {object} props
|
|
10
|
+
* @param {object} props.config - The configuration for the view.
|
|
11
|
+
* @param {string} props.config.title - The title of the card.
|
|
12
|
+
* @param {string} props.config.model - The name of the data model.
|
|
13
|
+
* @param {string[]} props.config.fields - An array of field names to display.
|
|
14
|
+
* @param {object} props.config.data - The data record to display.
|
|
15
|
+
*/
|
|
16
|
+
const FlexViewCard = ({ config }) => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const { models } = useModelContext();
|
|
19
|
+
const { title, model: modelName, fields: fieldNames, data } = config;
|
|
20
|
+
|
|
21
|
+
const model = models.find(m => m.name === modelName);
|
|
22
|
+
|
|
23
|
+
if (!model || !data) {
|
|
24
|
+
return <div className="flex-view-card-error">{t('assistant.flexView.loadingError', 'Erreur de chargement de la vue.')}</div>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fieldsToDisplay = model.fields.filter(field => fieldNames.includes(field.name));
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex-view-card">
|
|
31
|
+
{title && <h4>{title}</h4>}
|
|
32
|
+
<div className="data-card-content">
|
|
33
|
+
{fieldsToDisplay.map(fieldDef => (
|
|
34
|
+
<div key={fieldDef.name} className="data-field-row">
|
|
35
|
+
<strong className="field-label" title={fieldDef.name}>{t(`field_${model.name}_${fieldDef.name}`, fieldDef.name)}:</strong>
|
|
36
|
+
<div className="field-value"><FlexDataRenderer value={data[fieldDef.name]} fieldDefinition={fieldDef} data={data} /></div>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default FlexViewCard;
|
|
@@ -6,12 +6,20 @@ import {Trans, useTranslation} from "react-i18next";
|
|
|
6
6
|
import "./HistoryDialog.scss";
|
|
7
7
|
import {Tooltip} from "react-tooltip";
|
|
8
8
|
import {CodeField} from "./Field.jsx";
|
|
9
|
+
import {Pagination} from "./Pagination.jsx";
|
|
10
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
9
11
|
|
|
10
12
|
// Helper to render values in a friendly way for the summary list
|
|
11
|
-
const renderSummaryValue = (value) => {
|
|
13
|
+
const renderSummaryValue = (value, field, t) => {
|
|
12
14
|
if (value === undefined || value === null) {
|
|
13
15
|
return <i className="value-empty">empty</i>;
|
|
14
16
|
}
|
|
17
|
+
if (field?.type === 'string_t' && t) {
|
|
18
|
+
// For string_t, the stored value is the key. We display the translated value.
|
|
19
|
+
const key = (typeof value === 'object' && value?.key) ? value.key : String(value);
|
|
20
|
+
const displayValue = t(key, key);
|
|
21
|
+
return <span className="value-text" title={key}>{displayValue}</span>;
|
|
22
|
+
}
|
|
15
23
|
if (typeof value === 'boolean') {
|
|
16
24
|
return <code className="value-boolean">{value.toString()}</code>;
|
|
17
25
|
}
|
|
@@ -31,16 +39,26 @@ const renderSummaryValue = (value) => {
|
|
|
31
39
|
// New sub-component for rendering a single field change
|
|
32
40
|
const ChangeDetail = ({ field, from, to }) => {
|
|
33
41
|
const { t } = useTranslation();
|
|
42
|
+
const { selectedModel } = useModelContext();
|
|
43
|
+
const fieldObj = selectedModel?.fields?.find(f => f.name === field);
|
|
34
44
|
|
|
35
|
-
const
|
|
36
|
-
|
|
45
|
+
const getTooltipText = (value, field) => {
|
|
46
|
+
if (field?.type === 'string_t') {
|
|
47
|
+
const key = (typeof value === 'object' && value?.key) ? value.key : String(value);
|
|
48
|
+
return t(key, key); // Affiche la valeur traduite dans la tooltip
|
|
49
|
+
}
|
|
50
|
+
return (typeof value === 'object' && value !== null) ? JSON.stringify(value, null, 2) : String(value);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const tt = getTooltipText(to, fieldObj);
|
|
54
|
+
const f = getTooltipText(from, fieldObj);
|
|
37
55
|
return (
|
|
38
56
|
<div className="change-detail">
|
|
39
57
|
<strong className="change-field-name" title={field}>{field}</strong>
|
|
40
58
|
<div className="change-values">
|
|
41
|
-
<div className="value-from" data-tooltip-id={"changeTooltip"} data-tooltip-content={f}>{renderSummaryValue(from)}</div>
|
|
59
|
+
<div className="value-from" data-tooltip-id={"changeTooltip"} data-tooltip-content={f}>{renderSummaryValue(from, fieldObj, t)}</div>
|
|
42
60
|
<div className="change-arrow">→</div>
|
|
43
|
-
<div className="value-to" data-tooltip-id={"changeTooltip"} data-tooltip-content={tt}>{renderSummaryValue(to)}</div>
|
|
61
|
+
<div className="value-to" data-tooltip-id={"changeTooltip"} data-tooltip-content={tt}>{renderSummaryValue(to, fieldObj, t)}</div>
|
|
44
62
|
</div>
|
|
45
63
|
</div>
|
|
46
64
|
);
|
|
@@ -49,6 +67,7 @@ const ChangeDetail = ({ field, from, to }) => {
|
|
|
49
67
|
// Sub-component for displaying a single history entry
|
|
50
68
|
const HistoryEntry = ({ entry, onPreview, isSelected }) => {
|
|
51
69
|
const { t } = useTranslation();
|
|
70
|
+
const { selectedModel } = useModelContext();
|
|
52
71
|
// The backend has transformed the data for us.
|
|
53
72
|
// _id: history doc id, _v: version, _op: operation type (i,u,d), _rid: original record id
|
|
54
73
|
const { _id, _v, _op, _updatedAt, _user, ...data } = entry; // ...data collects the remaining fields
|
|
@@ -83,12 +102,13 @@ const HistoryEntry = ({ entry, onPreview, isSelected }) => {
|
|
|
83
102
|
}
|
|
84
103
|
return (
|
|
85
104
|
<div className="snapshot-list">
|
|
86
|
-
{snapshotEntries.map(([key, value]) =>
|
|
87
|
-
|
|
105
|
+
{snapshotEntries.map(([key, value]) => {
|
|
106
|
+
const fieldObj = selectedModel?.fields?.find(f => f.name === key);
|
|
107
|
+
return (<div className="snapshot-item" key={key}>
|
|
88
108
|
<strong className="snapshot-key" title={key}>{key}:</strong>
|
|
89
|
-
<span className="snapshot-value">{renderSummaryValue(value)}</span>
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
109
|
+
<span className="snapshot-value">{renderSummaryValue(value, fieldObj, t)}</span>
|
|
110
|
+
</div>);
|
|
111
|
+
})}
|
|
92
112
|
</div>
|
|
93
113
|
);
|
|
94
114
|
};
|
|
@@ -118,6 +138,9 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
|
118
138
|
const [previewData, setPreviewData] = useState(null);
|
|
119
139
|
const [previewLoading, setPreviewLoading] = useState(false);
|
|
120
140
|
const [selectedVersion, setSelectedVersion] = useState(null);
|
|
141
|
+
const [page, setPage] = useState(1);
|
|
142
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
143
|
+
const elementsPerPage = 10;
|
|
121
144
|
const { t, i18n } = useTranslation();
|
|
122
145
|
|
|
123
146
|
const fetchHistory = useCallback(async () => {
|
|
@@ -125,10 +148,12 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
|
125
148
|
setLoading(true);
|
|
126
149
|
setError(null);
|
|
127
150
|
try {
|
|
128
|
-
const
|
|
151
|
+
const params = new URLSearchParams({ page, limit: elementsPerPage });
|
|
152
|
+
const query = await fetch(`/api/data/history/${modelName}/${recordId}?${params.toString()}`);
|
|
129
153
|
const response = await query.json();
|
|
130
154
|
if (response.success) {
|
|
131
155
|
setHistory(response.data);
|
|
156
|
+
setTotalCount(response.count || 0);
|
|
132
157
|
} else {
|
|
133
158
|
setError(response.error || i18n.t("history.error", "Could not load history."));
|
|
134
159
|
}
|
|
@@ -138,7 +163,7 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
|
138
163
|
} finally {
|
|
139
164
|
setLoading(false);
|
|
140
165
|
}
|
|
141
|
-
}, [modelName, recordId, i18n]);
|
|
166
|
+
}, [modelName, recordId, i18n, page, elementsPerPage]);
|
|
142
167
|
|
|
143
168
|
useEffect(() => {
|
|
144
169
|
fetchHistory();
|
|
@@ -208,7 +233,7 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
|
208
233
|
<div className="history-list-container">
|
|
209
234
|
{loading && <div><Trans i18nKey={"history.loading"}>Chargement de l'historique..</Trans></div>}
|
|
210
235
|
{error && <div className="error-message">{error}</div>}
|
|
211
|
-
{!loading && !error && (
|
|
236
|
+
{!loading && !error && (<>
|
|
212
237
|
<div className="history-list">
|
|
213
238
|
{history.length > 0 ? (
|
|
214
239
|
history.map(entry =>
|
|
@@ -222,7 +247,15 @@ export const HistoryDialog = ({ modelName, recordId, onClose }) => {
|
|
|
222
247
|
<div><Trans i18nKey={"history.noHistory"}>Aucun historique trouvé pour cet enregistrement.</Trans></div>
|
|
223
248
|
)}
|
|
224
249
|
</div>
|
|
225
|
-
|
|
250
|
+
{totalCount > elementsPerPage && <Pagination
|
|
251
|
+
totalCount={totalCount}
|
|
252
|
+
page={page}
|
|
253
|
+
setPage={setPage}
|
|
254
|
+
elementsPerPage={elementsPerPage}
|
|
255
|
+
visibleItemsCount={5}
|
|
256
|
+
hasPreviousNext={true}
|
|
257
|
+
/>}
|
|
258
|
+
</>)}
|
|
226
259
|
</div>
|
|
227
260
|
<div className="history-preview-container">
|
|
228
261
|
<div className="preview-header">
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Dialog } from './Dialog.jsx';
|
|
4
|
+
import {TextField, SelectField, NumberField, CodeField, ModelField} from './Field.jsx';
|
|
5
|
+
import Button from './Button.jsx';
|
|
6
|
+
import { FaSave, FaTimes } from 'react-icons/fa';
|
|
7
|
+
import './HtmlViewBuilderModal.scss';
|
|
8
|
+
|
|
9
|
+
const HtmlViewBuilderModal = ({ isOpen, onClose, onSave, models, initialConfig }) => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const [config, setConfig] = useState({});
|
|
12
|
+
const [jsonError, setJsonError] = useState('');
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isOpen) {
|
|
16
|
+
setConfig(initialConfig || {
|
|
17
|
+
title: '',
|
|
18
|
+
model: '',
|
|
19
|
+
limit: 10,
|
|
20
|
+
filter: {},
|
|
21
|
+
template: '',
|
|
22
|
+
css: '',
|
|
23
|
+
...initialConfig
|
|
24
|
+
});
|
|
25
|
+
setJsonError('');
|
|
26
|
+
}
|
|
27
|
+
}, [isOpen, initialConfig]);
|
|
28
|
+
|
|
29
|
+
const modelOptions = useMemo(() => {
|
|
30
|
+
return (models || []).map(m => ({ label: m.name, value: m.name }));
|
|
31
|
+
}, [models]);
|
|
32
|
+
|
|
33
|
+
const handleChange = (e) => {
|
|
34
|
+
const { name, value } = e.target || e;
|
|
35
|
+
setConfig(prev => ({ ...prev, [name]: value }));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleCodeChange = (name, value) => {
|
|
39
|
+
setConfig(prev => ({ ...prev, [name]: value }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleFilterChange = (codeValue) => {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(codeValue);
|
|
45
|
+
setConfig(prev => ({ ...prev, filter: parsed }));
|
|
46
|
+
setJsonError('');
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setJsonError(t('dashboards.invalidJson', 'JSON invalide'));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleSave = () => {
|
|
53
|
+
if (!config.model || !config.template) {
|
|
54
|
+
console.error("Model and template are required.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (jsonError) return;
|
|
58
|
+
onSave(config);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!isOpen) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Dialog
|
|
65
|
+
isModal={true}
|
|
66
|
+
isClosable={true}
|
|
67
|
+
onClose={onClose}
|
|
68
|
+
title={initialConfig?.id ? t('dashboards.editHtmlViewTitle', 'Modifier la Vue HTML') : t('dashboards.addHtmlViewTitle', 'Ajouter une Vue HTML')}
|
|
69
|
+
className="html-view-builder-modal"
|
|
70
|
+
>
|
|
71
|
+
<div className="form-grid">
|
|
72
|
+
<TextField name="title" label={t('dashboards.widgetTitle', 'Titre du widget')} value={config.title || ''} onChange={handleChange} />
|
|
73
|
+
<ModelField name="model" label={t('model', 'Modèle')} value={config.model || ''} onChange={(option) => handleChange({ name: 'model', value: option?.value })} required />
|
|
74
|
+
<NumberField name="limit" label={t('dashboards.dataLimit', 'Limite de données')} value={config.limit ?? 10} onChange={handleChange} min={1} max={100} />
|
|
75
|
+
<CodeField name="filter" label={t('filter', 'Filtre (JSON)')} value={typeof config.filter === 'string' ? config.filter : JSON.stringify(config.filter || {}, null, 2)} language="json" onChange={(e) => handleFilterChange(e.value)} error={jsonError} />
|
|
76
|
+
<CodeField name="template" label={t('dashboards.htmlTemplate', 'Template HTML')} value={config.template || ''} language="html" onChange={(e) => handleCodeChange('template', e.value)} required height="200px" />
|
|
77
|
+
<CodeField name="css" label={t('dashboards.cssStyles', 'Styles CSS')} value={config.css || ''} language="css" onChange={(e) => handleCodeChange('css', e.value)} height="150px" />
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex actions right">
|
|
80
|
+
<Button onClick={onClose} className="btn-secondary">
|
|
81
|
+
<FaTimes /> {t('btns.cancel', 'Annuler')}
|
|
82
|
+
</Button>
|
|
83
|
+
<Button onClick={handleSave} className="btn-primary" disabled={!!jsonError}>
|
|
84
|
+
<FaSave /> {t('btns.save', 'Enregistrer')}
|
|
85
|
+
</Button>
|
|
86
|
+
</div>
|
|
87
|
+
</Dialog>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default HtmlViewBuilderModal;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.html-view-builder-modal {
|
|
2
|
+
.form-grid {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: 1fr;
|
|
5
|
+
gap: 1rem;
|
|
6
|
+
padding-bottom: 1rem;
|
|
7
|
+
|
|
8
|
+
.code-editor-container {
|
|
9
|
+
grid-column: 1 / -1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.actions {
|
|
14
|
+
margin-top: 1rem;
|
|
15
|
+
padding-top: 1rem;
|
|
16
|
+
border-top: 1px solid var(--border-color);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, {useEffect, useMemo} from 'react';
|
|
2
|
+
import './HtmlViewCard.scss';
|
|
3
|
+
import {renderHtmlTemplate} from "./DashboardHtmlViewItem.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Affiche une vue HTML personnalisée, généralement générée par l'assistant IA.
|
|
7
|
+
* @param {object} props
|
|
8
|
+
* @param {object} props.config - La configuration de la vue.
|
|
9
|
+
* @param {string} props.config.title - Le titre de la carte.
|
|
10
|
+
* @param {string} props.config.template - La chaîne de template HTML Handlebars.
|
|
11
|
+
* @param {Array<object>} props.config.data - Le tableau d'enregistrements de données à afficher.
|
|
12
|
+
*/
|
|
13
|
+
const HtmlViewCard = ({ config }) => {
|
|
14
|
+
const { title, template, data, css } = config;
|
|
15
|
+
|
|
16
|
+
// Génère un ID unique et stable pour ce composant
|
|
17
|
+
const containerId = useMemo(() => `html-view-${Math.random().toString(36).substring(2, 9)}`, []);
|
|
18
|
+
|
|
19
|
+
// Effet pour injecter et nettoyer le CSS
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!css) return;
|
|
22
|
+
|
|
23
|
+
const styleElement = document.createElement('style');
|
|
24
|
+
// Remplace le placeholder par notre ID unique pour scoper le CSS
|
|
25
|
+
styleElement.innerHTML = css.replace(/\{\{containerId\}\}/g, containerId);
|
|
26
|
+
document.head.appendChild(styleElement);
|
|
27
|
+
|
|
28
|
+
// Fonction de nettoyage : retire le style quand le composant est démonté
|
|
29
|
+
return () => {
|
|
30
|
+
document.head.removeChild(styleElement);
|
|
31
|
+
};
|
|
32
|
+
}, [css, containerId]);
|
|
33
|
+
|
|
34
|
+
const renderedHtml = renderHtmlTemplate(template, data);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div id={containerId} className="html-view-card">
|
|
38
|
+
{title && <h4>{title}</h4>}
|
|
39
|
+
<div className="html-view-content" dangerouslySetInnerHTML={{ __html: renderedHtml }} />
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default HtmlViewCard;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.html-view-card {
|
|
2
|
+
background-color: var(--bg-color-dark);
|
|
3
|
+
border: 1px solid var(--border-color);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
margin-top: 12px;
|
|
6
|
+
font-size: 0.9rem;
|
|
7
|
+
|
|
8
|
+
h4 {
|
|
9
|
+
margin-top: 0;
|
|
10
|
+
margin-bottom: 12px;
|
|
11
|
+
color: var(--text-color-light);
|
|
12
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
13
|
+
padding-bottom: 8px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.html-view-content {
|
|
17
|
+
ul, ol {
|
|
18
|
+
padding-left: 20px;
|
|
19
|
+
margin-top: 8px;
|
|
20
|
+
margin-bottom: 8px;
|
|
21
|
+
}
|
|
22
|
+
li { margin-bottom: 4px; }
|
|
23
|
+
a {
|
|
24
|
+
color: var(--primary-color);
|
|
25
|
+
text-decoration: none;
|
|
26
|
+
&:hover { text-decoration: underline; }
|
|
27
|
+
}
|
|
28
|
+
h1, h2, h3 {
|
|
29
|
+
margin-top: 1em;
|
|
30
|
+
margin-bottom: 0.5em;
|
|
31
|
+
color: var(--text-color);
|
|
32
|
+
}
|
|
33
|
+
p { line-height: 1.5; }
|
|
34
|
+
}
|
|
35
|
+
}
|
package/client/src/KPIDialog.jsx
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Dialog } from './Dialog.jsx'; // Assure-toi que Dialog est importé
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import {FaAd, FaPlus} from "react-icons/fa";
|
|
5
|
+
import {getUserHash} from "../../src/data.js";
|
|
6
|
+
import {useNavigate} from "react-router-dom";
|
|
7
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
4
8
|
|
|
5
9
|
function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
|
|
6
10
|
const { t } = useTranslation();
|
|
7
11
|
const [searchTerm, setSearchTerm] = useState('');
|
|
8
|
-
|
|
12
|
+
const nav = useNavigate()
|
|
13
|
+
const {me} = useAuthContext()
|
|
9
14
|
const filteredKpis = (availableKpis || []).filter(kpi =>
|
|
10
15
|
kpi.name.value.toLowerCase().includes(searchTerm.toLowerCase())
|
|
11
16
|
);
|
|
@@ -32,6 +37,11 @@ function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
|
|
|
32
37
|
<li>{t('dashboards.no_kpi_found', 'Aucun KPI disponible ou correspondant.')}</li>
|
|
33
38
|
)}
|
|
34
39
|
</ul>
|
|
40
|
+
<div className={"flex mg-2"}>
|
|
41
|
+
<button onClick={() => {
|
|
42
|
+
nav(`/user/${getUserHash(me)}/?model=kpi`);
|
|
43
|
+
}} className={"btn"}><FaPlus />{t('dashboards.addKpi', 'Nouvel indicateur KPI')}</button>
|
|
44
|
+
</div>
|
|
35
45
|
</Dialog>
|
|
36
46
|
);
|
|
37
47
|
}
|
|
@@ -14,7 +14,6 @@ const KanbanCard = ({ card, model, subItemsField }) => {
|
|
|
14
14
|
// Logique du Drag & Drop natif pour la carte
|
|
15
15
|
const lang = (tr.i18n.resolvedLanguage || tr.i18n.language).split(/[-_]/)?.[0];
|
|
16
16
|
|
|
17
|
-
console.log(subItems)
|
|
18
17
|
const getfield = (model, data) => {
|
|
19
18
|
const v = getDataAsString(model, data, tr, models);
|
|
20
19
|
if(!v){
|
|
@@ -26,7 +25,7 @@ const KanbanCard = ({ card, model, subItemsField }) => {
|
|
|
26
25
|
<strong>guid</strong> : ${it.guid}<br />
|
|
27
26
|
<strong>type</strong> : ${it.mimeType}<br />
|
|
28
27
|
<strong>size</strong> : ${it.size} bytes<br />
|
|
29
|
-
<strong>timestamp</strong> : ${it.
|
|
28
|
+
<strong>timestamp</strong> : ${it.createdAt ? new Date(it.createdAt).toLocaleString(lang) : ''}
|
|
30
29
|
`;
|
|
31
30
|
return <a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
|
|
32
31
|
data-tooltip-id={"tooltipFile"} data-tooltip-html={r}
|
|
@@ -225,7 +225,7 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
|
|
|
225
225
|
delete field['_isNewField'];
|
|
226
226
|
let otherFields = [];
|
|
227
227
|
// Check for specific field types
|
|
228
|
-
switch (field.type) {
|
|
228
|
+
switch ((field.type)) {
|
|
229
229
|
case 'relation':
|
|
230
230
|
otherFields = ['relation', 'multiple', 'relationFilter'];
|
|
231
231
|
break;
|
|
@@ -245,10 +245,11 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
|
|
|
245
245
|
case 'phone':
|
|
246
246
|
case 'password':
|
|
247
247
|
case 'code':
|
|
248
|
-
|
|
248
|
+
const t = (field.itemsType || field.type);
|
|
249
|
+
if (t === 'code')
|
|
249
250
|
otherFields = ['maxlength', 'language', 'conditionBuilder'];
|
|
250
|
-
else if( ['string_t', 'string'].includes(
|
|
251
|
-
otherFields = ['maxlength', 'multiline'];
|
|
251
|
+
else if( ['string_t', 'string'].includes(t))
|
|
252
|
+
otherFields = ['maxlength', 'multiline', 'mask', 'replacement'];
|
|
252
253
|
else
|
|
253
254
|
otherFields = ['maxlength'];
|
|
254
255
|
break;
|
|
@@ -660,11 +661,10 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
|
|
|
660
661
|
{/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
|
|
661
662
|
{(showModel || !useAI || initialModel) && (
|
|
662
663
|
<div className="actions flex">
|
|
663
|
-
|
|
664
|
+
|
|
664
665
|
<Button type="button" onClick={handleAddField}>
|
|
665
666
|
<FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
|
|
666
667
|
</Button>
|
|
667
|
-
)}
|
|
668
668
|
|
|
669
669
|
<Button type="submit">
|
|
670
670
|
<FaSave />
|