@applica-software-guru/react-admin 1.2.123 → 1.3.126
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/bitbucket-pipelines.yml +3 -3
- package/dist/ApplicaAdmin.d.ts +1 -1
- package/dist/ApplicaAdmin.d.ts.map +1 -1
- package/dist/components/Notification.d.ts +2 -2
- package/dist/components/ra-forms/TabbedForm.d.ts +2 -2
- package/dist/components/ra-forms/TableForm/TableFormIterator.d.ts +2 -2
- package/dist/components/ra-forms/TableForm/TableFormIterator.d.ts.map +1 -1
- package/dist/components/ra-lists/Datagrid.d.ts +2 -2
- package/dist/components/ra-pages/GenericErrorPage.d.ts +3 -0
- package/dist/components/ra-pages/GenericErrorPage.d.ts.map +1 -0
- package/dist/components/ra-pages/index.d.ts +5 -5
- package/dist/components/ra-pages/index.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useMemoizedObject.d.ts +10 -0
- package/dist/hooks/useMemoizedObject.d.ts.map +1 -0
- package/dist/i18n/MissingMessageHandler.d.ts +19 -0
- package/dist/i18n/MissingMessageHandler.d.ts.map +1 -0
- package/dist/i18n/createI18nProvider.d.ts +8 -5
- package/dist/i18n/createI18nProvider.d.ts.map +1 -1
- package/dist/i18n/index.d.ts +0 -1
- package/dist/i18n/useI18nProvider.d.ts +5 -5
- package/dist/i18n/useI18nProvider.d.ts.map +1 -1
- package/dist/react-admin.cjs.js +62 -62
- package/dist/react-admin.cjs.js.map +1 -1
- package/dist/react-admin.es.js +10912 -10864
- package/dist/react-admin.es.js.map +1 -1
- package/dist/react-admin.umd.js +64 -64
- package/dist/react-admin.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/ApplicaAdmin.tsx +20 -44
- package/src/assets/genericError.png +0 -0
- package/src/components/ra-forms/TableForm/TableFormIterator.tsx +76 -27
- package/src/components/ra-forms/TableForm/TableFormIteratorItem.tsx +2 -2
- package/src/components/ra-pages/GenericErrorPage.tsx +23 -0
- package/src/components/ra-pages/index.ts +5 -5
- package/src/hooks/index.jsx +1 -0
- package/src/hooks/useMemoizedObject.tsx +27 -0
- package/src/i18n/MissingMessageHandler.ts +60 -0
- package/src/i18n/createI18nProvider.tsx +97 -0
- package/src/i18n/index.jsx +0 -1
- package/src/i18n/useI18nProvider.tsx +23 -0
- package/dist/i18n/useI18nCatcher.d.ts +0 -28
- package/dist/i18n/useI18nCatcher.d.ts.map +0 -1
- package/src/i18n/createI18nProvider.jsx +0 -15
- package/src/i18n/useI18nCatcher.ts +0 -109
- package/src/i18n/useI18nProvider.jsx +0 -4
package/package.json
CHANGED
package/src/ApplicaAdmin.tsx
CHANGED
|
@@ -1,33 +1,14 @@
|
|
|
1
1
|
import { AdminProps, AuthProvider, DataProvider } from 'react-admin';
|
|
2
2
|
import { AppConfigProvider, MenuConfigProvider, MenuPropTypes, ThemeConfig, ThemeConfigProvider } from './contexts';
|
|
3
3
|
import { CatchResult, useCliErrorCatcher } from './dev';
|
|
4
|
-
import { Layout, LoginPage, MainIcon, Notification, SmallIcon } from './components';
|
|
4
|
+
import { GenericErrorPage, Layout, LoginPage, MainIcon, Notification, SmallIcon } from './components';
|
|
5
5
|
import React, { useMemo } from 'react';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { useI18nProvider } from './i18n';
|
|
8
7
|
import Admin from './Admin';
|
|
9
|
-
import { I18nCatcherBodyBuilderResultProps } from './i18n/useI18nCatcher';
|
|
10
8
|
import { MenuProps } from './types';
|
|
11
9
|
import PropTypes from 'prop-types';
|
|
12
10
|
import { QueryClient } from 'react-query';
|
|
13
|
-
|
|
14
|
-
const languageResponseMapper = (response: any) =>
|
|
15
|
-
response.reduce(
|
|
16
|
-
(messages: any, message: any) => ({
|
|
17
|
-
...messages,
|
|
18
|
-
[message.lang]: {
|
|
19
|
-
...(messages[message.lang] || {}),
|
|
20
|
-
[message.code]: message.text
|
|
21
|
-
}
|
|
22
|
-
}),
|
|
23
|
-
{}
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const i18nBodyRequestMapper = (lang: string, message: string): I18nCatcherBodyBuilderResultProps => ({
|
|
27
|
-
lang,
|
|
28
|
-
code: message,
|
|
29
|
-
text: message
|
|
30
|
-
});
|
|
11
|
+
import { ThemeCustomization } from './themes';
|
|
31
12
|
|
|
32
13
|
const queryClient = new QueryClient({
|
|
33
14
|
defaultOptions: {
|
|
@@ -194,19 +175,6 @@ const ApplicaAdmin = ({
|
|
|
194
175
|
enablePasswordRecover,
|
|
195
176
|
...props
|
|
196
177
|
}: ApplicaAdminProps) => {
|
|
197
|
-
const languages = useI18nLanguages({
|
|
198
|
-
apiUrl,
|
|
199
|
-
endpoint: '/i18n/messages',
|
|
200
|
-
mapper: languageResponseMapper
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
useI18nCatcher({
|
|
204
|
-
bodyBuilder: i18nBodyRequestMapper,
|
|
205
|
-
apiUrl,
|
|
206
|
-
endpoint: '/i18n/message',
|
|
207
|
-
loading: languages?.loading,
|
|
208
|
-
enabled: development
|
|
209
|
-
});
|
|
210
178
|
useCliErrorCatcher({
|
|
211
179
|
apiUrl,
|
|
212
180
|
endpoint: '/cli/error',
|
|
@@ -261,17 +229,25 @@ const ApplicaAdmin = ({
|
|
|
261
229
|
},
|
|
262
230
|
[logoMain, logoIcon, name, version, notification, enableNotification]
|
|
263
231
|
);
|
|
232
|
+
const i18nProvider = useI18nProvider({
|
|
233
|
+
apiUrl: apiUrl,
|
|
234
|
+
defaultLocale: defaultLocale,
|
|
235
|
+
allowMissing: development,
|
|
236
|
+
createMissing: development
|
|
237
|
+
});
|
|
264
238
|
|
|
265
|
-
if (
|
|
266
|
-
return
|
|
239
|
+
if (i18nProvider === undefined) {
|
|
240
|
+
return <></>;
|
|
241
|
+
} else if (i18nProvider.error) {
|
|
242
|
+
return (
|
|
243
|
+
<ThemeConfigProvider initialConfig={themeConfig}>
|
|
244
|
+
<ThemeCustomization themeOverrides={theme}>
|
|
245
|
+
<GenericErrorPage />
|
|
246
|
+
</ThemeCustomization>
|
|
247
|
+
</ThemeConfigProvider>
|
|
248
|
+
);
|
|
267
249
|
}
|
|
268
250
|
|
|
269
|
-
const i18nProivder = createI18nProvider({
|
|
270
|
-
languages,
|
|
271
|
-
defaultLocale,
|
|
272
|
-
allowMissing: development
|
|
273
|
-
});
|
|
274
|
-
|
|
275
251
|
return (
|
|
276
252
|
<AppConfigProvider>
|
|
277
253
|
<MenuConfigProvider menu={menu}>
|
|
@@ -283,7 +259,7 @@ const ApplicaAdmin = ({
|
|
|
283
259
|
queryClient={queryClient}
|
|
284
260
|
dataProvider={dataProvider}
|
|
285
261
|
authProvider={authProvider}
|
|
286
|
-
i18nProvider={
|
|
262
|
+
i18nProvider={i18nProvider}
|
|
287
263
|
loginPage={login}
|
|
288
264
|
{...props}
|
|
289
265
|
/>
|
|
Binary file
|
|
@@ -15,14 +15,14 @@ import {
|
|
|
15
15
|
TableRow,
|
|
16
16
|
Typography
|
|
17
17
|
} from '@mui/material';
|
|
18
|
-
import { RaRecord, useRecordContext, useTranslate, useTranslateLabel } from 'ra-core';
|
|
18
|
+
import { FormDataConsumer, RaRecord, useRecordContext, useTranslate, useTranslateLabel } from 'ra-core';
|
|
19
19
|
import { styled, useTheme } from '@mui/material/styles';
|
|
20
20
|
|
|
21
21
|
import { PlusCircleOutlined } from '@ant-design/icons';
|
|
22
22
|
import PropTypes from 'prop-types';
|
|
23
23
|
import { TableFormIteratorContext } from './TableFormIteratorContext';
|
|
24
24
|
import { TableFormIteratorItem } from './TableFormIteratorItem';
|
|
25
|
-
import { UseFieldArrayReturn } from 'react-hook-form';
|
|
25
|
+
import { UseFieldArrayReturn, useFormContext } from 'react-hook-form';
|
|
26
26
|
import get from 'lodash/get';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -44,13 +44,27 @@ import get from 'lodash/get';
|
|
|
44
44
|
* @returns
|
|
45
45
|
*/
|
|
46
46
|
const TableFormIterator = (props: TableFormIteratorProps) => {
|
|
47
|
-
const {
|
|
47
|
+
const {
|
|
48
|
+
children,
|
|
49
|
+
resource,
|
|
50
|
+
source,
|
|
51
|
+
label,
|
|
52
|
+
disableActions = false,
|
|
53
|
+
disableAdd = false,
|
|
54
|
+
disableRemove = false,
|
|
55
|
+
className,
|
|
56
|
+
empty,
|
|
57
|
+
template = {}
|
|
58
|
+
} = props;
|
|
48
59
|
const [confirmIsOpen, setConfirmIsOpen] = useState<boolean>(false);
|
|
49
|
-
const { fields, remove, replace } = useArrayInput(props);
|
|
60
|
+
const { fields, remove, replace, append } = useArrayInput(props);
|
|
61
|
+
const { resetField } = useFormContext();
|
|
62
|
+
|
|
50
63
|
const theme = useTheme();
|
|
51
64
|
const translate = useTranslate();
|
|
52
65
|
const record = useRecordContext(props);
|
|
53
|
-
const initialDefaultValue = useRef({});
|
|
66
|
+
const initialDefaultValue = useRef(template || {});
|
|
67
|
+
|
|
54
68
|
const translateLabel = useTranslateLabel();
|
|
55
69
|
const removeField = useCallback(
|
|
56
70
|
(index: number) => {
|
|
@@ -66,6 +80,50 @@ const TableFormIterator = (props: TableFormIteratorProps) => {
|
|
|
66
80
|
for (const k in initialDefaultValue.current) initialDefaultValue.current[k] = null;
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
const addField = useCallback(
|
|
84
|
+
(item: any = undefined) => {
|
|
85
|
+
let defaultValue = item;
|
|
86
|
+
if (item == null) {
|
|
87
|
+
defaultValue = initialDefaultValue.current;
|
|
88
|
+
if (
|
|
89
|
+
Children.count(children) === 1 &&
|
|
90
|
+
React.isValidElement(Children.only(children)) &&
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
!Children.only(children).props.source &&
|
|
93
|
+
// Make sure it's not a FormDataConsumer
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
Children.map(children, (input) => React.isValidElement(input) && input.type !== FormDataConsumer).some(Boolean)
|
|
96
|
+
) {
|
|
97
|
+
// ArrayInput used for an array of scalar values
|
|
98
|
+
// (e.g. tags: ['foo', 'bar'])
|
|
99
|
+
defaultValue = '';
|
|
100
|
+
} else {
|
|
101
|
+
// ArrayInput used for an array of objects
|
|
102
|
+
// (e.g. authors: [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Doe' }])
|
|
103
|
+
defaultValue = defaultValue || ({} as Record<string, unknown>);
|
|
104
|
+
Children.forEach(children, (input) => {
|
|
105
|
+
if (React.isValidElement(input) && input.type !== FormDataConsumer && input.props.source) {
|
|
106
|
+
defaultValue[input.props.source] = input.props.defaultValue ?? null;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const newField = { ...defaultValue, ...template };
|
|
112
|
+
append(newField);
|
|
113
|
+
// Make sure the newly added inputs are not considered dirty by react-hook-form
|
|
114
|
+
resetField(`${source}.${fields.length}`, { defaultValue });
|
|
115
|
+
},
|
|
116
|
+
[append, children, resetField, source, fields.length, template]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// add field and call the onClick event of the button passed as addButton prop
|
|
120
|
+
const handleAddButtonClick = (originalOnClickHandler: any) => (event: MouseEvent) => {
|
|
121
|
+
addField();
|
|
122
|
+
if (originalOnClickHandler) {
|
|
123
|
+
originalOnClickHandler(event);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
69
127
|
const handleArrayClear = useCallback(() => {
|
|
70
128
|
replace([]);
|
|
71
129
|
setConfirmIsOpen(false);
|
|
@@ -95,7 +153,7 @@ const TableFormIterator = (props: TableFormIteratorProps) => {
|
|
|
95
153
|
source,
|
|
96
154
|
disableAdd,
|
|
97
155
|
template,
|
|
98
|
-
|
|
156
|
+
onClick: handleAddButtonClick((props?.addButton as any)?.props?.onClick)
|
|
99
157
|
})}
|
|
100
158
|
|
|
101
159
|
<TableContainer component={Paper} sx={{ mt: 2, border: `1px solid ${tableBorderColor}` }}>
|
|
@@ -177,34 +235,25 @@ const TableFormIterator = (props: TableFormIteratorProps) => {
|
|
|
177
235
|
};
|
|
178
236
|
|
|
179
237
|
const AddTableRow = (props: any) => {
|
|
180
|
-
const { label, disableAdd,
|
|
181
|
-
const { append } = useArrayInput(props);
|
|
238
|
+
const { label, disableAdd, onClick } = props;
|
|
182
239
|
const theme = useTheme();
|
|
183
240
|
// @ts-ignore
|
|
184
241
|
const tableBorderColor = theme.palette.mode === 'dark' ? theme.palette.grey.A400 : theme.palette.grey.A800;
|
|
185
242
|
const iconColor = theme.palette.mode === 'light' ? '#000000' : '#FFFFFF';
|
|
186
243
|
|
|
187
|
-
const handleAddButtonClick = useCallback((
|
|
188
|
-
originalOnClickHandler: React.MouseEventHandler<Element>
|
|
189
|
-
) => (event: React.MouseEvent<Element, MouseEvent>) => {
|
|
190
|
-
append(template);
|
|
191
|
-
originalOnClickHandler && originalOnClickHandler(event);
|
|
192
|
-
}, [append, template]);
|
|
193
|
-
|
|
194
244
|
return (
|
|
195
245
|
<Stack justifyContent={'space-between'} alignItems={'center'} flexDirection={'row'}>
|
|
196
|
-
<Typography>{label}</Typography>
|
|
197
|
-
{!disableAdd &&
|
|
198
|
-
React.isValidElement(props?.addButton) ?
|
|
246
|
+
{label !== false && <Typography>{label}</Typography>}
|
|
247
|
+
{!disableAdd &&
|
|
248
|
+
(React.isValidElement(props?.addButton) ? (
|
|
199
249
|
React.cloneElement(props?.addButton, {
|
|
200
|
-
onClick:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
<IconButton size="small" color="secondary" sx={{ border: `1px solid ${tableBorderColor}` }} onClick={() => append(template)}>
|
|
250
|
+
onClick: onClick
|
|
251
|
+
})
|
|
252
|
+
) : (
|
|
253
|
+
<IconButton size="small" color="secondary" sx={{ border: `1px solid ${tableBorderColor}` }} onClick={onClick}>
|
|
205
254
|
<PlusCircleOutlined style={{ color: iconColor }} />
|
|
206
255
|
</IconButton>
|
|
207
|
-
|
|
256
|
+
))}
|
|
208
257
|
</Stack>
|
|
209
258
|
);
|
|
210
259
|
};
|
|
@@ -222,7 +271,7 @@ const StyledTableFormIterator = styled(TableFormIterator, {
|
|
|
222
271
|
}));
|
|
223
272
|
|
|
224
273
|
AddTableRow.propTypes = {
|
|
225
|
-
label: PropTypes.string,
|
|
274
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
226
275
|
source: PropTypes.string,
|
|
227
276
|
defaultValue: PropTypes.any
|
|
228
277
|
};
|
|
@@ -237,7 +286,7 @@ TableFormIterator.propTypes = {
|
|
|
237
286
|
formState: PropTypes.object,
|
|
238
287
|
record: PropTypes.object,
|
|
239
288
|
source: PropTypes.string,
|
|
240
|
-
label: PropTypes.string,
|
|
289
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
|
241
290
|
resource: PropTypes.string,
|
|
242
291
|
translate: PropTypes.func,
|
|
243
292
|
disableAdd: PropTypes.bool,
|
|
@@ -261,7 +310,7 @@ export interface TableFormIteratorProps extends Partial<UseFieldArrayReturn> {
|
|
|
261
310
|
submitFailed?: boolean;
|
|
262
311
|
};
|
|
263
312
|
record?: RaRecord;
|
|
264
|
-
label?: string;
|
|
313
|
+
label?: string | boolean;
|
|
265
314
|
resource?: string;
|
|
266
315
|
source?: string;
|
|
267
316
|
sx?: SxProps;
|
|
@@ -47,14 +47,14 @@ export const TableFormIteratorItem = React.forwardRef((props: TableFormIteratorI
|
|
|
47
47
|
resource,
|
|
48
48
|
disabled,
|
|
49
49
|
...inputProps,
|
|
50
|
-
label:
|
|
50
|
+
label: ''
|
|
51
51
|
})}
|
|
52
52
|
</TableCell>
|
|
53
53
|
);
|
|
54
54
|
})}
|
|
55
55
|
|
|
56
56
|
{!disableActions && (
|
|
57
|
-
<TableCell sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', border: 0, pt: 2 }}>
|
|
57
|
+
<TableCell sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', border: 0, pt: 2.5 }}>
|
|
58
58
|
<ActionsMenu horizontal>
|
|
59
59
|
<Typography color="error" onClick={() => setConfirmIsOpen(true)}>
|
|
60
60
|
{translate('ra.action.delete')}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Button, Container, Stack, Typography } from '@mui/material';
|
|
3
|
+
//@ts-ignore
|
|
4
|
+
import imgSrc from '../../assets/genericError.png';
|
|
5
|
+
|
|
6
|
+
function GenericErrorPage() {
|
|
7
|
+
const onReload = useCallback(() => location.reload(), []);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Container>
|
|
11
|
+
<Stack alignItems="center" px={2} py={{ xs: 4, sm: 8 }} spacing={2} textAlign="center">
|
|
12
|
+
<img src={imgSrc} style={{ width: '100%', maxWidth: 450 }} />
|
|
13
|
+
<Typography variant="h2">Ops! An error occurred...</Typography>
|
|
14
|
+
<Typography>We're fixing the problem. Please try again later.</Typography>
|
|
15
|
+
<Button onClick={onReload} variant="contained">
|
|
16
|
+
Reload
|
|
17
|
+
</Button>
|
|
18
|
+
</Stack>
|
|
19
|
+
</Container>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default GenericErrorPage;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export {
|
|
1
|
+
export { default as ActivatePage } from './ActivatePage';
|
|
2
|
+
export { default as LoginPage } from './LoginPage';
|
|
3
|
+
export { default as RecoverPage } from './RecoverPage';
|
|
4
|
+
export { default as RegisterPage } from './RegisterPage';
|
|
5
|
+
export { default as GenericErrorPage } from './GenericErrorPage';
|
package/src/hooks/index.jsx
CHANGED
|
@@ -8,3 +8,4 @@ import useThemeConfig from './useThemeConfig';
|
|
|
8
8
|
export { useAppConfig, useMenu, useBreadcrumbs, useLocalStorage, useThemeConfig, useResourceTitle, useMenuConfig };
|
|
9
9
|
export { useSx } from './mui';
|
|
10
10
|
export { useRefDimensions } from './useRefDimensions';
|
|
11
|
+
export { useMemoizedObject } from './useMemoizedObject';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Questo hook memoizza in automatico un oggetto utilizzando il'equality check del suo valore come criterio di discriminazione.
|
|
6
|
+
* NOTA BENE!!! L'equality check di un oggetto JS è un discorso estremamente complesso.
|
|
7
|
+
* Pertanto questo hook va usato con estrema parsimonia, solo quando strettamente necessario e solo se si padroneggia a pieno il tema di cui sopra e le sue implicazioni nelle logiche di render condizionale di React
|
|
8
|
+
* @param object
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
function useMemoizedObject<T extends object>(object: T): T {
|
|
12
|
+
if (!_.isPlainObject(object)) {
|
|
13
|
+
throw new Error('[useMemoizedObject]: param must be a plain JS object');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [memoObj, setMemoObj] = useState(object);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!_.isEqual(object, memoObj)) {
|
|
20
|
+
setMemoObj(object);
|
|
21
|
+
}
|
|
22
|
+
}, [object, memoObj]);
|
|
23
|
+
|
|
24
|
+
return memoObj;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { useMemoizedObject };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
type IMissingMessageHandlerConstructorData = {
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type IMissingMessageHandler = {
|
|
8
|
+
handleMessage(data: { code: string; lang: string }): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class MissingMessageHandler implements IMissingMessageHandler {
|
|
12
|
+
#headers: Headers;
|
|
13
|
+
#apiUrl: string;
|
|
14
|
+
#handledMessages: Array<{ code: string; lang: string }> = [];
|
|
15
|
+
|
|
16
|
+
constructor(data: IMissingMessageHandlerConstructorData) {
|
|
17
|
+
const { apiUrl } = data ?? {};
|
|
18
|
+
if (!_.isString(apiUrl) || _.isEmpty(apiUrl)) {
|
|
19
|
+
throw new Error('[MissingMessageHandler] constructor: please provide a valid apiUrl');
|
|
20
|
+
}
|
|
21
|
+
this.#apiUrl = apiUrl;
|
|
22
|
+
this.#headers = new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleMessage(data: { code: string; lang: string }): void {
|
|
26
|
+
if (!_.isPlainObject(data)) {
|
|
27
|
+
throw new Error('[MissingMessageHandler] handleMessage: please provide valid message data');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { code, lang } = data;
|
|
31
|
+
|
|
32
|
+
if (!_.isString(code) || _.isEmpty(code)) {
|
|
33
|
+
throw new Error('[MissingMessageHandler] handleMessage: please provide valid code');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!_.isString(lang) || _.isEmpty(lang)) {
|
|
37
|
+
throw new Error('[MissingMessageHandler] handleMessage: please provide valid lang');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (_.some(this.#handledMessages, (message) => _.isEqual(message, { code, lang }))) {
|
|
41
|
+
return;
|
|
42
|
+
} else {
|
|
43
|
+
this.#handledMessages.push({ code, lang });
|
|
44
|
+
fetch(`${this.#apiUrl}/i18n/message`, { method: 'put', headers: this.#headers, body: JSON.stringify({ code, lang, text: code }) })
|
|
45
|
+
.then((response) => response.json())
|
|
46
|
+
.then(() => {
|
|
47
|
+
//eslint-disable-next-line
|
|
48
|
+
console.log(`[MissingMessageHandler] handleMessage: created message for lang ${lang} and code ${code}`);
|
|
49
|
+
})
|
|
50
|
+
.catch((e) => {
|
|
51
|
+
//eslint-disable-next-line
|
|
52
|
+
console.error(e);
|
|
53
|
+
this.#handledMessages = _.reject(this.#handledMessages, (m) => _.isEqual(m, { code, lang }));
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default MissingMessageHandler;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Locale, TranslationMessages } from 'ra-core';
|
|
2
|
+
import polyglotI18nProvider from 'ra-i18n-polyglot';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import MissingMessageHandler from './MissingMessageHandler';
|
|
5
|
+
|
|
6
|
+
type IMessage = {
|
|
7
|
+
id: string;
|
|
8
|
+
code: string;
|
|
9
|
+
lang: string;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
type IDictionary = { [key: string]: string };
|
|
13
|
+
type ITranslations = { [key: string]: IDictionary };
|
|
14
|
+
type ICreateI18nProviderData = {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
defaultLocale: string;
|
|
17
|
+
allowMissing?: boolean;
|
|
18
|
+
createMissing?: boolean;
|
|
19
|
+
};
|
|
20
|
+
type IPolyglotOptions = {
|
|
21
|
+
phrases?: IDictionary;
|
|
22
|
+
locale?: string;
|
|
23
|
+
allowMissing?: boolean;
|
|
24
|
+
onMissingKey?: (key: string, options: IPolyglotOptions, locale: string) => string;
|
|
25
|
+
interpolation?: {
|
|
26
|
+
prefix?: string;
|
|
27
|
+
suffix?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function createI18nProvider(data: ICreateI18nProviderData): Promise<any> {
|
|
32
|
+
const { apiUrl, defaultLocale, allowMissing = false, createMissing = false } = data,
|
|
33
|
+
defaultLanguages: Array<Locale> = [{ locale: defaultLocale, name: defaultLocale }],
|
|
34
|
+
headers = new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }),
|
|
35
|
+
missingMessageHandler = new MissingMessageHandler({ apiUrl: apiUrl }),
|
|
36
|
+
getLanguages = (): Promise<Locale> => {
|
|
37
|
+
return fetch(`${apiUrl}/i18n/languages`, { headers: headers, method: 'get' })
|
|
38
|
+
.then((response) => response.json())
|
|
39
|
+
.then((response) => {
|
|
40
|
+
const { responseCode, locales } = response;
|
|
41
|
+
return responseCode !== 'ok' ? defaultLanguages : locales;
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
return Promise.resolve(defaultLanguages);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
getMessages = (): Promise<ITranslations> => {
|
|
48
|
+
return fetch(`${apiUrl}/i18n/messages`, { headers: headers, method: 'get' })
|
|
49
|
+
.then((response) => response.json())
|
|
50
|
+
.then((response: Array<IMessage>): ITranslations => {
|
|
51
|
+
return response.reduce(
|
|
52
|
+
(messages: ITranslations, message: IMessage) => ({
|
|
53
|
+
...messages,
|
|
54
|
+
[message.lang]: {
|
|
55
|
+
...(messages[message.lang] || {}),
|
|
56
|
+
[message.code]: message.text
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
{}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
onMissingKey = (key: string, options: IPolyglotOptions, locale: string): string => {
|
|
64
|
+
if (!allowMissing) {
|
|
65
|
+
//eslint-disable-next-line
|
|
66
|
+
console.error(`Warning: Missing translation for key: "${key}"`);
|
|
67
|
+
}
|
|
68
|
+
if (createMissing) {
|
|
69
|
+
if (!_.isString(key)) {
|
|
70
|
+
//eslint-disable-next-line
|
|
71
|
+
console.error('[createI18nProvider] onMissingKey: provided key is not a string');
|
|
72
|
+
} else if (key.match(new RegExp(/[\s[\]]/, 'gm'))) {
|
|
73
|
+
//eslint-disable-next-line
|
|
74
|
+
console.error(`[createI18nProvider] onMissingKey: illegal character in key ${key}`);
|
|
75
|
+
} else {
|
|
76
|
+
missingMessageHandler.handleMessage({ code: key, lang: locale });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return key;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return Promise.all([getLanguages(), getMessages()]).then((responses) => {
|
|
83
|
+
const [languages, messages] = responses;
|
|
84
|
+
function getMessages(locale: string): TranslationMessages {
|
|
85
|
+
//@ts-ignore
|
|
86
|
+
return _.get(messages, locale, {});
|
|
87
|
+
}
|
|
88
|
+
return polyglotI18nProvider(getMessages, defaultLocale, languages, {
|
|
89
|
+
allowMissing: allowMissing || createMissing,
|
|
90
|
+
onMissingKey: onMissingKey
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type { ICreateI18nProviderData };
|
|
96
|
+
|
|
97
|
+
export default createI18nProvider;
|
package/src/i18n/index.jsx
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import createI18nProvider, { ICreateI18nProviderData } from './createI18nProvider';
|
|
3
|
+
import { useMemoizedObject } from '../hooks';
|
|
4
|
+
import { I18nProvider } from 'ra-core';
|
|
5
|
+
|
|
6
|
+
function useI18nProvider(data: ICreateI18nProviderData): undefined | I18nProvider | { error: boolean } {
|
|
7
|
+
const [provider, setProvider] = useState<undefined | I18nProvider | { error: boolean }>(),
|
|
8
|
+
memoizedData = useMemoizedObject(data);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
createI18nProvider(memoizedData)
|
|
12
|
+
.then((provider) => {
|
|
13
|
+
setProvider(provider);
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {
|
|
16
|
+
setProvider({ error: true });
|
|
17
|
+
});
|
|
18
|
+
}, [memoizedData]);
|
|
19
|
+
|
|
20
|
+
return provider;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default useI18nProvider;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export type I18nCatcherBodyBuilderResultProps = {
|
|
2
|
-
lang: string;
|
|
3
|
-
code: string;
|
|
4
|
-
text: string;
|
|
5
|
-
} | {
|
|
6
|
-
message: {
|
|
7
|
-
lang: string;
|
|
8
|
-
code: string;
|
|
9
|
-
text: string;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
export type I18nCatcherBodyBuilderProps = (lang: string, message: string) => I18nCatcherBodyBuilderResultProps;
|
|
13
|
-
export type UseI18nCatcherProps = {
|
|
14
|
-
apiUrl: string;
|
|
15
|
-
enabled?: boolean;
|
|
16
|
-
endpoint?: string;
|
|
17
|
-
loading?: boolean;
|
|
18
|
-
bodyBuilder: I18nCatcherBodyBuilderProps;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Hook che consente di catturare ed inoltrare al server API le stringhe non tradotte.
|
|
22
|
-
*
|
|
23
|
-
* @param {UseI18nCatcherProps}
|
|
24
|
-
* @returns {boolean}
|
|
25
|
-
*/
|
|
26
|
-
declare const useI18nCatcher: ({ apiUrl, enabled, endpoint, loading, bodyBuilder }: UseI18nCatcherProps) => boolean;
|
|
27
|
-
export default useI18nCatcher;
|
|
28
|
-
//# sourceMappingURL=useI18nCatcher.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useI18nCatcher.d.ts","sourceRoot":"","sources":["../../../src/i18n/useI18nCatcher.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,iCAAiC,GACzC;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH,CAAC;AAEN,MAAM,MAAM,2BAA2B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,iCAAiC,CAAC;AA0B/G,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,2BAA2B,CAAC;CAC1C,CAAC;AAEF;;;;;GAKG;AACH,QAAA,MAAM,cAAc,wDAUjB,mBAAmB,YAmCrB,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { get } from 'lodash';
|
|
2
|
-
import polyglotI18nProvider from 'ra-i18n-polyglot';
|
|
3
|
-
|
|
4
|
-
const createI18nProvider = ({ languages, defaultLocale, allowMissing = false }) =>
|
|
5
|
-
polyglotI18nProvider(
|
|
6
|
-
(locale) => {
|
|
7
|
-
const messages = get(languages, locale, {});
|
|
8
|
-
return messages;
|
|
9
|
-
},
|
|
10
|
-
defaultLocale,
|
|
11
|
-
[{ locale: defaultLocale }],
|
|
12
|
-
{ allowMissing }
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
export default createI18nProvider;
|