Refactor getChooserConfig / filterEntityData into subclasses of ModalWorkflowSource

This means we're not artificially forcing four different entity types into the same code path, and makes it possible to define new entity types outside of this module.

Also relax the eslint no-unused-vars to allow unused function parameters - having multiple classes following the same interface is a legitimate use of this.
pull/7225/head
Matt Westcott 2020-12-18 21:10:22 +00:00 zatwierdzone przez Matt Westcott
rodzic 55fe295346
commit 9fad84b768
5 zmienionych plików z 224 dodań i 181 usunięć

Wyświetl plik

@ -12,7 +12,13 @@ export { default as Document } from './decorators/Document';
export { default as ImageBlock } from './blocks/ImageBlock';
export { default as EmbedBlock } from './blocks/EmbedBlock';
import ModalWorkflowSource from './sources/ModalWorkflowSource';
import {
ModalWorkflowSource,
ImageModalWorkflowSource,
EmbedModalWorkflowSource,
LinkModalWorkflowSource,
DocumentModalWorkflowSource
} from './sources/ModalWorkflowSource';
import Tooltip from './Tooltip/Tooltip';
import TooltipEntity from './decorators/TooltipEntity';
import EditorFallback from './EditorFallback/EditorFallback';
@ -149,6 +155,10 @@ export default {
registerPlugin,
// Components exposed for third-party reuse.
ModalWorkflowSource,
ImageModalWorkflowSource,
EmbedModalWorkflowSource,
LinkModalWorkflowSource,
DocumentModalWorkflowSource,
Tooltip,
TooltipEntity,
};

Wyświetl plik

