Skip to content

Commit

Permalink
feat: add (rough) functionality for inline embedded resources
Browse files Browse the repository at this point in the history
  • Loading branch information
msieroslawska committed Oct 13, 2023
1 parent a237d46 commit 5212942
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,16 @@ export const EmbedEntityWidget = ({ isDisabled, canInsertBlocks }: EmbedEntityWi
)}
{inlineEntryEmbedEnabled && (
<EmbeddedInlineToolbarIcon
nodeType={INLINES.EMBEDDED_ENTRY}
isDisabled={!!isDisabled || isLinkActive(editor)}
onClose={onCloseEntityDropdown}
/>
)}
<EmbeddedInlineToolbarIcon
nodeType={INLINES.EMBEDDED_RESOURCE}
isDisabled={!!isDisabled || isLinkActive(editor)}
onClose={onCloseEntityDropdown}
/>
{blockAssetEmbedEnabled && (
<EmbeddedBlockToolbarIcon
isDisabled={!!isDisabled}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { FieldAPI } from '@contentful/app-sdk';

import getAllowedResourcesForNodeType from './getAllowedResourcesForNodeType';
import getLinkedContentTypeIdsForNodeType from './getLinkedContentTypeIdsForNodeType';

/**
Expand All @@ -9,13 +12,23 @@ import getLinkedContentTypeIdsForNodeType from './getLinkedContentTypeIdsForNode
* @param {string} nodeType
* @returns {object}
*/
export default function newEntitySelectorConfigFromRichTextField(field, nodeType) {

type EntitySelectorConfig = {
entityType: string;
locale: string | null;
contentTypes: string[];
};

export const newEntitySelectorConfigFromRichTextField = (
field: FieldAPI,
nodeType
): EntitySelectorConfig => {
return {
entityType: getEntityTypeFromRichTextNode(nodeType),
locale: field.locale || null, // Will fall back to default locale.
contentTypes: getLinkedContentTypeIdsForNodeType(field, nodeType),
};
}
};

function getEntityTypeFromRichTextNode(nodeType): 'Entry' | 'Asset' | never {
const words = nodeType.split('-');
Expand All @@ -27,3 +40,18 @@ function getEntityTypeFromRichTextNode(nodeType): 'Entry' | 'Asset' | never {
}
throw new Error(`RichText node type \`${nodeType}\` has no associated \`entityType\``);
}

/**
* Returns a config for the entity selector based on a given rich text field and a
* rich text node type that the entity should be picked for. Takes the field
* validations for the given node type into account.
*
* @param {object} field
* @param {string} nodeType
* @returns {object}
*/
export const newResourceEntitySelectorConfigFromRichTextField = (field, nodeType) => {
return {
allowedResources: getAllowedResourcesForNodeType(field, nodeType),
};
};

This file was deleted.

26 changes: 18 additions & 8 deletions packages/rich-text/src/plugins/EmbeddedEntityInline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import { FieldAppSDK } from '@contentful/app-sdk';
import { INLINES } from '@contentful/rich-text-types';

import { PlatePlugin, Node } from '../../internal/types';
import {
createInlineEntryNode,
getWithEmbeddedEntryInlineEvents,
} from '../shared/EmbeddedInlineUtil';
import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
import { LinkedEntityInline } from './LinkedEntityInline';

export function createEmbeddedEntityInlinePlugin(sdk: FieldAppSDK): PlatePlugin {
const htmlAttributeName = 'data-embedded-entity-inline-id';
const nodeType = INLINES.EMBEDDED_ENTRY;

return {
key: INLINES.EMBEDDED_ENTRY,
type: INLINES.EMBEDDED_ENTRY,
key: nodeType,
type: nodeType,
isElement: true,
isInline: true,
isVoid: true,
Expand All @@ -22,7 +20,7 @@ export function createEmbeddedEntityInlinePlugin(sdk: FieldAppSDK): PlatePlugin
hotkey: 'mod+shift+2',
},
handlers: {
onKeyDown: getWithEmbeddedEntryInlineEvents(sdk),
onKeyDown: getWithEmbeddedEntryInlineEvents(nodeType, sdk),
},
deserializeHtml: {
rules: [
Expand All @@ -31,7 +29,19 @@ export function createEmbeddedEntityInlinePlugin(sdk: FieldAppSDK): PlatePlugin
},
],
withoutChildren: true,
getNode: (el): Node => createInlineEntryNode(el.getAttribute(htmlAttributeName) as string),
getNode: (el): Node => ({
type: nodeType,
children: [{ text: '' }],
data: {
target: {
sys: {
id: el.getAttribute('data-entity-id'),
type: 'Link',
linkType: el.getAttribute('data-entity-type'),
},
},
},
}),
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';

import { Entry } from '@contentful/app-sdk';
import { InlineEntryCard, MenuItem, Text } from '@contentful/f36-components';
import { ResourceLink, ResourceInfo, useResource } from '@contentful/field-editor-reference';
import { entityHelpers } from '@contentful/field-editor-shared';
import { FieldAppSDK } from '@contentful/field-editor-shared';
import { INLINES } from '@contentful/rich-text-types';

const { getEntryTitle, getEntryStatus } = entityHelpers;

interface FetchingWrappedInlineResourceCardProps {
link: ResourceLink['sys'];
sdk: FieldAppSDK;
isSelected: boolean;
isDisabled: boolean;
onRemove: (event: React.MouseEvent<Element, MouseEvent>) => void;
onEntityFetchComplete?: VoidFunction;
}

export function FetchingWrappedInlineResourceCard(props: FetchingWrappedInlineResourceCardProps) {
const { link, onEntityFetchComplete } = props;
const { data, status: requestStatus } = useResource(link.linkType, link.urn);

React.useEffect(() => {
if (requestStatus === 'success') {
onEntityFetchComplete?.();
}
}, [onEntityFetchComplete, requestStatus]);

if (requestStatus === 'error') {
return (
<InlineEntryCard
title="Entry missing or inaccessible"
testId={INLINES.EMBEDDED_RESOURCE}
isSelected={props.isSelected}
/>
);
}

if (requestStatus === 'loading' || data === undefined) {
return <InlineEntryCard isLoading />;
}

const { resource: entry, contentType, defaultLocaleCode } = data as ResourceInfo<Entry>;

const title = getEntryTitle({
entry,
contentType,
defaultLocaleCode,
localeCode: defaultLocaleCode,
defaultTitle: 'Untitled',
});
const status = getEntryStatus(entry?.sys);

return (
<InlineEntryCard
testId={INLINES.EMBEDDED_RESOURCE}
isSelected={props.isSelected}
title={`${data?.contentType.name}: ${title}`}
status={status}
actions={[
<MenuItem key="remove" onClick={props.onRemove} disabled={props.isDisabled} testId="delete">
Remove
</MenuItem>,
]}
>
<Text>{title}</Text>
</InlineEntryCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

import { ResourceLink } from '@contentful/field-editor-reference';
import { useSelected, useReadOnly } from 'slate-react';

import { useContentfulEditor } from '../../ContentfulEditorProvider';
import { Element, findNodePath, removeNodes, RenderElementProps } from '../../internal';
import { useSdkContext } from '../../SdkProvider';
import { useLinkTracking } from '../links-tracking';
import { LinkedInlineWrapper } from '../shared/LinkedInlineWrapper';
import { FetchingWrappedInlineResourceCard } from './FetchingWrappedInlineResourceCard';

export type LinkedResourceInlineProps = {
element: Element & {
data: {
target: ResourceLink;
};
};
attributes: Pick<RenderElementProps, 'attributes'>;
children: Pick<RenderElementProps, 'children'>;
};

export function LinkedResourceInline(props: LinkedResourceInlineProps) {
const { attributes, children, element } = props;
const { onEntityFetchComplete } = useLinkTracking();
const isSelected = useSelected();
const editor = useContentfulEditor();
const sdk = useSdkContext();
const isDisabled = useReadOnly();
const link = element.data.target.sys;

function handleRemoveClick() {
if (!editor) return;
const pathToElement = findNodePath(editor, element);
removeNodes(editor, { at: pathToElement });
}

return (
<LinkedInlineWrapper
attributes={attributes}
link={element.data.target}
card={
<FetchingWrappedInlineResourceCard
sdk={sdk}
link={link}
isDisabled={isDisabled}
isSelected={isSelected}
onRemove={handleRemoveClick}
onEntityFetchComplete={onEntityFetchComplete}
/>
}
>
{children}
</LinkedInlineWrapper>
);
}
46 changes: 46 additions & 0 deletions packages/rich-text/src/plugins/EmbeddedResourceInline/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FieldAppSDK } from '@contentful/app-sdk';
import { INLINES } from '@contentful/rich-text-types';

import { PlatePlugin } from '../../internal';
import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
import { LinkedResourceInline } from './LinkedResourceInline';

export function createEmbeddedResourceInlinePlugin(sdk: FieldAppSDK): PlatePlugin {
const htmlAttributeName = 'data-embedded-resource-inline-id';

return {
key: INLINES.EMBEDDED_RESOURCE,
type: INLINES.EMBEDDED_RESOURCE,
isElement: true,
isInline: true,
isVoid: true,
component: LinkedResourceInline,
options: {
hotkey: 'mod+shift+p',
},
handlers: {
onKeyDown: getWithEmbeddedEntryInlineEvents(INLINES.EMBEDDED_RESOURCE, sdk),
},
deserializeHtml: {
rules: [
{
validAttribute: htmlAttributeName,
},
],
withoutChildren: true,
getNode: (el) => ({
type: INLINES.EMBEDDED_RESOURCE,
children: [{ text: '' }],
data: {
target: {
sys: {
urn: el.getAttribute('data-entity-id'),
linkType: el.getAttribute('data-entity-type'),
type: 'ResourceLink',
},
},
},
}),
},
};
}
2 changes: 2 additions & 0 deletions packages/rich-text/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './EmbeddedEntityBlock';
import { createEmbeddedEntityInlinePlugin } from './EmbeddedEntityInline';
import { createEmbeddedResourceBlockPlugin } from './EmbeddedResourceBlock';
import { createEmbeddedResourceInlinePlugin } from './EmbeddedResourceInline';
import { createHeadingPlugin } from './Heading';
import { createHrPlugin } from './Hr';
import { createHyperlinkPlugin } from './Hyperlink';
Expand Down Expand Up @@ -60,6 +61,7 @@ export const getPlugins = (
// Inline elements
createHyperlinkPlugin(sdk),
createEmbeddedEntityInlinePlugin(sdk),
createEmbeddedResourceInlinePlugin(sdk),

// Marks
createMarksPlugin(),
Expand Down
6 changes: 4 additions & 2 deletions packages/rich-text/src/plugins/shared/EmbeddedBlockUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { BLOCKS, TEXT_CONTAINERS } from '@contentful/rich-text-types';
import { HotkeyPlugin } from '@udecode/plate-common';
import isHotkey from 'is-hotkey';

import {
newEntitySelectorConfigFromRichTextField,
newResourceEntitySelectorConfigFromRichTextField,
} from '../../helpers/config';
import {
focus,
getNodeEntryFromSelection,
insertEmptyParagraph,
moveToTheNextChar,
} from '../../helpers/editor';
import newEntitySelectorConfigFromRichTextField from '../../helpers/newEntitySelectorConfigFromRichTextField';
import newResourceEntitySelectorConfigFromRichTextField from '../../helpers/newResourceEntitySelectorConfigFromRichTextField';
import { watchCurrentSlide } from '../../helpers/sdkNavigatorSlideIn';
import {
getText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { css } from 'emotion';
import { useContentfulEditor } from '../../ContentfulEditorProvider';
import { moveToTheNextChar } from '../../helpers/editor';
import { useSdkContext } from '../../SdkProvider';
import { selectEntityAndInsert } from '../shared/EmbeddedInlineUtil';
import { selectEntityAndInsert, selectResourceEntityAndInsert } from '../shared/EmbeddedInlineUtil';

const styles = {
icon: css({
Expand All @@ -29,10 +29,15 @@ const styles = {

interface EmbeddedInlineToolbarIconProps {
onClose: () => void;
nodeType: string;
isDisabled: boolean;
}

export function EmbeddedInlineToolbarIcon(props: EmbeddedInlineToolbarIconProps) {
export function EmbeddedInlineToolbarIcon({
onClose,
nodeType,
isDisabled,
}: EmbeddedInlineToolbarIconProps) {
const editor = useContentfulEditor();
const sdk: FieldAppSDK = useSdkContext();

Expand All @@ -41,15 +46,20 @@ export function EmbeddedInlineToolbarIcon(props: EmbeddedInlineToolbarIconProps)

if (!editor) return;

props.onClose();
onClose();

if (nodeType == INLINES.EMBEDDED_RESOURCE) {
await selectResourceEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
} else {
await selectEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
}

await selectEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
moveToTheNextChar(editor);
}

return (
<Menu.Item
disabled={props.isDisabled}
disabled={isDisabled}
className="rich-text__entry-link-block-button"
testId={`toolbar-toggle-${INLINES.EMBEDDED_ENTRY}`}
onClick={handleClick}
Expand Down
Loading

0 comments on commit 5212942

Please sign in to comment.