[Canvas][tech-debt] Add Typescript to apps directory (#73766)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2020-08-07 11:21:44 -04:00 committed by GitHub
parent 7dc33f9ba8
commit c6c300e8f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 274 additions and 263 deletions

View file

@ -6,8 +6,8 @@
import React from 'react';
import { mount } from 'enzyme';
// @ts-expect-error untyped local
import { ExportApp } from '../export_app';
import { ExportApp } from '../export_app.component';
import { CanvasWorkpad } from '../../../../../types';
jest.mock('style-it', () => ({
it: (css: string, Component: any) => Component,
@ -23,7 +23,7 @@ jest.mock('../../../../components/link', () => ({
describe('<ExportApp />', () => {
test('renders as expected', () => {
const sampleWorkpad = {
const sampleWorkpad = ({
id: 'my-workpad-abcd',
css: '',
pages: [
@ -34,7 +34,7 @@ describe('<ExportApp />', () => {
elements: [3, 4, 5, 6],
},
],
};
} as any) as CanvasWorkpad;
const page1 = mount(
<ExportApp workpad={sampleWorkpad} selectedPageIndex={0} initializeWorkpad={() => {}} />

View file

@ -0,0 +1,63 @@
/*
* 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, useEffect } from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error untyped library
import Style from 'style-it';
// @ts-expect-error untyped local
import { WorkpadPage } from '../../../components/workpad_page';
import { Link } from '../../../components/link';
import { CanvasWorkpad } from '../../../../types';
interface Props {
workpad: CanvasWorkpad;
selectedPageIndex: number;
initializeWorkpad: () => void;
}
export const ExportApp: FC<Props> = ({ workpad, selectedPageIndex, initializeWorkpad }) => {
const { id, pages, height, width } = workpad;
const activePage = pages[selectedPageIndex];
const pageElementCount = activePage.elements.length;
useEffect(() => initializeWorkpad());
return (
<div className="canvasExport" data-shared-page={selectedPageIndex + 1}>
<div className="canvasExport__stage">
<div className="canvasLayout__stageHeader">
<Link name="loadWorkpad" params={{ id }}>
Edit Workpad
</Link>
</div>
{Style.it(
workpad.css,
<div className="canvasExport__stageContent" data-shared-items-count={pageElementCount}>
<WorkpadPage
isSelected
key={activePage.id}
pageId={activePage.id}
height={height}
width={width}
registerLayout={() => {}}
unregisterLayout={() => {}}
/>
</div>
)}
</div>
</div>
);
};
ExportApp.propTypes = {
workpad: PropTypes.shape({
id: PropTypes.string.isRequired,
pages: PropTypes.array.isRequired,
}).isRequired,
selectedPageIndex: PropTypes.number.isRequired,
initializeWorkpad: PropTypes.func.isRequired,
};

View file

@ -1,59 +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 Style from 'style-it';
import { WorkpadPage } from '../../../components/workpad_page';
import { Link } from '../../../components/link';
export class ExportApp extends React.PureComponent {
static propTypes = {
workpad: PropTypes.shape({
id: PropTypes.string.isRequired,
pages: PropTypes.array.isRequired,
}).isRequired,
selectedPageIndex: PropTypes.number.isRequired,
initializeWorkpad: PropTypes.func.isRequired,
};
componentDidMount() {
this.props.initializeWorkpad();
}
render() {
const { workpad, selectedPageIndex } = this.props;
const { pages, height, width } = workpad;
const activePage = pages[selectedPageIndex];
const pageElementCount = activePage.elements.length;
return (
<div className="canvasExport" data-shared-page={selectedPageIndex + 1}>
<div className="canvasExport__stage">
<div className="canvasLayout__stageHeader">
<Link name="loadWorkpad" params={{ id: this.props.workpad.id }}>
Edit Workpad
</Link>
</div>
{Style.it(
workpad.css,
<div className="canvasExport__stageContent" data-shared-items-count={pageElementCount}>
<WorkpadPage
isSelected
key={activePage.id}
pageId={activePage.id}
height={height}
width={width}
registerLayout={() => {}}
unregisterLayout={() => {}}
/>
</div>
)}
</div>
</div>
);
}
}

View file

@ -0,0 +1,21 @@
/*
* 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 { initializeWorkpad } from '../../../state/actions/workpad';
import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad';
import { ExportApp as Component } from './export_app.component';
import { State } from '../../../../types';
export const ExportApp = connect(
(state: State) => ({
workpad: getWorkpad(state),
selectedPageIndex: getSelectedPageIndex(state),
}),
(dispatch) => ({
initializeWorkpad: () => dispatch(initializeWorkpad()),
})
)(Component);

View file

@ -1,30 +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, branch, renderComponent } from 'recompose';
import { initializeWorkpad } from '../../../state/actions/workpad';
import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad';
import { LoadWorkpad } from './load_workpad';
import { ExportApp as Component } from './export_app';
const mapStateToProps = (state) => ({
workpad: getWorkpad(state),
selectedPageIndex: getSelectedPageIndex(state),
});
const mapDispatchToProps = (dispatch) => ({
initializeWorkpad() {
dispatch(initializeWorkpad());
},
});
const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))];
export const ExportApp = compose(
connect(mapStateToProps, mapDispatchToProps),
...branches
)(Component);

View file

@ -4,6 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export const LoadWorkpad = () => <div>Load a workpad...</div>;
export { ExportApp } from './export_app';
export { ExportApp as ExportAppComponent } from './export_app.component';

View file

@ -4,10 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Dispatch } from 'redux';
// @ts-expect-error Untyped local
import * as workpadService from '../../lib/workpad_service';
import { setWorkpad } from '../../state/actions/workpad';
// @ts-expect-error Untyped local
import { fetchAllRenderables } from '../../state/actions/elements';
// @ts-expect-error Untyped local
import { setPage } from '../../state/actions/pages';
// @ts-expect-error Untyped local
import { setAssets } from '../../state/actions/assets';
import { ExportApp } from './export';
@ -18,7 +23,13 @@ export const routes = [
{
name: 'exportWorkpad',
path: '/pdf/:id/page/:page',
action: (dispatch) => async ({ params, router }) => {
action: (dispatch: Dispatch) => async ({
params,
// @ts-expect-error Fix when Router is typed.
router,
}: {
params: { id: string; page: string };
}) => {
// load workpad if given a new id via url param
const fetchedWorkpad = await workpadService.get(params.id);
const pageNumber = parseInt(params.page, 10);

View file

@ -4,12 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { FC } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
// @ts-expect-error untyped local
import { WorkpadManager } from '../../../components/workpad_manager';
// @ts-expect-error untyped local
import { setDocTitle } from '../../../lib/doc_title';
export const HomeApp = ({ onLoad = () => {} }) => {
interface Props {
onLoad: () => void;
}
export const HomeApp: FC<Props> = ({ onLoad = () => {} }) => {
onLoad();
setDocTitle('Canvas');
return (

View file

@ -6,12 +6,10 @@
import { connect } from 'react-redux';
import { resetWorkpad } from '../../../state/actions/workpad';
import { HomeApp as Component } from './home_app';
import { HomeApp as Component } from './home_app.component';
const mapDispatchToProps = (dispatch) => ({
export const HomeApp = connect(null, (dispatch) => ({
onLoad() {
dispatch(resetWorkpad());
},
});
export const HomeApp = connect(null, mapDispatchToProps)(Component);
}))(Component);

View file

@ -4,6 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
export const LoadWorkpad = () => <div>Load a workpad...</div>;
export { HomeApp } from './home_app';
export { HomeApp as HomeAppComponent } from './home_app.component';

View file

@ -8,6 +8,7 @@ import * as home from './home';
import * as workpad from './workpad';
import * as exp from './export';
// @ts-expect-error Router and routes are not yet strongly typed
export const routes = [].concat(workpad.routes, home.routes, exp.routes);
export const apps = [workpad.WorkpadApp, home.HomeApp, exp.ExportApp];

View file

@ -4,17 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ErrorStrings } from '../../../i18n';
import { Dispatch } from 'redux';
// @ts-expect-error
import * as workpadService from '../../lib/workpad_service';
import { notifyService } from '../../services';
import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs';
// @ts-expect-error
import { getDefaultWorkpad } from '../../state/defaults';
import { setWorkpad } from '../../state/actions/workpad';
// @ts-expect-error
import { setAssets, resetAssets } from '../../state/actions/assets';
// @ts-expect-error
import { setPage } from '../../state/actions/pages';
import { getWorkpad } from '../../state/selectors/workpad';
// @ts-expect-error
import { setZoomScale } from '../../state/actions/transient';
import { ErrorStrings } from '../../../i18n';
import { WorkpadApp } from './workpad_app';
import { State } from '../../../types';
const { workpadRoutes: strings } = ErrorStrings;
@ -25,7 +32,8 @@ export const routes = [
{
name: 'createWorkpad',
path: '/create',
action: (dispatch) => async ({ router }) => {
// @ts-expect-error Fix when Router is typed.
action: (dispatch: Dispatch) => async ({ router }) => {
const newWorkpad = getDefaultWorkpad();
try {
await workpadService.create(newWorkpad);
@ -46,7 +54,13 @@ export const routes = [
{
name: 'loadWorkpad',
path: '/:id(/page/:page)',
action: (dispatch, getState) => async ({ params, router }) => {
action: (dispatch: Dispatch, getState: () => State) => async ({
params,
// @ts-expect-error Fix when Router is typed.
router,
}: {
params: { id: string; page?: string };
}) => {
// load workpad if given a new id via url param
const state = getState();
const currentWorkpad = getWorkpad(state);
@ -70,10 +84,10 @@ export const routes = [
// fetch the workpad again, to get changes
const workpad = getWorkpad(getState());
const pageNumber = parseInt(params.page, 10);
const pageNumber = params.page ? parseInt(params.page, 10) : null;
// no page provided, append current page to url
if (isNaN(pageNumber)) {
if (!pageNumber || isNaN(pageNumber)) {
return router.redirectTo('loadWorkpad', { id: workpad.id, page: workpad.page + 1 });
}

View file

@ -1,41 +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, branch, renderComponent } from 'recompose';
import { selectToplevelNodes } from '../../../state/actions/transient';
import { canUserWrite, getAppReady } from '../../../state/selectors/app';
import { getWorkpad, isWriteable } from '../../../state/selectors/workpad';
import { LoadWorkpad } from './load_workpad';
import { WorkpadApp as Component } from './workpad_app';
import { withElementsLoadedTelemetry } from './workpad_telemetry';
export { WORKPAD_CONTAINER_ID } from './workpad_app';
const mapStateToProps = (state) => {
const appReady = getAppReady(state);
return {
isWriteable: isWriteable(state) && canUserWrite(state),
appReady: typeof appReady === 'object' ? appReady : { ready: appReady },
workpad: getWorkpad(state),
};
};
const mapDispatchToProps = (dispatch) => ({
deselectElement(ev) {
ev && ev.stopPropagation();
dispatch(selectToplevelNodes([]));
},
});
const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))];
export const WorkpadApp = compose(
connect(mapStateToProps, mapDispatchToProps),
...branches,
withElementsLoadedTelemetry
)(Component);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { WorkpadApp } from './workpad_app';
export { WorkpadApp as WorkpadAppComponent } from './workpad_app.component';

View file

@ -0,0 +1,83 @@
/*
* 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, MouseEventHandler, useRef } from 'react';
import PropTypes from 'prop-types';
import { Sidebar } from '../../../components/sidebar';
import { Toolbar } from '../../../components/toolbar';
// @ts-expect-error Untyped local
import { Workpad } from '../../../components/workpad';
import { WorkpadHeader } from '../../../components/workpad_header';
import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
import { CommitFn } from '../../../../types';
export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
interface Props {
deselectElement?: MouseEventHandler;
isWriteable: boolean;
}
export const WorkpadApp: FC<Props> = ({ deselectElement, isWriteable }) => {
const interactivePageLayout = useRef<CommitFn | null>(null); // future versions may enable editing on multiple pages => use array then
const registerLayout = (newLayout: CommitFn) => {
if (interactivePageLayout.current !== newLayout) {
interactivePageLayout.current = newLayout;
}
};
const unregisterLayout = (oldLayout: CommitFn) => {
if (interactivePageLayout.current === oldLayout) {
interactivePageLayout.current = null;
}
};
const commit = interactivePageLayout.current || (() => {});
return (
<div className="canvasLayout">
<div className="canvasLayout__rows">
<div className="canvasLayout__cols">
<div className="canvasLayout__stage">
<div className="canvasLayout__stageHeader">
<WorkpadHeader commit={commit} />
</div>
<div
id={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
className={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
onMouseDown={deselectElement}
>
{/* NOTE: canvasWorkpadContainer is used for exporting */}
<div
id={WORKPAD_CONTAINER_ID}
className="canvasWorkpadContainer canvasLayout__stageContentOverflow"
>
<Workpad registerLayout={registerLayout} unregisterLayout={unregisterLayout} />
</div>
</div>
</div>
{isWriteable && (
<div className="canvasLayout__sidebar hide-for-sharing">
<Sidebar commit={commit} />
</div>
)}
</div>
<div className="canvasLayout__footer hide-for-sharing">
<Toolbar />
</div>
</div>
</div>
);
};
WorkpadApp.propTypes = {
isWriteable: PropTypes.bool.isRequired,
deselectElement: PropTypes.func,
};

View file

@ -1,81 +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 { Sidebar } from '../../../components/sidebar';
import { Toolbar } from '../../../components/toolbar';
import { Workpad } from '../../../components/workpad';
import { WorkpadHeader } from '../../../components/workpad_header';
import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
export class WorkpadApp extends React.PureComponent {
static propTypes = {
isWriteable: PropTypes.bool.isRequired,
deselectElement: PropTypes.func,
};
interactivePageLayout = null; // future versions may enable editing on multiple pages => use array then
registerLayout(newLayout) {
if (this.interactivePageLayout !== newLayout) {
this.interactivePageLayout = newLayout;
}
}
unregisterLayout(oldLayout) {
if (this.interactivePageLayout === oldLayout) {
this.interactivePageLayout = null;
}
}
render() {
const { isWriteable, deselectElement } = this.props;
return (
<div className="canvasLayout">
<div className="canvasLayout__rows">
<div className="canvasLayout__cols">
<div className="canvasLayout__stage">
<div className="canvasLayout__stageHeader">
<WorkpadHeader commit={this.interactivePageLayout || (() => {})} />
</div>
<div
id={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
className={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
onMouseDown={deselectElement}
>
{/* NOTE: canvasWorkpadContainer is used for exporting */}
<div
id={WORKPAD_CONTAINER_ID}
className="canvasWorkpadContainer canvasLayout__stageContentOverflow"
>
<Workpad
registerLayout={this.registerLayout.bind(this)}
unregisterLayout={this.unregisterLayout.bind(this)}
/>
</div>
</div>
</div>
{isWriteable && (
<div className="canvasLayout__sidebar hide-for-sharing">
<Sidebar />
</div>
)}
</div>
<div className="canvasLayout__footer hide-for-sharing">
<Toolbar />
</div>
</div>
</div>
);
}
}

View file

@ -0,0 +1,32 @@
/*
* 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 { MouseEventHandler } from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
// @ts-expect-error untyped local
import { selectToplevelNodes } from '../../../state/actions/transient';
import { canUserWrite } from '../../../state/selectors/app';
import { getWorkpad, isWriteable } from '../../../state/selectors/workpad';
import { WorkpadApp as Component } from './workpad_app.component';
import { withElementsLoadedTelemetry } from './workpad_telemetry';
import { State } from '../../../../types';
export { WORKPAD_CONTAINER_ID } from './workpad_app.component';
const mapDispatchToProps = (dispatch: Dispatch): { deselectElement: MouseEventHandler } => ({
deselectElement: (ev) => {
ev.stopPropagation();
dispatch(selectToplevelNodes([]));
},
});
export const WorkpadApp = connect(
(state: State) => ({
isWriteable: isWriteable(state) && canUserWrite(state),
workpad: getWorkpad(state),
}),
mapDispatchToProps
)(withElementsLoadedTelemetry(Component));

View file

@ -18,22 +18,25 @@ import { EditMenu } from './edit_menu';
import { ElementMenu } from './element_menu';
import { ShareMenu } from './share_menu';
import { ViewMenu } from './view_menu';
import { CommitFn } from '../../../types';
const { WorkpadHeader: strings } = ComponentStrings;
export interface Props {
isWriteable: boolean;
toggleWriteable: () => void;
canUserWrite: boolean;
commit: (type: string, payload: any) => any;
commit: CommitFn;
onSetWriteable?: (writeable: boolean) => void;
}
export const WorkpadHeader: FunctionComponent<Props> = ({
isWriteable,
canUserWrite,
toggleWriteable,
commit,
onSetWriteable = () => {},
}) => {
const toggleWriteable = () => onSetWriteable(!isWriteable);
const keyHandler = (action: string) => {
if (action === 'EDITING') {
toggleWriteable();
@ -145,6 +148,7 @@ export const WorkpadHeader: FunctionComponent<Props> = ({
WorkpadHeader.propTypes = {
isWriteable: PropTypes.bool,
toggleWriteable: PropTypes.func,
commit: PropTypes.func.isRequired,
onSetWriteable: PropTypes.func,
canUserWrite: PropTypes.bool,
};

View file

@ -10,37 +10,16 @@ import { canUserWrite } from '../../state/selectors/app';
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
import { setWriteable } from '../../state/actions/workpad';
import { State } from '../../../types';
import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component';
import { WorkpadHeader as Component } from './workpad_header.component';
interface StateProps {
isWriteable: boolean;
canUserWrite: boolean;
selectedPage: string;
}
interface DispatchProps {
setWriteable: (isWorkpadWriteable: boolean) => void;
}
const mapStateToProps = (state: State): StateProps => ({
const mapStateToProps = (state: State) => ({
isWriteable: isWriteable(state) && canUserWrite(state),
canUserWrite: canUserWrite(state),
selectedPage: getSelectedPage(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)),
onSetWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)),
});
const mergeProps = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: ComponentProps
): ComponentProps => ({
...stateProps,
...dispatchProps,
...ownProps,
toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable),
});
export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component);
export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -76,3 +76,7 @@ export interface CanvasWorkpadBoundingBox {
top: number;
bottom: number;
}
export type LayoutState = any;
export type CommitFn = (type: string, payload: any) => LayoutState;