feat: Code Widget + Markdown Widget Internal Overhaul (#2828)
* wip - upgrade to slate 0.43 * wip * wip * wip * wip * wip * wip * wip * finish list handling logic * add plugins directory * tests wip * setup testing * wip * add selection commands * finish list testing * stuff * add codemirror * abstract codemirror from slate * wip * wip * wip * wip * wip * wip * wip * wip * wip * codemirror mostly working, some bugs * upgrade to slate 46 * upgrade to slate 47 * wip * wip * progress * wip * mostly working links with surrounding marks * wip * tests passing * add test * fix formatting * update snapshots * close self closing tag in markdown html output * wip - commonmark * hold on commonmark work * all tests passing * fix e2e specs * ignore tests in esm builds * break/backspace plugins wip * finish enter/backspace spec * fix soft break handling * wip - editor component deletion * add insertion points * make insertion points invisible * fix empty mark nodes output to markdown * fix pasting * improve insertion points * add static bottom insertion point * improve click handling at insertion points * restore current table functionality * add paste support for Slate fragments * support cut/copy markdown, paste between rich/raw editor * fix copy paste * wip - paste/select bug fixing * fixed known slate issues * split plugins * fix editor toggles * force text cursor in code widget * wip - reorg plugins * finish markdown control reorg * configure plugin types * quote block adjacent handling with tests * wip * finish quote logic and tests * fix copy paste plugin migration regressions * fix force insert before node * fix trailing insertion point * remove empty headers * codemirror working properly in markdown widget * return focus to codemirror on lang select enter * fix state issues for widgets with local state * wip - vim working, just need to work out distribution * add settings pane * wip - default modes * fix deps * add programming language data * implement linguist langs in code widget * everything built in * remove old registration code, fix focus styling * fix/update linting setup * fix js lint errors * remove stylelint from format script * fix remaining linting errors * fix reducer test failures * chore: update commitlint for worktree support * chore: fix remaining tests * chore: drop unused monaco plugin * chore: remove extraneous global styles rendering * chore: fix failing tests * fix: tests * fix: quote/list nesting (tests still broken) * fix: update quote tests * chore: bring back code widget test config * fix: autofocus * fix: code blocks without the code widget * fix: code editor component state issues * fix: error * fix: add code block test, few fixes * chore: remove notes * fix: [wip] update stateful shortcodes on undo/redo * fix: support code styled links, handle unknown langs * fix: few fixes * fix: autofocus on insert, focus on all clicks * fix: linting * fix: autofocus * fix: update code block fixture * fix: remove unused cypress snapshot plugin * fix: drop node 8 test, add node 12 * fix: use lodash.flatten instead of Array.flat * fix: remove console logs
This commit is contained in:
committed by
Erez Rokah
parent
be46293f82
commit
18c579d0e9
@ -33,6 +33,7 @@
|
||||
"gotrue-js": "^0.9.24",
|
||||
"gray-matter": "^4.0.2",
|
||||
"history": "^4.7.2",
|
||||
"immer": "^3.1.3",
|
||||
"js-base64": "^2.5.1",
|
||||
"js-yaml": "^3.12.2",
|
||||
"jwt-decode": "^2.1.0",
|
||||
|
@ -1,9 +1,8 @@
|
||||
/** @jsx jsx */
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from '@emotion/styled';
|
||||
import { jsx, css } from '@emotion/core';
|
||||
import { css } from '@emotion/core';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
|
@ -399,6 +399,7 @@ export class Editor extends React.Component {
|
||||
logoutUser,
|
||||
deployPreview,
|
||||
loadDeployPreview,
|
||||
draftKey,
|
||||
slug,
|
||||
t,
|
||||
} = this.props;
|
||||
@ -421,6 +422,7 @@ export class Editor extends React.Component {
|
||||
|
||||
return (
|
||||
<EditorInterface
|
||||
draftKey={draftKey}
|
||||
entry={entryDraft.get('entry')}
|
||||
getAsset={boundGetAsset}
|
||||
collection={collection}
|
||||
@ -474,6 +476,7 @@ function mapStateToProps(state, ownProps) {
|
||||
const currentStatus = unpublishedEntry && unpublishedEntry.getIn(['metaData', 'status']);
|
||||
const deployPreview = selectDeployPreview(state, collectionName, slug);
|
||||
const localBackup = entryDraft.get('localBackup');
|
||||
const draftKey = entryDraft.get('key');
|
||||
return {
|
||||
collection,
|
||||
collections,
|
||||
@ -493,6 +496,7 @@ function mapStateToProps(state, ownProps) {
|
||||
currentStatus,
|
||||
deployPreview,
|
||||
localBackup,
|
||||
draftKey,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
/** @jsx jsx */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { jsx, ClassNames, Global, css as coreCss } from '@emotion/core';
|
||||
import { ClassNames, Global, css as coreCss } from '@emotion/core';
|
||||
import styled from '@emotion/styled';
|
||||
import { partial, uniqueId } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { colors, colorsRaw, transitions, lengths, borders } from 'netlify-cms-ui-default';
|
||||
import { FieldLabel, colors, transitions, lengths, borders } from 'netlify-cms-ui-default';
|
||||
import { resolveWidget, getEditorComponents } from 'Lib/registry';
|
||||
import { clearFieldErrors, loadEntry } from 'Actions/entries';
|
||||
import { addAsset } from 'Actions/media';
|
||||
@ -27,48 +26,6 @@ import Widget from './Widget';
|
||||
* this.
|
||||
*/
|
||||
const styleStrings = {
|
||||
label: `
|
||||
color: ${colors.controlLabel};
|
||||
background-color: ${colors.textFieldBorder};
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
border: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 3px 6px 2px;
|
||||
margin: 0;
|
||||
transition: all ${transitions.main};
|
||||
position: relative;
|
||||
|
||||
/**
|
||||
* Faux outside curve into top of input
|
||||
*/
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -4px;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-bottom-left-radius: 3px;
|
||||
background-color: #fff;
|
||||
}
|
||||
`,
|
||||
labelActive: `
|
||||
background-color: ${colors.active};
|
||||
color: ${colors.textLight};
|
||||
`,
|
||||
labelError: `
|
||||
background-color: ${colors.errorText};
|
||||
color: ${colorsRaw.white};
|
||||
`,
|
||||
widget: `
|
||||
display: block;
|
||||
width: 100%;
|
||||
@ -85,6 +42,7 @@ const styleStrings = {
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
|
||||
select& {
|
||||
text-indent: 14px;
|
||||
@ -155,6 +113,8 @@ class EditorControl extends React.Component {
|
||||
clearFieldErrors: PropTypes.func.isRequired,
|
||||
loadEntry: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isEditorComponent: PropTypes.bool,
|
||||
isNewEditorComponent: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -186,6 +146,10 @@ class EditorControl extends React.Component {
|
||||
clearSearch,
|
||||
clearFieldErrors,
|
||||
loadEntry,
|
||||
className,
|
||||
isSelected,
|
||||
isEditorComponent,
|
||||
isNewEditorComponent,
|
||||
t,
|
||||
} = this.props;
|
||||
const widgetName = field.get('widget');
|
||||
@ -199,11 +163,11 @@ class EditorControl extends React.Component {
|
||||
return (
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<ControlContainer>
|
||||
<ControlContainer className={className}>
|
||||
{widget.globalStyles && <Global styles={coreCss`${widget.globalStyles}`} />}
|
||||
<ControlErrorsList>
|
||||
{errors &&
|
||||
errors.map(
|
||||
{errors && (
|
||||
<ControlErrorsList>
|
||||
{errors.map(
|
||||
error =>
|
||||
error.message &&
|
||||
typeof error.message === 'string' && (
|
||||
@ -212,25 +176,15 @@ class EditorControl extends React.Component {
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ControlErrorsList>
|
||||
<label
|
||||
className={cx(
|
||||
css`
|
||||
${styleStrings.label};
|
||||
`,
|
||||
this.state.styleActive &&
|
||||
css`
|
||||
${styleStrings.labelActive};
|
||||
`,
|
||||
!!errors &&
|
||||
css`
|
||||
${styleStrings.labelError};
|
||||
`,
|
||||
)}
|
||||
</ControlErrorsList>
|
||||
)}
|
||||
<FieldLabel
|
||||
isActive={isSelected || this.state.styleActive}
|
||||
hasErrors={!!errors}
|
||||
htmlFor={this.uniqueFieldId}
|
||||
>
|
||||
{`${field.get('label', field.get('name'))}${isFieldOptional ? ' (optional)' : ''}`}
|
||||
</label>
|
||||
</FieldLabel>
|
||||
<Widget
|
||||
classNameWrapper={cx(
|
||||
css`
|
||||
@ -239,7 +193,7 @@ class EditorControl extends React.Component {
|
||||
{
|
||||
[css`
|
||||
${styleStrings.widgetActive};
|
||||
`]: this.state.styleActive,
|
||||
`]: isSelected || this.state.styleActive,
|
||||
},
|
||||
{
|
||||
[css`
|
||||
@ -273,10 +227,11 @@ class EditorControl extends React.Component {
|
||||
onRemoveInsertedMedia={removeInsertedMedia}
|
||||
onAddAsset={addAsset}
|
||||
getAsset={boundGetAsset}
|
||||
hasActiveStyle={this.state.styleActive}
|
||||
hasActiveStyle={isSelected || this.state.styleActive}
|
||||
setActiveStyle={() => this.setState({ styleActive: true })}
|
||||
setInactiveStyle={() => this.setState({ styleActive: false })}
|
||||
resolveWidget={resolveWidget}
|
||||
widget={widget}
|
||||
getEditorComponents={getEditorComponents}
|
||||
ref={processControlRef && partial(processControlRef, field)}
|
||||
controlRef={controlRef}
|
||||
@ -289,10 +244,12 @@ class EditorControl extends React.Component {
|
||||
isFetching={isFetching}
|
||||
fieldsErrors={fieldsErrors}
|
||||
onValidateObject={onValidateObject}
|
||||
isEditorComponent={isEditorComponent}
|
||||
isNewEditorComponent={isNewEditorComponent}
|
||||
t={t}
|
||||
/>
|
||||
{fieldHint && (
|
||||
<ControlHint active={this.state.styleActive} error={!!errors}>
|
||||
<ControlHint active={isSelected || this.state.styleActive} error={!!errors}>
|
||||
{fieldHint}
|
||||
</ControlHint>
|
||||
)}
|
||||
|
@ -44,6 +44,7 @@ export default class Widget extends Component {
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
resolveWidget: PropTypes.func.isRequired,
|
||||
widget: PropTypes.object.isRequired,
|
||||
getEditorComponents: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool,
|
||||
controlRef: PropTypes.func,
|
||||
@ -56,6 +57,8 @@ export default class Widget extends Component {
|
||||
loadEntry: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
onValidateObject: PropTypes.func,
|
||||
isEditorComponent: PropTypes.bool,
|
||||
isNewEditorComponent: PropTypes.bool,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
@ -238,6 +241,7 @@ export default class Widget extends Component {
|
||||
editorControl,
|
||||
uniqueFieldId,
|
||||
resolveWidget,
|
||||
widget,
|
||||
getEditorComponents,
|
||||
query,
|
||||
queryHits,
|
||||
@ -247,6 +251,8 @@ export default class Widget extends Component {
|
||||
loadEntry,
|
||||
fieldsErrors,
|
||||
controlRef,
|
||||
isEditorComponent,
|
||||
isNewEditorComponent,
|
||||
t,
|
||||
} = this.props;
|
||||
return React.createElement(controlComponent, {
|
||||
@ -275,6 +281,7 @@ export default class Widget extends Component {
|
||||
hasActiveStyle,
|
||||
editorControl,
|
||||
resolveWidget,
|
||||
widget,
|
||||
getEditorComponents,
|
||||
query,
|
||||
queryHits,
|
||||
@ -282,6 +289,8 @@ export default class Widget extends Component {
|
||||
clearFieldErrors,
|
||||
isFetching,
|
||||
loadEntry,
|
||||
isEditorComponent,
|
||||
isNewEditorComponent,
|
||||
fieldsErrors,
|
||||
controlRef,
|
||||
t,
|
||||
|
@ -4,12 +4,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { css, Global } from '@emotion/core';
|
||||
import styled from '@emotion/styled';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { colors, colorsRaw, components, transitions } from 'netlify-cms-ui-default';
|
||||
import { colors, colorsRaw, components, transitions, IconButton } from 'netlify-cms-ui-default';
|
||||
import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync';
|
||||
import EditorControlPane from './EditorControlPane/EditorControlPane';
|
||||
import EditorPreviewPane from './EditorPreviewPane/EditorPreviewPane';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
import EditorToggle from './EditorToggle';
|
||||
|
||||
const PREVIEW_VISIBLE = 'cms.preview-visible';
|
||||
const SCROLL_SYNC_ENABLED = 'cms.scroll-sync-enabled';
|
||||
@ -27,6 +26,10 @@ const styles = {
|
||||
`,
|
||||
};
|
||||
|
||||
const EditorToggle = styled(IconButton)`
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const ReactSplitPaneGlobalStyles = () => (
|
||||
<Global
|
||||
styles={css`
|
||||
@ -175,6 +178,7 @@ class EditorInterface extends Component {
|
||||
onLogoutClick,
|
||||
loadDeployPreview,
|
||||
deployPreview,
|
||||
draftKey,
|
||||
} = this.props;
|
||||
|
||||
const { previewVisible, scrollSyncEnabled, showEventBlocker } = this.state;
|
||||
@ -255,22 +259,26 @@ class EditorInterface extends Component {
|
||||
loadDeployPreview={loadDeployPreview}
|
||||
deployPreview={deployPreview}
|
||||
/>
|
||||
<Editor>
|
||||
<Editor key={draftKey}>
|
||||
<ViewControls>
|
||||
<EditorToggle
|
||||
enabled={collectionPreviewEnabled}
|
||||
active={previewVisible}
|
||||
onClick={this.handleTogglePreview}
|
||||
icon="eye"
|
||||
title="Toggle preview"
|
||||
/>
|
||||
<EditorToggle
|
||||
enabled={collectionPreviewEnabled && previewVisible}
|
||||
active={scrollSyncEnabled}
|
||||
onClick={this.handleToggleScrollSync}
|
||||
icon="scroll"
|
||||
title="Sync scrolling"
|
||||
/>
|
||||
{collectionPreviewEnabled && (
|
||||
<EditorToggle
|
||||
isActive={previewVisible}
|
||||
onClick={this.handleTogglePreview}
|
||||
size="large"
|
||||
type="eye"
|
||||
title="Toggle preview"
|
||||
/>
|
||||
)}
|
||||
{collectionPreviewEnabled && previewVisible && (
|
||||
<EditorToggle
|
||||
isActive={scrollSyncEnabled}
|
||||
onClick={this.handleToggleScrollSync}
|
||||
size="large"
|
||||
type="scroll"
|
||||
title="Sync scrolling"
|
||||
/>
|
||||
)}
|
||||
</ViewControls>
|
||||
{collectionPreviewEnabled && this.state.previewVisible ? (
|
||||
editorWithPreview
|
||||
@ -312,6 +320,7 @@ EditorInterface.propTypes = {
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
deployPreview: ImmutablePropTypes.map,
|
||||
loadDeployPreview: PropTypes.func.isRequired,
|
||||
draftKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EditorInterface;
|
||||
|
@ -26,6 +26,7 @@ export default class PreviewPane extends React.Component {
|
||||
const { getAsset, entry } = props;
|
||||
const widget = resolveWidget(field.get('widget'));
|
||||
const key = idx ? field.get('name') + '_' + idx : field.get('name');
|
||||
const valueIsInMap = value && !widget.allowMapValue && Map.isMap(value);
|
||||
|
||||
/**
|
||||
* Use an HOC to provide conditional updates for all previews.
|
||||
@ -36,7 +37,7 @@ export default class PreviewPane extends React.Component {
|
||||
key={key}
|
||||
field={field}
|
||||
getAsset={getAsset}
|
||||
value={value && Map.isMap(value) ? value.get(field.get('name')) : value}
|
||||
value={valueIsInMap ? value.get(field.get('name')) : value}
|
||||
entry={entry}
|
||||
fieldsMetaData={metadata}
|
||||
/>
|
||||
|
@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { Icon, colors, colorsRaw, shadows, buttons } from 'netlify-cms-ui-default';
|
||||
|
||||
const EditorToggleButton = styled.button`
|
||||
${buttons.button};
|
||||
${shadows.dropMiddle};
|
||||
background-color: ${colorsRaw.white};
|
||||
color: ${props => colors[props.isActive ? `active` : `inactive`]};
|
||||
border-radius: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const EditorToggle = ({ enabled, active, onClick, icon, title }) =>
|
||||
!enabled ? null : (
|
||||
<EditorToggleButton onClick={onClick} isActive={active} title={title}>
|
||||
<Icon type={icon} size="large" />
|
||||
</EditorToggleButton>
|
||||
);
|
||||
|
||||
EditorToggle.propTypes = {
|
||||
enabled: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EditorToggle;
|
@ -1,8 +1,7 @@
|
||||
/** @jsx jsx */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { jsx, css, Global } from '@emotion/core';
|
||||
import { css, Global } from '@emotion/core';
|
||||
import { translate } from 'react-polyglot';
|
||||
import reduxNotificationsStyles from 'redux-notifications/lib/styles.css';
|
||||
import { shadows, colors, lengths } from 'netlify-cms-ui-default';
|
||||
|
@ -1,8 +1,7 @@
|
||||
/** @jsx jsx */
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { jsx, css } from '@emotion/core';
|
||||
import { css } from '@emotion/core';
|
||||
import styled from '@emotion/styled';
|
||||
import moment from 'moment';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Map } from 'immutable';
|
||||
import produce from 'immer';
|
||||
import { oneLine } from 'common-tags';
|
||||
import EditorComponent from 'ValueObjects/EditorComponent';
|
||||
|
||||
@ -23,6 +24,7 @@ export default {
|
||||
getPreviewTemplate,
|
||||
registerWidget,
|
||||
getWidget,
|
||||
getWidgets,
|
||||
resolveWidget,
|
||||
registerEditorComponent,
|
||||
getEditorComponents,
|
||||
@ -81,10 +83,12 @@ export function registerWidget(name, control, preview) {
|
||||
name: widgetName,
|
||||
controlComponent: control,
|
||||
previewComponent: preview,
|
||||
allowMapValue,
|
||||
globalStyles,
|
||||
...options
|
||||
} = name;
|
||||
if (registry.widgets[widgetName]) {
|
||||
console.error(oneLine`
|
||||
console.warn(oneLine`
|
||||
Multiple widgets registered with name "${widgetName}". Only the last widget registered with
|
||||
this name will be used.
|
||||
`);
|
||||
@ -92,7 +96,7 @@ export function registerWidget(name, control, preview) {
|
||||
if (!control) {
|
||||
throw Error(`Widget "${widgetName}" registered without \`controlComponent\`.`);
|
||||
}
|
||||
registry.widgets[widgetName] = { control, preview, globalStyles };
|
||||
registry.widgets[widgetName] = { control, preview, globalStyles, allowMapValue, ...options };
|
||||
} else {
|
||||
console.error('`registerWidget` failed, called with incorrect arguments.');
|
||||
}
|
||||
@ -100,6 +104,11 @@ export function registerWidget(name, control, preview) {
|
||||
export function getWidget(name) {
|
||||
return registry.widgets[name];
|
||||
}
|
||||
export function getWidgets() {
|
||||
return produce(Object.entries(registry.widgets), draft => {
|
||||
return draft.map(([key, value]) => ({ name: key, ...value }));
|
||||
});
|
||||
}
|
||||
export function resolveWidget(name) {
|
||||
return getWidget(name || 'string') || getWidget('unknown');
|
||||
}
|
||||
@ -109,7 +118,19 @@ export function resolveWidget(name) {
|
||||
*/
|
||||
export function registerEditorComponent(component) {
|
||||
const plugin = EditorComponent(component);
|
||||
registry.editorComponents = registry.editorComponents.set(plugin.get('id'), plugin);
|
||||
if (plugin.type === 'code-block') {
|
||||
const codeBlock = registry.editorComponents.find(c => c.type === 'code-block');
|
||||
|
||||
if (codeBlock) {
|
||||
console.warn(oneLine`
|
||||
Only one editor component of type "code-block" may be registered. Previously registered code
|
||||
block component(s) will be overwritten.
|
||||
`);
|
||||
registry.editorComponents = registry.editorComponents.delete(codeBlock.id);
|
||||
}
|
||||
}
|
||||
|
||||
registry.editorComponents = registry.editorComponents.set(plugin.id, plugin);
|
||||
}
|
||||
export function getEditorComponents() {
|
||||
return registry.editorComponents;
|
||||
|
@ -2,12 +2,15 @@ import { Map, List, fromJS } from 'immutable';
|
||||
import * as actions from 'Actions/entries';
|
||||
import reducer from '../entryDraft';
|
||||
|
||||
jest.mock('uuid/v4', () => jest.fn(() => '1'));
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
mediaFiles: List(),
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
|
||||
const entry = {
|
||||
@ -23,7 +26,8 @@ const entry = {
|
||||
describe('entryDraft reducer', () => {
|
||||
describe('DRAFT_CREATE_FROM_ENTRY', () => {
|
||||
it('should create draft from the entry', () => {
|
||||
expect(reducer(initialState, actions.createDraftFromEntry(fromJS(entry)))).toEqual(
|
||||
const state = reducer(initialState, actions.createDraftFromEntry(fromJS(entry)));
|
||||
expect(state).toEqual(
|
||||
fromJS({
|
||||
entry: {
|
||||
...entry,
|
||||
@ -33,6 +37,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -40,7 +45,8 @@ describe('entryDraft reducer', () => {
|
||||
|
||||
describe('DRAFT_CREATE_EMPTY', () => {
|
||||
it('should create a new draft ', () => {
|
||||
expect(reducer(initialState, actions.emptyDraftCreated(fromJS(entry)))).toEqual(
|
||||
const state = reducer(initialState, actions.emptyDraftCreated(fromJS(entry)));
|
||||
expect(state).toEqual(
|
||||
fromJS({
|
||||
entry: {
|
||||
...entry,
|
||||
@ -50,6 +56,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -127,6 +134,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -144,6 +152,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -161,6 +170,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -181,6 +191,7 @@ describe('entryDraft reducer', () => {
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
key: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -201,6 +212,7 @@ describe('entryDraft reducer', () => {
|
||||
entry,
|
||||
mediaFiles: [{ id: '1' }],
|
||||
},
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import uuid from 'uuid/v4';
|
||||
import {
|
||||
DRAFT_CREATE_FROM_ENTRY,
|
||||
DRAFT_CREATE_EMPTY,
|
||||
@ -30,6 +31,7 @@ const initialState = Map({
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
|
||||
const entryDraftReducer = (state = Map(), action) => {
|
||||
@ -46,6 +48,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.set('fieldsMetaData', action.payload.metadata || Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', false);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_EMPTY:
|
||||
// New Entry
|
||||
@ -56,6 +59,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', false);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_FROM_LOCAL_BACKUP:
|
||||
// Local Backup
|
||||
@ -69,6 +73,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', true);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_DUPLICATE_FROM_ENTRY:
|
||||
// Duplicate Entry
|
||||
|
@ -1,39 +1,35 @@
|
||||
import { Record, fromJS } from 'immutable';
|
||||
import { fromJS } from 'immutable';
|
||||
import { isFunction } from 'lodash';
|
||||
|
||||
const catchesNothing = /.^/;
|
||||
/* eslint-disable no-unused-vars */
|
||||
const EditorComponent = Record({
|
||||
id: null,
|
||||
label: 'unnamed component',
|
||||
icon: 'exclamation-triangle',
|
||||
fields: [],
|
||||
pattern: catchesNothing,
|
||||
fromBlock(match) {
|
||||
return {};
|
||||
},
|
||||
toBlock(attributes) {
|
||||
return 'Plugin';
|
||||
},
|
||||
toPreview(attributes) {
|
||||
return 'Plugin';
|
||||
},
|
||||
});
|
||||
/* eslint-enable */
|
||||
const bind = fn => isFunction(fn) && fn.bind(null);
|
||||
|
||||
export default function createEditorComponent(config) {
|
||||
const configObj = new EditorComponent({
|
||||
id: config.id || config.label.replace(/[^A-Z0-9]+/gi, '_'),
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
fields: fromJS(config.fields),
|
||||
pattern: config.pattern,
|
||||
fromBlock: isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null,
|
||||
toBlock: isFunction(config.toBlock) ? config.toBlock.bind(null) : null,
|
||||
toPreview: isFunction(config.toPreview)
|
||||
? config.toPreview.bind(null)
|
||||
: config.toBlock.bind(null),
|
||||
});
|
||||
const {
|
||||
id = null,
|
||||
label = 'unnamed component',
|
||||
icon = 'exclamation-triangle',
|
||||
type = 'shortcode',
|
||||
widget = 'object',
|
||||
pattern = catchesNothing,
|
||||
fields = [],
|
||||
fromBlock,
|
||||
toBlock,
|
||||
toPreview,
|
||||
...remainingConfig
|
||||
} = config;
|
||||
|
||||
return configObj;
|
||||
return {
|
||||
id: id || label.replace(/[^A-Z0-9]+/gi, '_'),
|
||||
label,
|
||||
type,
|
||||
icon,
|
||||
widget,
|
||||
pattern,
|
||||
fromBlock: bind(fromBlock) || (() => ({})),
|
||||
toBlock: bind(toBlock) || (() => 'Plugin'),
|
||||
toPreview: bind(toPreview) || (!widget && (bind(toBlock) || (() => 'Plugin'))),
|
||||
fields: fromJS(fields),
|
||||
...remainingConfig,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user