fix: media library url entry (#775)

This commit is contained in:
Daniel Lautzenheiser 2023-05-02 16:34:59 -04:00 committed by GitHub
parent 7d0a705eee
commit a7ab1a7c0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 199 additions and 119 deletions

View File

@ -63,5 +63,6 @@ export const isSelectionExpanded = jest.fn();
export const isText = jest.fn();
export const someNode = jest.fn();
export const usePlateSelection = jest.fn();
export const isMarkActive = jest.fn();
export default {};

View File

@ -157,6 +157,7 @@ const Field: FC<FieldProps> = ({
{renderedHint}
{renderedErrorMessage}
</div>
{endAdornment ? (
<div
className={classNames(
`
@ -167,6 +168,7 @@ const Field: FC<FieldProps> = ({
>
{endAdornment}
</div>
) : null}
</div>
);
};

View File

@ -1,9 +1,11 @@
import React from 'react';
import { Image as ImageIcon } from '@styled-icons/material-outlined/Image';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
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';
@ -28,6 +30,20 @@ const Image = <EF extends BaseField = UnknownField>({
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 (
<img
key="image"

View File

@ -1,5 +1,6 @@
import React from 'react';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { isEmpty } from '../../../lib/util/string.util';
import Image from '../../common/image/Image';
import InlineEditTextField from './InlineEditTextField';
@ -14,6 +15,7 @@ interface CurrentMediaDetailsProps {
url?: string | string[];
alt?: string;
insertOptions?: MediaLibrarInsertOptions;
forImage: boolean;
onUrlChange: (url: string) => void;
onAltChange: (alt: string) => void;
}
@ -25,27 +27,46 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
url,
alt,
insertOptions,
forImage,
onUrlChange,
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 (
<div
className="
grid
grid-cols-media-preview
className={classNames(
`
items-center
px-5
py-4
border-b
border-gray-200/75
dark:border-slate-500/75
"
`,
forImage
? `
grid
grid-cols-media-preview
`
: `
flex
w-full
`,
)}
>
{forImage ? (
<Image
key="image-preview"
src={url}
collection={collection}
field={field}
@ -64,15 +85,25 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
object-cover
"
/>
) : null}
<div
className="
className={classNames(
`
flex
flex-col
h-full
p-0
pl-4
gap-2
"
`,
forImage
? `
pl-4
`
: `
w-full
pl-1.5
`,
)}
>
<InlineEditTextField
label="URL"
@ -80,7 +111,11 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
onChange={insertOptions?.chooseUrl ? onUrlChange : undefined}
/>
{insertOptions?.showAlt ? (
<InlineEditTextField label="Alt" value={alt} onChange={onAltChange} />
<InlineEditTextField
label={forImage ? 'Alt' : 'Text'}
value={alt}
onChange={onAltChange}
/>
) : null}
</div>
</div>

View File

@ -96,6 +96,7 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
{!editing || !onChange ? (
<div
key="value"
tabIndex={0}
className={classNames(
`
flex
@ -110,7 +111,6 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
rounded-md
border
text-slate-600
dark:font-semibold
dark:text-gray-100
`,
onChange
@ -125,6 +125,7 @@ const InlineEditTextField: FC<InlineEditTextFieldProps> = ({
`,
)}
onClick={handleValueClick}
onFocus={handleValueClick}
>
{internalValue}
</div>

View File

@ -484,6 +484,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
url={url}
alt={alt}
insertOptions={insertOptions}
forImage={forImage}
onUrlChange={handleURLChange}
onAltChange={handleAltChange}
/>

View File

@ -253,7 +253,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
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}>
<PlateProvider<MdValue>
id={id}
@ -272,7 +272,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
disabled={disabled}
/>
<div key="editor-wrapper" ref={editorContainerRef} className="w-full overflow-hidden">
<div key="editor-wrapper" ref={editorContainerRef} className="w-full">
<Plate
key="editor"
id={id}

View File

@ -2,9 +2,7 @@ import PopperUnstyled from '@mui/base/PopperUnstyled';
import {
ELEMENT_LINK,
ELEMENT_TD,
findNodePath,
getNode,
getParentNode,
getSelectionBoundingClientRect,
getSelectionText,
isElement,
@ -20,7 +18,6 @@ import { useFocused } from 'slate-react';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import { VOID_ELEMENTS } from '../../serialization/slate/ast-types';
import BasicElementToolbarButtons from '../buttons/BasicElementToolbarButtons';
import BasicMarkToolbarButtons from '../buttons/BasicMarkToolbarButtons';
import MediaToolbarButtons from '../buttons/MediaToolbarButtons';
@ -156,30 +153,15 @@ const BalloonToolbar: FC<BalloonToolbarProps> = ({
!useMdx ? <ShortcodeToolbarButton key="shortcode-button" disabled={disabled} /> : null,
].filter(Boolean);
// if (isInTableCell) {
// return allButtons;
// }
// Empty paragraph, not first line
// Empty table cell
if (
isInTableCell &&
editor.children.length > 1 &&
node &&
((isElement(node) && isElementEmpty(editor, node)) || (isText(node) && isEmpty(node.text)))
) {
const path = findNodePath(editor, node) ?? [];
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 [];
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -4,6 +4,7 @@
import '@testing-library/jest-dom';
import { screen } from '@testing-library/react';
import {
ELEMENT_LINK,
findNodePath,
getNode,
getParentNode,
@ -11,6 +12,7 @@ import {
isElementEmpty,
someNode,
usePlateEditorState,
usePlateSelection,
} from '@udecode/plate';
import React, { useRef } from 'react';
import { useFocused } from 'slate-react';
@ -25,6 +27,7 @@ import BalloonToolbar from '../BalloonToolbar';
import type { Config, MarkdownField } from '@staticcms/core/interface';
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
import type { TRange } from '@udecode/plate';
import type { FC } from 'react';
interface BalloonToolbarWrapperProps {
@ -54,6 +57,7 @@ const config = createMockConfig({
describe(BalloonToolbar.name, () => {
const mockUseEditor = usePlateEditorState as jest.Mock;
const mockUsePlateSelection = usePlateSelection as jest.Mock;
let mockEditor: MdEditor;
const mockGetNode = getNode as jest.Mock;
@ -67,26 +71,6 @@ describe(BalloonToolbar.name, () => {
beforeEach(() => {
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 = {
selection: undefined,
} as unknown as MdEditor;
@ -99,45 +83,57 @@ describe(BalloonToolbar.name, () => {
expect(screen.queryAllByRole('button').length).toBe(0);
});
describe('empty node toolbar', () => {
describe('empty node toolbar inside table', () => {
interface EmptyNodeToolbarSetupOptions {
useMdx?: boolean;
}
const emptyNodeToolbarSetup = ({ useMdx }: EmptyNodeToolbarSetupOptions = {}) => {
mockEditor = {
selection: undefined,
selection: {
anchor: {
path: [1, 0],
offset: 0,
},
focus: {
path: [1, 0],
offset: 0,
},
} as TRange,
children: [
{
type: 'p',
children: [{ text: '' }],
},
{
type: 'p',
type: 'td',
children: [{ text: '' }],
},
],
} as unknown as MdEditor;
mockUseEditor.mockReturnValue(mockEditor);
mockUsePlateSelection.mockReturnValue(mockEditor.selection);
mockGetNode.mockReturnValue({ text: '' });
mockIsElement.mockReturnValue(true);
mockIsElementEmpty.mockReturnValue(true);
mockSomeNode.mockReturnValue(false);
mockSomeNode.mockImplementation((_editor, { match: { type } }) => type !== ELEMENT_LINK);
mockUseFocused.mockReturnValue(true);
mockFindNodePath.mockReturnValue([1, 0]);
mockGetParentNode.mockReturnValue([
{
type: 'p',
type: 'td',
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', () => {
@ -148,8 +144,14 @@ describe(BalloonToolbar.name, () => {
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).not.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-image')).toBeInTheDocument();
@ -165,8 +167,14 @@ describe(BalloonToolbar.name, () => {
expect(screen.queryByTestId('toolbar-button-code')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-strikethrough')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).toBeInTheDocument();
expect(screen.queryByTestId('toolbar-button-add-table')).toBeInTheDocument();
expect(screen.queryByTestId('font-type-select')).not.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-image')).toBeInTheDocument();

View File

@ -1,3 +1,4 @@
import { TableAdd } from '@styled-icons/fluentui-system-regular/TableAdd';
import { Add as AddIcon } from '@styled-icons/material/Add';
import { Code as CodeIcon } from '@styled-icons/material/Code';
import { FormatQuote as FormatQuoteIcon } from '@styled-icons/material/FormatQuote';
@ -6,7 +7,9 @@ import {
ELEMENT_CODE_BLOCK,
ELEMENT_IMAGE,
ELEMENT_LINK,
ELEMENT_TABLE,
insertEmptyCodeBlock,
insertTable,
toggleNodeType,
} from '@udecode/plate';
import React, { useCallback } from 'react';
@ -43,6 +46,13 @@ const AddButtons: FC<AddButtonsProps> = ({ collection, field, disabled }) => {
});
}, [editor]);
const handleTableAdd = useCallback(() => {
insertTable(editor, {
rowCount: 2,
colCount: 2,
});
}, [editor]);
return (
<Menu
label={<AddIcon className="h-5 w-5" aria-hidden="true" />}
@ -74,6 +84,11 @@ const AddButtons: FC<AddButtonsProps> = ({ collection, field, disabled }) => {
Code Block
</MenuItemButton>
</MenuGroup>
<MenuGroup>
<MenuItemButton key={ELEMENT_TABLE} onClick={handleTableAdd} startIcon={TableAdd}>
Table
</MenuItemButton>
</MenuGroup>
<MenuGroup>
<ImageToolbarButton
key={ELEMENT_IMAGE}

View File

@ -1,6 +1,6 @@
import { Image as ImageIcon } from '@styled-icons/material/Image';
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 useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
@ -36,12 +36,14 @@ const ImageToolbarButton: FC<ImageToolbarButtonProps> = ({
[editor],
);
const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]);
const openMediaLibrary = useMediaInsert(
{
path: currentValue?.url ?? '',
alt: currentValue?.alt,
},
{ collection, field, forImage: true },
{ collection, field, forImage: true, insertOptions: { chooseUrl, showAlt: true } },
handleInsert,
);

View File

@ -1,6 +1,6 @@
import { Link as LinkIcon } from '@styled-icons/material/Link';
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 useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
@ -41,6 +41,8 @@ const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
[editor],
);
const chooseUrl = useMemo(() => field.choose_url ?? true, [field.choose_url]);
const isLink = !!editor?.selection && someNode(editor, { match: { type: ELEMENT_LINK } });
const controlID = useUUID();
@ -49,7 +51,7 @@ const LinkToolbarButton: FC<LinkToolbarButtonProps> = ({
path: currentValue?.url ?? '',
alt: currentValue?.alt,
},
{ collection, field, controlID, forImage: true },
{ collection, field, controlID, forImage: false, insertOptions: { chooseUrl, showAlt: true } },
handleInsert,
);

View File

@ -45,7 +45,7 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
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(() => {
onFocus?.();

View File

@ -22,7 +22,6 @@ const TableHeaderCellElement: FC<PlateRenderElementProps<MdValue, MdTableCellEle
text-sm
border-r
border-gray-200
last:border-0
dark:bg-slate-700
dark:border-gray-800
"

View File

@ -27,7 +27,19 @@ const TableElement: FC<PlateRenderElementProps<MdValue, MdTableElement>> = ({
>
{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>
</>
) : null}

View File

@ -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 |
| 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) |
| 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
@ -231,9 +231,13 @@ const ImageControl = ({ src, onChange, controlProps }) => {
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 [
h('button', { type: 'button', onClick: handleOpenMediaLibrary }, 'Upload'),
@ -253,9 +257,13 @@ const ImageControl = ({ src, onChange, controlProps }) => {
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 (
<>
@ -275,11 +283,7 @@ import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import type { WidgetControlProps, MediaPath } from '@staticcms/core/interface';
import type { FC } from 'react';
const FileControl: FC<WidgetControlProps<string, MyField>> = ({
src,
onChange,
controlProps
}) => {
const FileControl: FC<WidgetControlProps<string, MyField>> = ({ src, onChange, controlProps }) => {
const { collection, field, entry } = controlProps;
const handleChange = ({ path }: MediaPath) => {