feat: v4.0.0 (#1016)
Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
682576ffc4
commit
799c7e6936
23
packages/core/src/widgets/boolean/BooleanControl.css
Normal file
23
packages/core/src/widgets/boolean/BooleanControl.css
Normal file
@ -0,0 +1,23 @@
|
||||
.CMS_WidgetBoolean_content {
|
||||
@apply flex
|
||||
gap-2
|
||||
items-center;
|
||||
}
|
||||
|
||||
.CMS_WidgetBoolean_prefix {
|
||||
color: var(--text-secondary);
|
||||
|
||||
@apply text-sm
|
||||
whitespace-nowrap;
|
||||
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.CMS_WidgetBoolean_suffix {
|
||||
color: var(--text-secondary);
|
||||
|
||||
@apply text-sm
|
||||
whitespace-nowrap;
|
||||
|
||||
line-height: 100%;
|
||||
}
|
@ -3,11 +3,14 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import Field from '@staticcms/core/components/common/field/Field';
|
||||
import Switch from '@staticcms/core/components/common/switch/Switch';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { BooleanField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { BooleanField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
|
||||
import './BooleanControl.css';
|
||||
|
||||
const classes = generateClassNames('WidgetBoolean', [
|
||||
'root',
|
||||
'error',
|
||||
@ -15,6 +18,9 @@ const classes = generateClassNames('WidgetBoolean', [
|
||||
'disabled',
|
||||
'for-single-list',
|
||||
'input',
|
||||
'content',
|
||||
'prefix',
|
||||
'suffix',
|
||||
]);
|
||||
|
||||
const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
|
||||
@ -43,6 +49,9 @@ const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const prefix = useMemo(() => field.prefix ?? '', [field.prefix]);
|
||||
const suffix = useMemo(() => field.suffix ?? '', [field.suffix]);
|
||||
|
||||
return (
|
||||
<Field
|
||||
inputRef={ref}
|
||||
@ -61,13 +70,17 @@ const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
|
||||
forSingleList && classes['for-single-list'],
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
ref={ref}
|
||||
value={internalValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
rootClassName={classes.input}
|
||||
/>
|
||||
<div className={classes.content}>
|
||||
{isNotEmpty(prefix) ? <div className={classes.prefix}>{prefix}</div> : null}
|
||||
<Switch
|
||||
ref={ref}
|
||||
value={internalValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
rootClassName={classes.input}
|
||||
/>
|
||||
{isNotEmpty(suffix) ? <div className={classes.suffix}>{suffix}</div> : null}
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import { mockBooleanField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import controlComponent from './BooleanControl';
|
||||
import schema from './schema';
|
||||
|
||||
import type { BooleanField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { BooleanField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const BooleanWidget = (): WidgetParam<boolean, BooleanField> => {
|
||||
return {
|
||||
|
@ -1,5 +1,7 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'boolean' },
|
||||
prefix: { type: 'string' },
|
||||
suffix: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
.CMS_WidgetCode_root {
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply relative
|
||||
flex
|
||||
flex-col
|
||||
border-b
|
||||
border-slate-400
|
||||
focus-within:border-blue-800
|
||||
dark:focus-within:border-blue-100;
|
||||
border-b;
|
||||
|
||||
&.CMS_WidgetCode_for-single-list {
|
||||
& .CMS_WidgetCode_field-wrapper {
|
||||
@ -19,8 +18,7 @@
|
||||
}
|
||||
|
||||
& .CMS_WidgetCode_expand-button-icon {
|
||||
@apply text-slate-300
|
||||
dark:text-slate-600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,17 +32,27 @@
|
||||
&:not(.CMS_WidgetCode_error) {
|
||||
&:not(.CMS_WidgetCode_disabled) {
|
||||
&:hover,
|
||||
&:focus {
|
||||
& .CMS_WidgetCode_label {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: var(--primary-main);
|
||||
|
||||
& .CMS_WidgetCode_expand-button-icon {
|
||||
@apply text-blue-500;
|
||||
& .CMS_WidgetCode_label {
|
||||
color: var(--primary-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.CMS_WidgetCode_error {
|
||||
&:not(.CMS_WidgetCode_disabled) {
|
||||
& .CMS_WidgetCode_label {
|
||||
color: var(--error-main);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--error-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CMS_WidgetCode_field-wrapper {
|
||||
@ -66,7 +74,6 @@
|
||||
focus:outline-none
|
||||
focus-visible:ring
|
||||
gap-2
|
||||
focus-visible:ring-opacity-75
|
||||
items-center;
|
||||
}
|
||||
|
||||
|
@ -7,21 +7,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import ErrorMessage from '@staticcms/core/components/common/field/ErrorMessage';
|
||||
import Hint from '@staticcms/core/components/common/field/Hint';
|
||||
import Label from '@staticcms/core/components/common/field/Label';
|
||||
import useTheme from '@staticcms/core/components/theme/hooks/useTheme';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import SettingsButton from './SettingsButton';
|
||||
import SettingsPane from './SettingsPane';
|
||||
import languages from './data/languages';
|
||||
|
||||
import type {
|
||||
CodeField,
|
||||
ProcessedCodeLanguage,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
import type { CodeField, ProcessedCodeLanguage, WidgetControlProps } from '@staticcms/core';
|
||||
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
@ -62,7 +57,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
errors,
|
||||
disabled,
|
||||
}) => {
|
||||
const theme = useAppSelector(selectTheme);
|
||||
const theme = useTheme();
|
||||
|
||||
const keys = useMemo(() => {
|
||||
const defaults = {
|
||||
@ -179,7 +174,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="list-field"
|
||||
data-testid={`code-field-${label}`}
|
||||
className={classNames(
|
||||
classes.root,
|
||||
disabled && classes.disabled,
|
||||
@ -203,7 +198,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
variant="inline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
{label.trim()}
|
||||
</Label>
|
||||
{open && allowLanguageSelection ? (
|
||||
<SettingsButton onClick={toggleSettings} disabled={disabled} />
|
||||
@ -230,7 +225,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
editable={true}
|
||||
onChange={handleChange}
|
||||
extensions={extensions}
|
||||
theme={theme}
|
||||
theme={theme.codemirror.theme}
|
||||
readOnly={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { CodeField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { CodeField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const classes = generateClassNames('WidgetCodePreview', ['root']);
|
||||
|
@ -1,7 +1,2 @@
|
||||
.CMS_WidgetCode_SettingsButton_root {
|
||||
}
|
||||
|
||||
.CMS_WidgetCode_SettingsButton_icon {
|
||||
@apply w-5
|
||||
h-5;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import type { FC, MouseEvent } from 'react';
|
||||
|
||||
import './SettingsButton.css';
|
||||
|
||||
const classes = generateClassNames('WidgetCode_SettingsButton', ['root', 'icon']);
|
||||
const classes = generateClassNames('WidgetCode_SettingsButton', ['root']);
|
||||
|
||||
export interface SettingsButtonProps {
|
||||
showClose?: boolean;
|
||||
@ -20,18 +20,15 @@ export interface SettingsButtonProps {
|
||||
const SettingsButton: FC<SettingsButtonProps> = ({ showClose = false, disabled, onClick }) => {
|
||||
return (
|
||||
<IconButton
|
||||
icon={showClose ? CloseIcon : SettingsIcon}
|
||||
onClick={onClick}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
disabled={disabled}
|
||||
className={classes.root}
|
||||
>
|
||||
{showClose ? (
|
||||
<CloseIcon className={classes.icon} />
|
||||
) : (
|
||||
<SettingsIcon className={classes.icon} />
|
||||
)}
|
||||
</IconButton>
|
||||
rootClassName={classes.root}
|
||||
aria-label="toggle settings"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
.CMS_WidgetCodeSettings_root {
|
||||
background: var(--background-light);
|
||||
|
||||
@apply absolute
|
||||
top-10
|
||||
top-[41px]
|
||||
bottom-0
|
||||
right-0
|
||||
w-40
|
||||
@ -8,12 +10,5 @@
|
||||
flex-col
|
||||
gap-2
|
||||
z-10
|
||||
shadow-sm
|
||||
bg-gray-100
|
||||
dark:bg-slate-800
|
||||
border-l
|
||||
border-l-slate-400
|
||||
border-t
|
||||
border-t-slate-300
|
||||
dark:border-t-slate-700;
|
||||
shadow-sm;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import React from 'react';
|
||||
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
import Label from '@staticcms/core/components/common/field/Label';
|
||||
import Select from '@staticcms/core/components/common/select/Select';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
@ -80,12 +81,14 @@ const SettingsPane: FC<SettingsPaneProps> = ({
|
||||
language,
|
||||
onChangeLanguage,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<div onKeyDown={e => isHotkey('esc', e) && hideSettings()} className={classes.root}>
|
||||
<SettingsSelect
|
||||
type="language"
|
||||
label="Language"
|
||||
placeholder="Select language"
|
||||
label={t('editor.editorWidgets.code.language')}
|
||||
placeholder={t('editor.editorWidgets.code.selectLanguage')}
|
||||
uniqueId={uniqueId}
|
||||
value={language}
|
||||
options={languages}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
|
||||
import type { ProcessedCodeLanguage } from '@staticcms/core';
|
||||
|
||||
const languages: ProcessedCodeLanguage[] = [
|
||||
{
|
||||
|
@ -2,7 +2,7 @@ import controlComponent from './CodeControl';
|
||||
import previewComponent from './CodePreview';
|
||||
import schema from './schema';
|
||||
|
||||
import type { CodeField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { CodeField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField> => {
|
||||
return {
|
||||
|
@ -3,7 +3,7 @@ import yaml from 'js-yaml';
|
||||
import uniq from 'lodash/uniq';
|
||||
import path from 'path';
|
||||
|
||||
import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
|
||||
import type { ProcessedCodeLanguage } from '@staticcms/core';
|
||||
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
|
||||
|
||||
const rawDataPath = '../data/languages-raw.yml';
|
||||
@ -26,7 +26,7 @@ function outputData(data: ProcessedCodeLanguage[]) {
|
||||
const filePath = path.resolve(__dirname, outputPath);
|
||||
return fs.writeFile(
|
||||
filePath,
|
||||
`import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
|
||||
`import type { ProcessedCodeLanguage } from '@staticcms/core';
|
||||
|
||||
const languages: ProcessedCodeLanguage[] = ${JSON.stringify(data, null, 2)};
|
||||
|
||||
|
@ -17,7 +17,8 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetColor_content {
|
||||
@apply flex
|
||||
@apply relative
|
||||
flex
|
||||
items-center
|
||||
pt-2
|
||||
px-3
|
||||
@ -38,7 +39,7 @@
|
||||
|
||||
.CMS_WidgetColor_color-picker-wrapper {
|
||||
@apply absolute
|
||||
bottom-0;
|
||||
-bottom-0.5;
|
||||
}
|
||||
|
||||
.CMS_WidgetColor_color-picker-backdrop {
|
||||
@ -50,7 +51,7 @@
|
||||
.CMS_WidgetColor_color-picker {
|
||||
@apply absolute
|
||||
z-20
|
||||
-top-3;
|
||||
top-0;
|
||||
}
|
||||
|
||||
.CMS_WidgetColor_input {
|
||||
@ -58,8 +59,3 @@
|
||||
|
||||
.CMS_WidgetColor_clear-button {
|
||||
}
|
||||
|
||||
.CMS_WidgetColor_clear-button-icon {
|
||||
@apply w-5
|
||||
h-5;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import TextField from '@staticcms/core/components/common/text-field/TextField';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { ColorField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { ColorField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { ChangeEvent, FC, MouseEvent, MouseEventHandler } from 'react';
|
||||
import type { ColorResult } from 'react-color';
|
||||
|
||||
@ -30,7 +30,6 @@ export const classes = generateClassNames('WidgetColor', [
|
||||
'color-picker',
|
||||
'input',
|
||||
'clear-button',
|
||||
'clear-button-icon',
|
||||
]);
|
||||
|
||||
const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
|
||||
@ -164,13 +163,13 @@ const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
|
||||
/>
|
||||
{showClearButton ? (
|
||||
<IconButton
|
||||
icon={CloseIcon}
|
||||
variant="text"
|
||||
onClick={handleClear}
|
||||
disabled={disabled}
|
||||
className={classes['clear-button']}
|
||||
>
|
||||
<CloseIcon className={classes['clear-button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['clear-button']}
|
||||
aria-label="clear"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Field>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { ColorField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { ColorField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const classes = generateClassNames('WidgetColorPreview', ['root']);
|
||||
|
@ -2,7 +2,7 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { mockColorField } from '@staticcms/test/data/fields.mock';
|
||||
@ -196,7 +196,7 @@ describe(ColorControl.name, () => {
|
||||
expect(input).not.toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
const field = getByTestId('field');
|
||||
const field = getByTestId('field-Mock Widget');
|
||||
await userEvent.click(field);
|
||||
});
|
||||
|
||||
|
@ -3,7 +3,7 @@ import previewComponent from './ColorPreview';
|
||||
import schema from './schema';
|
||||
import validator from './validator';
|
||||
|
||||
import type { ColorField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { ColorField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const ColorWidget = (): WidgetParam<string, ColorField> => {
|
||||
return {
|
||||
|
@ -2,7 +2,7 @@ import validateColor from 'validate-color';
|
||||
|
||||
import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes';
|
||||
|
||||
import type { ColorField, FieldValidationMethod } from '@staticcms/core/interface';
|
||||
import type { ColorField, FieldValidationMethod } from '@staticcms/core';
|
||||
|
||||
const validator: FieldValidationMethod<string, ColorField> = ({ value, t }) => {
|
||||
if (typeof value !== 'string') {
|
||||
|
@ -2,15 +2,49 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetDateTime_wrapper {
|
||||
@apply !w-date-widget;
|
||||
}
|
||||
|
||||
.CMS_WidgetDateTime_date-input {
|
||||
.CMS_WidgetDateTime_date {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.CMS_WidgetDateTime_time-input {
|
||||
.CMS_WidgetDateTime_time {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.CMS_WidgetDateTime_datetime-input {
|
||||
@apply truncate;
|
||||
.CMS_WidgetDateTime_datetime {
|
||||
}
|
||||
|
||||
.CMS_WidgetDateTime_inputs {
|
||||
@apply flex
|
||||
items-center
|
||||
w-full
|
||||
ps-1.5
|
||||
pe-2.5
|
||||
gap-2;
|
||||
|
||||
& .CMS_WidgetDateTime_input-wrapper {
|
||||
@apply flex-grow;
|
||||
|
||||
& .CMS_WidgetDateTime_input {
|
||||
@apply py-1
|
||||
pl-2;
|
||||
}
|
||||
|
||||
& .MuiOutlinedInput-root {
|
||||
& .MuiOutlinedInput-notchedOutline {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
&.Mui-focused {
|
||||
& .MuiOutlinedInput-notchedOutline {
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .MuiIconButton-root {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,23 @@
|
||||
import { unstable_useForkRef as useForkRef } from '@mui/utils';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker';
|
||||
import { MobileDateTimePicker } from '@mui/x-date-pickers/MobileDateTimePicker';
|
||||
import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker';
|
||||
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
|
||||
import formatDate from 'date-fns/format';
|
||||
import parse from 'date-fns/parse';
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Field from '@staticcms/core/components/common/field/Field';
|
||||
import TextField from '@staticcms/core/components/common/text-field/TextField';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import NowButton from './components/NowButton';
|
||||
import {
|
||||
DEFAULT_DATETIME_FORMAT,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_TIMEZONE_FORMAT,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from './constants';
|
||||
import { DEFAULT_DATETIME_FORMAT } from './constants';
|
||||
import { useDatetimeFormats } from './datetime.util';
|
||||
import { localToUTC } from './utc.util';
|
||||
|
||||
import type { TextFieldProps as MuiTextFieldProps } from '@mui/material/TextField';
|
||||
import type { TextFieldProps } from '@staticcms/core/components/common/text-field/TextField';
|
||||
import type { DateTimeField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { DateTimeField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import './DateTimeControl.css';
|
||||
@ -36,28 +29,11 @@ export const classes = generateClassNames('WidgetDateTime', [
|
||||
'disabled',
|
||||
'for-single-list',
|
||||
'wrapper',
|
||||
'date-input',
|
||||
'time-input',
|
||||
'datetime-input',
|
||||
'inputs',
|
||||
'input-wrapper',
|
||||
'input',
|
||||
]);
|
||||
|
||||
function convertMuiTextFieldProps({
|
||||
inputProps,
|
||||
disabled,
|
||||
onClick,
|
||||
}: MuiTextFieldProps): TextFieldProps {
|
||||
const value: string = inputProps?.value ?? '';
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
value,
|
||||
disabled,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onChange: () => {},
|
||||
onClick,
|
||||
};
|
||||
}
|
||||
|
||||
const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
field,
|
||||
label,
|
||||
@ -67,7 +43,6 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
errors,
|
||||
hasErrors,
|
||||
forSingleList,
|
||||
t,
|
||||
onChange,
|
||||
}) => {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
@ -80,72 +55,16 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const timezoneExtra = useMemo(
|
||||
() => (field.picker_utc ? '' : DEFAULT_TIMEZONE_FORMAT),
|
||||
[field.picker_utc],
|
||||
);
|
||||
|
||||
const { format, dateFormat, timeFormat } = useMemo(() => {
|
||||
// dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
|
||||
const dateFormat: string | boolean = field.date_format ?? true;
|
||||
// show time-picker? false hides it, true shows it using default format
|
||||
const timeFormat: string | boolean = field.time_format ?? true;
|
||||
|
||||
let finalFormat = field.format;
|
||||
if (timeFormat === false) {
|
||||
finalFormat = field.format ?? DEFAULT_DATE_FORMAT;
|
||||
} else if (dateFormat === false) {
|
||||
finalFormat = field.format ?? `${DEFAULT_TIME_FORMAT}${timezoneExtra}`;
|
||||
} else {
|
||||
finalFormat = field.format ?? `${DEFAULT_DATETIME_FORMAT}${timezoneExtra}`;
|
||||
}
|
||||
|
||||
return {
|
||||
format: finalFormat,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
};
|
||||
}, [field.date_format, field.format, field.time_format, timezoneExtra]);
|
||||
|
||||
const inputFormat = useMemo(() => {
|
||||
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
|
||||
const formatParts: string[] = [];
|
||||
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
|
||||
formatParts.push(dateFormat);
|
||||
} else if (dateFormat !== false) {
|
||||
formatParts.push(DEFAULT_DATE_FORMAT);
|
||||
}
|
||||
|
||||
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
|
||||
formatParts.push(timeFormat);
|
||||
} else if (timeFormat !== false) {
|
||||
formatParts.push(`${DEFAULT_TIME_FORMAT}${timezoneExtra}`);
|
||||
}
|
||||
|
||||
if (formatParts.length > 0) {
|
||||
return formatParts.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (timeFormat === false) {
|
||||
return format ?? DEFAULT_DATE_FORMAT;
|
||||
}
|
||||
|
||||
if (dateFormat === false) {
|
||||
return format ?? `${DEFAULT_TIME_FORMAT}${timezoneExtra}`;
|
||||
}
|
||||
|
||||
return format ?? `${DEFAULT_DATETIME_FORMAT}${timezoneExtra}`;
|
||||
}, [dateFormat, format, timeFormat, timezoneExtra]);
|
||||
const { storageFormat, dateFormat, timeFormat, displayFormat } = useDatetimeFormats(field);
|
||||
|
||||
const defaultValue = useMemo(() => {
|
||||
const today = field.picker_utc ? localToUTC(new Date()) : new Date();
|
||||
return field.default === undefined
|
||||
? format
|
||||
? formatDate(today, format)
|
||||
? storageFormat
|
||||
? formatDate(today, storageFormat)
|
||||
: formatDate(today, DEFAULT_DATETIME_FORMAT)
|
||||
: field.default;
|
||||
}, [field.default, field.picker_utc, format]);
|
||||
}, [field.default, field.picker_utc, storageFormat]);
|
||||
|
||||
const [internalRawValue, setInternalValue] = useState(value);
|
||||
const internalValue = useMemo(
|
||||
@ -163,12 +82,12 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
return valueToParse;
|
||||
}
|
||||
|
||||
return format ? parse(valueToParse, format, new Date()) : parseISO(valueToParse);
|
||||
}, [defaultValue, format, internalValue]);
|
||||
return storageFormat ? parse(valueToParse, storageFormat, new Date()) : parseISO(valueToParse);
|
||||
}, [defaultValue, storageFormat, internalValue]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(datetime: Date | null) => {
|
||||
if (datetime === null) {
|
||||
if (datetime === null || isNaN(datetime.getTime())) {
|
||||
setInternalValue(defaultValue);
|
||||
onChange(defaultValue);
|
||||
return;
|
||||
@ -176,121 +95,96 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
|
||||
const adjustedValue = field.picker_utc ? localToUTC(datetime) : datetime;
|
||||
|
||||
const formattedValue = formatDate(adjustedValue, format);
|
||||
const formattedValue = formatDate(adjustedValue, storageFormat);
|
||||
setInternalValue(formattedValue);
|
||||
onChange(formattedValue);
|
||||
},
|
||||
[defaultValue, field.picker_utc, format, onChange],
|
||||
[defaultValue, field.picker_utc, storageFormat, onChange],
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const rootRef = useForkRef(ref, inputRef);
|
||||
|
||||
const dateTimePicker = useMemo(() => {
|
||||
if (dateFormat && !timeFormat) {
|
||||
return (
|
||||
<MobileDatePicker
|
||||
key="mobile-date-picker"
|
||||
inputFormat={inputFormat}
|
||||
label={label}
|
||||
<DatePicker
|
||||
key="date-picker"
|
||||
format={displayFormat}
|
||||
value={dateValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
renderInput={props => (
|
||||
<>
|
||||
<TextField
|
||||
key="mobile-date-input"
|
||||
data-testid="date-input"
|
||||
{...convertMuiTextFieldProps(props)}
|
||||
inputRef={ref}
|
||||
cursor="pointer"
|
||||
inputClassName={classes['date-input']}
|
||||
/>
|
||||
<NowButton
|
||||
key="mobile-date-now"
|
||||
t={t}
|
||||
handleChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
className={classes['input-wrapper']}
|
||||
inputRef={rootRef}
|
||||
slotProps={{
|
||||
textField: {
|
||||
inputProps: {
|
||||
'data-testid': 'date-input',
|
||||
className: classes.input,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dateFormat && timeFormat) {
|
||||
return (
|
||||
<MobileTimePicker
|
||||
<TimePicker
|
||||
key="time-picker"
|
||||
label={label}
|
||||
inputFormat={inputFormat}
|
||||
format={displayFormat}
|
||||
value={dateValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
renderInput={props => (
|
||||
<>
|
||||
<TextField
|
||||
key="mobile-time-input"
|
||||
data-testid="time-input"
|
||||
{...convertMuiTextFieldProps(props)}
|
||||
inputRef={ref}
|
||||
cursor="pointer"
|
||||
inputClassName={classes['time-input']}
|
||||
/>
|
||||
<NowButton
|
||||
key="mobile-date-now"
|
||||
t={t}
|
||||
handleChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
className={classes['input-wrapper']}
|
||||
inputRef={rootRef}
|
||||
slotProps={{
|
||||
textField: {
|
||||
inputProps: {
|
||||
'data-testid': 'time-input',
|
||||
className: classes.input,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileDateTimePicker
|
||||
key="mobile-date-time-picker"
|
||||
inputFormat={inputFormat}
|
||||
label={label}
|
||||
<DateTimePicker
|
||||
key="date-time-picker"
|
||||
format={displayFormat}
|
||||
value={dateValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
renderInput={props => (
|
||||
<>
|
||||
<TextField
|
||||
key="mobile-date-time-input"
|
||||
data-testid="date-time-input"
|
||||
{...convertMuiTextFieldProps(props)}
|
||||
inputRef={ref}
|
||||
cursor="pointer"
|
||||
inputClassName={classes['datetime-input']}
|
||||
/>
|
||||
<NowButton
|
||||
key="mobile-date-now"
|
||||
t={t}
|
||||
handleChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
className={classes['input-wrapper']}
|
||||
inputRef={rootRef}
|
||||
slotProps={{
|
||||
textField: {
|
||||
inputProps: {
|
||||
'data-testid': 'date-time-input',
|
||||
className: classes.input,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
dateFormat,
|
||||
dateValue,
|
||||
handleChange,
|
||||
handleClose,
|
||||
handleOpen,
|
||||
inputFormat,
|
||||
disabled,
|
||||
label,
|
||||
t,
|
||||
timeFormat,
|
||||
displayFormat,
|
||||
dateValue,
|
||||
disabled,
|
||||
handleChange,
|
||||
handleOpen,
|
||||
handleClose,
|
||||
rootRef,
|
||||
]);
|
||||
|
||||
return (
|
||||
@ -300,7 +194,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
errors={errors}
|
||||
hint={field.hint}
|
||||
forSingleList={forSingleList}
|
||||
cursor="pointer"
|
||||
cursor="text"
|
||||
disabled={disabled}
|
||||
rootClassName={classNames(
|
||||
classes.root,
|
||||
@ -311,9 +205,17 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
||||
)}
|
||||
wrapperClassName={classes.wrapper}
|
||||
>
|
||||
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
|
||||
{dateTimePicker}
|
||||
</LocalizationProvider>
|
||||
<div className={classes['inputs']}>
|
||||
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
|
||||
{dateTimePicker}
|
||||
</LocalizationProvider>
|
||||
<NowButton
|
||||
key="date-now"
|
||||
field={field}
|
||||
handleChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { DateTimeField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { DateTimeField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const classes = generateClassNames('WidgetDateTimePreview', ['root']);
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { fireEvent, getByText } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom';
|
||||
import { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import { mockDateField, mockDateTimeField, mockTimeField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
@ -85,8 +85,7 @@ async function selectDate(
|
||||
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
|
||||
await act(async () => {
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
await userEventActions.click(input);
|
||||
});
|
||||
|
||||
@ -102,8 +101,7 @@ async function selectTime(
|
||||
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
|
||||
await act(async () => {
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
await userEventActions.click(input);
|
||||
});
|
||||
|
||||
@ -120,8 +118,7 @@ async function selectDateTime(
|
||||
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
|
||||
await act(async () => {
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
await userEventActions.click(input);
|
||||
});
|
||||
|
||||
@ -169,9 +166,8 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label' });
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35');
|
||||
});
|
||||
|
||||
it('should use default if provided', () => {
|
||||
@ -179,46 +175,42 @@ describe(DateTimeControl.name, () => {
|
||||
label: 'I am a label',
|
||||
field: {
|
||||
...mockDateTimeField,
|
||||
default: '2023-01-10T06:23:15.000-10:00',
|
||||
default: '2023-01-10T06:23:15-10:00',
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-01-10T06:23:15.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-01-10T06:23:15');
|
||||
});
|
||||
|
||||
it('should only use prop value as initial value', async () => {
|
||||
const { rerender, getByTestId } = renderControl({ value: '2023-02-12T10:15:35.000-10:00' });
|
||||
const { rerender, getByTestId } = renderControl({ value: '2023-02-12T10:15:35-10:00' });
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35');
|
||||
|
||||
rerender({ value: '2023-02-18T14:37:02.000-10:00' });
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
|
||||
rerender({ value: '2023-02-18T14:37:02-10:00' });
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35');
|
||||
});
|
||||
|
||||
it('should use prop value exclusively if field is i18n duplicate', async () => {
|
||||
const { rerender, getByTestId } = renderControl({
|
||||
field: { ...mockDateTimeField, i18n: 'duplicate' },
|
||||
duplicate: true,
|
||||
value: '2023-02-12T10:15:35.000-10:00',
|
||||
value: '2023-02-12T10:15:35-10:00',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35');
|
||||
|
||||
rerender({ value: '2023-02-18T14:37:02.000-10:00' });
|
||||
expect(input).toHaveValue('2023-02-18T14:37:02.000-10:00');
|
||||
rerender({ value: '2023-02-18T14:37:02-10:00' });
|
||||
expect(input).toHaveValue('2023-02-18T14:37:02');
|
||||
});
|
||||
|
||||
it('should disable input and now button if disabled', () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', disabled: true });
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toBeDisabled();
|
||||
|
||||
const nowButton = getByTestId('datetime-now');
|
||||
@ -228,12 +220,11 @@ describe(DateTimeControl.name, () => {
|
||||
it('should focus current date in modal on field click', async () => {
|
||||
const { getByTestId } = renderControl();
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).not.toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
const field = getByTestId('field');
|
||||
const field = getByTestId('field-Mock Widget');
|
||||
await userEventActions.click(field);
|
||||
});
|
||||
|
||||
@ -249,8 +240,7 @@ describe(DateTimeControl.name, () => {
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
|
||||
await act(async () => {
|
||||
await userEventActions.click(input);
|
||||
@ -266,7 +256,7 @@ describe(DateTimeControl.name, () => {
|
||||
await userEventActions.click(days[0]);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T10:15:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T10:15:35-10:00');
|
||||
|
||||
const hours = document.querySelectorAll('.MuiClockNumber-root');
|
||||
expect(hours.length).toBe(12);
|
||||
@ -281,7 +271,7 @@ describe(DateTimeControl.name, () => {
|
||||
fireEvent.touchEnd(square!, hourClockEvent);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:15:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:15:35-10:00');
|
||||
|
||||
const minutes = document.querySelectorAll('.MuiClockNumber-root');
|
||||
expect(minutes.length).toBe(12);
|
||||
@ -293,7 +283,7 @@ describe(DateTimeControl.name, () => {
|
||||
fireEvent.touchEnd(square!, minuteClockEvent);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:05:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:05:35-10:00');
|
||||
});
|
||||
|
||||
it('should set value to current date and time when now button is clicked', async () => {
|
||||
@ -304,22 +294,21 @@ describe(DateTimeControl.name, () => {
|
||||
label: 'I am a label',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12T10:15:35');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
|
||||
expect(input).toHaveValue('2023-02-01T02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
|
||||
expect(input).toHaveValue('2023-02-01T02:20:35');
|
||||
|
||||
await act(async () => {
|
||||
const nowButton = getByTestId('datetime-now');
|
||||
await userEventActions.click(nowButton);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-12T10:15:36.000-10:00'); // Testing framework moves the time forward by a second by this point
|
||||
expect(input).toHaveValue('2023-02-12T10:15:36.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-12T10:15:36-10:00'); // Testing framework moves the time forward by a second by this point
|
||||
expect(input).toHaveValue('2023-02-12T10:15:36');
|
||||
});
|
||||
|
||||
describe('format', () => {
|
||||
@ -335,14 +324,13 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('02/12/2023 10:15:35.000-10:00');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('02/12/2023 10:15:35');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
|
||||
expect(input).toHaveValue('02/01/2023 02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
|
||||
expect(input).toHaveValue('02/01/2023 02:20:35');
|
||||
});
|
||||
|
||||
it('uses custom time display format', async () => {
|
||||
@ -357,13 +345,12 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12 10:15 am');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
|
||||
expect(input).toHaveValue('2023-02-01 02:20 am');
|
||||
});
|
||||
|
||||
@ -380,13 +367,12 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('02/12/2023 10:15 am');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
|
||||
expect(input).toHaveValue('02/01/2023 02:20 am');
|
||||
});
|
||||
|
||||
@ -404,8 +390,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('02/12/2023 10:15 am');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 3, 20, 'pm');
|
||||
@ -426,8 +411,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12 10:15');
|
||||
|
||||
await selectDateTime(getByTestId, 1, 3, 20, 'pm');
|
||||
@ -446,9 +430,8 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-02-12T20:15:35.000');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-02-12T20:15:35');
|
||||
});
|
||||
|
||||
it('should use default if provided (assuming default is already in UTC)', () => {
|
||||
@ -456,13 +439,12 @@ describe(DateTimeControl.name, () => {
|
||||
label: 'I am a label',
|
||||
field: {
|
||||
...utcField,
|
||||
default: '2023-01-10T06:23:15.000',
|
||||
default: '2023-01-10T06:23:15',
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('2023-01-10T06:23:15.000');
|
||||
const input = getByTestId('date-time-input');
|
||||
expect(input).toHaveValue('2023-01-10T06:23:15');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -471,8 +453,7 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', field: mockDateField });
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
});
|
||||
|
||||
@ -485,8 +466,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-01-10');
|
||||
});
|
||||
|
||||
@ -496,8 +476,7 @@ describe(DateTimeControl.name, () => {
|
||||
value: '2023-02-12',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
|
||||
rerender({ value: '2023-02-18' });
|
||||
@ -511,8 +490,7 @@ describe(DateTimeControl.name, () => {
|
||||
value: '2023-02-12',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
|
||||
rerender({ value: '2023-02-18' });
|
||||
@ -526,8 +504,7 @@ describe(DateTimeControl.name, () => {
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toBeDisabled();
|
||||
|
||||
const nowButton = getByTestId('datetime-now');
|
||||
@ -537,12 +514,11 @@ describe(DateTimeControl.name, () => {
|
||||
it('should focus current date in modal on field click', async () => {
|
||||
const { getByTestId } = renderControl({ field: mockDateField });
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).not.toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
const field = getByTestId('field');
|
||||
const field = getByTestId('field-Mock Widget');
|
||||
await userEventActions.click(field);
|
||||
});
|
||||
|
||||
@ -558,8 +534,7 @@ describe(DateTimeControl.name, () => {
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
|
||||
await act(async () => {
|
||||
await userEventActions.click(input);
|
||||
@ -587,8 +562,7 @@ describe(DateTimeControl.name, () => {
|
||||
field: mockDateField,
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
|
||||
await selectDate(getByTestId, 1);
|
||||
@ -618,8 +592,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('02/12/2023');
|
||||
|
||||
await selectDate(getByTestId, 1);
|
||||
@ -641,8 +614,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('02/12/2023');
|
||||
|
||||
await selectDate(getByTestId, 1);
|
||||
@ -663,8 +635,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
|
||||
await selectDate(getByTestId, 1);
|
||||
@ -683,8 +654,7 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-02-12');
|
||||
});
|
||||
|
||||
@ -697,8 +667,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('date-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('date-input');
|
||||
expect(input).toHaveValue('2023-01-10');
|
||||
});
|
||||
});
|
||||
@ -708,9 +677,8 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', field: mockTimeField });
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('10:15:35.000-10:00');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15:35');
|
||||
});
|
||||
|
||||
it('should use default if provided', () => {
|
||||
@ -718,43 +686,40 @@ describe(DateTimeControl.name, () => {
|
||||
label: 'I am a label',
|
||||
field: {
|
||||
...mockTimeField,
|
||||
default: '06:23:15.000-10:00',
|
||||
default: '06:23:15-10:00',
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('06:23:15.000-10:00');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('06:23:15');
|
||||
});
|
||||
|
||||
it('should only use prop value as initial value', async () => {
|
||||
const { rerender, getByTestId } = renderControl({
|
||||
field: mockTimeField,
|
||||
value: '10:15:35.000-10:00',
|
||||
value: '10:15:35-10:00',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('10:15:35.000-10:00');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15:35');
|
||||
|
||||
rerender({ value: '14:37:02.000-10:00' });
|
||||
expect(input).toHaveValue('10:15:35.000-10:00');
|
||||
rerender({ value: '14:37:02-10:00' });
|
||||
expect(input).toHaveValue('10:15:35');
|
||||
});
|
||||
|
||||
it('should use prop value exclusively if field is i18n duplicate', async () => {
|
||||
const { rerender, getByTestId } = renderControl({
|
||||
field: { ...mockTimeField, i18n: 'duplicate' },
|
||||
duplicate: true,
|
||||
value: '10:15:35.000-10:00',
|
||||
value: '10:15:35-10:00',
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
|
||||
expect(input).toHaveValue('10:15:35.000-10:00');
|
||||
expect(input).toHaveValue('10:15:35');
|
||||
|
||||
rerender({ value: '14:37:02.000-10:00' });
|
||||
expect(input).toHaveValue('14:37:02.000-10:00');
|
||||
rerender({ value: '14:37:02-10:00' });
|
||||
expect(input).toHaveValue('14:37:02');
|
||||
});
|
||||
|
||||
it('should disable input and now button if disabled', () => {
|
||||
@ -764,8 +729,7 @@ describe(DateTimeControl.name, () => {
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
|
||||
@ -776,12 +740,11 @@ describe(DateTimeControl.name, () => {
|
||||
it('should focus current time in modal on field click', async () => {
|
||||
const { getByTestId } = renderControl({ field: mockTimeField });
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).not.toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
const field = getByTestId('field');
|
||||
const field = getByTestId('field-Mock Widget');
|
||||
await userEventActions.click(field);
|
||||
});
|
||||
|
||||
@ -797,8 +760,7 @@ describe(DateTimeControl.name, () => {
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
|
||||
await act(async () => {
|
||||
await userEventActions.click(input);
|
||||
@ -819,7 +781,7 @@ describe(DateTimeControl.name, () => {
|
||||
fireEvent.touchEnd(square!, hourClockEvent);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('01:15:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('01:15:35-10:00');
|
||||
|
||||
const minutes = document.querySelectorAll('.MuiClockNumber-root');
|
||||
expect(minutes.length).toBe(12);
|
||||
@ -831,7 +793,7 @@ describe(DateTimeControl.name, () => {
|
||||
fireEvent.touchEnd(square!, minuteClockEvent);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('01:05:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('01:05:35-10:00');
|
||||
});
|
||||
|
||||
it('should set value to current time when now button is clicked', async () => {
|
||||
@ -843,22 +805,21 @@ describe(DateTimeControl.name, () => {
|
||||
field: mockTimeField,
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('10:15:35.000-10:00');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15:35');
|
||||
|
||||
await selectTime(getByTestId, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('02:20:35.000-10:00');
|
||||
expect(input).toHaveValue('02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('02:20:35-10:00');
|
||||
expect(input).toHaveValue('02:20:35');
|
||||
|
||||
await act(async () => {
|
||||
const nowButton = getByTestId('datetime-now');
|
||||
await userEventActions.click(nowButton);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('10:15:36.000-10:00'); // Testing framework moves the time forward by a second by this point
|
||||
expect(input).toHaveValue('10:15:36.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('10:15:36-10:00'); // Testing framework moves the time forward by a second by this point
|
||||
expect(input).toHaveValue('10:15:36');
|
||||
});
|
||||
|
||||
describe('format', () => {
|
||||
@ -874,13 +835,12 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15 am');
|
||||
|
||||
await selectTime(getByTestId, 2, 20, 'am');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('02:20:35.000-10:00');
|
||||
expect(onChange).toHaveBeenLastCalledWith('02:20:35-10:00');
|
||||
expect(input).toHaveValue('02:20 am');
|
||||
});
|
||||
|
||||
@ -897,8 +857,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15 am');
|
||||
|
||||
await selectTime(getByTestId, 3, 20, 'pm');
|
||||
@ -919,8 +878,7 @@ describe(DateTimeControl.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('10:15');
|
||||
|
||||
await selectTime(getByTestId, 3, 20, 'pm');
|
||||
@ -939,9 +897,8 @@ describe(DateTimeControl.name, () => {
|
||||
it("should default to today's date if no default is provided", () => {
|
||||
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('20:15:35.000');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('20:15:35');
|
||||
});
|
||||
|
||||
it('should use default if provided (assuming default is already in UTC)', () => {
|
||||
@ -949,13 +906,12 @@ describe(DateTimeControl.name, () => {
|
||||
label: 'I am a label',
|
||||
field: {
|
||||
...utcField,
|
||||
default: '06:23:15.000',
|
||||
default: '06:23:15',
|
||||
},
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('time-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toHaveValue('06:23:15.000');
|
||||
const input = getByTestId('time-input');
|
||||
expect(input).toHaveValue('06:23:15');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,12 +12,12 @@ describe('DateTime getDefaultValue', () => {
|
||||
|
||||
describe('datetime', () => {
|
||||
it("should use today's date", () => {
|
||||
expect(getDefaultValue(undefined, mockDateTimeField)).toEqual('2023-02-12T10:15:35.000');
|
||||
expect(getDefaultValue(undefined, mockDateTimeField)).toEqual('2023-02-12T10:15:35');
|
||||
});
|
||||
|
||||
it('should use provided default', () => {
|
||||
expect(getDefaultValue('2022-06-18T14:30:01.000', mockDateTimeField)).toEqual(
|
||||
'2022-06-18T14:30:01.000',
|
||||
expect(getDefaultValue('2022-06-18T14:30:01', mockDateTimeField)).toEqual(
|
||||
'2022-06-18T14:30:01',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -34,11 +34,11 @@ describe('DateTime getDefaultValue', () => {
|
||||
|
||||
describe('time', () => {
|
||||
it("should use today's date", () => {
|
||||
expect(getDefaultValue(undefined, mockTimeField)).toEqual('10:15:35.000');
|
||||
expect(getDefaultValue(undefined, mockTimeField)).toEqual('10:15:35');
|
||||
});
|
||||
|
||||
it('should use provided default', () => {
|
||||
expect(getDefaultValue('14:30:01.000', mockTimeField)).toEqual('14:30:01.000');
|
||||
expect(getDefaultValue('14:30:01', mockTimeField)).toEqual('14:30:01');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,4 @@
|
||||
.CMS_WidgetDateTime_NowButton_root {
|
||||
@apply absolute
|
||||
inset-y-1
|
||||
right-3
|
||||
flex
|
||||
@apply flex
|
||||
items-center;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import Button from '@staticcms/core/components/common/button/Button';
|
||||
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { DateTimeField } from '@staticcms/core';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
import './NowButton.css';
|
||||
@ -12,10 +13,13 @@ const classes = generateClassNames('WidgetDateTime_NowButton', ['root', 'button'
|
||||
|
||||
export interface NowButtonProps {
|
||||
handleChange: (value: Date) => void;
|
||||
field: DateTimeField;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const NowButton: FC<TranslatedProps<NowButtonProps>> = ({ disabled, t, handleChange }) => {
|
||||
const NowButton: FC<NowButtonProps> = ({ disabled, field, handleChange }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@ -31,8 +35,10 @@ const NowButton: FC<TranslatedProps<NowButtonProps>> = ({ disabled, t, handleCha
|
||||
data-testid="datetime-now"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={classes.button}
|
||||
aria-label={`set ${field.label ?? field.name} to now`}
|
||||
>
|
||||
{t('editor.editorWidgets.datetime.now')}
|
||||
</Button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DEFAULT_TIME_FORMAT = 'HH:mm:ss.SSS';
|
||||
export const DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS";
|
||||
export const DEFAULT_TIME_FORMAT = 'HH:mm:ss';
|
||||
export const DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
|
||||
export const DEFAULT_TIMEZONE_FORMAT = 'XXX';
|
||||
|
89
packages/core/src/widgets/datetime/datetime.util.ts
Normal file
89
packages/core/src/widgets/datetime/datetime.util.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import {
|
||||
DEFAULT_DATETIME_FORMAT,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_TIMEZONE_FORMAT,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from './constants';
|
||||
|
||||
import type { DateTimeField, DateTimeFormats } from '@staticcms/core';
|
||||
|
||||
function getDisplayFormat(
|
||||
dateFormat: string | boolean,
|
||||
timeFormat: string | boolean,
|
||||
storageFormat: string,
|
||||
) {
|
||||
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
|
||||
const formatParts: string[] = [];
|
||||
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
|
||||
formatParts.push(dateFormat);
|
||||
} else if (dateFormat !== false) {
|
||||
formatParts.push(DEFAULT_DATE_FORMAT);
|
||||
}
|
||||
|
||||
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
|
||||
formatParts.push(timeFormat);
|
||||
} else if (timeFormat !== false) {
|
||||
formatParts.push(`${DEFAULT_TIME_FORMAT}`);
|
||||
}
|
||||
|
||||
if (formatParts.length > 0) {
|
||||
return formatParts.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (timeFormat === false) {
|
||||
return storageFormat ?? DEFAULT_DATE_FORMAT;
|
||||
}
|
||||
|
||||
if (dateFormat === false) {
|
||||
return storageFormat ?? `${DEFAULT_TIME_FORMAT}`;
|
||||
}
|
||||
|
||||
return storageFormat ?? `${DEFAULT_DATETIME_FORMAT}`;
|
||||
}
|
||||
|
||||
export function getDatetimeFormats(field: DateTimeField): DateTimeFormats;
|
||||
export function getDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined;
|
||||
export function getDatetimeFormats(field: DateTimeField | undefined) {
|
||||
if (!field) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timezoneExtra = field.picker_utc ? '' : DEFAULT_TIMEZONE_FORMAT;
|
||||
|
||||
// dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
|
||||
const dateFormat: string | boolean = field.date_format ?? true;
|
||||
// show time-picker? false hides it, true shows it using default format
|
||||
const timeFormat: string | boolean = field.time_format ?? true;
|
||||
|
||||
let storageFormat = field.format;
|
||||
let shouldAddTimezoneExtra = false;
|
||||
if (timeFormat === false) {
|
||||
storageFormat = field.format ?? DEFAULT_DATE_FORMAT;
|
||||
} else if (dateFormat === false) {
|
||||
storageFormat = field.format ?? DEFAULT_TIME_FORMAT;
|
||||
shouldAddTimezoneExtra = !field.format;
|
||||
} else {
|
||||
storageFormat = field.format ?? DEFAULT_DATETIME_FORMAT;
|
||||
shouldAddTimezoneExtra = !field.format;
|
||||
}
|
||||
|
||||
const displayFormat = getDisplayFormat(dateFormat, timeFormat, storageFormat);
|
||||
|
||||
return {
|
||||
storageFormat: `${storageFormat}${shouldAddTimezoneExtra ? timezoneExtra : ''}`,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
displayFormat,
|
||||
timezoneExtra,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDatetimeFormats(field: DateTimeField): DateTimeFormats;
|
||||
export function useDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined;
|
||||
export function useDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined {
|
||||
return useMemo(() => getDatetimeFormats(field), [field]);
|
||||
}
|
@ -4,7 +4,7 @@ import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT } from './constants';
|
||||
import { localToUTC } from './utc.util';
|
||||
|
||||
import type { DateTimeField, FieldGetDefaultMethod } from '@staticcms/core/interface';
|
||||
import type { DateTimeField, FieldGetDefaultMethod } from '@staticcms/core';
|
||||
|
||||
const getDefaultValue: FieldGetDefaultMethod<string | Date, DateTimeField> = (
|
||||
defaultValue,
|
||||
|
@ -3,7 +3,7 @@ import previewComponent from './DateTimePreview';
|
||||
import getDefaultValue from './getDefaultValue';
|
||||
import schema from './schema';
|
||||
|
||||
import type { DateTimeField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { DateTimeField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const DateTimeWidget = (): WidgetParam<string | Date, DateTimeField> => {
|
||||
return {
|
||||
|
@ -5,7 +5,7 @@
|
||||
border-transparent;
|
||||
|
||||
&.CMS_WidgetFileImage_drag-over-active {
|
||||
@apply border-blue-500;
|
||||
border-color: var(--primary-main);
|
||||
|
||||
& .CMS_WidgetFileImage_drop-area {
|
||||
@apply opacity-100;
|
||||
@ -29,6 +29,9 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_drop-area {
|
||||
color: var(--primary-light);
|
||||
background: color-mix(in srgb, var(--background-main) 75%, transparent);
|
||||
|
||||
@apply absolute
|
||||
inset-0
|
||||
flex
|
||||
@ -36,10 +39,6 @@
|
||||
justify-center
|
||||
pointer-events-none
|
||||
font-bold
|
||||
text-blue-500
|
||||
bg-white/75
|
||||
dark:text-blue-400
|
||||
dark:bg-slate-800/75
|
||||
transition-opacity
|
||||
opacity-0;
|
||||
}
|
||||
|
@ -3,16 +3,16 @@ import React from 'react';
|
||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithDefaults,
|
||||
Entry,
|
||||
FileOrImageField,
|
||||
WidgetPreviewProps,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface FileLinkProps {
|
||||
value: string;
|
||||
collection: Collection<FileOrImageField>;
|
||||
collection: CollectionWithDefaults<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
entry: Entry;
|
||||
}
|
||||
|
@ -3,22 +3,23 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { act, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import { configLoaded } from '@staticcms/core/actions/config';
|
||||
import { applyDefaults, configLoaded } from '@staticcms/core/actions/config';
|
||||
import {
|
||||
insertMedia,
|
||||
mediaDisplayURLSuccess,
|
||||
mediaInserted,
|
||||
} from '@staticcms/core/actions/mediaLibrary';
|
||||
import { store } from '@staticcms/core/store';
|
||||
import { createMockCollection } from '@staticcms/test/data/collections.mock';
|
||||
import { createMockConfig } from '@staticcms/test/data/config.mock';
|
||||
import { createMockFolderCollection } from '@staticcms/test/data/collections.mock';
|
||||
import { createNoDefaultsMockConfig } from '@staticcms/test/data/config.mock';
|
||||
import { mockFileField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
import withFileControl from '../withFileControl';
|
||||
|
||||
import type { Config, MediaFile } from '@staticcms/core/interface';
|
||||
import type { Config, ConfigWithDefaults, FileOrImageField, MediaFile } from '@staticcms/core';
|
||||
import type { WidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
|
||||
jest.mock('@staticcms/core/backend');
|
||||
|
||||
@ -55,21 +56,27 @@ jest.mock('@staticcms/core/lib/hooks/useMediaAsset', () => (url: string) => url)
|
||||
|
||||
describe('File Control', () => {
|
||||
const FileControl = withFileControl();
|
||||
const collection = createMockCollection({}, mockFileField);
|
||||
const config = createMockConfig({
|
||||
const collection = createMockFolderCollection({}, mockFileField);
|
||||
const originalConfig = createNoDefaultsMockConfig({
|
||||
collections: [collection],
|
||||
});
|
||||
|
||||
const mockInsertMedia = insertMedia as jest.Mock;
|
||||
|
||||
const renderControl = createWidgetControlHarness(
|
||||
FileControl,
|
||||
{ field: mockFileField, config },
|
||||
{ withMediaLibrary: true },
|
||||
);
|
||||
let renderControl: WidgetControlHarness<string | string[], FileOrImageField>;
|
||||
|
||||
beforeEach(() => {
|
||||
store.dispatch(configLoaded(config as unknown as Config));
|
||||
const config = applyDefaults(originalConfig);
|
||||
|
||||
renderControl = createWidgetControlHarness(
|
||||
FileControl,
|
||||
{ field: mockFileField, config },
|
||||
{ withMediaLibrary: true },
|
||||
);
|
||||
|
||||
store.dispatch(
|
||||
configLoaded(config as unknown as ConfigWithDefaults, originalConfig as unknown as Config),
|
||||
);
|
||||
store.dispatch(mediaDisplayURLSuccess('12345', 'path/to/file1.txt'));
|
||||
store.dispatch(mediaDisplayURLSuccess('67890', 'path/to/file2.png'));
|
||||
|
||||
|
@ -5,36 +5,35 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_card {
|
||||
background: var(--background-dark);
|
||||
border-color: var(--background-main);
|
||||
|
||||
@apply w-image-card
|
||||
h-image-card
|
||||
rounded-md
|
||||
shadow-sm
|
||||
overflow-hidden
|
||||
cursor-pointer
|
||||
border
|
||||
bg-gray-50/75
|
||||
border-gray-200/75
|
||||
dark:bg-slate-800
|
||||
dark:border-slate-600/75;
|
||||
border;
|
||||
|
||||
&:hover {
|
||||
& .CMS_WidgetFileImage_SortableImage_controls-wrapper {
|
||||
@apply visible
|
||||
bg-blue-200/25
|
||||
dark:bg-blue-400/60;
|
||||
background: color-mix(in srgb, var(--primary-main) 25%, transparent);
|
||||
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_handle {
|
||||
--tw-ring-color: color-mix(in srgb, var(--primary-light) 50%, transparent);
|
||||
|
||||
@apply absolute
|
||||
inset-0
|
||||
rounded-md
|
||||
z-20
|
||||
overflow-visible
|
||||
focus:ring-4
|
||||
focus:ring-gray-200
|
||||
dark:focus:ring-slate-700
|
||||
focus-visible:outline-none;
|
||||
}
|
||||
|
||||
@ -48,33 +47,31 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_controls {
|
||||
background: color-mix(in srgb, var(--background-dark) 90%, transparent);
|
||||
|
||||
@apply absolute
|
||||
top-2
|
||||
right-2
|
||||
flex
|
||||
gap-1;
|
||||
gap-1
|
||||
rounded;
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_replace-button {
|
||||
@apply text-white
|
||||
dark:text-white
|
||||
bg-gray-900/25
|
||||
dark:hover:text-blue-100
|
||||
dark:hover:bg-blue-800/80;
|
||||
color: var(--primary-contrast-color);
|
||||
background: color-mix(in srgb, var(--primary-dark) 15%, transparent);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_remove-button {
|
||||
@apply relative
|
||||
text-red-400
|
||||
bg-gray-900/25
|
||||
dark:hover:text-red-600
|
||||
dark:hover:bg-red-800/40
|
||||
z-30;
|
||||
}
|
||||
color: var(--error-main);
|
||||
background: color-mix(in srgb, var(--error-dark) 15%, transparent);
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_button-icon {
|
||||
@apply w-5
|
||||
h-5;
|
||||
@apply relative
|
||||
z-30;
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableImage_content {
|
||||
|
@ -8,7 +8,7 @@ import IconButton from '@staticcms/core/components/common/button/IconButton';
|
||||
import Image from '@staticcms/core/components/common/image/Image';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { Collection, FileOrImageField } from '@staticcms/core/interface';
|
||||
import type { CollectionWithDefaults, FileOrImageField } from '@staticcms/core';
|
||||
import type { FC, MouseEventHandler } from 'react';
|
||||
|
||||
import './SortableImage.css';
|
||||
@ -21,7 +21,6 @@ const classes = generateClassNames('WidgetFileImage_SortableImage', [
|
||||
'controls',
|
||||
'replace-button',
|
||||
'remove-button',
|
||||
'button-icon',
|
||||
'content',
|
||||
'image',
|
||||
]);
|
||||
@ -29,7 +28,7 @@ const classes = generateClassNames('WidgetFileImage_SortableImage', [
|
||||
export interface SortableImageProps {
|
||||
id: string;
|
||||
itemValue: string;
|
||||
collection: Collection<FileOrImageField>;
|
||||
collection: CollectionWithDefaults<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
onRemove?: MouseEventHandler;
|
||||
onReplace?: MouseEventHandler;
|
||||
@ -98,24 +97,24 @@ const SortableImage: FC<SortableImageProps> = ({
|
||||
<div className={classes.controls}>
|
||||
{onReplace ? (
|
||||
<IconButton
|
||||
icon={CameraAltIcon}
|
||||
key="replace"
|
||||
variant="text"
|
||||
onClick={handleReplace}
|
||||
className={classes['replace-button']}
|
||||
>
|
||||
<CameraAltIcon className={classes['button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['replace-button']}
|
||||
aria-label="replace image"
|
||||
/>
|
||||
) : null}
|
||||
{onRemove ? (
|
||||
<IconButton
|
||||
icon={DeleteIcon}
|
||||
key="remove"
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={handleRemove}
|
||||
className={classes['remove-button']}
|
||||
>
|
||||
<DeleteIcon className={classes['button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['remove-button']}
|
||||
aria-label="remove image"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,6 +4,8 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableLink_card {
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply w-full
|
||||
shadow-sm
|
||||
overflow-hidden
|
||||
@ -11,7 +13,6 @@
|
||||
border-l-2
|
||||
border-b
|
||||
border-solid
|
||||
border-l-slate-400
|
||||
p-2;
|
||||
}
|
||||
|
||||
@ -27,22 +28,7 @@
|
||||
gap-1;
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableLink_replace-button {
|
||||
@apply text-white
|
||||
dark:text-white
|
||||
dark:hover:text-blue-100
|
||||
dark:hover:bg-blue-800/80;
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableLink_remove-button {
|
||||
@apply relative
|
||||
text-red-400
|
||||
dark:hover:text-red-600
|
||||
dark:hover:bg-red-800/40
|
||||
z-30;
|
||||
}
|
||||
|
||||
.CMS_WidgetFileImage_SortableLink_button-icon {
|
||||
@apply w-5
|
||||
h-5;
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ const classes = generateClassNames('WidgetFileImage_SortableLink', [
|
||||
'controls',
|
||||
'replace-button',
|
||||
'remove-button',
|
||||
'button-icon',
|
||||
]);
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 100;
|
||||
@ -88,24 +87,25 @@ const SortableLink: FC<SortableLinkProps> = ({ id, itemValue, onRemove, onReplac
|
||||
<div className={classes.controls}>
|
||||
{onReplace ? (
|
||||
<IconButton
|
||||
icon={ModeEditIcon}
|
||||
key="replace"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
onClick={handleReplace}
|
||||
className={classes['replace-button']}
|
||||
>
|
||||
<ModeEditIcon className={classes['button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['replace-button']}
|
||||
aria-label="replace link"
|
||||
/>
|
||||
) : null}
|
||||
{onRemove ? (
|
||||
<IconButton
|
||||
icon={DeleteIcon}
|
||||
key="remove"
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={handleRemove}
|
||||
className={classes['remove-button']}
|
||||
>
|
||||
<DeleteIcon className={classes['button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['remove-button']}
|
||||
aria-label="remove link"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import schema from './schema';
|
||||
import withFileControl, { getValidFileValue } from './withFileControl';
|
||||
|
||||
import type { WithFileControlProps } from './withFileControl';
|
||||
import type { FileOrImageField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { FileOrImageField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const controlComponent = withFileControl();
|
||||
|
||||
|
@ -28,7 +28,7 @@ import SortableImage from './components/SortableImage';
|
||||
import SortableLink from './components/SortableLink';
|
||||
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import type { FileOrImageField, MediaPath, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { FileOrImageField, MediaPath, WidgetControlProps } from '@staticcms/core';
|
||||
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
@ -355,7 +355,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
<div key="controls" className={widgetFileImageClasses.actions}>
|
||||
<Button
|
||||
buttonRef={uploadButtonRef}
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
key="upload"
|
||||
onClick={handleOpenMediaLibrary}
|
||||
@ -366,7 +366,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
</Button>
|
||||
{chooseUrl ? (
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
key="choose-url"
|
||||
onClick={handleUrl(subject)}
|
||||
@ -387,7 +387,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
<div key="controls" className={widgetFileImageClasses.actions}>
|
||||
<Button
|
||||
buttonRef={uploadButtonRef}
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
key="add-replace"
|
||||
onClick={handleOpenMediaLibrary}
|
||||
@ -403,7 +403,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
{chooseUrl ? (
|
||||
allowsMultiple ? (
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
key="choose-url"
|
||||
onClick={handleUrl(subject)}
|
||||
@ -414,7 +414,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
key="replace-url"
|
||||
onClick={handleUrl(subject)}
|
||||
|
@ -3,16 +3,16 @@ import React from 'react';
|
||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithDefaults,
|
||||
Entry,
|
||||
FileOrImageField,
|
||||
WidgetPreviewProps,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface ImageAssetProps {
|
||||
value: string;
|
||||
collection: Collection<FileOrImageField>;
|
||||
collection: CollectionWithDefaults<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
entry: Entry;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import withFileControl, { getValidFileValue } from '../file/withFileControl';
|
||||
import previewComponent from './ImagePreview';
|
||||
import schema from './schema';
|
||||
|
||||
import type { FileOrImageField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { FileOrImageField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const controlComponent = withFileControl({ forImage: true });
|
||||
|
||||
|
@ -36,11 +36,6 @@
|
||||
w-6;
|
||||
}
|
||||
|
||||
.CMS_WidgetKeyValue_delete-button-icon {
|
||||
@apply h-5
|
||||
w-5;
|
||||
}
|
||||
|
||||
.CMS_WidgetKeyValue_actions {
|
||||
@apply px-3
|
||||
mt-3;
|
||||
|
@ -10,7 +10,7 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import { createEmptyPair } from './util';
|
||||
|
||||
import type { KeyValueField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { KeyValueField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { ChangeEvent, FC, MouseEvent } from 'react';
|
||||
import type { Pair } from './types';
|
||||
|
||||
@ -28,7 +28,6 @@ const classes = generateClassNames('WidgetKeyValue', [
|
||||
'header-action-cell-content',
|
||||
'row',
|
||||
'delete-button',
|
||||
'delete-button-icon',
|
||||
'actions',
|
||||
'add-button',
|
||||
]);
|
||||
@ -159,19 +158,20 @@ const StringControl: FC<WidgetControlProps<Pair[], KeyValueField>> = ({
|
||||
variant="contained"
|
||||
/>
|
||||
<IconButton
|
||||
icon={CloseIcon}
|
||||
data-testid={`remove-button-${index}`}
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={handleRemove(index)}
|
||||
disabled={disabled}
|
||||
className={classes['delete-button']}
|
||||
>
|
||||
<CloseIcon className={classes['delete-button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['delete-button']}
|
||||
aria-label="delete"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={classes.actions}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
onClick={handleAdd}
|
||||
className={classes['add-button']}
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { KeyValueField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { KeyValueField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
import type { Pair } from './types';
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import { mockKeyValueField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
@ -198,7 +198,7 @@ describe(KeyValueControl.name, () => {
|
||||
expect(keyInput).not.toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
const field = getByTestId('field');
|
||||
const field = getByTestId('field-Mock Widget');
|
||||
await userEvent.click(field);
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,7 @@ import '@testing-library/jest-dom';
|
||||
|
||||
import converters from '../converters';
|
||||
|
||||
import type { KeyValueField } from '@staticcms/core/interface';
|
||||
import type { KeyValueField } from '@staticcms/core';
|
||||
|
||||
describe('converters key value', () => {
|
||||
const keyValueField: KeyValueField = {
|
||||
|
@ -6,7 +6,7 @@ import '@testing-library/jest-dom';
|
||||
import validator from '../validator';
|
||||
import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes';
|
||||
|
||||
import type { KeyValueField } from '@staticcms/core/interface';
|
||||
import type { KeyValueField } from '@staticcms/core';
|
||||
|
||||
describe('validator key value', () => {
|
||||
const t = jest.fn();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createEmptyPair } from './util';
|
||||
|
||||
import type { FieldStorageConverters, KeyValueField } from '@staticcms/core/interface';
|
||||
import type { FieldStorageConverters, KeyValueField } from '@staticcms/core';
|
||||
import type { Pair } from './types';
|
||||
|
||||
const converters: FieldStorageConverters<Pair[], KeyValueField, Record<string, string>> = {
|
||||
@ -13,10 +13,13 @@ const converters: FieldStorageConverters<Pair[], KeyValueField, Record<string, s
|
||||
: [createEmptyPair()];
|
||||
},
|
||||
serialize(cmsValue) {
|
||||
return cmsValue?.reduce((acc, pair) => {
|
||||
acc[pair.key] = pair.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
return cmsValue?.reduce(
|
||||
(acc, pair) => {
|
||||
acc[pair.key] = pair.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import converters from './converters';
|
||||
import schema from './schema';
|
||||
import validator from './validator';
|
||||
|
||||
import type { KeyValueField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { KeyValueField, WidgetParam } from '@staticcms/core';
|
||||
import type { Pair } from './types';
|
||||
|
||||
const KeyValueWidget = (): WidgetParam<Pair[], KeyValueField> => {
|
||||
|
@ -2,7 +2,7 @@ import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { validateMinMax } from '@staticcms/core/lib/widgets/validations';
|
||||
|
||||
import type { FieldError, FieldValidationMethod, KeyValueField } from '@staticcms/core/interface';
|
||||
import type { FieldError, FieldValidationMethod, KeyValueField } from '@staticcms/core';
|
||||
import type { Pair } from './types';
|
||||
|
||||
const validator: FieldValidationMethod<Pair[], KeyValueField> = ({ field, value, t }) => {
|
||||
|
@ -6,7 +6,7 @@ import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import widgetListClasses from './ListControl.classes';
|
||||
|
||||
import type { ListField, ValueOrNestedValue, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { ListField, ValueOrNestedValue, WidgetControlProps } from '@staticcms/core';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
|
||||
const DelimitedListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
|
@ -1,24 +1,32 @@
|
||||
.CMS_WidgetList_root {
|
||||
&.CMS_WidgetList_disabled {
|
||||
& .CMS_WidgetList_expand-button-icon {
|
||||
@apply text-slate-300
|
||||
dark:text-slate-600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.CMS_WidgetList_error) {
|
||||
&:not(.CMS_WidgetList_disabled) {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
& > .CMS_WidgetList_field-wrapper {
|
||||
& > .CMS_WidgetList_expand-button {
|
||||
& .CMS_WidgetList_summary {
|
||||
color: var(--primary-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.CMS_WidgetList_error {
|
||||
& .CMS_WidgetList_content {
|
||||
@apply border-l-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.CMS_WidgetList_disabled) {
|
||||
&:not(.CMS_WidgetList_error) {
|
||||
&:hover,
|
||||
&:focus-with {
|
||||
& .CMS_WidgetList_summary,
|
||||
& .CMS_WidgetList_expand-button-icon {
|
||||
@apply text-blue-500;
|
||||
&:not(.CMS_WidgetList_disabled) {
|
||||
& > .CMS_WidgetList_field-wrapper {
|
||||
& > .CMS_WidgetList_expand-button {
|
||||
& .CMS_WidgetList_summary {
|
||||
color: var(--error-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,13 +47,12 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetList_field-wrapper {
|
||||
border-color: var(--background-divider);
|
||||
|
||||
@apply relative
|
||||
flex
|
||||
flex-col
|
||||
border-b
|
||||
border-slate-400
|
||||
focus-within:border-blue-800
|
||||
dark:focus-within:border-blue-100;
|
||||
border-b;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_field {
|
||||
@ -67,7 +74,6 @@
|
||||
focus:outline-none
|
||||
focus-visible:ring
|
||||
gap-2
|
||||
focus-visible:ring-opacity-75
|
||||
items-center;
|
||||
}
|
||||
|
||||
@ -78,8 +84,9 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetList_content {
|
||||
@apply text-sm
|
||||
text-gray-500;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_error-message {
|
||||
|
@ -27,7 +27,7 @@ import type {
|
||||
ObjectValue,
|
||||
ValueOrNestedValue,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
|
||||
import './ListControl.css';
|
||||
@ -361,10 +361,12 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
|
||||
{types && types.length ? (
|
||||
<Menu
|
||||
label={t('editor.editorWidgets.list.addType', { item: label })}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
buttonClassName={widgetListClasses['add-types-button']}
|
||||
data-testid="list-type-add"
|
||||
disabled={disabled}
|
||||
aria-label="add type options dropdown"
|
||||
>
|
||||
<MenuGroup>
|
||||
{types.map((type, idx) =>
|
||||
@ -382,6 +384,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
|
||||
</Menu>
|
||||
) : (
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
onClick={handleAdd}
|
||||
className={widgetListClasses['add-button']}
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
const classes = generateClassNames('WidgetListPreview', ['root']);
|
||||
@ -55,9 +55,7 @@ const ListPreview: FC<WidgetPreviewProps<ValueOrNestedValue[], ListField>> = ({
|
||||
!['object', 'list'].includes(field.fields[0].widget)) ||
|
||||
(!field.fields && !field.types) ? (
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
{value?.map((item, index) => (
|
||||
<li key={index}>{String(item)}</li>
|
||||
))}
|
||||
{value?.map((item, index) => <li key={index}>{String(item)}</li>)}
|
||||
</ul>
|
||||
) : (
|
||||
renderNestedList(value)
|
||||
|
@ -4,14 +4,14 @@
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import '@testing-library/jest-dom';
|
||||
import { act, getByTestId, queryByTestId, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
import ListControl from '../ListControl';
|
||||
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import type { ListField, ValueOrNestedValue } from '@staticcms/core/interface';
|
||||
import type { ListField, ValueOrNestedValue } from '@staticcms/core';
|
||||
|
||||
jest.unmock('uuid');
|
||||
|
||||
|
@ -8,7 +8,7 @@ import Label from '@staticcms/core/components/common/field/Label';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import widgetListClasses from '../ListControl.classes';
|
||||
|
||||
import type { FieldError, ListField } from '@staticcms/core/interface';
|
||||
import type { FieldError, ListField } from '@staticcms/core';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
export interface ListFieldWrapperProps {
|
||||
@ -44,12 +44,12 @@ const ListFieldWrapper: FC<ListFieldWrapperProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="list-field"
|
||||
data-testid={`list-field-${openLabel?.trim()}`}
|
||||
className={classNames(
|
||||
widgetListClasses.root,
|
||||
disabled && widgetListClasses.disabled,
|
||||
field.required !== false && widgetListClasses.required,
|
||||
hasErrors && widgetListClasses.error,
|
||||
(hasErrors || hasChildErrors) && widgetListClasses.error,
|
||||
forSingleList && widgetListClasses['for-single-list'],
|
||||
open && widgetListClasses.open,
|
||||
)}
|
||||
@ -68,7 +68,7 @@ const ListFieldWrapper: FC<ListFieldWrapperProps> = ({
|
||||
variant="inline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{open ? openLabel : closedLabel}
|
||||
{open ? openLabel.trim() : closedLabel.trim()}
|
||||
</Label>
|
||||
<ChevronRightIcon className={widgetListClasses['expand-button-icon']} />
|
||||
</button>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import partial from 'lodash/partial';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslate } from 'react-polyglot';
|
||||
|
||||
import EditorControl from '@staticcms/core/components/entry-editor/editor-control-pane/EditorControl';
|
||||
import useHasChildErrors from '@staticcms/core/lib/hooks/useHasChildErrors';
|
||||
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import {
|
||||
addFileTemplateFields,
|
||||
@ -17,12 +17,13 @@ import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import type {
|
||||
Entry,
|
||||
EntryData,
|
||||
Field,
|
||||
ListField,
|
||||
ObjectField,
|
||||
ObjectValue,
|
||||
ValueOrNestedValue,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { FC, MouseEvent } from 'react';
|
||||
import type { t } from 'react-polyglot';
|
||||
|
||||
@ -31,6 +32,7 @@ function handleSummary(
|
||||
entry: Entry,
|
||||
label: string,
|
||||
item: ValueOrNestedValue,
|
||||
fields: Field[],
|
||||
t: t,
|
||||
): string {
|
||||
if (typeof item === 'object' && !(item instanceof Date) && !Array.isArray(item)) {
|
||||
@ -41,7 +43,7 @@ function handleSummary(
|
||||
},
|
||||
};
|
||||
const data = addFileTemplateFields(entry.path, labeledItem);
|
||||
return compileStringTemplate(summary, null, '', data);
|
||||
return compileStringTemplate(summary, null, '', data, fields);
|
||||
}
|
||||
|
||||
return isNotNullish(item) ? String(item) : t('editor.editorWidgets.list.noValue');
|
||||
@ -102,7 +104,7 @@ const ListItem: FC<ListItemProps> = ({
|
||||
listeners,
|
||||
handleRemove,
|
||||
}) => {
|
||||
const t = useTranslate() as t;
|
||||
const t = useTranslate();
|
||||
|
||||
const [summary, objectField] = useMemo((): [string, ListField | ObjectField] => {
|
||||
const childObjectField: ObjectField = {
|
||||
@ -138,7 +140,14 @@ const ListItem: FC<ListItemProps> = ({
|
||||
const summary =
|
||||
'summary' in itemType && itemType.summary ? itemType.summary : field.summary;
|
||||
const labelReturn = summary
|
||||
? `${label} - ${handleSummary(summary, entry, label, mixedObjectValue, t)}`
|
||||
? `${label} - ${handleSummary(
|
||||
summary,
|
||||
entry,
|
||||
label,
|
||||
mixedObjectValue,
|
||||
itemType.fields,
|
||||
t,
|
||||
)}`
|
||||
: label;
|
||||
|
||||
return [labelReturn ?? t('editor.editorWidgets.list.noValue'), itemType];
|
||||
@ -165,7 +174,7 @@ const ListItem: FC<ListItemProps> = ({
|
||||
|
||||
const summary = field.summary;
|
||||
const labelReturn = summary
|
||||
? handleSummary(summary, entry, String(labelFieldValue), objectValue, t)
|
||||
? handleSummary(summary, entry, String(labelFieldValue), objectValue, multiFields, t)
|
||||
: labelFieldValue
|
||||
? String(labelFieldValue)
|
||||
: undefined;
|
||||
|
@ -4,8 +4,14 @@
|
||||
flex-col;
|
||||
|
||||
&.CMS_WidgetList_ListItem_error {
|
||||
& .CMS_WidgetList_ListItem_content {
|
||||
@apply border-l-red-500;
|
||||
& > .MuiCollapse-root {
|
||||
& > .MuiCollapse-wrapper {
|
||||
& > .MuiCollapse-wrapperInner {
|
||||
& > .CMS_WidgetList_ListItem_content {
|
||||
border-color: var(--error-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,18 +20,24 @@
|
||||
&:hover {
|
||||
& .CMS_WidgetList_ListItem_summary-label,
|
||||
& .CMS_WidgetList_ListItem_expand-button-icon {
|
||||
@apply text-blue-500;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
& .CMS_WidgetList_ListItem_summary-label,
|
||||
& .CMS_WidgetList_ListItem_expand-button-icon {
|
||||
@apply text-blue-500;
|
||||
color: var(--primary-main);
|
||||
}
|
||||
|
||||
& .CMS_WidgetList_ListItem_content {
|
||||
@apply border-l-blue-500;
|
||||
& > .MuiCollapse-root {
|
||||
& > .MuiCollapse-wrapper {
|
||||
& > .MuiCollapse-wrapperInner {
|
||||
& > .CMS_WidgetList_ListItem_content {
|
||||
border-color: var(--primary-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,8 +45,7 @@
|
||||
|
||||
&.CMS_WidgetList_ListItem_disabled {
|
||||
& .CMS_WidgetList_ListItem_expand-button-icon {
|
||||
@apply text-slate-300
|
||||
dark:text-slate-600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,16 +63,16 @@
|
||||
flex-col;
|
||||
|
||||
&.CMS_WidgetList_ListItem_error {
|
||||
& .CMS_WidgetList_ListItem_content {
|
||||
@apply border-l-red-500;
|
||||
& > .CMS_WidgetList_ListItem_content {
|
||||
border-color: var(--error-main);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.CMS_WidgetList_ListItem_error) {
|
||||
&:not(.CMS_WidgetList_ListItem_disabled) {
|
||||
&:focus-within {
|
||||
& .CMS_WidgetList_ListItem_content {
|
||||
@apply border-l-blue-500;
|
||||
& > .CMS_WidgetList_ListItem_content {
|
||||
border-color: var(--primary-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,7 +100,6 @@
|
||||
focus:outline-none
|
||||
focus-visible:ring
|
||||
gap-2
|
||||
focus-visible:ring-opacity-75
|
||||
items-center;
|
||||
}
|
||||
|
||||
@ -118,30 +128,33 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetList_ListItem_not-open-placeholder {
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply ml-8
|
||||
border-b
|
||||
border-slate-400;
|
||||
border-b;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_ListItem_content {
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply relative
|
||||
ml-4
|
||||
text-sm
|
||||
text-gray-500
|
||||
border-l-2
|
||||
border-solid
|
||||
border-l-slate-400;
|
||||
flex
|
||||
gap-2;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_ListItem_content-fields {
|
||||
@apply relative
|
||||
flex-grow;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_ListItem_single-field-controls {
|
||||
@apply absolute
|
||||
right-3
|
||||
top-0
|
||||
h-full
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
z-10;
|
||||
@apply flex
|
||||
flex-shrink-0;
|
||||
}
|
||||
|
||||
.CMS_WidgetList_ListItem_drag-handle {
|
||||
|
@ -10,7 +10,7 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import type { FC, MouseEvent, ReactNode } from 'react';
|
||||
|
||||
import './ListItemWrapper.css';
|
||||
|
||||
@ -30,6 +30,7 @@ const classes = generateClassNames('WidgetList_ListItem', [
|
||||
'button-icon',
|
||||
'not-open-placeholder',
|
||||
'content',
|
||||
'content-fields',
|
||||
'single-field-controls',
|
||||
'drag-handle',
|
||||
'drag-handle-icon',
|
||||
@ -40,7 +41,7 @@ export interface DragHandleProps {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const DragHandle = ({ listeners, disabled }: DragHandleProps) => {
|
||||
const DragHandle: FC<DragHandleProps> = ({ listeners, disabled }) => {
|
||||
return (
|
||||
<span
|
||||
data-testid="drag-handle"
|
||||
@ -65,7 +66,7 @@ export interface ListItemWrapperProps {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const ListItemWrapper = ({
|
||||
const ListItemWrapper: FC<ListItemWrapperProps> = ({
|
||||
label,
|
||||
summary,
|
||||
collapsed = false,
|
||||
@ -75,7 +76,7 @@ const ListItemWrapper = ({
|
||||
children,
|
||||
isSingleField,
|
||||
disabled,
|
||||
}: ListItemWrapperProps) => {
|
||||
}) => {
|
||||
const [open, setOpen] = useState(!collapsed);
|
||||
|
||||
const handleOpenToggle = useCallback(() => {
|
||||
@ -87,15 +88,17 @@ const ListItemWrapper = ({
|
||||
<div className={classes.controls}>
|
||||
{onRemove ? (
|
||||
<IconButton
|
||||
icon={CloseIcon}
|
||||
data-testid="remove-button"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
onClick={onRemove}
|
||||
disabled={disabled}
|
||||
className={classes['remove-button']}
|
||||
>
|
||||
<CloseIcon className={classes['button-icon']} />
|
||||
</IconButton>
|
||||
rootClassName={classes['remove-button']}
|
||||
iconClassName={classes['button-icon']}
|
||||
aria-label="remove"
|
||||
/>
|
||||
) : null}
|
||||
{listeners ? <DragHandle listeners={listeners} disabled={disabled} /> : null}
|
||||
</div>
|
||||
@ -106,7 +109,7 @@ const ListItemWrapper = ({
|
||||
if (isSingleField) {
|
||||
return (
|
||||
<div
|
||||
data-testid="list-item-field"
|
||||
data-testid={`list-item-field-${label?.trim()}`}
|
||||
className={classNames(
|
||||
classes['single-field-root'],
|
||||
hasErrors && classes.error,
|
||||
@ -114,7 +117,7 @@ const ListItemWrapper = ({
|
||||
)}
|
||||
>
|
||||
<div data-testid="list-item-objects" className={classes.content}>
|
||||
{children}
|
||||
<div className={classes['content-fields']}>{children}</div>
|
||||
<div className={classes['single-field-controls']}>{renderedControls}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,7 +126,7 @@ const ListItemWrapper = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="list-item-field"
|
||||
data-testid={`list-item-field-${label?.trim()}`}
|
||||
className={classNames(
|
||||
classes.root,
|
||||
hasErrors && classes.error,
|
||||
@ -136,6 +139,7 @@ const ListItemWrapper = ({
|
||||
data-testid="list-item-expand-button"
|
||||
className={classes['expand-button']}
|
||||
onClick={handleOpenToggle}
|
||||
aria-label={!open ? 'expand' : 'collapse'}
|
||||
>
|
||||
<ChevronRightIcon className={classes['expand-button-icon']} />
|
||||
<div className={classes.summary}>
|
||||
@ -148,7 +152,7 @@ const ListItemWrapper = ({
|
||||
data-testid="item-label"
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
{label.trim()}
|
||||
</Label>
|
||||
{!open ? <span data-testid="item-summary">{summary}</span> : null}
|
||||
</div>
|
||||
@ -157,7 +161,9 @@ const ListItemWrapper = ({
|
||||
</div>
|
||||
{!open ? <div className={classes['not-open-placeholder']}></div> : null}
|
||||
<Collapse in={open} appear={false}>
|
||||
<div className={classes.content}>{children}</div>
|
||||
<div className={classes.content}>
|
||||
<div className={classes['content-fields']}>{children}</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import controlComponent from './ListControl';
|
||||
import previewComponent from './ListPreview';
|
||||
import schema from './schema';
|
||||
|
||||
import type { ListField, ValueOrNestedValue, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { ListField, ValueOrNestedValue, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const ListWidget = (): WidgetParam<ValueOrNestedValue[], ListField> => {
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ListField, ObjectField, ObjectValue } from '@staticcms/core/interface';
|
||||
import type { ListField, ObjectField, ObjectValue } from '@staticcms/core';
|
||||
|
||||
export const TYPES_KEY = 'types';
|
||||
export const TYPE_KEY = 'type_key';
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { MapField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { MapField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const classes = generateClassNames('WidgetMapPreview', ['root']);
|
||||
|
@ -2,7 +2,7 @@ import previewComponent from './MapPreview';
|
||||
import schema from './schema';
|
||||
import withMapControl from './withMapControl';
|
||||
|
||||
import type { MapField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { MapField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const controlComponent = withMapControl();
|
||||
|
||||
|
@ -12,7 +12,7 @@ import Field from '@staticcms/core/components/common/field/Field';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { MapField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { MapField, WidgetControlProps } from '@staticcms/core';
|
||||
import type { Geometry } from 'ol/geom';
|
||||
import type { FC } from 'react';
|
||||
|
||||
|
@ -10,7 +10,7 @@ import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
|
||||
import useMdx from './plate/hooks/useMdx';
|
||||
import { processShortcodeConfigToMdx } from './plate/serialization/slate/processShortcodeConfig';
|
||||
|
||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
import type { UseMdxState } from './plate/hooks/useMdx';
|
||||
|
||||
|
@ -2,7 +2,7 @@ import withMarkdownControl from './withMarkdownControl';
|
||||
import previewComponent from './MarkdownPreview';
|
||||
import schema from './schema';
|
||||
|
||||
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
||||
import type { MarkdownField, WidgetParam } from '@staticcms/core';
|
||||
|
||||
const controlComponent = withMarkdownControl({ useMdx: false });
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
||||
|
||||
import { getShortcode } from '../../../lib/registry';
|
||||
|
||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export interface WithShortcodeMdxComponentProps {
|
||||
|
@ -54,13 +54,14 @@ import {
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { useTranslate } from 'react-polyglot';
|
||||
|
||||
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import widgetMarkdownClasses from '../MarkdownControl.classes';
|
||||
import { CodeBlockElement, withShortcodeElement } from './components';
|
||||
import { BalloonToolbar } from './components/balloon-toolbar';
|
||||
import { BlockquoteElement } from './components/nodes/blockquote';
|
||||
import CodeElement from './components/nodes/code/Code';
|
||||
import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
@ -84,7 +85,6 @@ import {
|
||||
TableHeaderCellElement,
|
||||
TableRowElement,
|
||||
} from './components/nodes/table';
|
||||
import CodeElement from './components/nodes/code/Code';
|
||||
import { Toolbar } from './components/toolbar';
|
||||
import editableProps from './editableProps';
|
||||
import { createMdPlugins, ELEMENT_SHORTCODE } from './plateTypes';
|
||||
@ -101,19 +101,18 @@ import { createTablePlugin } from './plugins/table';
|
||||
import { trailingBlockPlugin } from './plugins/trailing-block';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithDefaults,
|
||||
Entry,
|
||||
MarkdownField,
|
||||
WidgetControlProps,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { AnyObject, AutoformatPlugin, PlatePlugin } from '@udecode/plate';
|
||||
import type { FC } from 'react';
|
||||
import type { t as T } from 'react-polyglot';
|
||||
import type { MdEditor, MdValue } from './plateTypes';
|
||||
|
||||
export interface PlateEditorProps {
|
||||
initialValue: MdValue;
|
||||
collection: Collection<MarkdownField>;
|
||||
collection: CollectionWithDefaults<MarkdownField>;
|
||||
entry: Entry;
|
||||
field: MarkdownField;
|
||||
useMdx: boolean;
|
||||
@ -134,7 +133,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
||||
onFocus,
|
||||
onBlur,
|
||||
}) => {
|
||||
const t = useTranslate() as T;
|
||||
const t = useTranslate();
|
||||
|
||||
const editorContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const innerEditorContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
@ -3,22 +3,18 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_BalloonToolbar_popper {
|
||||
background: var(--background-main);
|
||||
|
||||
@apply absolute
|
||||
max-h-60
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
bg-white
|
||||
p-1
|
||||
text-base
|
||||
shadow-md
|
||||
ring-1
|
||||
ring-black
|
||||
ring-opacity-5
|
||||
focus:outline-none
|
||||
sm:text-sm
|
||||
z-[100]
|
||||
dark:bg-slate-700
|
||||
dark:shadow-lg;
|
||||
z-[100];
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_BalloonToolbar_content {
|
||||
|
@ -38,12 +38,12 @@ import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import { getToolbarButtons } from '../../hooks/useToolbarButtons';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithDefaults,
|
||||
MarkdownField,
|
||||
MarkdownToolbarButtonType,
|
||||
} from '@staticcms/core/interface';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
} from '@staticcms/core';
|
||||
import type { ClientRectObject } from '@udecode/plate';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import './BalloonToolbar.css';
|
||||
|
||||
@ -91,7 +91,7 @@ const DEFAULT_TABLE_SELECTION_BUTTONS: MarkdownToolbarButtonType[] = [
|
||||
export interface BalloonToolbarProps {
|
||||
useMdx: boolean;
|
||||
containerRef: HTMLElement | null;
|
||||
collection: Collection<MarkdownField>;
|
||||
collection: CollectionWithDefaults<MarkdownField>;
|
||||
field: MarkdownField;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
@ -20,15 +20,15 @@ import {
|
||||
import React, { useRef } from 'react';
|
||||
import { useFocused } from 'slate-react';
|
||||
|
||||
import { configLoaded } from '@staticcms/core/actions/config';
|
||||
import { applyDefaults, configLoaded } from '@staticcms/core/actions/config';
|
||||
import { store } from '@staticcms/core/store';
|
||||
import { createMockCollection } from '@staticcms/test/data/collections.mock';
|
||||
import { createMockConfig } from '@staticcms/test/data/config.mock';
|
||||
import { createMockFolderCollectionWithDefaults } from '@staticcms/test/data/collections.mock';
|
||||
import { createNoDefaultsMockConfig } from '@staticcms/test/data/config.mock';
|
||||
import { mockMarkdownField } from '@staticcms/test/data/fields.mock';
|
||||
import { renderWithProviders } from '@staticcms/test/test-utils';
|
||||
import BalloonToolbar from '../BalloonToolbar';
|
||||
|
||||
import type { Config, MarkdownField } from '@staticcms/core/interface';
|
||||
import type { Config, ConfigWithDefaults } from '@staticcms/core';
|
||||
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
|
||||
import type { TRange } from '@udecode/plate';
|
||||
import type { FC } from 'react';
|
||||
@ -48,7 +48,7 @@ const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false
|
||||
key="balloon-toolbar"
|
||||
useMdx={useMdx}
|
||||
containerRef={ref.current}
|
||||
collection={createMockCollection({}, mockMarkdownField)}
|
||||
collection={createMockFolderCollectionWithDefaults({}, mockMarkdownField)}
|
||||
field={mockMarkdownField}
|
||||
disabled={false}
|
||||
/>
|
||||
@ -56,9 +56,11 @@ const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false
|
||||
);
|
||||
};
|
||||
|
||||
const config = createMockConfig({
|
||||
const originalConfig = createNoDefaultsMockConfig({
|
||||
collections: [],
|
||||
}) as unknown as Config<MarkdownField>;
|
||||
}) as unknown as Config;
|
||||
|
||||
let config: ConfigWithDefaults;
|
||||
|
||||
describe(BalloonToolbar.name, () => {
|
||||
const mockUseEditor = usePlateEditorState as jest.Mock;
|
||||
@ -79,7 +81,9 @@ describe(BalloonToolbar.name, () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
store.dispatch(configLoaded(config as unknown as Config));
|
||||
config = applyDefaults(originalConfig) as unknown as ConfigWithDefaults;
|
||||
|
||||
store.dispatch(configLoaded(config, originalConfig));
|
||||
|
||||
mockEditor = {
|
||||
selection: undefined,
|
||||
|
@ -3,6 +3,7 @@ import { ELEMENT_BLOCKQUOTE } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import BlockToolbarButton from './common/BlockToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,10 +13,13 @@ export interface BlockquoteToolbarButtonProps {
|
||||
}
|
||||
|
||||
const BlockquoteToolbarButton: FC<BlockquoteToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<BlockToolbarButton
|
||||
label="Blockquote"
|
||||
tooltip="Insert blockquote"
|
||||
id="blockquote"
|
||||
label={t('editor.editorWidgets.markdown.quote')}
|
||||
tooltip={t('editor.editorWidgets.markdown.insertQuote')}
|
||||
icon={FormatQuoteIcon}
|
||||
type={ELEMENT_BLOCKQUOTE}
|
||||
disabled={disabled}
|
||||
|
@ -3,6 +3,7 @@ import { MARK_BOLD } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import MarkToolbarButton from './common/MarkToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface BoldToolbarButtonProps {
|
||||
}
|
||||
|
||||
const BoldToolbarButton: FC<BoldToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<MarkToolbarButton
|
||||
tooltip="Bold"
|
||||
id="bold"
|
||||
tooltip={t('editor.editorWidgets.markdown.bold')}
|
||||
type={MARK_BOLD}
|
||||
variant={variant}
|
||||
icon={FormatBoldIcon}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface CodeBlockToolbarButtonsProps {
|
||||
}
|
||||
|
||||
const CodeBlockToolbarButtons: FC<CodeBlockToolbarButtonsProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleCodeBlockOnClick = useCallback(() => {
|
||||
@ -23,8 +26,9 @@ const CodeBlockToolbarButtons: FC<CodeBlockToolbarButtonsProps> = ({ disabled, v
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
label="Code block"
|
||||
tooltip="Insert code block"
|
||||
id="code-block"
|
||||
label={t('editor.editorWidgets.markdown.codeBlock')}
|
||||
tooltip={t('editor.editorWidgets.markdown.insertCodeBlock')}
|
||||
icon={CodeIcon}
|
||||
onClick={handleCodeBlockOnClick}
|
||||
disabled={disabled}
|
||||
|
@ -3,6 +3,7 @@ import { MARK_CODE } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import MarkToolbarButton from './common/MarkToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface CodeToolbarButtonProps {
|
||||
}
|
||||
|
||||
const CodeToolbarButton: FC<CodeToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<MarkToolbarButton
|
||||
tooltip="Code"
|
||||
id="code"
|
||||
tooltip={t('editor.editorWidgets.markdown.code')}
|
||||
type={MARK_CODE}
|
||||
icon={CodeIcon}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -16,6 +17,8 @@ const DecreaseIndentToolbarButton: FC<DecreaseIndentToolbarButtonProps> = ({
|
||||
disabled,
|
||||
variant,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleOutdent = useCallback(() => {
|
||||
@ -24,7 +27,8 @@ const DecreaseIndentToolbarButton: FC<DecreaseIndentToolbarButtonProps> = ({
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Decrease indent"
|
||||
id="decrease-ident"
|
||||
tooltip={t('editor.editorWidgets.markdown.decreaseIndent')}
|
||||
onClick={handleOutdent}
|
||||
icon={FormatIndentDecreaseIcon}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface DeleteColumnToolbarButtonProps {
|
||||
}
|
||||
|
||||
const DeleteColumnToolbarButton: FC<DeleteColumnToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleDeleteColumn = useCallback(() => {
|
||||
@ -21,7 +24,8 @@ const DeleteColumnToolbarButton: FC<DeleteColumnToolbarButtonProps> = ({ disable
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Delete column"
|
||||
id="delete-column"
|
||||
tooltip={t('editor.editorWidgets.markdown.table.deleteColumn')}
|
||||
icon={TableDeleteColumn}
|
||||
onClick={handleDeleteColumn}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface DeleteRowToolbarButtonProps {
|
||||
}
|
||||
|
||||
const DeleteRowToolbarButton: FC<DeleteRowToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleDeleteRow = useCallback(() => {
|
||||
@ -21,7 +24,8 @@ const DeleteRowToolbarButton: FC<DeleteRowToolbarButtonProps> = ({ disabled, var
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Delete row"
|
||||
id="delete-row"
|
||||
tooltip={t('editor.editorWidgets.markdown.table.deleteRow')}
|
||||
icon={TableDeleteRow}
|
||||
onClick={handleDeleteRow}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface DeleteTableToolbarButtonProps {
|
||||
}
|
||||
|
||||
const DeleteTableToolbarButton: FC<DeleteTableToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleDeleteTable = useCallback(() => {
|
||||
@ -21,7 +24,8 @@ const DeleteTableToolbarButton: FC<DeleteTableToolbarButtonProps> = ({ disabled,
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Delete table"
|
||||
id="delete-table"
|
||||
tooltip={t('editor.editorWidgets.markdown.table.deleteTable')}
|
||||
icon={TableDismiss}
|
||||
onClick={handleDeleteTable}
|
||||
disabled={disabled}
|
||||
|
@ -5,15 +5,16 @@
|
||||
|
||||
&.CMS_WidgetMarkdown_FontTypeSelect_disabled {
|
||||
& .CMS_WidgetMarkdown_FontTypeSelect_select {
|
||||
@apply text-gray-300
|
||||
border-gray-200
|
||||
dark:border-gray-600
|
||||
dark:text-gray-500;
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--background-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_FontTypeSelect_select {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply flex
|
||||
items-center
|
||||
justify-between
|
||||
@ -25,42 +26,35 @@
|
||||
w-full
|
||||
h-6
|
||||
border
|
||||
rounded-sm
|
||||
text-gray-800
|
||||
border-gray-600
|
||||
dark:border-gray-400
|
||||
dark:text-gray-100;
|
||||
rounded-sm;
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_FontTypeSelect_popper {
|
||||
background: var(--background-main);
|
||||
|
||||
@apply max-h-40
|
||||
overflow-auto
|
||||
rounded-md
|
||||
bg-white
|
||||
shadow-md
|
||||
ring-1
|
||||
ring-black
|
||||
ring-opacity-5
|
||||
focus:outline-none
|
||||
sm:text-sm
|
||||
dark:bg-slate-800
|
||||
dark:shadow-lg
|
||||
z-[101];
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_FontTypeSelect_option {
|
||||
&:hover {
|
||||
color: var(--primary-contrast-color);
|
||||
background: var(--primary-main);
|
||||
}
|
||||
|
||||
@apply relative
|
||||
select-none
|
||||
py-2
|
||||
px-4
|
||||
cursor-pointer
|
||||
hover:bg-blue-500
|
||||
hover:text-white
|
||||
dark:hover:bg-blue-500;
|
||||
cursor-pointer;
|
||||
|
||||
&.CMS_WidgetMarkdown_FontTypeSelect_option-selected {
|
||||
@apply bg-blue-500/25
|
||||
dark:bg-blue-700/20;
|
||||
background: var(--primary-main);
|
||||
|
||||
& .CMS_WidgetMarkdown_FontTypeSelect_option-label {
|
||||
@apply font-medium;
|
||||
|
@ -18,6 +18,7 @@ import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { SelectRootSlotProps } from '@mui/base/Select';
|
||||
import type { FC, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||
@ -44,31 +45,31 @@ type Option = {
|
||||
const types: Option[] = [
|
||||
{
|
||||
value: ELEMENT_H1,
|
||||
label: 'Heading 1',
|
||||
label: 'editor.editorWidgets.headingOptions.headingOne',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_H2,
|
||||
label: 'Heading 2',
|
||||
label: 'editor.editorWidgets.headingOptions.headingTwo',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_H3,
|
||||
label: 'Heading 3',
|
||||
label: 'editor.editorWidgets.headingOptions.headingThree',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_H4,
|
||||
label: 'Heading 4',
|
||||
label: 'editor.editorWidgets.headingOptions.headingFour',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_H5,
|
||||
label: 'Heading 5',
|
||||
label: 'editor.editorWidgets.headingOptions.headingFive',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_H6,
|
||||
label: 'Heading 6',
|
||||
label: 'editor.editorWidgets.headingOptions.headingSix',
|
||||
},
|
||||
{
|
||||
value: ELEMENT_PARAGRAPH,
|
||||
label: 'Paragraph',
|
||||
label: 'editor.editorWidgets.markdown.paragraph',
|
||||
},
|
||||
];
|
||||
|
||||
@ -93,6 +94,8 @@ export interface FontTypeSelectProps {
|
||||
* Toolbar button to toggle the type of elements in selection.
|
||||
*/
|
||||
const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
const [version, setVersion] = useState(0);
|
||||
|
||||
@ -151,7 +154,7 @@ const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span className={classes['option-label']}>{type.label}</span>
|
||||
<span className={classes['option-label']}>{t(type.label)}</span>
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -16,6 +17,8 @@ const IncreaseIndentToolbarButton: FC<IncreaseIndentToolbarButtonProps> = ({
|
||||
disabled,
|
||||
variant,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleIndent = useCallback(() => {
|
||||
@ -24,7 +27,8 @@ const IncreaseIndentToolbarButton: FC<IncreaseIndentToolbarButtonProps> = ({
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Increase indent"
|
||||
id="increase-ident"
|
||||
tooltip={t('editor.editorWidgets.markdown.increaseIndent')}
|
||||
onClick={handleIndent}
|
||||
icon={FormatIndentIncreaseIcon}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface InsertColumnToolbarButtonProps {
|
||||
}
|
||||
|
||||
const InsertColumnToolbarButton: FC<InsertColumnToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleInsertTableColumn = useCallback(() => {
|
||||
@ -21,7 +24,8 @@ const InsertColumnToolbarButton: FC<InsertColumnToolbarButtonProps> = ({ disable
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Insert column"
|
||||
id="insert-column"
|
||||
tooltip={t('editor.editorWidgets.markdown.table.insertColumn')}
|
||||
icon={TableInsertColumn}
|
||||
onClick={handleInsertTableColumn}
|
||||
disabled={disabled}
|
||||
|
@ -2,12 +2,13 @@ import { Image as ImageIcon } from '@styled-icons/material/Image';
|
||||
import { ELEMENT_IMAGE, getAboveNode, setNodes } from '@udecode/plate';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
|
||||
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
|
||||
import type { CollectionWithDefaults, MarkdownField, MediaPath } from '@staticcms/core';
|
||||
import type { MdImageElement } from '@staticcms/markdown/plate/plateTypes';
|
||||
import type { FC } from 'react';
|
||||
import type { BaseSelection } from 'slate';
|
||||
@ -15,7 +16,7 @@ import type { BaseSelection } from 'slate';
|
||||
export interface InsertImageToolbarButtonProps {
|
||||
variant: 'button' | 'menu';
|
||||
currentValue?: { url: string; alt?: string };
|
||||
collection: Collection<MarkdownField>;
|
||||
collection: CollectionWithDefaults<MarkdownField>;
|
||||
field: MarkdownField;
|
||||
disabled: boolean;
|
||||
}
|
||||
@ -27,6 +28,8 @@ const InsertImageToolbarButton: FC<InsertImageToolbarButtonProps> = ({
|
||||
currentValue,
|
||||
disabled,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const [selection, setSelection] = useState<BaseSelection>();
|
||||
const editor = useMdPlateEditorState();
|
||||
const handleInsert = useCallback(
|
||||
@ -83,8 +86,9 @@ const InsertImageToolbarButton: FC<InsertImageToolbarButtonProps> = ({
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
label="Image"
|
||||
tooltip="Insert image"
|
||||
id="image"
|
||||
label={t('editor.editorWidgets.markdown.image')}
|
||||
tooltip={t('editor.editorWidgets.markdown.insertImage')}
|
||||
icon={ImageIcon}
|
||||
onClick={handleOpenMediaLibrary}
|
||||
disabled={disabled}
|
||||
|
@ -12,13 +12,14 @@ import {
|
||||
} from '@udecode/plate';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
|
||||
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
|
||||
import type { CollectionWithDefaults, MarkdownField, MediaPath } from '@staticcms/core';
|
||||
import type { MdLinkElement } from '@staticcms/markdown/plate/plateTypes';
|
||||
import type { TText } from '@udecode/plate';
|
||||
import type { FC } from 'react';
|
||||
@ -27,7 +28,7 @@ import type { Location } from 'slate';
|
||||
export interface InsertLinkToolbarButtonProps {
|
||||
variant: 'button' | 'menu';
|
||||
currentValue?: { url: string; alt?: string };
|
||||
collection: Collection<MarkdownField>;
|
||||
collection: CollectionWithDefaults<MarkdownField>;
|
||||
field: MarkdownField;
|
||||
disabled: boolean;
|
||||
}
|
||||
@ -39,6 +40,8 @@ const InsertLinkToolbarButton: FC<InsertLinkToolbarButtonProps> = ({
|
||||
currentValue,
|
||||
disabled,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
const handleInsert = useCallback(
|
||||
({ path: newUrl, alt: newText }: MediaPath<string>) => {
|
||||
@ -107,8 +110,9 @@ const InsertLinkToolbarButton: FC<InsertLinkToolbarButtonProps> = ({
|
||||
|
||||
return !isLink ? (
|
||||
<ToolbarButton
|
||||
label="Link"
|
||||
tooltip="Insert link"
|
||||
id="link"
|
||||
label={t('editor.editorWidgets.markdown.link')}
|
||||
tooltip={t('editor.editorWidgets.markdown.insertLink')}
|
||||
icon={LinkIcon}
|
||||
onClick={handleOpenMediaLibrary}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -13,6 +14,8 @@ export interface InsertRowToolbarButtonProps {
|
||||
}
|
||||
|
||||
const InsertRowToolbarButton: FC<InsertRowToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleInsertTableRow = useCallback(() => {
|
||||
@ -21,7 +24,8 @@ const InsertRowToolbarButton: FC<InsertRowToolbarButtonProps> = ({ disabled, var
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip="Insert row"
|
||||
id="insert-row"
|
||||
tooltip={t('editor.editorWidgets.markdown.table.insertRow')}
|
||||
icon={TableInsertRow}
|
||||
onClick={handleInsertTableRow}
|
||||
disabled={disabled}
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { useMdPlateEditorState } from '../../plateTypes';
|
||||
import ToolbarButton from './common/ToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -16,6 +17,8 @@ const InsertTableToolbarButton: FC<InsertTableToolbarButtonProps> = ({
|
||||
disabled,
|
||||
variant = 'button',
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const editor = useMdPlateEditorState();
|
||||
|
||||
const handleTableAdd = useCallback(() => {
|
||||
@ -27,8 +30,9 @@ const InsertTableToolbarButton: FC<InsertTableToolbarButtonProps> = ({
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
label="Table"
|
||||
tooltip="Insert table"
|
||||
id="insert-table"
|
||||
label={t('editor.editorWidgets.markdown.table.table')}
|
||||
tooltip={t('editor.editorWidgets.markdown.table.insertTable')}
|
||||
icon={TableAdd}
|
||||
onClick={handleTableAdd}
|
||||
disabled={disabled}
|
||||
|
@ -3,6 +3,7 @@ import { MARK_ITALIC } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import MarkToolbarButton from './common/MarkToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface ItalicToolbarButtonsProp {
|
||||
}
|
||||
|
||||
const ItalicToolbarButton: FC<ItalicToolbarButtonsProp> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<MarkToolbarButton
|
||||
tooltip="Italic"
|
||||
id="italic"
|
||||
tooltip={t('editor.editorWidgets.markdown.italic')}
|
||||
type={MARK_ITALIC}
|
||||
variant={variant}
|
||||
icon={FormatItalicIcon}
|
||||
|
@ -3,6 +3,7 @@ import { ELEMENT_OL } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import ListToolbarButton from './common/ListToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface OrderedListToolbarButtonProps {
|
||||
}
|
||||
|
||||
const OrderedListToolbarButton: FC<OrderedListToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<ListToolbarButton
|
||||
tooltip="Numbered list"
|
||||
id="numbered-list"
|
||||
tooltip={t('editor.editorWidgets.markdown.numberedList')}
|
||||
type={ELEMENT_OL}
|
||||
icon={FormatListNumberedIcon}
|
||||
disabled={disabled}
|
||||
|
@ -48,10 +48,12 @@ const ShortcodeToolbarButton: FC<ShortcodeToolbarButtonProps> = ({ disabled }) =
|
||||
data-testid="toolbar-button-shortcode"
|
||||
keepMounted
|
||||
hideDropdownIcon
|
||||
color="secondary"
|
||||
variant="text"
|
||||
rootClassName={classes.root}
|
||||
buttonClassName={classes.button}
|
||||
disabled={disabled}
|
||||
aria-label="add shortcode"
|
||||
>
|
||||
<MenuGroup>
|
||||
{Object.keys(configs).map(name => {
|
||||
|
@ -3,6 +3,7 @@ import { MARK_STRIKETHROUGH } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import MarkToolbarButton from './common/MarkToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface StrikethroughToolbarButtonProps {
|
||||
}
|
||||
|
||||
const StrikethroughToolbarButton: FC<StrikethroughToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<MarkToolbarButton
|
||||
tooltip="Strikethrough"
|
||||
id="strikethrough"
|
||||
tooltip={t('editor.editorWidgets.markdown.strikethrough')}
|
||||
type={MARK_STRIKETHROUGH}
|
||||
variant={variant}
|
||||
icon={FormatStrikethroughIcon}
|
||||
|
@ -3,6 +3,7 @@ import { ELEMENT_UL } from '@udecode/plate';
|
||||
import React from 'react';
|
||||
|
||||
import ListToolbarButton from './common/ListToolbarButton';
|
||||
import { useTranslate } from '@staticcms/core/lib';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ export interface UnorderedListToolbarButtonProps {
|
||||
}
|
||||
|
||||
const UnorderedListToolbarButton: FC<UnorderedListToolbarButtonProps> = ({ disabled, variant }) => {
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<ListToolbarButton
|
||||
tooltip="List"
|
||||
id="bulleted-list"
|
||||
tooltip={t('editor.editorWidgets.markdown.bulletedList')}
|
||||
type={ELEMENT_UL}
|
||||
icon={FormatListBulletedIcon}
|
||||
disabled={disabled}
|
||||
|
@ -5,10 +5,8 @@
|
||||
|
||||
&.CMS_WidgetMarkdown_ToolbarButton_active {
|
||||
&:not(.CMS_WidgetMarkdown_ToolbarButton_custom-active-color) {
|
||||
@apply text-blue-500
|
||||
bg-gray-100
|
||||
dark:text-blue-500
|
||||
dark:bg-slate-800;
|
||||
color: var(--primary-main);
|
||||
background: color-mix(in srgb, var(--primary-main) 15%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ const classes = generateClassNames('WidgetMarkdown_ToolbarButton', [
|
||||
]);
|
||||
|
||||
export interface ToolbarButtonProps {
|
||||
id: string;
|
||||
label?: string;
|
||||
tooltip: string;
|
||||
active?: boolean;
|
||||
@ -31,6 +32,7 @@ export interface ToolbarButtonProps {
|
||||
}
|
||||
|
||||
const ToolbarButton: FC<ToolbarButtonProps> = ({
|
||||
id,
|
||||
icon: Icon,
|
||||
tooltip,
|
||||
label,
|
||||
@ -79,9 +81,10 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
|
||||
<Button
|
||||
key="button"
|
||||
aria-label={label ?? tooltip}
|
||||
color="secondary"
|
||||
title={label ?? tooltip}
|
||||
variant="text"
|
||||
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
|
||||
data-testid={`toolbar-button-${id}`}
|
||||
onClick={handleOnClick}
|
||||
className={classNames(
|
||||
classes.root,
|
||||
|
@ -1,11 +1,13 @@
|
||||
.CMS_WidgetMarkdown_ColorButton_root {
|
||||
&.CMS_WidgetMarkdown_ColorButton_is-bright-color {
|
||||
& .CMS_WidgetMarkdown_ColorButton_avatar {
|
||||
border: 1px solid rgba(209, 213, 219, 1);
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply border;
|
||||
}
|
||||
|
||||
& .CMS_WidgetMarkdown_ColorButton_check-icon {
|
||||
@apply text-black;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,7 +19,8 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_ColorButton_check-icon {
|
||||
color: var(--primary-contrast-color);
|
||||
|
||||
@apply h-5
|
||||
w-5
|
||||
text-white;
|
||||
w-5;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ const ColorButton: FC<ColorButtonProps> = ({
|
||||
onClick={handleOnClick}
|
||||
sx={{ p: 0 }}
|
||||
className={classNames(classes.root, isBrightColor && classes['is-bright-color'])}
|
||||
aria-label={value}
|
||||
>
|
||||
<Avatar
|
||||
alt={name}
|
||||
|
@ -1,20 +1,16 @@
|
||||
.CMS_WidgetMarkdown_MediaPopover_root {
|
||||
background: var(--background-main);
|
||||
|
||||
@apply absolute
|
||||
max-h-60
|
||||
overflow-auto
|
||||
rounded-md
|
||||
bg-white
|
||||
p-1
|
||||
text-base
|
||||
shadow-md
|
||||
ring-1
|
||||
ring-black
|
||||
ring-opacity-5
|
||||
focus:outline-none
|
||||
sm:text-sm
|
||||
z-40
|
||||
dark:bg-slate-700
|
||||
dark:shadow-lg;
|
||||
z-40;
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_MediaPopover_content {
|
||||
@ -28,8 +24,8 @@
|
||||
}
|
||||
|
||||
.CMS_WidgetMarkdown_MediaPopover_divider {
|
||||
border-color: var(--background-light);
|
||||
|
||||
@apply w-[1px]
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-slate-600;
|
||||
border;
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithDefaults,
|
||||
FileOrImageField,
|
||||
MarkdownField,
|
||||
MediaPath,
|
||||
} from '@staticcms/core/interface';
|
||||
} from '@staticcms/core';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import './MediaPopover.css';
|
||||
@ -30,7 +30,7 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
|
||||
url: string;
|
||||
text?: string;
|
||||
forImage?: boolean;
|
||||
collection: Collection<T>;
|
||||
collection: CollectionWithDefaults<T>;
|
||||
field: T;
|
||||
onMediaToggle?: (open: boolean) => void;
|
||||
onMediaChange: (newValue: MediaPath<string>) => void;
|
||||
@ -101,16 +101,16 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
||||
className={classes.root}
|
||||
>
|
||||
<div key="edit-content" contentEditable={false} className={classes.content}>
|
||||
<Button onClick={handleOpenMediaLibrary} variant="text" size="small">
|
||||
<Button onClick={handleOpenMediaLibrary} color="secondary" variant="text" size="small">
|
||||
{forImage ? 'Edit Image' : 'Edit Link'}
|
||||
</Button>
|
||||
<div className={classes.divider} />
|
||||
{!forImage ? (
|
||||
<Button href={url} variant="text" size="small" onClick={noop}>
|
||||
<Button href={url} color="secondary" variant="text" size="small" onClick={noop}>
|
||||
<OpenInNewIcon className={classes.icon} title="Open In New Tab" />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={onRemove} variant="text" size="small">
|
||||
<Button onClick={onRemove} color="secondary" variant="text" size="small">
|
||||
<DeleteForeverIcon className={classes.icon} title="Delete" />
|
||||
</Button>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user