feat: i18n support (#387)

This commit is contained in:
Daniel Lautzenheiser
2023-01-18 15:08:40 -05:00
committed by GitHub
parent 7372c3735b
commit a01f30ef69
22 changed files with 580 additions and 425 deletions

View File

@ -0,0 +1,8 @@
---
description: Kaffee ist ein kleiner Baum oder Strauch, der in seiner wilden Form
im Unterholz des Waldes wächst und traditionell kommerziell unter anderen
Bäumen angebaut wurde, die Schatten spendeten. Die waldähnliche Struktur
schattiger Kaffeefarmen bietet Lebensraum für eine Vielzahl von wandernden und
ansässigen Arten.
date: 2023-01-18T14:41:54.252-05:00
---

View File

@ -0,0 +1,8 @@
---
slug: file1
description: Coffee is a small tree or shrub that grows in the forest understory
in its wild form, and traditionally was grown commercially under other trees
that provided shade. The forest-like structure of shade coffee farms provides
habitat for a great number of migratory and resident species.
date: 2023-01-18T14:41:54.252-05:00
---

View File

@ -0,0 +1,8 @@
---
description: Le café est un petit arbre ou un arbuste qui pousse dans le
sous-étage de la forêt sous sa forme sauvage et qui était traditionnellement
cultivé commercialement sous d\'autres arbres qui fournissaient de l\'ombre.
La structure forestière des plantations de café d\'ombre fournit un habitat à
un grand nombre d\'espèces migratrices et résidentes.
date: 2023-01-18T14:41:54.252-05:00
---

View File

@ -7,6 +7,19 @@ local_backend: true
media_folder: /packages/core/dev-test/backends/proxy/assets/upload
public_folder: /backends/proxy/assets/upload
i18n:
# Required and can be one of multiple_folders, multiple_files or single_file
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
# single_file - persists a single file in `<folder>/<slug>.<extension>`
structure: multiple_files
# Required - a list of locales to show in the editor UI
locales: [en, de, fr]
# Optional, defaults to the first item in locales.
# The locale to be used for fields validation and as a baseline for the entry.
defaultLocale: en
collections:
- name: posts
label: Posts
@ -469,3 +482,24 @@ collections:
- label: File
name: file
widget: file
- name: i18n_playground
label: i18n Playground
i18n: true
folder: packages/core/dev-test/backends/proxy/_i18n_playground
identifier_field: slug
create: true
fields:
# The slug field will be omitted from the translation.
- name: slug
label: Slug
widget: string
# same as 'i18n: translate'. Allows translation of the description field
- name: description
label: Description
widget: text
i18n: true
# The date field will be duplicated from the default locale.
- name: date
label: Date
widget: datetime
i18n: duplicate

View File

@ -3,6 +3,19 @@ backend:
site_url: 'https://example.com'
media_folder: assets/uploads
locale: en
i18n:
# Required and can be one of multiple_folders, multiple_files or single_file
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
# single_file - persists a single file in `<folder>/<slug>.<extension>`
structure: multiple_files
# Required - a list of locales to show in the editor UI
locales: [en, de, fr]
# Optional, defaults to the first item in locales.
# The locale to be used for fields validation and as a baseline for the entry.
defaultLocale: en
collections:
- name: posts
label: Posts
@ -1188,3 +1201,24 @@ collections:
- label: File
name: file
widget: file
- name: i18n_playground
label: i18n Playground
i18n: true
folder: _i18n_playground
identifier_field: slug
create: true
fields:
# The slug field will be omitted from the translation.
- name: slug
label: Slug
widget: string
# same as 'i18n: translate'. Allows translation of the description field
- name: description
label: Description
widget: text
i18n: true
# The date field will be duplicated from the default locale.
- name: date
label: Date
widget: datetime
i18n: duplicate

File diff suppressed because one or more lines are too long

View File

@ -469,10 +469,14 @@ export function changeDraftField({
} as const;
}
export function changeDraftFieldValidation(path: string, errors: FieldError[]) {
export function changeDraftFieldValidation(
path: string,
errors: FieldError[],
i18n?: I18nSettings,
) {
return {
type: DRAFT_VALIDATION_ERRORS,
payload: { path, errors },
payload: { path, errors, i18n },
} as const;
}
@ -662,12 +666,12 @@ export function loadEntries(collection: Collection, page = 0) {
const backend = currentBackend(configState.config);
const append = !!(page && !isNaN(page) && page > 0);
console.log('Trying to load page', page);
const loadAllEntries = 'nested' in collection || hasI18n(collection);
const append = !!(page && !isNaN(page) && page > 0) && !loadAllEntries;
dispatch(entriesLoading(collection));
try {
const loadAllEntries = 'nested' in collection || hasI18n(collection);
const response: {
cursor?: Cursor;
pagination?: number;
@ -683,7 +687,7 @@ export function loadEntries(collection: Collection, page = 0) {
? Cursor.create({
actions: ['next'],
meta: { usingOldPaginationAPI: true },
data: { nextPage: page + 1 },
data: { nextPage: loadAllEntries ? -1 : page + 1 },
})
: Cursor.create(response.cursor),
};
@ -749,7 +753,12 @@ export function traverseCollectionCursor(collection: Collection, action: string)
// Handle cursors representing pages in the old, integer-based pagination API
if (cursor.meta?.usingOldPaginationAPI ?? false) {
return dispatch(loadEntries(collection, cursor.data!.nextPage as number));
const nextPage = (cursor.data!.nextPage as number) ?? -1;
if (nextPage < 0) {
return;
}
return dispatch(loadEntries(collection, nextPage));
}
try {

View File

@ -92,31 +92,35 @@ const Editor = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collection, createBackup, slug]);
console.log('VERSION', version);
const [submitted, setSubmitted] = useState(false);
const handlePersistEntry = useCallback(
async (opts: EditorPersistOptions = {}) => {
(opts: EditorPersistOptions = {}) => {
const { createNew = false, duplicate = false } = opts;
if (!entryDraft.entry) {
return;
}
try {
await persistEntry(collection);
setVersion(version + 1);
deleteBackup();
if (createNew) {
navigateToNewEntry(collection.name);
if (duplicate && entryDraft.entry) {
createDraftDuplicateFromEntry(entryDraft.entry);
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
setSubmitted(true);
setTimeout(async () => {
try {
await persistEntry(collection);
setVersion(version + 1);
deleteBackup();
if (createNew) {
navigateToNewEntry(collection.name);
if (duplicate && entryDraft.entry) {
createDraftDuplicateFromEntry(entryDraft.entry);
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
}, 100);
},
[
collection,

View File

@ -178,7 +178,7 @@ const EditorControl = ({
const [dirty, setDirty] = useState(!isEmpty(value));
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path), [path]);
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path, i18n), [i18n, path]);
const errors = useAppSelector(fieldErrorsSelector);
const hasErrors = (submitted || dirty) && Boolean(errors.length);
@ -192,17 +192,17 @@ const EditorControl = ({
);
useEffect(() => {
if (!dirty && !submitted) {
if ((!dirty && !submitted) || isHidden) {
return;
}
const validateValue = async () => {
const errors = await validate(field, value, widget, t);
dispatch(changeDraftFieldValidation(path, errors));
dispatch(changeDraftFieldValidation(path, errors, i18n));
};
validateValue();
}, [dispatch, field, path, t, value, widget, dirty, submitted]);
}, [dirty, dispatch, field, i18n, isHidden, path, submitted, t, value, widget]);
const handleChangeDraftField = useCallback(
(value: ValueOrNestedValue) => {

View File

@ -1,3 +1,4 @@
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
@ -7,7 +8,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { changeDraftField as changeDraftFieldAction } from '@staticcms/core/actions/entries';
import confirm from '@staticcms/core/components/UI/Confirm';
import {
getI18nInfo,
getLocaleDataPath,
@ -18,6 +18,7 @@ import {
} from '@staticcms/core/lib/i18n';
import EditorControl from './EditorControl';
import type { ButtonProps } from '@mui/material/Button';
import type {
Collection,
Entry,
@ -42,24 +43,55 @@ const ControlPaneContainer = styled('div')`
const LocaleRowWrapper = styled('div')`
display: flex;
gap: 8px;
`;
const DefaultLocaleWrittingIn = styled('div')`
display: flex;
align-items: center;
height: 36.5px;
`;
interface LocaleDropdownProps {
locales: string[];
defaultLocale: string;
dropdownText: string;
onLocaleChange: (locale: string) => void;
color: ButtonProps['color'];
canChangeLocale: boolean;
onLocaleChange?: (locale: string) => void;
}
const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdownProps) => {
const LocaleDropdown = ({
locales,
defaultLocale,
dropdownText,
color,
canChangeLocale,
onLocaleChange,
}: LocaleDropdownProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}, []);
const handleClose = useCallback(() => {
setAnchorEl(null);
}, []);
const handleLocaleChange = useCallback(
(locale: string) => {
onLocaleChange?.(locale);
handleClose();
},
[handleClose, onLocaleChange],
);
if (!canChangeLocale) {
return <DefaultLocaleWrittingIn>{dropdownText}</DefaultLocaleWrittingIn>;
}
return (
<div>
<Button
@ -68,6 +100,9 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
variant="contained"
endIcon={<KeyboardArrowDownIcon />}
color={color}
>
{dropdownText}
</Button>
@ -80,11 +115,17 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
'aria-labelledby': 'basic-button',
}}
>
{locales.map(locale => (
<MenuItem key={locale} onClick={() => onLocaleChange(locale)}>
{locale}
</MenuItem>
))}
{locales
.filter(locale => locale !== defaultLocale)
.map(locale => (
<MenuItem
key={locale}
onClick={() => handleLocaleChange(locale)}
sx={{ minWidth: '80px' }}
>
{locale}
</MenuItem>
))}
</Menu>
</div>
);
@ -110,8 +151,8 @@ const EditorControlPane = ({
fields,
fieldsErrors,
submitted,
changeDraftField,
locale,
canChangeLocale = false,
onLocaleChange,
t,
}: TranslatedProps<EditorControlPaneProps>) => {
@ -128,40 +169,6 @@ const EditorControlPane = ({
return undefined;
}, [collection, locale]);
const copyFromOtherLocale = useCallback(
({ targetLocale }: { targetLocale?: string }) =>
async (sourceLocale: string) => {
if (!targetLocale) {
return;
}
if (
!(await confirm({
title: 'editor.editorControlPane.i18n.copyFromLocaleConfirmTitle',
body: {
key: 'editor.editorControlPane.i18n.copyFromLocaleConfirmBody',
options: { locale: sourceLocale.toUpperCase() },
},
}))
) {
return;
}
fields.forEach(field => {
if (isFieldTranslatable(field, targetLocale, sourceLocale)) {
const copyValue = getFieldValue(
field,
entry,
sourceLocale !== i18n?.defaultLocale,
sourceLocale,
);
changeDraftField({ path: field.name, field, value: copyValue, i18n });
}
});
},
[fields, entry, i18n, changeDraftField],
);
if (!collection || !fields) {
return null;
}
@ -176,16 +183,14 @@ const EditorControlPane = ({
<LocaleRowWrapper>
<LocaleDropdown
locales={i18n.locales}
defaultLocale={i18n.defaultLocale}
dropdownText={t('editor.editorControlPane.i18n.writingInLocale', {
locale: locale?.toUpperCase(),
})}
color="primary"
canChangeLocale={canChangeLocale}
onLocaleChange={onLocaleChange}
/>
<LocaleDropdown
locales={i18n.locales.filter(l => l !== locale)}
dropdownText={t('editor.editorControlPane.i18n.copyFromLocale')}
onLocaleChange={copyFromOtherLocale({ targetLocale: locale })}
/>
</LocaleRowWrapper>
) : null}
{fields.map(field => {
@ -222,7 +227,8 @@ export interface EditorControlPaneOwnProps {
fieldsErrors: FieldsErrors;
submitted: boolean;
locale?: string;
onLocaleChange: (locale: string) => void;
canChangeLocale?: boolean;
onLocaleChange?: (locale: string) => void;
}
function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps) {

View File

@ -31,15 +31,6 @@ const StyledSplitPane = styled('div')`
display: grid;
grid-template-columns: min(864px, 50%) auto;
height: calc(100vh - 64px);
> div:nth-of-type(2)::before {
content: '';
width: 2px;
height: calc(100vh - 64px);
position: relative;
background-color: rgb(223, 223, 227);
display: block;
}
`;
const NoPreviewContainer = styled('div')`
@ -78,14 +69,23 @@ const PreviewPaneContainer = styled(
`,
);
const ControlPaneContainer = styled(PreviewPaneContainer)`
padding: 24px 16px 16px;
position: relative;
overflow-x: hidden;
display: flex;
align-items: flex-start;
justify-content: center;
`;
interface ControlPaneContainerProps {
$hidden?: boolean;
}
const ControlPaneContainer = styled(
PreviewPaneContainer,
transientOptions,
)<ControlPaneContainerProps>(
({ $hidden = false }) => `
padding: 24px 16px 16px;
position: relative;
overflow-x: hidden;
display: ${$hidden ? 'none' : 'flex'};
align-items: flex-start;
justify-content: center;
`,
);
const StyledViewControls = styled('div')`
position: fixed;
@ -127,7 +127,7 @@ interface EditorInterfaceProps {
collection: Collection;
fields: Field[] | undefined;
fieldsErrors: FieldsErrors;
onPersist: (opts?: EditorPersistOptions) => Promise<void>;
onPersist: (opts?: EditorPersistOptions) => void;
onDelete: () => Promise<void>;
onDuplicate: () => void;
showDelete: boolean;
@ -177,23 +177,15 @@ const EditorInterface = ({
}, [loadScroll]);
const { locales, defaultLocale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
const [selectedLocale, setSelectedLocale] = useState(locales?.[0]);
const switchToDefaultLocale = useCallback(() => {
if (hasI18n(collection)) {
const { defaultLocale } = getI18nInfo(collection);
setSelectedLocale(defaultLocale);
}
}, [collection]);
const [selectedLocale, setSelectedLocale] = useState<string>(locales?.[1] ?? 'en');
const handleOnPersist = useCallback(
async (opts: EditorPersistOptions = {}) => {
const { createNew = false, duplicate = false } = opts;
await switchToDefaultLocale();
// TODO Trigger field validation on persist
// this.controlPaneRef.validate();
// await switchToDefaultLocale();
onPersist({ createNew, duplicate });
},
[onPersist, switchToDefaultLocale],
[onPersist],
);
const handleTogglePreview = useCallback(() => {
@ -237,37 +229,54 @@ const EditorInterface = ({
const collectionI18nEnabled = hasI18n(collection);
const editor = (
<ControlPaneContainer id="control-pane" $overFlow>
<ControlPaneContainer key={defaultLocale} id="control-pane">
<EditorControlPane
collection={collection}
entry={entry}
fields={fields}
fieldsErrors={fieldsErrors}
locale={selectedLocale}
onLocaleChange={handleLocaleChange}
locale={defaultLocale}
submitted={submitted}
t={t}
/>
</ControlPaneContainer>
);
const editorLocale = (
<ControlPaneContainer $overFlow={!scrollSyncEnabled}>
<EditorControlPane
collection={collection}
entry={entry}
fields={fields}
fieldsErrors={fieldsErrors}
locale={locales?.[1]}
onLocaleChange={handleLocaleChange}
submitted={submitted}
t={t}
/>
</ControlPaneContainer>
const editorLocale = useMemo(
() =>
(locales ?? [])
.filter(locale => locale !== defaultLocale)
.map(locale => (
<ControlPaneContainer key={locale} $hidden={locale !== selectedLocale}>
<EditorControlPane
collection={collection}
entry={entry}
fields={fields}
fieldsErrors={fieldsErrors}
locale={locale}
onLocaleChange={handleLocaleChange}
submitted={submitted}
canChangeLocale
t={t}
/>
</ControlPaneContainer>
)),
[
collection,
defaultLocale,
entry,
fields,
fieldsErrors,
handleLocaleChange,
locales,
selectedLocale,
submitted,
t,
],
);
const previewEntry = collectionI18nEnabled
? getPreviewEntry(entry, selectedLocale, defaultLocale)
? getPreviewEntry(entry, selectedLocale[0], defaultLocale)
: entry;
const editorWithPreview = (
@ -291,7 +300,9 @@ const EditorInterface = ({
<div>
<StyledSplitPane>
<ScrollSyncPane>{editor}</ScrollSyncPane>
<ScrollSyncPane>{editorLocale}</ScrollSyncPane>
<ScrollSyncPane>
<>{editorLocale}</>
</ScrollSyncPane>
</StyledSplitPane>
</div>
</ScrollSync>

View File

@ -44,18 +44,15 @@ export function getI18nFilesDepth(collection: Collection, depth: number) {
}
export function isFieldTranslatable(field: Field, locale?: string, defaultLocale?: string) {
const isTranslatable = locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
return isTranslatable;
return locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
}
export function isFieldDuplicate(field: Field, locale?: string, defaultLocale?: string) {
const isDuplicate = locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
return isDuplicate;
return locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
}
export function isFieldHidden(field: Field, locale?: string, defaultLocale?: string) {
const isHidden = locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
return isHidden;
return locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
}
export function getLocaleDataPath(locale: string) {
@ -205,8 +202,15 @@ export function getI18nBackup(
if (!data) {
return acc;
}
entry.data = data;
return { ...acc, [locale]: { raw: entryToRaw(entry) } };
return {
...acc,
[locale]: {
raw: entryToRaw({
...entry,
data,
}),
},
};
}, {} as Record<string, { raw: string }>);
return i18nBackup;
@ -386,9 +390,9 @@ export function duplicateI18nFields(
locales
.filter(l => l !== defaultLocale)
.forEach(l => {
entryDraft = get(
entryDraft = set(
entryDraft,
['entry', ...getDataPath(l, defaultLocale), ...fieldPath],
['entry', ...getDataPath(l, defaultLocale), ...fieldPath].join('.'),
value,
);
});
@ -414,8 +418,10 @@ export function getPreviewEntry(
if (!locale || locale === defaultLocale) {
return entry;
}
entry.data = entry.i18n?.[locale]?.data as EntryData;
return entry;
return {
...entry,
data: entry.i18n?.[locale]?.data as EntryData,
};
}
export function serializeI18n(

View File

@ -94,9 +94,6 @@ const de: LocalePhrasesRoot = {
},
i18n: {
writingInLocale: 'Aktuelle Sprache: %{locale}',
copyFromLocale: 'Aus anderer Sprache übernehmen',
copyFromLocaleConfirmBody:
'Wollen Sie wirklich die Daten aus der Sprache %{locale} übernehmen?\nAlle bishergen Inhalte werden überschrieben.',
},
},
editor: {

View File

@ -98,10 +98,6 @@ const en: LocalePhrasesRoot = {
},
i18n: {
writingInLocale: 'Writing in %{locale}',
copyFromLocale: 'Fill in from another locale',
copyFromLocaleConfirmTitle: 'Fill in data from locale',
copyFromLocaleConfirmBody:
'Do you want to fill in data from %{locale} locale?\nAll existing content will be overwritten.',
},
},
editor: {

View File

@ -95,9 +95,6 @@ const tr: LocalePhrasesRoot = {
},
i18n: {
writingInLocale: '%{locale} için yazılıyor',
copyFromLocale: 'Başka bir dilden doldurun',
copyFromLocaleConfirmBody:
'Verileri %{locale} dilinden mi doldurmak istiyorsun?\nVarolan bütün verilerin üzerine yazılacak.',
},
},
editor: {

View File

@ -181,6 +181,7 @@ function entries(
const payload = action.payload;
const loadedEntries = payload.entries;
const page = payload.page;
const append = payload.append;
const entities = {
...state.entities,
@ -196,7 +197,9 @@ function entries(
pages[payload.collection] = {
page: page ?? undefined,
ids: [...(pages[payload.collection]?.ids ?? []), ...loadedEntries.map(entry => entry.slug)],
ids: append
? [...(pages[payload.collection]?.ids ?? []), ...loadedEntries.map(entry => entry.slug)]
: [...loadedEntries.map(entry => entry.slug)],
isFetching: false,
};

View File

@ -162,10 +162,14 @@ function entryDraftReducer(
entry: set(newState.entry, `${dataPath.join('.')}.${path}`, value),
};
console.log('BEFORE I18N', { ...newState.entry });
if (i18n) {
newState = duplicateI18nFields(newState, field, i18n.locales, i18n.defaultLocale);
}
console.log('AFTER I18N', { ...newState.entry });
const newData = get(newState.entry, dataPath) ?? {};
return {
@ -175,12 +179,16 @@ function entryDraftReducer(
}
case DRAFT_VALIDATION_ERRORS: {
const { path, errors } = action.payload;
const { path, errors, i18n } = action.payload;
const fieldsErrors = { ...state.fieldsErrors };
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
const fullPath = `${dataPath.join('.')}.${path}`;
if (errors.length === 0) {
delete fieldsErrors[path];
delete fieldsErrors[fullPath];
} else {
fieldsErrors[path] = action.payload.errors;
fieldsErrors[fullPath] = action.payload.errors;
}
return {
...state,

View File

@ -1,9 +1,14 @@
/* eslint-disable import/prefer-default-export */
import { getDataPath } from '@staticcms/core/lib/i18n';
import type { I18nSettings } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
export const selectFieldErrors = (path: string) => (state: RootState) => {
return state.entryDraft.fieldsErrors[path] ?? [];
};
export const selectFieldErrors =
(path: string, i18n: I18nSettings | undefined) => (state: RootState) => {
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
const fullPath = `${dataPath.join('.')}.${path}`;
return state.entryDraft.fieldsErrors[fullPath] ?? [];
};
export function selectEditingDraft(state: RootState) {
return state.entryDraft.entry;

View File

@ -168,6 +168,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
label={label}
value={inputDate}
onChange={handleChange}
disabled={isDisabled}
renderInput={params => (
<TextField
key="mobile-date-input"
@ -198,6 +199,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
inputFormat={inputFormat}
value={inputDate}
onChange={handleChange}
disabled={isDisabled}
renderInput={params => (
<TextField
key="time-input"
@ -227,6 +229,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
label={label}
value={inputDate}
onChange={handleChange}
disabled={isDisabled}
renderInput={params => (
<TextField
key="mobile-date-time-input"