From eb71e599ce2dbdb2062389257d64f7761e9e6e08 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 21 Jul 2020 14:12:56 -0400 Subject: [PATCH 1/4] [pre-req] Convert Page Manager, Page Preview, DOM Preview (#70370) Co-authored-by: Elastic Machine Co-authored-by: Corey Robertson --- x-pack/plugins/canvas/i18n/components.ts | 16 ++ .../{dom_preview.js => dom_preview.tsx} | 53 +++++-- .../public/components/dom_preview/index.js | 9 -- .../index.js => dom_preview/index.ts} | 2 +- .../components/link/{index.js => index.ts} | 0 .../canvas/public/components/link/link.js | 63 -------- .../canvas/public/components/link/link.tsx | 72 +++++++++ .../public/components/page_manager/index.js | 37 ----- .../public/components/page_manager/index.ts | 31 ++++ .../{page_manager.js => page_manager.tsx} | 148 ++++++++++-------- .../public/components/page_preview/index.ts | 24 +++ .../{page_controls.js => page_controls.tsx} | 21 ++- .../{page_preview.js => page_preview.tsx} | 35 ++--- .../public/components/toolbar/toolbar.tsx | 3 +- .../canvas/public/lib/create_handlers.ts | 2 +- 15 files changed, 292 insertions(+), 224 deletions(-) rename x-pack/plugins/canvas/public/components/dom_preview/{dom_preview.js => dom_preview.tsx} (71%) delete mode 100644 x-pack/plugins/canvas/public/components/dom_preview/index.js rename x-pack/plugins/canvas/public/components/{page_preview/index.js => dom_preview/index.ts} (84%) rename x-pack/plugins/canvas/public/components/link/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/canvas/public/components/link/link.js create mode 100644 x-pack/plugins/canvas/public/components/link/link.tsx delete mode 100644 x-pack/plugins/canvas/public/components/page_manager/index.js create mode 100644 x-pack/plugins/canvas/public/components/page_manager/index.ts rename x-pack/plugins/canvas/public/components/page_manager/{page_manager.js => page_manager.tsx} (63%) create mode 100644 x-pack/plugins/canvas/public/components/page_preview/index.ts rename x-pack/plugins/canvas/public/components/page_preview/{page_controls.js => page_controls.tsx} (75%) rename x-pack/plugins/canvas/public/components/page_preview/{page_preview.js => page_preview.tsx} (56%) diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 78083f26a38b..acc55d50ae19 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -567,6 +567,22 @@ export const ComponentStrings = { pageNumber, }, }), + getAddPageTooltip: () => + i18n.translate('xpack.canvas.pageManager.addPageTooltip', { + defaultMessage: 'Add a new page to this workpad', + }), + getConfirmRemoveTitle: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', { + defaultMessage: 'Remove Page', + }), + getConfirmRemoveDescription: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', { + defaultMessage: 'Are you sure you want to remove this page?', + }), + getConfirmRemoveButtonLabel: () => + i18n.translate('xpack.canvas.pageManager.removeButtonLabel', { + defaultMessage: 'Remove', + }), }, PagePreviewPageControls: { getClonePageAriaLabel: () => diff --git a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx similarity index 71% rename from x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js rename to x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx index f74862af8d10..6ec0276c2f49 100644 --- a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js +++ b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.tsx @@ -4,30 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -export class DomPreview extends React.Component { +interface Props { + elementId: string; + height: number; +} + +export class DomPreview extends PureComponent { static propTypes = { elementId: PropTypes.string.isRequired, height: PropTypes.number.isRequired, }; + _container: HTMLDivElement | null = null; + _content: HTMLDivElement | null = null; + _observer: MutationObserver | null = null; + _original: Element | null = null; + _updateTimeout: number = 0; + componentDidMount() { this.update(); } componentWillUnmount() { clearTimeout(this._updateTimeout); - this._observer && this._observer.disconnect(); // observer not guaranteed to exist - } - _container = null; - _content = null; - _observer = null; - _original = null; - _updateTimeout = null; + if (this._observer) { + this._observer.disconnect(); // observer not guaranteed to exist + } + } update = () => { if (!this._content || !this._container) { @@ -38,7 +46,10 @@ export class DomPreview extends React.Component { const originalChanged = currentOriginal !== this._original; if (originalChanged) { - this._observer && this._observer.disconnect(); + if (this._observer) { + this._observer.disconnect(); + } + this._original = currentOriginal; if (this._original) { @@ -50,12 +61,16 @@ export class DomPreview extends React.Component { this._observer.observe(this._original, config); } else { clearTimeout(this._updateTimeout); // to avoid the assumption that we fully control when `update` is called - this._updateTimeout = setTimeout(this.update, 30); + this._updateTimeout = window.setTimeout(this.update, 30); return; } } - const thumb = this._original.cloneNode(true); + if (!this._original) { + return; + } + + const thumb = this._original.cloneNode(true) as HTMLDivElement; thumb.id += '-thumb'; const originalStyle = window.getComputedStyle(this._original, null); @@ -66,9 +81,10 @@ export class DomPreview extends React.Component { const scale = thumbHeight / originalHeight; const thumbWidth = originalWidth * scale; - if (this._content.hasChildNodes()) { + if (this._content.firstChild) { this._content.removeChild(this._content.firstChild); } + this._content.appendChild(thumb); this._content.style.cssText = `transform: scale(${scale}); transform-origin: top left;`; @@ -76,13 +92,16 @@ export class DomPreview extends React.Component { // Copy canvas data const originalCanvas = this._original.querySelectorAll('canvas'); - const thumbCanvas = thumb.querySelectorAll('canvas'); + const thumbCanvas = (thumb as Element).querySelectorAll('canvas'); // Cloned canvas elements are blank and need to be explicitly redrawn if (originalCanvas.length > 0) { - Array.from(originalCanvas).map((img, i) => - thumbCanvas[i].getContext('2d').drawImage(img, 0, 0) - ); + Array.from(originalCanvas).map((img, i) => { + const context = thumbCanvas[i].getContext('2d'); + if (context) { + context.drawImage(img, 0, 0); + } + }); } }; diff --git a/x-pack/plugins/canvas/public/components/dom_preview/index.js b/x-pack/plugins/canvas/public/components/dom_preview/index.js deleted file mode 100644 index 283f92c7ecd9..000000000000 --- a/x-pack/plugins/canvas/public/components/dom_preview/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DomPreview as Component } from './dom_preview'; - -export const DomPreview = Component; diff --git a/x-pack/plugins/canvas/public/components/page_preview/index.js b/x-pack/plugins/canvas/public/components/dom_preview/index.ts similarity index 84% rename from x-pack/plugins/canvas/public/components/page_preview/index.js rename to x-pack/plugins/canvas/public/components/dom_preview/index.ts index d72d6403dd5b..19980b7c2cfe 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/index.js +++ b/x-pack/plugins/canvas/public/components/dom_preview/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PagePreview } from './page_preview'; +export { DomPreview } from './dom_preview'; diff --git a/x-pack/plugins/canvas/public/components/link/index.js b/x-pack/plugins/canvas/public/components/link/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/components/link/index.js rename to x-pack/plugins/canvas/public/components/link/index.ts diff --git a/x-pack/plugins/canvas/public/components/link/link.js b/x-pack/plugins/canvas/public/components/link/link.js deleted file mode 100644 index d97316419059..000000000000 --- a/x-pack/plugins/canvas/public/components/link/link.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiLink } from '@elastic/eui'; - -import { ComponentStrings } from '../../../i18n'; - -const { Link: strings } = ComponentStrings; - -const isModifiedEvent = (ev) => !!(ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey); - -export class Link extends React.PureComponent { - static propTypes = { - target: PropTypes.string, - onClick: PropTypes.func, - name: PropTypes.string.isRequired, - params: PropTypes.object, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]).isRequired, - }; - - static contextTypes = { - router: PropTypes.object, - }; - - navigateTo = (name, params) => (ev) => { - if (this.props.onClick) { - this.props.onClick(ev); - } - - if ( - !ev.defaultPrevented && // onClick prevented default - ev.button === 0 && // ignore everything but left clicks - !this.props.target && // let browser handle "target=_blank" etc. - !isModifiedEvent(ev) // ignore clicks with modifier keys - ) { - ev.preventDefault(); - this.context.router.navigateTo(name, params); - } - }; - - render() { - try { - const { name, params, children, ...linkArgs } = this.props; - const { router } = this.context; - const href = router.getFullPath(router.create(name, params)); - const props = { - ...linkArgs, - href, - onClick: this.navigateTo(name, params), - }; - - return {children}; - } catch (e) { - console.error(e); - return
{strings.getErrorMessage(e.message)}
; - } - } -} diff --git a/x-pack/plugins/canvas/public/components/link/link.tsx b/x-pack/plugins/canvas/public/components/link/link.tsx new file mode 100644 index 000000000000..b0289fba842d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/link/link.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, MouseEvent, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { EuiLink, EuiLinkProps } from '@elastic/eui'; +import { RouterContext } from '../router'; + +import { ComponentStrings } from '../../../i18n'; + +const { Link: strings } = ComponentStrings; + +const isModifiedEvent = (ev: MouseEvent) => + !!(ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey); + +interface Props { + name: string; + params: Record; +} + +export const Link: FC = ({ + onClick, + target, + name, + params, + children, + ...linkArgs +}) => { + const router = useContext(RouterContext); + + if (router) { + const navigateTo = (ev: MouseEvent) => { + if (onClick) { + onClick(ev); + } + + if ( + !ev.defaultPrevented && // onClick prevented default + ev.button === 0 && // ignore everything but left clicks + !target && // let browser handle "target=_blank" etc. + !isModifiedEvent(ev) // ignore clicks with modifier keys + ) { + ev.preventDefault(); + router.navigateTo(name, params); + } + }; + + try { + return ( + + {children} + + ); + } catch (e) { + return
{strings.getErrorMessage(e.message)}
; + } + } + + return
{strings.getErrorMessage('Router Undefined')}
; +}; + +Link.contextTypes = { + router: PropTypes.object, +}; + +Link.propTypes = { + name: PropTypes.string.isRequired, + params: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.js b/x-pack/plugins/canvas/public/components/page_manager/index.js deleted file mode 100644 index a198b7b8c3d8..000000000000 --- a/x-pack/plugins/canvas/public/components/page_manager/index.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { compose, withState } from 'recompose'; -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager'; - -const mapStateToProps = (state) => { - const { id, css } = getWorkpad(state); - - return { - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: id, - workpadCSS: css || DEFAULT_WORKPAD_CSS, - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - addPage: () => dispatch(pageActions.addPage()), - movePage: (id, position) => dispatch(pageActions.movePage(id, position)), - duplicatePage: (id) => dispatch(pageActions.duplicatePage(id)), - removePage: (id) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = compose( - connect(mapStateToProps, mapDispatchToProps), - withState('deleteId', 'setDeleteId', null) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_manager/index.ts b/x-pack/plugins/canvas/public/components/page_manager/index.ts new file mode 100644 index 000000000000..d19540cd6a68 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager'; +import { State } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onAddPage: () => dispatch(pageActions.addPage()), + onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), + onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), +}); + +export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.js b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx similarity index 63% rename from x-pack/plugins/canvas/public/components/page_manager/page_manager.js rename to x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx index 3e2ff9dfe2b2..edc0d6201495 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.js +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx @@ -4,38 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd'; +// @ts-expect-error untyped dependency import Style from 'style-it'; + import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { PagePreview } from '../page_preview'; import { ComponentStrings } from '../../../i18n'; +import { CanvasPage } from '../../../types'; const { PageManager: strings } = ComponentStrings; -export class PageManager extends React.PureComponent { +export interface Props { + isWriteable: boolean; + onAddPage: () => void; + onMovePage: (pageId: string, position: number) => void; + onPreviousPage: () => void; + onRemovePage: (pageId: string) => void; + pages: CanvasPage[]; + selectedPage?: string; + workpadCSS?: string; + workpadId: string; +} + +interface State { + showTrayPop: boolean; + removeId: string | null; +} + +export class PageManager extends Component { static propTypes = { isWriteable: PropTypes.bool.isRequired, + onAddPage: PropTypes.func.isRequired, + onMovePage: PropTypes.func.isRequired, + onPreviousPage: PropTypes.func.isRequired, + onRemovePage: PropTypes.func.isRequired, pages: PropTypes.array.isRequired, - workpadId: PropTypes.string.isRequired, - addPage: PropTypes.func.isRequired, - movePage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, - duplicatePage: PropTypes.func.isRequired, - removePage: PropTypes.func.isRequired, selectedPage: PropTypes.string, - deleteId: PropTypes.string, - setDeleteId: PropTypes.func.isRequired, workpadCSS: PropTypes.string, + workpadId: PropTypes.string.isRequired, }; - state = { - showTrayPop: true, - }; + constructor(props: Props) { + super(props); + this.state = { + showTrayPop: true, + removeId: null, + }; + } + + _isMounted: boolean = false; + _activePageRef: HTMLDivElement | null = null; + _pageListRef: HTMLDivElement | null = null; componentDidMount() { // keep track of whether or not the component is mounted, to prevent rogue setState calls @@ -44,11 +69,13 @@ export class PageManager extends React.PureComponent { // gives the tray pop animation time to finish setTimeout(() => { this.scrollToActivePage(); - this._isMounted && this.setState({ showTrayPop: false }); + if (this._isMounted) { + this.setState({ showTrayPop: false }); + } }, 1000); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { // scrolls to the active page on the next tick, otherwise new pages don't scroll completely into view if (prevProps.selectedPage !== this.props.selectedPage) { setTimeout(this.scrollToActivePage, 0); @@ -60,33 +87,33 @@ export class PageManager extends React.PureComponent { } scrollToActivePage = () => { - if (this.activePageRef && this.pageListRef) { + if (this._activePageRef && this._pageListRef) { // not all target browsers support element.scrollTo // TODO: replace this with something more cross-browser, maybe scrollIntoView - if (!this.pageListRef.scrollTo) { + if (!this._pageListRef.scrollTo) { return; } - const pageOffset = this.activePageRef.offsetLeft; + const pageOffset = this._activePageRef.offsetLeft; const { left: pageLeft, right: pageRight, width: pageWidth, - } = this.activePageRef.getBoundingClientRect(); + } = this._activePageRef.getBoundingClientRect(); const { left: listLeft, right: listRight, width: listWidth, - } = this.pageListRef.getBoundingClientRect(); + } = this._pageListRef.getBoundingClientRect(); if (pageLeft < listLeft) { - this.pageListRef.scrollTo({ + this._pageListRef.scrollTo({ left: pageOffset, behavior: 'smooth', }); } if (pageRight > listRight) { - this.pageListRef.scrollTo({ + this._pageListRef.scrollTo({ left: pageOffset - listWidth + pageWidth, behavior: 'smooth', }); @@ -94,22 +121,29 @@ export class PageManager extends React.PureComponent { } }; - confirmDelete = (pageId) => { - this._isMounted && this.props.setDeleteId(pageId); + onConfirmRemove = (removeId: string) => { + if (this._isMounted) { + this.setState({ removeId }); + } }; - resetDelete = () => this._isMounted && this.props.setDeleteId(null); + resetRemove = () => this._isMounted && this.setState({ removeId: null }); + + doRemove = () => { + const { onPreviousPage, onRemovePage, selectedPage } = this.props; + const { removeId } = this.state; + this.resetRemove(); + + if (removeId === selectedPage) { + onPreviousPage(); + } - doDelete = () => { - const { previousPage, removePage, deleteId, selectedPage } = this.props; - this.resetDelete(); - if (deleteId === selectedPage) { - previousPage(); + if (removeId !== null) { + onRemovePage(removeId); } - removePage(deleteId); }; - onDragEnd = ({ draggableId: pageId, source, destination }) => { + onDragEnd: DragDropContextProps['onDragEnd'] = ({ draggableId: pageId, source, destination }) => { // dropped outside the list if (!destination) { return; @@ -117,18 +151,11 @@ export class PageManager extends React.PureComponent { const position = destination.index - source.index; - this.props.movePage(pageId, position); + this.props.onMovePage(pageId, position); }; - renderPage = (page, i) => { - const { - isWriteable, - selectedPage, - workpadId, - movePage, - duplicatePage, - workpadCSS, - } = this.props; + renderPage = (page: CanvasPage, i: number) => { + const { isWriteable, selectedPage, workpadId, workpadCSS } = this.props; const pageNumber = i + 1; return ( @@ -141,7 +168,7 @@ export class PageManager extends React.PureComponent { }`} ref={(el) => { if (page.id === selectedPage) { - this.activePageRef = el; + this._activePageRef = el; } provided.innerRef(el); }} @@ -163,16 +190,7 @@ export class PageManager extends React.PureComponent { {Style.it( workpadCSS,
- +
)} @@ -185,8 +203,8 @@ export class PageManager extends React.PureComponent { }; render() { - const { pages, addPage, deleteId, isWriteable } = this.props; - const { showTrayPop } = this.state; + const { pages, onAddPage, isWriteable } = this.props; + const { showTrayPop, removeId } = this.state; return ( @@ -200,7 +218,7 @@ export class PageManager extends React.PureComponent { showTrayPop ? 'canvasPageManager--trayPop' : '' }`} ref={(el) => { - this.pageListRef = el; + this._pageListRef = el; provided.innerRef(el); }} {...provided.droppableProps} @@ -216,11 +234,11 @@ export class PageManager extends React.PureComponent {