fix: media library url entry (#775)
This commit is contained in:
parent
7d0a705eee
commit
a7ab1a7c0d
@ -63,5 +63,6 @@ export const isSelectionExpanded = jest.fn();
|
|||||||
export const isText = jest.fn();
|
export const isText = jest.fn();
|
||||||
export const someNode = jest.fn();
|
export const someNode = jest.fn();
|
||||||
export const usePlateSelection = jest.fn();
|
export const usePlateSelection = jest.fn();
|
||||||
|
export const isMarkActive = jest.fn();
|
||||||
|
|
||||||
export default {};
|
export default {};
|
||||||
|
@ -157,16 +157,18 @@ const Field: FC<FieldProps> = ({
|
|||||||
{renderedHint}
|
{renderedHint}
|
||||||
{renderedErrorMessage}
|
{renderedErrorMessage}
|
||||||
</div>
|
</div>
|
||||||
<div
|
{endAdornment ? (
|
||||||
className={classNames(
|
<div
|
||||||
`
|
className={classNames(
|
||||||
|
`
|
||||||
pr-2
|
pr-2
|
||||||
`,
|
`,
|
||||||
!noPadding && '-mb-3',
|
!noPadding && '-mb-3',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{endAdornment}
|
{endAdornment}
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Image as ImageIcon } from '@styled-icons/material-outlined/Image';
|
||||||
|
|
||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
import type { BaseField, Collection, MediaField, UnknownField } from '@staticcms/core/interface';
|
import type { BaseField, Collection, MediaField, UnknownField } from '@staticcms/core/interface';
|
||||||
|
|
||||||
@ -28,6 +30,20 @@ const Image = <EF extends BaseField = UnknownField>({
|
|||||||
|
|
||||||
const assetSource = useMediaAsset(src, collection, field, entry);
|
const assetSource = useMediaAsset(src, collection, field, entry);
|
||||||
|
|
||||||
|
if (isEmpty(src)) {
|
||||||
|
return (
|
||||||
|
<ImageIcon
|
||||||
|
className="
|
||||||
|
p-10
|
||||||
|
rounded-md
|
||||||
|
border
|
||||||
|
border-gray-200/75
|
||||||
|
dark:border-slate-600/75
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
key="image"
|
key="image"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { isEmpty } from '../../../lib/util/string.util';
|
import { isEmpty } from '../../../lib/util/string.util';
|
||||||
import Image from '../../common/image/Image';
|
import Image from '../../common/image/Image';
|
||||||
import InlineEditTextField from './InlineEditTextField';
|
import InlineEditTextField from './InlineEditTextField';
|
||||||
@ -14,6 +15,7 @@ interface CurrentMediaDetailsProps {
|
|||||||
url?: string | string[];
|
url?: string | string[];
|
||||||
alt?: string;
|
alt?: string;
|
||||||
insertOptions?: MediaLibrarInsertOptions;
|
insertOptions?: MediaLibrarInsertOptions;
|
||||||
|
forImage: boolean;
|
||||||
onUrlChange: (url: string) => void;
|
onUrlChange: (url: string) => void;
|
||||||
onAltChange: (alt: string) => void;
|
onAltChange: (alt: string) => void;
|
||||||
}
|
}
|
||||||
@ -25,54 +27,83 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
|
|||||||
url,
|
url,
|
||||||
alt,
|
alt,
|
||||||
insertOptions,
|
insertOptions,
|
||||||
|
forImage,
|
||||||
onUrlChange,
|
onUrlChange,
|
||||||
onAltChange,
|
onAltChange,
|
||||||
}) => {
|
}) => {
|
||||||
if (!field || !canInsert || typeof url !== 'string' || isEmpty(url)) {
|
if (
|
||||||
|
!field ||
|
||||||
|
!canInsert ||
|
||||||
|
Array.isArray(url) ||
|
||||||
|
(!insertOptions?.chooseUrl &&
|
||||||
|
!insertOptions?.showAlt &&
|
||||||
|
(typeof url !== 'string' || isEmpty(url)))
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="
|
className={classNames(
|
||||||
grid
|
`
|
||||||
grid-cols-media-preview
|
items-center
|
||||||
items-center
|
px-5
|
||||||
px-5
|
py-4
|
||||||
py-4
|
border-b
|
||||||
border-b
|
|
||||||
border-gray-200/75
|
|
||||||
dark:border-slate-500/75
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={url}
|
|
||||||
collection={collection}
|
|
||||||
field={field}
|
|
||||||
className="
|
|
||||||
w-media-preview-image
|
|
||||||
h-media-preview-image
|
|
||||||
rounded-md
|
|
||||||
shadow-sm
|
|
||||||
overflow-hidden
|
|
||||||
group/media-card
|
|
||||||
border
|
|
||||||
bg-gray-50/75
|
|
||||||
border-gray-200/75
|
border-gray-200/75
|
||||||
dark:bg-slate-800
|
dark:border-slate-500/75
|
||||||
dark:border-slate-600/75
|
`,
|
||||||
object-cover
|
forImage
|
||||||
"
|
? `
|
||||||
/>
|
grid
|
||||||
|
grid-cols-media-preview
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
flex
|
||||||
|
w-full
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{forImage ? (
|
||||||
|
<Image
|
||||||
|
key="image-preview"
|
||||||
|
src={url}
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
|
className="
|
||||||
|
w-media-preview-image
|
||||||
|
h-media-preview-image
|
||||||
|
rounded-md
|
||||||
|
shadow-sm
|
||||||
|
overflow-hidden
|
||||||
|
group/media-card
|
||||||
|
border
|
||||||
|
bg-gray-50/75
|
||||||
|
border-gray-200/75
|
||||||
|
dark:bg-slate-800
|
||||||
|
dark:border-slate-600/75
|
||||||
|
object-cover
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className="
|
className={classNames(
|
||||||
flex
|
`
|
||||||
flex-col
|
flex
|
||||||
h-full
|
flex-col
|
||||||
p-0
|
h-full
|
||||||
pl-4
|
p-0
|
||||||
gap-2
|
gap-2
|
||||||
"
|
`,
|
||||||
|
forImage
|
||||||
|
? `
|
||||||
|
pl-4
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
w-full
|
||||||
|
pl-1.5
|
||||||
|
`,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<InlineEditTextField
|
<InlineEditTextField
|
||||||
label="URL"
|
label="URL"
|
||||||
@ -80,7 +111,11 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
|
|||||||
onChange={insertOptions?.chooseUrl ? onUrlChange : undefined}
|
onChange={insertOptions?.chooseUrl ? onUrlChange : undefined}
|
||||||
/>
|
/>
|
||||||
{insertOptions?.showAlt ? (
|
{insertOptions?.showAlt ? (
|
||||||
<InlineEditTextField label="Alt" value={alt} onChange={onAltChange} />
|
<InlineEditTextField
|
||||||
|
label={forImage ? 'Alt' : 'Text'}
|
||||||
|
value={alt}
|
||||||
|
onChange={onAltChange}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,6 +96,7 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
|
|||||||
{!editing || !onChange ? (
|
{!editing || !onChange ? (
|
||||||
<div
|
<div
|
||||||
key="value"
|
key="value"
|
||||||
|
tabIndex={0}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
`
|
||||||
flex
|
flex
|
||||||
@ -110,7 +111,6 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
|
|||||||
rounded-md
|
rounded-md
|
||||||
border
|
border
|
||||||
text-slate-600
|
text-slate-600
|
||||||
dark:font-semibold
|
|
||||||
dark:text-gray-100
|
dark:text-gray-100
|
||||||
`,
|
`,
|
||||||
onChange
|
onChange
|
||||||
@ -125,6 +125,7 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
onClick={handleValueClick}
|
onClick={handleValueClick}
|
||||||
|
onFocus={handleValueClick}
|
||||||
>
|
>
|
||||||
{internalValue}
|
{internalValue}
|
||||||
</div>
|
</div>
|
||||||
|
@ -484,6 +484,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
|
|||||||
url={url}
|
url={url}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
insertOptions={insertOptions}
|
insertOptions={insertOptions}
|
||||||
|
forImage={forImage}
|
||||||
onUrlChange={handleURLChange}
|
onUrlChange={handleURLChange}
|
||||||
onAltChange={handleAltChange}
|
onAltChange={handleAltChange}
|
||||||
/>
|
/>
|
||||||
|
@ -253,7 +253,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
<div className="relative px-3 py-5 pb-0 mb-5">
|
<div className="relative px-3 py-5 pb-0">
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<PlateProvider<MdValue>
|
<PlateProvider<MdValue>
|
||||||
id={id}
|
id={id}
|
||||||
@ -272,7 +272,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div key="editor-wrapper" ref={editorContainerRef} className="w-full overflow-hidden">
|
<div key="editor-wrapper" ref={editorContainerRef} className="w-full">
|
||||||
<Plate
|
<Plate
|
||||||
key="editor"
|
key="editor"
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -2,9 +2,7 @@ import PopperUnstyled from '@mui/base/PopperUnstyled';
|
|||||||
import {
|
import {
|
||||||
ELEMENT_LINK,
|
ELEMENT_LINK,
|
||||||
ELEMENT_TD,
|
ELEMENT_TD,
|
||||||
findNodePath,
|
|
||||||
getNode,
|
getNode,
|
||||||
getParentNode,
|
|
||||||
getSelectionBoundingClientRect,
|
getSelectionBoundingClientRect,
|
||||||
getSelectionText,
|
getSelectionText,
|
||||||
isElement,
|
isElement,
|
||||||
@ -20,7 +18,6 @@ import { useFocused } from 'slate-react';
|
|||||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
|
||||||
import { VOID_ELEMENTS } from '../../serialization/slate/ast-types';
|
|
||||||
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
|
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
|
||||||
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
|
||||||
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
|
||||||
@ -156,29 +153,14 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
|
|||||||
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" disabled={disabled} /> : null,
|
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" disabled={disabled} /> : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
// if (isInTableCell) {
|
// Empty table cell
|
||||||
// return allButtons;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Empty paragraph, not first line
|
|
||||||
if (
|
if (
|
||||||
|
isInTableCell &&
|
||||||
editor.children.length > 1 &&
|
editor.children.length > 1 &&
|
||||||
node &&
|
node &&
|
||||||
((isElement(node) && isElementEmpty(editor, node)) || (isText(node) && isEmpty(node.text)))
|
((isElement(node) && isElementEmpty(editor, node)) || (isText(node) && isEmpty(node.text)))
|
||||||
) {
|
) {
|
||||||
const path = findNodePath(editor, node) ?? [];
|
return allButtons;
|
||||||
const parent = getParentNode(editor, path);
|
|
||||||
if (
|
|
||||||
path.length > 0 &&
|
|
||||||
path[0] !== 0 &&
|
|
||||||
parent &&
|
|
||||||
parent.length > 0 &&
|
|
||||||
'children' in parent[0] &&
|
|
||||||
!VOID_ELEMENTS.includes(parent[0].type as string) &&
|
|
||||||
parent[0].children.length === 1
|
|
||||||
) {
|
|
||||||
return allButtons;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import {
|
import {
|
||||||
|
ELEMENT_LINK,
|
||||||
findNodePath,
|
findNodePath,
|
||||||
getNode,
|
getNode,
|
||||||
getParentNode,
|
getParentNode,
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
isElementEmpty,
|
isElementEmpty,
|
||||||
someNode,
|
someNode,
|
||||||
usePlateEditorState,
|
usePlateEditorState,
|
||||||
|
usePlateSelection,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useFocused } from 'slate-react';
|
import { useFocused } from 'slate-react';
|
||||||
@ -25,6 +27,7 @@ import BalloonToolbar from '../BalloonToolbar';
|
|||||||
|
|
||||||
import type { Config, MarkdownField } from '@staticcms/core/interface';
|
import type { Config, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
|
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
|
||||||
|
import type { TRange } from '@udecode/plate';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
interface BalloonToolbarWrapperProps {
|
interface BalloonToolbarWrapperProps {
|
||||||
@ -54,6 +57,7 @@ const config = createMockConfig({
|
|||||||
|
|
||||||
describe(BalloonToolbar.name, () => {
|
describe(BalloonToolbar.name, () => {
|
||||||
const mockUseEditor = usePlateEditorState as jest.Mock;
|
const mockUseEditor = usePlateEditorState as jest.Mock;
|
||||||
|
const mockUsePlateSelection = usePlateSelection as jest.Mock;
|
||||||
let mockEditor: MdEditor;
|
let mockEditor: MdEditor;
|
||||||
|
|
||||||
const mockGetNode = getNode as jest.Mock;
|
const mockGetNode = getNode as jest.Mock;
|
||||||
@ -67,26 +71,6 @@ describe(BalloonToolbar.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store.dispatch(configLoaded(config as unknown as Config));
|
store.dispatch(configLoaded(config as unknown as Config));
|
||||||
|
|
||||||
// entry = {
|
|
||||||
// collection: 'posts',
|
|
||||||
// slug: '2022-12-13-post-number-1',
|
|
||||||
// path: '_posts/2022-12-13-post-number-1.md',
|
|
||||||
// partial: false,
|
|
||||||
// raw: '--- title: "This is post # 1" draft: false date: 2022-12-13T00:00:00.000Z --- # The post is number 1\n\nAnd some text',
|
|
||||||
// label: '',
|
|
||||||
// author: '',
|
|
||||||
// mediaFiles: [],
|
|
||||||
// isModification: null,
|
|
||||||
// newRecord: false,
|
|
||||||
// updatedOn: '',
|
|
||||||
// data: {
|
|
||||||
// title: 'This is post # 1',
|
|
||||||
// draft: false,
|
|
||||||
// date: '2022-12-13T00:00:00.000Z',
|
|
||||||
// body: '# The post is number 1\n\nAnd some text',
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
|
|
||||||
mockEditor = {
|
mockEditor = {
|
||||||
selection: undefined,
|
selection: undefined,
|
||||||
} as unknown as MdEditor;
|
} as unknown as MdEditor;
|
||||||
@ -99,45 +83,57 @@ describe(BalloonToolbar.name, () => {
|
|||||||
expect(screen.queryAllByRole('button').length).toBe(0);
|
expect(screen.queryAllByRole('button').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('empty node toolbar', () => {
|
describe('empty node toolbar inside table', () => {
|
||||||
interface EmptyNodeToolbarSetupOptions {
|
interface EmptyNodeToolbarSetupOptions {
|
||||||
useMdx?: boolean;
|
useMdx?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
|
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
|
||||||
mockEditor = {
|
mockEditor = {
|
||||||
selection: undefined,
|
selection: {
|
||||||
|
anchor: {
|
||||||
|
path: [1, 0],
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
focus: {
|
||||||
|
path: [1, 0],
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
} as TRange,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'p',
|
||||||
children: [{ text: '' }],
|
children: [{ text: '' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'td',
|
||||||
children: [{ text: '' }],
|
children: [{ text: '' }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown as MdEditor;
|
} as unknown as MdEditor;
|
||||||
|
|
||||||
mockUseEditor.mockReturnValue(mockEditor);
|
mockUseEditor.mockReturnValue(mockEditor);
|
||||||
|
mockUsePlateSelection.mockReturnValue(mockEditor.selection);
|
||||||
|
|
||||||
mockGetNode.mockReturnValue({ text: '' });
|
mockGetNode.mockReturnValue({ text: '' });
|
||||||
mockIsElement.mockReturnValue(true);
|
mockIsElement.mockReturnValue(true);
|
||||||
mockIsElementEmpty.mockReturnValue(true);
|
mockIsElementEmpty.mockReturnValue(true);
|
||||||
mockSomeNode.mockReturnValue(false);
|
mockSomeNode.mockImplementation((_editor, { match: { type } }) => type !== ELEMENT_LINK);
|
||||||
mockUseFocused.mockReturnValue(true);
|
mockUseFocused.mockReturnValue(true);
|
||||||
|
|
||||||
mockFindNodePath.mockReturnValue([1, 0]);
|
mockFindNodePath.mockReturnValue([1, 0]);
|
||||||
mockGetParentNode.mockReturnValue([
|
mockGetParentNode.mockReturnValue([
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'td',
|
||||||
children: [{ text: '' }],
|
children: [{ text: '' }],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { rerender } = renderWithProviders(<BalloonToolbarWrapper />);
|
const result = renderWithProviders(<BalloonToolbarWrapper />);
|
||||||
|
|
||||||
rerender(<BalloonToolbarWrapper useMdx={useMdx} />);
|
result.rerender(<BalloonToolbarWrapper useMdx={useMdx} />);
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders empty node toolbar for markdown', () => {
|
it('renders empty node toolbar for markdown', () => {
|
||||||
@ -148,8 +144,14 @@ describe(BalloonToolbar.name, () => {
|
|||||||
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
|
expect(screen.queryByTestId('font-type-select')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-table')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
||||||
|
|
||||||
@ -165,8 +167,14 @@ describe(BalloonToolbar.name, () => {
|
|||||||
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
|
expect(screen.queryByTestId('font-type-select')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
|
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-row')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-row')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-insert-column')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-column')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('toolbar-button-delete-table')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-insert-link')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
expect(screen.queryByTestId('toolbar-button-insert-image')).toBeInTheDocument();
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TableAdd } from '@styled-icons/fluentui-system-regular/TableAdd';
|
||||||
import { Add as AddIcon } from '@styled-icons/material/Add';
|
import { Add as AddIcon } from '@styled-icons/material/Add';
|
||||||
import { Code as CodeIcon } from '@styled-icons/material/Code';
|
import { Code as CodeIcon } from '@styled-icons/material/Code';
|
||||||
import { FormatQuote as FormatQuoteIcon } from '@styled-icons/material/FormatQuote';
|
import { FormatQuote as FormatQuoteIcon } from '@styled-icons/material/FormatQuote';
|
||||||
@ -6,7 +7,9 @@ import {
|
|||||||
ELEMENT_CODE_BLOCK,
|
ELEMENT_CODE_BLOCK,
|
||||||
ELEMENT_IMAGE,
|
ELEMENT_IMAGE,
|
||||||
ELEMENT_LINK,
|
ELEMENT_LINK,
|
||||||
|
ELEMENT_TABLE,
|
||||||
insertEmptyCodeBlock,
|
insertEmptyCodeBlock,
|
||||||
|
insertTable,
|
||||||
toggleNodeType,
|
toggleNodeType,
|
||||||
} from '@udecode/plate';
|
} from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
@ -43,6 +46,13 @@ const AddButtons: FC<AddButtonsProps> = ({ collection, field, disabled }) => {
|
|||||||
});
|
});
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
|
const handleTableAdd = useCallback(() => {
|
||||||
|
insertTable(editor, {
|
||||||
|
rowCount: 2,
|
||||||
|
colCount: 2,
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
label={<AddIcon className="h-5 w-5" aria-hidden="true" />}
|
label={<AddIcon className="h-5 w-5" aria-hidden="true" />}
|
||||||
@ -74,6 +84,11 @@ const AddButtons: FC<AddButtonsProps> = ({ collection, field, disabled }) => {
|
|||||||
Code Block
|
Code Block
|
||||||
</MenuItemButton>
|
</MenuItemButton>
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
|
<MenuGroup>
|
||||||
|
<MenuItemButton key={ELEMENT_TABLE} onClick={handleTableAdd} startIcon={TableAdd}>
|
||||||
|
Table
|
||||||
|
</MenuItemButton>
|
||||||
|
</MenuGroup>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
<ImageToolbarButton
|
<ImageToolbarButton
|
||||||
key={ELEMENT_IMAGE}
|
key={ELEMENT_IMAGE}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Image as ImageIcon } from '@styled-icons/material/Image';
|
import { Image as ImageIcon } from '@styled-icons/material/Image';
|
||||||
import { ELEMENT_IMAGE, insertImage } from '@udecode/plate';
|
import { ELEMENT_IMAGE, insertImage } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
|
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
|
||||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||||
@ -36,12 +36,14 @@ const ImageToolbarButton: FC<ImageToolbarButtonProps> = ({
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]);
|
||||||
|
|
||||||
const openMediaLibrary = useMediaInsert(
|
const openMediaLibrary = useMediaInsert(
|
||||||
{
|
{
|
||||||
path: currentValue?.url ?? '',
|
path: currentValue?.url ?? '',
|
||||||
alt: currentValue?.alt,
|
alt: currentValue?.alt,
|
||||||
},
|
},
|
||||||
{ collection, field, forImage: true },
|
{ collection, field, forImage: true, insertOptions: { chooseUrl, showAlt: true } },
|
||||||
handleInsert,
|
handleInsert,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Link as LinkIcon } from '@styled-icons/material/Link';
|
import { Link as LinkIcon } from '@styled-icons/material/Link';
|
||||||
import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
|
import { ELEMENT_LINK, insertLink, someNode } from '@udecode/plate';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
|
import MenuItemButton from '@staticcms/core/components/common/menu/MenuItemButton';
|
||||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||||
@ -41,6 +41,8 @@ const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]);
|
||||||
|
|
||||||
const isLink = !!editor?.selection && someNode(editor, { match: { type: ELEMENT_LINK } });
|
const isLink = !!editor?.selection && someNode(editor, { match: { type: ELEMENT_LINK } });
|
||||||
|
|
||||||
const controlID = useUUID();
|
const controlID = useUUID();
|
||||||
@ -49,7 +51,7 @@ const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
|
|||||||
path: currentValue?.url ?? '',
|
path: currentValue?.url ?? '',
|
||||||
alt: currentValue?.alt,
|
alt: currentValue?.alt,
|
||||||
},
|
},
|
||||||
{ collection, field, controlID, forImage: true },
|
{ collection, field, controlID, forImage: false, insertOptions: { chooseUrl, showAlt: true } },
|
||||||
handleInsert,
|
handleInsert,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
onMediaToggle?.(false);
|
onMediaToggle?.(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const chooseUrl = useMemo(() => field.choose_url ?? false, [field.choose_url]);
|
const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
|
@ -22,7 +22,6 @@ const TableHeaderCellElement: FC<PlateRenderElementProps<MdValue, MdTableCellEle
|
|||||||
text-sm
|
text-sm
|
||||||
border-r
|
border-r
|
||||||
border-gray-200
|
border-gray-200
|
||||||
last:border-0
|
|
||||||
dark:bg-slate-700
|
dark:bg-slate-700
|
||||||
dark:border-gray-800
|
dark:border-gray-800
|
||||||
"
|
"
|
||||||
|
@ -27,7 +27,19 @@ const TableElement: FC<PlateRenderElementProps<MdValue, MdTableElement>> = ({
|
|||||||
>
|
>
|
||||||
{children ? (
|
{children ? (
|
||||||
<>
|
<>
|
||||||
<thead key="thead">{children[0]}</thead>
|
<thead
|
||||||
|
key="thead"
|
||||||
|
className="
|
||||||
|
border-r
|
||||||
|
border-b
|
||||||
|
bg-slate-300
|
||||||
|
border-gray-200
|
||||||
|
dark:bg-slate-700
|
||||||
|
dark:border-gray-800
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{children[0]}
|
||||||
|
</thead>
|
||||||
<tbody key="tbody">{children.slice(1)}</tbody>
|
<tbody key="tbody">{children.slice(1)}</tbody>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -22,7 +22,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
|
|||||||
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
|
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
|
||||||
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
|
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
|
||||||
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
|
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
|
||||||
| choose_url | boolean | `false` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
|
| choose_url | boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@ -231,9 +231,13 @@ const ImageControl = ({ src, onChange, controlProps }) => {
|
|||||||
onChange({ src: path });
|
onChange({ src: path });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenMediaLibrary = useMediaInsert(src, { collection: collection, field }, handleChange);
|
const handleOpenMediaLibrary = useMediaInsert(
|
||||||
|
src,
|
||||||
|
{ collection: collection, field },
|
||||||
|
handleChange,
|
||||||
|
);
|
||||||
|
|
||||||
const assetSource = useMediaAsset(src , collection, field, entry);
|
const assetSource = useMediaAsset(src, collection, field, entry);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
|
h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
|
||||||
@ -253,9 +257,13 @@ const ImageControl = ({ src, onChange, controlProps }) => {
|
|||||||
onChange({ src: path });
|
onChange({ src: path });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenMediaLibrary = useMediaInsert(src ,{ collection: collection, field }, handleChange);
|
const handleOpenMediaLibrary = useMediaInsert(
|
||||||
|
src,
|
||||||
|
{ collection: collection, field },
|
||||||
|
handleChange,
|
||||||
|
);
|
||||||
|
|
||||||
const assetSource = useMediaAsset(src , collection, field, entry);
|
const assetSource = useMediaAsset(src, collection, field, entry);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -275,11 +283,7 @@ import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
|||||||
import type { WidgetControlProps, MediaPath } from '@staticcms/core/interface';
|
import type { WidgetControlProps, MediaPath } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
const FileControl: FC<WidgetControlProps<string, MyField>> = ({
|
const FileControl: FC<WidgetControlProps<string, MyField>> = ({ src, onChange, controlProps }) => {
|
||||||
src,
|
|
||||||
onChange,
|
|
||||||
controlProps
|
|
||||||
}) => {
|
|
||||||
const { collection, field, entry } = controlProps;
|
const { collection, field, entry } = controlProps;
|
||||||
|
|
||||||
const handleChange = ({ path }: MediaPath) => {
|
const handleChange = ({ path }: MediaPath) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user