@ -17,138 +17,6 @@ MUTABILITY[DOCUMENT] = 'MUTABLE';
MUTABILITY[ENTITY_TYPE.IMAGE] = 'IMMUTABLE';
MUTABILITY[EMBED] = 'IMMUTABLE';
export const getChooserConfig = (entityType, entity, selectedText) => {
let url;
let urlParams;
switch (entityType.type) {
case ENTITY_TYPE.IMAGE:
if (entity) {
const data = entity.getData();
url = `${global.chooserUrls.imageChooser}${data.id}/select_format/`;
urlParams = {
format: data.format,
alt_text: data.alt,
};
} else {
url = `${global.chooserUrls.imageChooser}?select_format=true`;
urlParams = {};
}
return {
url,
urlParams,
onload: global.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
case EMBED:
urlParams = {};
if (entity) {
urlParams.url = entity.getData().url;
}
return {
url: global.chooserUrls.embedsChooser,
urlParams,
onload: global.EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
case ENTITY_TYPE.LINK:
url = global.chooserUrls.pageChooser;
urlParams = {
page_type: 'wagtailcore.page',
allow_external_link: true,
allow_email_link: true,
allow_phone_link: true,
allow_anchor_link: true,
link_text: selectedText,
};
if (entity) {
const data = entity.getData();
if (data.id) {
if (data.parentId !== null) {
url = `${global.chooserUrls.pageChooser}${data.parentId}/`;
} else {
url = global.chooserUrls.pageChooser;
}
} else if (data.url.startsWith('mailto:')) {
url = global.chooserUrls.emailLinkChooser;
urlParams.link_url = data.url.replace('mailto:', '');
} else if (data.url.startsWith('tel:')) {
url = global.chooserUrls.phoneLinkChooser;
urlParams.link_url = data.url.replace('tel:', '');
} else if (data.url.startsWith('#')) {
url = global.chooserUrls.anchorLinkChooser;
urlParams.link_url = data.url.replace('#', '');
} else {
url = global.chooserUrls.externalLinkChooser;
urlParams.link_url = data.url;
}
}
return {
url,
urlParams,
onload: global.PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
case DOCUMENT:
return {
url: global.chooserUrls.documentChooser,
urlParams: {},
onload: global.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
default:
return {
url: null,
urlParams: {},
onload: {},
};
}
};
export const filterEntityData = (entityType, data) => {
switch (entityType.type) {
case ENTITY_TYPE.IMAGE:
return {
id: data.id,
src: data.preview.url,
alt: data.alt,
format: data.format,
};
case EMBED:
return {
embedType: data.embedType,
url: data.url,
providerName: data.providerName,
authorName: data.authorName,
thumbnail: data.thumbnail,
title: data.title,
};
case ENTITY_TYPE.LINK:
if (data.id) {
return {
url: data.url,
id: data.id,
parentId: data.parentId,
};
}
return {
url: data.url,
};
case DOCUMENT:
return {
url: data.url,
filename: data.filename,
id: data.id,
};
default:
return {};
}
};
/**
* Interfaces with Wagtail's ModalWorkflow to open the chooser,
* and create new content in Draft.js based on the data.
@ -161,10 +29,20 @@ class ModalWorkflowSource extends Component {
this.onClose = this.onClose.bind(this);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getChooserConfig(entity, selectedText) {
throw new TypeError('Subclasses of ModalWorkflowSource must implement getChooserConfig');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
filterEntityData(data) {
throw new TypeError('Subclasses of ModalWorkflowSource must implement filterEntityData');
}
componentDidMount() {
const { onClose, entityType, entity, editorState } = this.props;
const { onClose, entity, editorState } = this.props;
const selectedText = getSelectionText(editorState);
const { url, urlParams, onload } = getChooserConfig(entityType, entity, selectedText);
const { url, urlParams, onload } = this.getChooserConfig(entity, selectedText);
$(document.body).on('hidden.bs.modal', this.onClose);
@ -198,7 +76,7 @@ class ModalWorkflowSource extends Component {
const { editorState, entity, entityKey, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const entityData = filterEntityData(entityType, data);
const entityData = this.filterEntityData(data);
const mutability = MUTABILITY[entityType.type];
let nextState;
@ -263,4 +141,145 @@ ModalWorkflowSource.defaultProps = {
entity: null,
};
export default ModalWorkflowSource;
class ImageModalWorkflowSource extends ModalWorkflowSource {
getChooserConfig(entity) {
let url;
let urlParams;
if (entity) {
const data = entity.getData();
url = `${global.chooserUrls.imageChooser}${data.id}/select_format/`;
urlParams = {
format: data.format,
alt_text: data.alt,
};
} else {
url = `${global.chooserUrls.imageChooser}?select_format=true`;
urlParams = {};
}
return {
url,
urlParams,
onload: global.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
}
filterEntityData(data) {
return {
id: data.id,
src: data.preview.url,
alt: data.alt,
format: data.format,
};
}
}
class EmbedModalWorkflowSource extends ModalWorkflowSource {
getChooserConfig(entity) {
const urlParams = {};
if (entity) {
urlParams.url = entity.getData().url;
}
return {
url: global.chooserUrls.embedsChooser,
urlParams,
onload: global.EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
}
filterEntityData(data) {
return {
embedType: data.embedType,
url: data.url,
providerName: data.providerName,
authorName: data.authorName,
thumbnail: data.thumbnail,
title: data.title,
};
}
}
class LinkModalWorkflowSource extends ModalWorkflowSource {
getChooserConfig(entity, selectedText) {
let url = global.chooserUrls.pageChooser;
const urlParams = {
page_type: 'wagtailcore.page',
allow_external_link: true,
allow_email_link: true,
allow_phone_link: true,
allow_anchor_link: true,
link_text: selectedText,
};
if (entity) {
const data = entity.getData();
if (data.id) {
if (data.parentId !== null) {
url = `${global.chooserUrls.pageChooser}${data.parentId}/`;
} else {
url = global.chooserUrls.pageChooser;
}
} else if (data.url.startsWith('mailto:')) {
url = global.chooserUrls.emailLinkChooser;
urlParams.link_url = data.url.replace('mailto:', '');
} else if (data.url.startsWith('tel:')) {
url = global.chooserUrls.phoneLinkChooser;
urlParams.link_url = data.url.replace('tel:', '');
} else if (data.url.startsWith('#')) {
url = global.chooserUrls.anchorLinkChooser;
urlParams.link_url = data.url.replace('#', '');
} else {
url = global.chooserUrls.externalLinkChooser;
urlParams.link_url = data.url;
}
}
return {
url,
urlParams,
onload: global.PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
}
filterEntityData(data) {
if (data.id) {
return {
url: data.url,
id: data.id,
parentId: data.parentId,
};
}
return {
url: data.url,
};
}
}
class DocumentModalWorkflowSource extends ModalWorkflowSource {
getChooserConfig() {
return {
url: global.chooserUrls.documentChooser,
urlParams: {},
onload: global.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS,
};
}
filterEntityData(data) {
return {
url: data.url,
filename: data.filename,
id: data.id,
};
}
}
export {
ModalWorkflowSource,
ImageModalWorkflowSource,
EmbedModalWorkflowSource,
LinkModalWorkflowSource,
DocumentModalWorkflowSource,
};

Wyświetl plik

@ -1,7 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import ModalWorkflowSource, { getChooserConfig, filterEntityData } from './ModalWorkflowSource';
import {
ImageModalWorkflowSource,
EmbedModalWorkflowSource,
LinkModalWorkflowSource,
DocumentModalWorkflowSource
} from './ModalWorkflowSource';
import * as DraftUtils from '../DraftUtils';
import { DraftUtils as DraftailUtils } from 'draftail';
import { EditorState, convertFromRaw, AtomicBlockUtils, RichUtils, Modifier } from 'draft-js';
@ -20,10 +25,10 @@ describe('ModalWorkflowSource', () => {
it('works', () => {
expect(shallow((
<ModalWorkflowSource
<ImageModalWorkflowSource
editorState={{}}
entityType={{}}
entity={{}}
entity={null}
onComplete={() => {}}
onClose={() => {}}
/>
@ -31,8 +36,9 @@ describe('ModalWorkflowSource', () => {
});
describe('#getChooserConfig', () => {
const imageSource = new ImageModalWorkflowSource();
it('IMAGE without entity', () => {
expect(getChooserConfig({ type: 'IMAGE' }, null, '')).toEqual({
expect(imageSource.getChooserConfig(null, '')).toEqual({
url: '/admin/images/chooser/?select_format=true',
urlParams: {},
onload: global.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS,
@ -41,7 +47,7 @@ describe('ModalWorkflowSource', () => {
it('IMAGE with entity', () => {
const entity = { getData: () => ({ id: 1, format: 'left', alt: 'alt' }) };
expect(getChooserConfig({ type: 'IMAGE' }, entity, '')).toEqual({
expect(imageSource.getChooserConfig(entity, '')).toEqual({
url: '/admin/images/chooser/1/select_format/',
urlParams: {
format: 'left',
@ -51,8 +57,9 @@ describe('ModalWorkflowSource', () => {
});
});
const embedSource = new EmbedModalWorkflowSource();
it('EMBED without entity', () => {
expect(getChooserConfig({ type: 'EMBED' }, null, '')).toEqual({
expect(embedSource.getChooserConfig(null, '')).toEqual({
url: '/admin/embeds/chooser/',
urlParams: {},
onload: global.EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS,
@ -61,46 +68,48 @@ describe('ModalWorkflowSource', () => {
it('EMBED with entity', () => {
const entity = { getData: () => ({ url: 'http://example.org/content' }) };
expect(getChooserConfig({ type: 'EMBED' }, entity, '')).toEqual({
expect(embedSource.getChooserConfig(entity, '')).toEqual({
url: '/admin/embeds/chooser/',
urlParams: { url: 'http://example.org/content' },
onload: global.EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS,
});
});
const documentSource = new DocumentModalWorkflowSource();
it('DOCUMENT', () => {
expect(getChooserConfig({ type: 'DOCUMENT' }, null, '')).toEqual({
expect(documentSource.getChooserConfig(null, '')).toEqual({
url: '/admin/documents/chooser/',
urlParams: {},
onload: global.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS,
});
});
const linkSource = new LinkModalWorkflowSource();
describe('LINK', () => {
it('no entity', () => {
expect(getChooserConfig({ type: 'LINK' }, null, '')).toMatchSnapshot();
expect(linkSource.getChooserConfig(null, '')).toMatchSnapshot();
});
it('page', () => {
expect(getChooserConfig({ type: 'LINK' }, {
expect(linkSource.getChooserConfig({
getData: () => ({ id: 2, parentId: 1 })
}, '')).toMatchSnapshot();
});
it('root page', () => {
expect(getChooserConfig({ type: 'LINK' }, {
expect(linkSource.getChooserConfig({
getData: () => ({ id: 1, parentId: null })
}, '')).toMatchSnapshot();
});
it('mail', () => {
expect(getChooserConfig({ type: 'LINK' }, {
expect(linkSource.getChooserConfig({
getData: () => ({ url: 'mailto:test@example.com' })
}, '')).toMatchSnapshot();
});
it('external', () => {
expect(getChooserConfig({ type: 'LINK' }, {
expect(linkSource.getChooserConfig({
getData: () => ({ url: 'https://www.example.com/' })
}, '')).toMatchSnapshot();
});
@ -108,8 +117,9 @@ describe('ModalWorkflowSource', () => {
});
describe('#filterEntityData', () => {
const imageSource = new ImageModalWorkflowSource();
it('IMAGE', () => {
expect(filterEntityData({ type: 'IMAGE' }, {
expect(imageSource.filterEntityData({
id: 53,
title: 'Test',
alt: 'Test',
@ -122,8 +132,9 @@ describe('ModalWorkflowSource', () => {
})).toMatchSnapshot();
});
const embedSource = new EmbedModalWorkflowSource();
it('EMBED', () => {
expect(filterEntityData({ type: 'EMBED' }, {
expect(embedSource.filterEntityData({
authorName: 'Test',
embedType: 'video',
providerName: 'YouTube',
@ -133,8 +144,9 @@ describe('ModalWorkflowSource', () => {
})).toMatchSnapshot();
});
const documentSource = new DocumentModalWorkflowSource();
it('DOCUMENT', () => {
expect(filterEntityData({ type: 'DOCUMENT' }, {
expect(documentSource.filterEntityData({
edit_link: '/admin/documents/edit/1/',
filename: 'test.pdf',
id: 1,
@ -143,13 +155,10 @@ describe('ModalWorkflowSource', () => {
})).toMatchSnapshot();
});
it('OTHER', () => {
expect(filterEntityData({ type: 'OTHER' }, {})).toEqual({});
});
const linkSource = new LinkModalWorkflowSource();
describe('LINK', () => {
it('page', () => {
expect(filterEntityData({ type: 'LINK' }, {
expect(linkSource.filterEntityData({
id: 60,
parentId: 1,
url: '/',
@ -159,7 +168,7 @@ describe('ModalWorkflowSource', () => {
});
it('mail', () => {
expect(filterEntityData({ type: 'LINK' }, {
expect(linkSource.filterEntityData({
prefer_this_title_as_link_text: false,
title: 'test@example.com',
url: 'mailto:test@example.com',
@ -167,7 +176,7 @@ describe('ModalWorkflowSource', () => {
});
it('anchor', () => {
expect(filterEntityData({ type: 'LINK' }, {
expect(linkSource.filterEntityData({
prefer_this_title_as_link_text: false,
title: 'testanchor',
url: '#testanchor',
@ -175,7 +184,7 @@ describe('ModalWorkflowSource', () => {
});
it('external', () => {
expect(filterEntityData({ type: 'LINK' }, {
expect(linkSource.filterEntityData({
prefer_this_title_as_link_text: false,
title: 'https://www.example.com/',
url: 'https://www.example.com/',
@ -186,10 +195,10 @@ describe('ModalWorkflowSource', () => {
it('#componentDidMount', () => {
const wrapper = shallow((
<ModalWorkflowSource
<EmbedModalWorkflowSource
editorState={EditorState.createEmpty()}
entityType={{}}
entity={{}}
entity={null}
onComplete={() => {}}
onClose={() => {}}
/>
@ -211,10 +220,10 @@ describe('ModalWorkflowSource', () => {
const onClose = jest.fn();
const wrapper = shallow((
<ModalWorkflowSource
<EmbedModalWorkflowSource
editorState={EditorState.createEmpty()}
entityType={{}}
entity={{}}
entity={null}
onComplete={() => {}}
onClose={onClose}
/>
@ -232,10 +241,10 @@ describe('ModalWorkflowSource', () => {
it('#componentWillUnmount', () => {
const wrapper = shallow((
<ModalWorkflowSource
<EmbedModalWorkflowSource
editorState={EditorState.createEmpty()}
entityType={{}}
entity={{}}
entity={null}
onComplete={() => {}}
onClose={() => {}}
/>
@ -268,10 +277,10 @@ describe('ModalWorkflowSource', () => {
});
editorState = EditorState.acceptSelection(editorState, selection);
const wrapper = shallow((
<ModalWorkflowSource
<LinkModalWorkflowSource
editorState={editorState}
entityType={{}}
entity={{}}
entity={null}
onComplete={onComplete}
onClose={() => {}}
/>
@ -308,12 +317,12 @@ describe('ModalWorkflowSource', () => {
});
editorState = EditorState.acceptSelection(editorState, selection);
const wrapper = shallow((
<ModalWorkflowSource
<LinkModalWorkflowSource
editorState={editorState}
entityType={{
block: () => {},
}}
entity={{}}
entity={null}
onComplete={onComplete}
onClose={() => {}}
/>
@ -333,6 +342,7 @@ describe('ModalWorkflowSource', () => {
jest.spyOn(DraftailUtils, 'updateBlockEntity');
const onComplete = jest.fn();
const close = jest.fn();
const entity = { getData: () => ({ id: 1, format: 'left', alt: 'alt' }) };
let editorState = EditorState.createWithContent(convertFromRaw({
blocks: [
@ -358,12 +368,12 @@ describe('ModalWorkflowSource', () => {
});
editorState = EditorState.acceptSelection(editorState, selection);
const wrapper = shallow((
<ModalWorkflowSource
<ImageModalWorkflowSource
editorState={editorState}
entityType={{
block: () => {},
}}
entity={{}}
entity={entity}
entityKey={'first'}
onComplete={onComplete}
onClose={() => {}}
@ -371,7 +381,7 @@ describe('ModalWorkflowSource', () => {
));
wrapper.instance().workflow = { close };
wrapper.instance().onChosen({});
wrapper.instance().onChosen({ id: 2, preview: { url: '/foo' }, alt: 'new image', format: 'left' });
expect(onComplete).toHaveBeenCalled();
expect(DraftailUtils.updateBlockEntity).toHaveBeenCalled();
@ -401,7 +411,7 @@ describe('ModalWorkflowSource', () => {
});
editorState = EditorState.acceptSelection(editorState, selection);
const wrapper = shallow((
<ModalWorkflowSource
<LinkModalWorkflowSource
editorState={editorState}
entityType={{}}
onComplete={onComplete}
@ -426,10 +436,10 @@ describe('ModalWorkflowSource', () => {
it('#onClose', () => {
const onClose = jest.fn();
const wrapper = shallow((
<ModalWorkflowSource
<LinkModalWorkflowSource
editorState={EditorState.createEmpty()}
entityType={{}}
entity={{}}
entity={null}
onComplete={() => {}}
onClose={onClose}
/>

Wyświetl plik

@ -19,22 +19,22 @@ window.draftail = draftail;
const plugins = [
{
type: 'DOCUMENT',
source: draftail.ModalWorkflowSource,
source: draftail.DocumentModalWorkflowSource,
decorator: Document,
},
{
type: 'LINK',
source: draftail.ModalWorkflowSource,
source: draftail.LinkModalWorkflowSource,
decorator: Link,
},
{
type: 'IMAGE',
source: draftail.ModalWorkflowSource,
source: draftail.ImageModalWorkflowSource,
block: ImageBlock,
},
{
type: 'EMBED',
source: draftail.ModalWorkflowSource,
source: draftail.EmbedModalWorkflowSource,
block: EmbedBlock,
},
];

Wyświetl plik

@ -270,5 +270,9 @@ Pages containing rich text editors also have access to:
// Wagtails Draftail-related APIs and components.
window.draftail;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;