diff --git a/packages/code-studio/src/dashboard/linker/LinkType.js b/packages/code-studio/src/dashboard/linker/LinkType.js deleted file mode 100644 index 07a8df9c4b..0000000000 --- a/packages/code-studio/src/dashboard/linker/LinkType.js +++ /dev/null @@ -1,5 +0,0 @@ -export default Object.freeze({ - INVALID: 'invalid', - FILTER_SOURCE: 'filterSource', - TABLE_LINK: 'tableLink', -}); diff --git a/packages/code-studio/src/dashboard/linker/Linker.jsx b/packages/code-studio/src/dashboard/linker/Linker.tsx similarity index 73% rename from packages/code-studio/src/dashboard/linker/Linker.jsx rename to packages/code-studio/src/dashboard/linker/Linker.tsx index 14a67c978e..6ac043f106 100644 --- a/packages/code-studio/src/dashboard/linker/Linker.jsx +++ b/packages/code-studio/src/dashboard/linker/Linker.tsx @@ -1,7 +1,7 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; +import React, { Component, ErrorInfo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; import shortid from 'shortid'; +import type GoldenLayout from 'golden-layout'; import memoize from 'memoize-one'; import { CSSTransition } from 'react-transition-group'; import { ThemeExport } from '@deephaven/components'; @@ -29,15 +29,102 @@ import { } from '../events'; import LayoutUtils from '../../layout/LayoutUtils'; import LinkerOverlayContent from './LinkerOverlayContent'; -import LinkerUtils from './LinkerUtils'; -import LinkType from './LinkType'; +import LinkerUtils, { Link, LinkColumn, LinkType } from './LinkerUtils'; import { PanelManager } from '../panels'; -import { UIPropTypes } from '../../include/prop-types'; const log = Log.module('Linker'); -class Linker extends Component { - constructor(props) { +export type PanelProps = { + glContainer: GoldenLayout.Container; + glEventHub: GoldenLayout.EventEmitter; +}; + +export type Panel = Component; + +export type LinkFilterMapValue = { + columnType: string; + text: string; + value: T; +}; + +export type LinkFilterMap = Map>; + +export type LinkDataMapValue = { + type: string; + text: string; + value: string; +}; + +export type LinkDataMap = Record; + +export type LinkablePanel = Panel & { + setFilterMap: (filterMap: LinkFilterMap) => void; + unsetFilterValue: () => void; +}; + +export function isLinkablePanel(panel: Panel): panel is LinkablePanel { + const p = panel as LinkablePanel; + return ( + typeof p.setFilterMap === 'function' && + typeof p.unsetFilterValue === 'function' + ); +} + +interface StateProps { + activeTool: string; + isolatedLinkerPanelId?: string; + links: Link[]; + timeZone: string; +} + +interface OwnProps { + layout: GoldenLayout; + panelManager: PanelManager; + localDashboardId: string; +} + +const mapState = (state: LinkerState, ownProps: OwnProps): StateProps => ({ + activeTool: getActiveTool(state), + isolatedLinkerPanelId: getIsolatedLinkerPanelIdForDashboard( + state, + ownProps.localDashboardId + ), + links: getLinksForDashboard(state, ownProps.localDashboardId), + timeZone: getTimeZone(state), +}); + +type DispatchProps = { + setActiveTool: (activeTool: string) => void; + setDashboardLinks: (dashboardId: string, links: Link[]) => void; + addDashboardLinks: (dashboardId: string, links: Link[]) => void; + deleteDashboardLinks: (dashboardId: string, linkIds: string[]) => void; + setDashboardIsolatedLinkerPanelId: ( + dashboardId: string, + panelId: string | null + ) => void; + setDashboardColumnSelectionValidator: ( + dashboardId: string, + columnValidator: ((panel: Panel, column?: LinkColumn) => boolean) | null + ) => void; +}; + +const connector = connect(mapState, { + setActiveTool: setActiveToolAction, + setDashboardLinks: setDashboardLinksAction, + addDashboardLinks: addDashboardLinksAction, + deleteDashboardLinks: deleteDashboardLinksAction, + setDashboardIsolatedLinkerPanelId: setDashboardIsolatedLinkerPanelIdAction, + setDashboardColumnSelectionValidator: setDashboardColumnSelectionValidatorAction, +}); + +export type LinkerProps = OwnProps & ConnectedProps; + +export type LinkerState = { + linkInProgress?: Link; +}; + +class Linker extends Component { + constructor(props: LinkerProps) { super(props); this.handleCancel = this.handleCancel.bind(this); @@ -56,16 +143,16 @@ class Linker extends Component { this.handleExited = this.handleExited.bind(this); this.isColumnSelectionValid = this.isColumnSelectionValid.bind(this); - this.state = { linkInProgress: null }; + this.state = { linkInProgress: undefined }; } - componentDidMount() { + componentDidMount(): void { const { layout } = this.props; this.startListening(layout); this.updateSelectionValidators(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: LinkerProps): void { const { activeTool, layout } = this.props; if (layout !== prevProps.layout) { this.stopListening(prevProps.layout); @@ -76,16 +163,16 @@ class Linker extends Component { } } - componentDidCatch(error, info) { + componentDidCatch(error: Error, info: ErrorInfo): void { log.error('componentDidCatch', error, info); } - componentWillUnmount() { + componentWillUnmount(): void { const { layout } = this.props; this.stopListening(layout); } - startListening(layout) { + startListening(layout: GoldenLayout): void { layout.on('stateChanged', this.handleLayoutStateChanged); const { eventHub } = layout; @@ -102,7 +189,7 @@ class Linker extends Component { eventHub.on(PanelEvent.CLOSED, this.handlePanelClosed); } - stopListening(layout) { + stopListening(layout: GoldenLayout): void { layout.off('stateChanged', this.handleLayoutStateChanged); const { eventHub } = layout; @@ -125,20 +212,20 @@ class Linker extends Component { const { setActiveTool } = this.props; setActiveTool(ToolType.DEFAULT); } - this.setState({ linkInProgress: null }); + this.setState({ linkInProgress: undefined }); } handleDone() { const { setActiveTool } = this.props; setActiveTool(ToolType.DEFAULT); - this.setState({ linkInProgress: null }); + this.setState({ linkInProgress: undefined }); } - handleChartColumnSelect(panel, column) { + handleChartColumnSelect(panel: Panel, column: LinkColumn): void { this.columnSelected(panel, column, true); } - handleFilterColumnSelect(panel, column) { + handleFilterColumnSelect(panel: Panel, column: LinkColumn): void { log.debug('handleFilterColumnSelect', this.isOverlayShown()); const { links, @@ -152,7 +239,7 @@ class Linker extends Component { link => link.start?.panelId === panelId || link.end?.panelId === panelId ); - if (!this.isOverlayShown()) { + if (!this.isOverlayShown() && panelId != null) { // Initial click on the filter source button with linker inactive // Show linker in isolated mode for panel setActiveTool(ToolType.LINKER); @@ -171,7 +258,7 @@ class Linker extends Component { this.columnSelected(panel, column, true); } - handleColumnsChanged(panel, columns) { + handleColumnsChanged(panel: Panel, columns: LinkColumn[]): void { log.debug('handleColumnsChanged', panel, columns); const { links } = this.props; const panelId = LayoutUtils.getIdFromPanel(panel); @@ -184,39 +271,43 @@ class Linker extends Component { ({ start, end }) => (start.panelId === panelId && LinkerUtils.findColumn(columns, start) == null) || - (end.panelId === panelId && + (end != null && + end.panelId === panelId && LinkerUtils.findColumn(columns, end) == null) ); this.deleteLinks(linksToDelete); } - handleGridColumnSelect(panel, column) { + handleGridColumnSelect(panel: Panel, column: LinkColumn): void { this.columnSelected(panel, column); } /** * Track a column selection and build the link from it. - * @param {React.Component} panel The panel component that is the source for the column selection - * @param {dh.Column} column The column that was selected - * @param {boolean} isAlwaysEndPoint True if the selection is always the end point, even if it's the first column selected. Defaults to false. - * @param {string} overrideIsolatedLinkerPanelId isolatedLinkerPanelId to use when method is called before prop changes propagate + * @param panel The panel component that is the source for the column selection + * @param column The column that was selected + * @param isAlwaysEndPoint True if the selection is always the end point, even if it's the first column selected. Defaults to false. + * @param overrideIsolatedLinkerPanelId isolatedLinkerPanelId to use when method is called before prop changes propagate */ columnSelected( - panel, - column, + panel: Panel, + column: LinkColumn, isAlwaysEndPoint = false, - overrideIsolatedLinkerPanelId = null - ) { + overrideIsolatedLinkerPanelId?: string + ): void { if (overrideIsolatedLinkerPanelId == null && !this.isOverlayShown()) { return; } const { isolatedLinkerPanelId } = this.props; const { linkInProgress } = this.state; const panelId = LayoutUtils.getIdFromPanel(panel); + if (panelId == null) { + return; + } const panelComponent = LayoutUtils.getComponentNameFromPanel(panel); const { name: columnName, type: columnType } = column; if (linkInProgress == null || linkInProgress.start == null) { - const newLink = { + const newLink: Link = { id: shortid.generate(), start: { panelId, @@ -224,9 +315,8 @@ class Linker extends Component { columnName, columnType, }, - end: null, // Link starts with type Invalid as linking a source to itself is not allowed - type: LinkType.INVALID, + type: 'invalid', isReversed: isAlwaysEndPoint, }; @@ -248,23 +338,27 @@ class Linker extends Component { overrideIsolatedLinkerPanelId ?? isolatedLinkerPanelId ); - if (type === LinkType.INVALID) { - log.debug('Ignore invalid link connection', linkInProgress, end); - return; - } - - // FILTER_SOURCE links have a limit of 1 link per target - // New link validation passed, delete existing links before adding the new one - if (type === LinkType.FILTER_SOURCE) { - const { links } = this.props; - const existingLinkPanelId = isReversed ? start.panelId : end.panelId; - // In cases with multiple targets per panel (i.e. chart filters) - // links would have to be filtered by panelId and columnName and columnType - const linksToDelete = links.filter( - ({ end: panelLinkEnd }) => - panelLinkEnd.panelId === existingLinkPanelId - ); - this.deleteLinks(linksToDelete); + switch (type) { + case 'invalid': + log.debug('Ignore invalid link connection', linkInProgress, end); + return; + case 'filterSource': { + // filterSource links have a limit of 1 link per target + // New link validation passed, delete existing links before adding the new one + const { links } = this.props; + const existingLinkPanelId = isReversed ? start.panelId : end.panelId; + // In cases with multiple targets per panel (i.e. chart filters) + // links would have to be filtered by panelId and columnName and columnType + const linksToDelete = links.filter( + ({ end: panelLinkEnd }) => + panelLinkEnd?.panelId === existingLinkPanelId + ); + this.deleteLinks(linksToDelete); + break; + } + case 'tableLink': + // No-op + break; } // Create a completed link from link in progress @@ -276,7 +370,7 @@ class Linker extends Component { }; log.info('creating link', newLink); - this.setState({ linkInProgress: null }, () => { + this.setState({ linkInProgress: undefined }, () => { // Adding link after updating state // otherwise both new link and linkInProgress could be rendered at the same time // resulting in "multiple children with same key" error @@ -285,7 +379,7 @@ class Linker extends Component { } } - unsetFilterValueForLink(link) { + unsetFilterValueForLink(link: Link): void { const { panelManager } = this.props; if (link.end) { const { end } = link; @@ -306,14 +400,14 @@ class Linker extends Component { /** * Set filters for a given panel ID - * @param {string} panelId ID of panel to set filters on - * @param {Map} filterMap Map of column name to column type, text, and value + * @param panelId ID of panel to set filters on + * @param filterMap Map of column name to column type, text, and value */ - setPanelFilterMap(panelId, filterMap) { + setPanelFilterMap(panelId: string, filterMap: LinkFilterMap) { log.debug('Set filter data for panel:', panelId, filterMap); const { panelManager } = this.props; const panel = panelManager.getOpenedPanelById(panelId); - if (panel && panel.setFilterMap) { + if (isLinkablePanel(panel)) { panel.setFilterMap(filterMap); } else if (!panel) { log.debug('panel no longer exists, ignoring setFilterMap', panelId); @@ -322,12 +416,12 @@ class Linker extends Component { } } - addLinks(links) { + addLinks(links: Link[]) { const { addDashboardLinks, localDashboardId } = this.props; addDashboardLinks(localDashboardId, links); } - deleteLinks(links, clearAll = false) { + deleteLinks(links: Link[], clearAll = false) { const { localDashboardId } = this.props; links.forEach(link => this.unsetFilterValueForLink(link)); if (clearAll) { @@ -342,7 +436,7 @@ class Linker extends Component { } } - handleAllLinksDeleted() { + handleAllLinksDeleted(): void { const { links, isolatedLinkerPanelId } = this.props; if (isolatedLinkerPanelId == null) { this.deleteLinks(links, true); @@ -354,10 +448,10 @@ class Linker extends Component { ); this.deleteLinks(isolatedLinks); } - this.setState({ linkInProgress: null }); + this.setState({ linkInProgress: undefined }); } - handleLinkDeleted(linkId) { + handleLinkDeleted(linkId: string) { const { links } = this.props; const link = links.find(l => l.id === linkId); if (link) { @@ -367,7 +461,7 @@ class Linker extends Component { } } - handleUpdateValues(panel, dataMap) { + handleUpdateValues(panel: Panel, dataMap: LinkDataMap) { const panelId = LayoutUtils.getIdFromPanel(panel); const { links, timeZone } = this.props; // Map of panel ID to filterMap @@ -376,7 +470,7 @@ class Linker extends Component { // combine them so they could be set in a single call per target panel for (let i = 0; i < links.length; i += 1) { const { start, end } = links[i]; - if (start.panelId === panelId) { + if (start.panelId === panelId && end != null) { const { panelId: endPanelId, columnName, columnType } = end; // Map of column name to column type and filter value const filterMap = panelFilterMap.has(endPanelId) @@ -408,15 +502,21 @@ class Linker extends Component { }); } - handlePanelCloned(panel, cloneConfig) { + handlePanelCloned(panel: Panel, cloneConfig: { id: string }) { const { links } = this.props; const panelId = LayoutUtils.getIdFromPanel(panel); const cloneId = cloneConfig.id; - const linksToAdd = LinkerUtils.cloneLinksForPanel(links, panelId, cloneId); - this.addLinks(linksToAdd); + if (panelId != null) { + const linksToAdd = LinkerUtils.cloneLinksForPanel( + links, + panelId, + cloneId + ); + this.addLinks(linksToAdd); + } } - handlePanelClosed(panelId) { + handlePanelClosed(panelId: string): void { // Delete links on PanelEvent.CLOSED instead of UNMOUNT // because the panels can get unmounted on errors and we want to keep the links if that happens log.debug(`Panel ${panelId} closed, deleting links.`); @@ -439,14 +539,14 @@ class Linker extends Component { /** * Delete all links for a provided panel ID. Needs to be done whenever a panel is closed or unmounted. - * @param {String} panelId The panel ID to delete links for + * @param panelId The panel ID to delete links for */ - deleteLinksForPanelId(panelId) { + deleteLinksForPanelId(panelId: string) { const { links } = this.props; for (let i = 0; i < links.length; i += 1) { const link = links[i]; const { start, end, id } = link; - if (start.panelId === panelId || end.panelId === panelId) { + if (start.panelId === panelId || end?.panelId === panelId) { this.handleLinkDeleted(id); } } @@ -495,7 +595,7 @@ class Linker extends Component { } } - updateLinkInProgressType(linkInProgress, type = LinkType.INVALID) { + updateLinkInProgressType(linkInProgress: Link, type: LinkType = 'invalid') { this.setState({ linkInProgress: { ...linkInProgress, @@ -504,7 +604,7 @@ class Linker extends Component { }); } - isColumnSelectionValid(panel, tableColumn = null) { + isColumnSelectionValid(panel: Panel, tableColumn?: LinkColumn): boolean { const { linkInProgress } = this.state; const { isolatedLinkerPanelId } = this.props; @@ -520,9 +620,13 @@ class Linker extends Component { } const { isReversed, start } = linkInProgress; + const panelId = LayoutUtils.getIdFromPanel(panel); + if (panelId == null) { + return false; + } const end = { - panelId: LayoutUtils.getIdFromPanel(panel), + panelId, panelComponent: LayoutUtils.getComponentNameFromPanel(panel), columnName: tableColumn.name, columnType: tableColumn.type, @@ -534,7 +638,7 @@ class Linker extends Component { this.updateLinkInProgressType(linkInProgress, type); - return type !== LinkType.INVALID; + return type !== 'invalid'; } render() { @@ -577,48 +681,4 @@ class Linker extends Component { } } -Linker.propTypes = { - layout: PropTypes.shape({ - eventHub: PropTypes.shape({ - on: PropTypes.func, - off: PropTypes.func, - }), - on: PropTypes.func, - off: PropTypes.func, - }).isRequired, - activeTool: PropTypes.string.isRequired, - panelManager: PropTypes.instanceOf(PanelManager).isRequired, - links: UIPropTypes.Links.isRequired, - isolatedLinkerPanelId: PropTypes.string, - localDashboardId: PropTypes.string.isRequired, - setActiveTool: PropTypes.func.isRequired, - setDashboardLinks: PropTypes.func.isRequired, - addDashboardLinks: PropTypes.func.isRequired, - deleteDashboardLinks: PropTypes.func.isRequired, - setDashboardIsolatedLinkerPanelId: PropTypes.func.isRequired, - setDashboardColumnSelectionValidator: PropTypes.func.isRequired, - timeZone: PropTypes.string.isRequired, -}; - -Linker.defaultProps = { - isolatedLinkerPanelId: null, -}; - -const mapStateToProps = (state, ownProps) => ({ - activeTool: getActiveTool(state), - isolatedLinkerPanelId: getIsolatedLinkerPanelIdForDashboard( - state, - ownProps.localDashboardId - ), - links: getLinksForDashboard(state, ownProps.localDashboardId), - timeZone: getTimeZone(state), -}); - -export default connect(mapStateToProps, { - setActiveTool: setActiveToolAction, - setDashboardLinks: setDashboardLinksAction, - addDashboardLinks: addDashboardLinksAction, - deleteDashboardLinks: deleteDashboardLinksAction, - setDashboardIsolatedLinkerPanelId: setDashboardIsolatedLinkerPanelIdAction, - setDashboardColumnSelectionValidator: setDashboardColumnSelectionValidatorAction, -})(Linker); +export default connector(Linker); diff --git a/packages/code-studio/src/dashboard/linker/LinkerLink.jsx b/packages/code-studio/src/dashboard/linker/LinkerLink.tsx similarity index 79% rename from packages/code-studio/src/dashboard/linker/LinkerLink.jsx rename to packages/code-studio/src/dashboard/linker/LinkerLink.tsx index 310eee5e39..6254bc1c6a 100644 --- a/packages/code-studio/src/dashboard/linker/LinkerLink.jsx +++ b/packages/code-studio/src/dashboard/linker/LinkerLink.tsx @@ -1,5 +1,4 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; +import React, { MouseEvent, PureComponent } from 'react'; import './LinkerLink.scss'; @@ -15,20 +14,44 @@ const TRIANGLE_HYPOTENUSE = Math.sqrt( const TRIANGLE_THETA = Math.asin((TRIANGLE_BASE * 0.5) / TRIANGLE_HEIGHT); const CLIP_RADIUS = 15; -export class LinkerLink extends PureComponent { - static makeCirclePath(x, y, r) { +export type LinkerLinkProps = { + x1: number; + y1: number; + x2: number; + y2: number; + id: string; + className: string; + onClick: (id: string) => void; +}; + +export class LinkerLink extends PureComponent { + static defaultProps = { + className: '', + onClick(): void { + // no-op + }, + }; + + /** + * Make an SVG path for a circle at the specified coordinates. + * @param x The x coordinate for the centre of the circle + * @param y The y coordinate for the centre of the circle + * @param r Radius of the circle + * @returns The SVG string path + */ + static makeCirclePath(x: number, y: number, r: number): string { return `M ${x} ${y} m -${r},0 a ${r},${r} 0 1,0 ${ r * 2 },0 a ${r},${r} 0 1,0 -${r * 2},0 z`; } - constructor(props) { + constructor(props: LinkerLinkProps) { super(props); this.handleClick = this.handleClick.bind(this); } - handleClick(event) { + handleClick(event: MouseEvent): void { event.stopPropagation(); event.preventDefault(); @@ -36,7 +59,7 @@ export class LinkerLink extends PureComponent { onClick(id); } - render() { + render(): JSX.Element { const { className, x1, y1, x2, y2, id } = this.props; // Path between the two points @@ -103,19 +126,4 @@ export class LinkerLink extends PureComponent { } } -LinkerLink.propTypes = { - x1: PropTypes.number.isRequired, - y1: PropTypes.number.isRequired, - x2: PropTypes.number.isRequired, - y2: PropTypes.number.isRequired, - id: PropTypes.string.isRequired, - onClick: PropTypes.func, - className: PropTypes.string, -}; - -LinkerLink.defaultProps = { - className: null, - onClick: () => {}, -}; - export default LinkerLink; diff --git a/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.jsx b/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.tsx similarity index 95% rename from packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.jsx rename to packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.tsx index c5adf0f545..39bbb8ab76 100644 --- a/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.jsx +++ b/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.test.tsx @@ -6,7 +6,7 @@ import LinkerOverlayContent from './LinkerOverlayContent'; const LINKER_OVERLAY_MESSAGE = 'TEST_MESSAGE'; function makeEventHub() { - const callbacks = {}; + const callbacks: Record void> = {}; return { on: jest.fn((eventName, callback) => { callbacks[eventName] = callback; diff --git a/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.jsx b/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.tsx similarity index 74% rename from packages/code-studio/src/dashboard/linker/LinkerOverlayContent.jsx rename to packages/code-studio/src/dashboard/linker/LinkerOverlayContent.tsx index bab07a2fa1..3091178baa 100644 --- a/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.jsx +++ b/packages/code-studio/src/dashboard/linker/LinkerOverlayContent.tsx @@ -1,45 +1,78 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ErrorInfo } from 'react'; import classNames from 'classnames'; import { ContextActions, GLOBAL_SHORTCUTS } from '@deephaven/components'; import Log from '@deephaven/log'; import { PanelManager } from '../panels'; -import { UIPropTypes } from '../../include/prop-types'; import LayoutUtils from '../../layout/LayoutUtils'; +import { Link, LinkPoint } from './LinkerUtils'; import LinkerLink from './LinkerLink'; -import LinkType from './LinkType'; import './LinkerOverlayContent.scss'; const log = Log.module('LinkerOverlayContent'); -export class LinkerOverlayContent extends Component { - constructor(props) { +// [x,y] screen coordinates used by the Linker +export type LinkerCoordinate = [number, number]; + +export type VisibleLink = { + x1: number; + y1: number; + x2: number; + y2: number; + id: string; + className: string; +}; + +export type LinkerOverlayContentProps = { + disabled?: boolean; + links: Link[]; + messageText: string; + onLinkDeleted: (linkId: string) => void; + onAllLinksDeleted: () => void; + onCancel: () => void; + onDone: () => void; + panelManager: PanelManager; +}; + +export type LinkerOverlayContentState = { + mouseX?: number; + mouseY?: number; +}; + +export class LinkerOverlayContent extends Component< + LinkerOverlayContentProps, + LinkerOverlayContentState +> { + static defaultProps = { + disabled: 'false', + }; + + constructor(props: LinkerOverlayContentProps) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleEscapePressed = this.handleEscapePressed.bind(this); this.state = { - mouseX: null, - mouseY: null, + mouseX: undefined, + mouseY: undefined, }; } - componentDidMount() { + componentDidMount(): void { window.addEventListener('mousemove', this.handleMouseMove, true); } // eslint-disable-next-line react/sort-comp - componentDidCatch(error, info) { + componentDidCatch(error: Error, info: ErrorInfo): void { log.error('componentDidCatch', error, info); } - componentWillUnmount() { + componentWillUnmount(): void { window.removeEventListener('mousemove', this.handleMouseMove, true); } /** Gets the on screen points for a link start or end spec */ - getPointFromLinkPoint(linkPoint) { + getPointFromLinkPoint(linkPoint: LinkPoint): LinkerCoordinate { const { panelManager } = this.props; const { panelId, columnName } = linkPoint; const panel = panelManager.getOpenedPanelById(panelId); @@ -67,22 +100,22 @@ export class LinkerOverlayContent extends Component { if (glContainer == null) { throw new Error(`Unable to find panel container for id: ${panelId}`); } - return LayoutUtils.getTabPoint(glContainer); + return LayoutUtils.getTabPoint(glContainer) as LinkerCoordinate; } - handleMouseMove(event) { + handleMouseMove(event: MouseEvent): void { this.setState({ mouseX: event.clientX, mouseY: event.clientY, }); } - handleEscapePressed() { + handleEscapePressed(): void { const { onCancel } = this.props; onCancel(); } - render() { + render(): JSX.Element { const { disabled, links, @@ -111,8 +144,8 @@ export class LinkerOverlayContent extends Component { const className = classNames( 'linker-link', { disabled }, - { 'link-filter-source': type === LinkType.FILTER_SOURCE }, - { 'link-invalid': type === LinkType.INVALID }, + { 'link-filter-source': type === 'filterSource' }, + { 'link-invalid': type === 'invalid' }, { interactive: link.end == null } ); return { x1, y1, x2, y2, id, className }; @@ -121,7 +154,7 @@ export class LinkerOverlayContent extends Component { return null; } }) - .filter(item => item != null); + .filter(item => item != null) as VisibleLink[]; return (
@@ -129,7 +162,6 @@ export class LinkerOverlayContent extends Component { {visibleLinks.map(({ x1, y1, x2, y2, id, className }) => ( name === columnName && type === columnType ); @@ -93,18 +131,19 @@ class LinkerUtils { /** * Clone links for a given panel id - * @param {object[]} links Original links array - * @param {string} panelId Original panel id - * @param {string} cloneId Cloned panel id - * @returns {object[]} Cloned links array or empty array if no new links added + * @param links Original links array + * @param panelId Original panel id + * @param cloneId Cloned panel id + * @returns Cloned links array or empty array if no new links added */ - static cloneLinksForPanel(links, panelId, cloneId) { - const clonedLinks = []; + static cloneLinksForPanel( + links: Link[], + panelId: string, + cloneId: string + ): Link[] { + const clonedLinks: Link[] = []; links.forEach(link => { - if ( - link.start?.panelId === panelId && - link.type !== LinkType.FILTER_SOURCE - ) { + if (link.start.panelId === panelId && link.type !== 'filterSource') { clonedLinks.push({ ...link, id: shortid.generate(), diff --git a/packages/iris-grid/src/formatters/DateTimeColumnFormatter.js b/packages/iris-grid/src/formatters/DateTimeColumnFormatter.ts similarity index 72% rename from packages/iris-grid/src/formatters/DateTimeColumnFormatter.js rename to packages/iris-grid/src/formatters/DateTimeColumnFormatter.ts index dfe1cbb83f..8b703987cb 100644 --- a/packages/iris-grid/src/formatters/DateTimeColumnFormatter.js +++ b/packages/iris-grid/src/formatters/DateTimeColumnFormatter.ts @@ -1,17 +1,19 @@ /* eslint class-methods-use-this: "off" */ import dh from '@deephaven/jsapi-shim'; import Log from '@deephaven/log'; -import TableColumnFormatter from './TableColumnFormatter'; +import TableColumnFormatter, { + TableColumnFormat, +} from './TableColumnFormatter'; const log = Log.module('DateTimeColumnFormatter'); class DateTimeColumnFormatter extends TableColumnFormatter { /** * Validates format object - * @param {Object} format Format object - * @returns {boolean} true for valid object + * @param format Format object + * @returns true for valid object */ - static isValid(format) { + static isValid(format: TableColumnFormat): boolean { try { dh.i18n.DateTimeFormat.format(format.formatString, new Date()); return true; @@ -21,10 +23,10 @@ class DateTimeColumnFormatter extends TableColumnFormatter { } static makeFormat( - label, - formatString, + label: string, + formatString: string, type = TableColumnFormatter.TYPE_CONTEXT_PRESET - ) { + ): TableColumnFormat { return { label, formatString, @@ -34,11 +36,14 @@ class DateTimeColumnFormatter extends TableColumnFormatter { /** * Check if the given formats match - * @param {?Object} formatA format object to check - * @param {?Object} formatB format object to check - * @returns {boolean} True if the formats match + * @param formatA format object to check + * @param formatB format object to check + * @returns True if the formats match */ - static isSameFormat(formatA, formatB) { + static isSameFormat( + formatA?: TableColumnFormat, + formatB?: TableColumnFormat + ): boolean { return ( formatA === formatB || (formatA != null && @@ -52,7 +57,10 @@ class DateTimeColumnFormatter extends TableColumnFormatter { static DEFAULT_TIME_ZONE_ID = 'America/New_York'; - static makeGlobalFormatStringMap(showTimeZone, showTSeparator) { + static makeGlobalFormatStringMap( + showTimeZone: boolean, + showTSeparator: boolean + ): Map { const separator = showTSeparator ? `'T'` : ' '; const tz = showTimeZone ? ' z' : ''; return new Map([ @@ -65,7 +73,10 @@ class DateTimeColumnFormatter extends TableColumnFormatter { ]); } - static getGlobalFormats(showTimeZone, showTSeparator) { + static getGlobalFormats( + showTimeZone: boolean, + showTSeparator: boolean + ): string[] { const formatStringMap = DateTimeColumnFormatter.makeGlobalFormatStringMap( showTimeZone, showTSeparator @@ -73,7 +84,10 @@ class DateTimeColumnFormatter extends TableColumnFormatter { return [...formatStringMap.keys()]; } - static makeFormatStringMap(showTimeZone, showTSeparator) { + static makeFormatStringMap( + showTimeZone: boolean, + showTSeparator: boolean + ): Map { const separator = showTSeparator ? `'T'` : ' '; const tz = showTimeZone ? ' z' : ''; return new Map([ @@ -91,7 +105,7 @@ class DateTimeColumnFormatter extends TableColumnFormatter { ]); } - static getFormats(showTimeZone, showTSeparator) { + static getFormats(showTimeZone: boolean, showTSeparator: boolean): string[] { const formatStringMap = DateTimeColumnFormatter.makeFormatStringMap( showTimeZone, showTSeparator @@ -99,11 +113,26 @@ class DateTimeColumnFormatter extends TableColumnFormatter { return [...formatStringMap.keys()]; } + dhTimeZone: unknown; + + defaultDateTimeFormatString: string; + + showTimeZone: boolean; + + showTSeparator: boolean; + + formatStringMap: Map; + constructor({ - timeZone: timeZoneParam, + timeZone: timeZoneParam = '', showTimeZone = true, showTSeparator = false, defaultDateTimeFormatString = DateTimeColumnFormatter.DEFAULT_DATETIME_FORMAT_STRING, + }: { + timeZone?: string; + showTimeZone?: boolean; + showTSeparator?: boolean; + defaultDateTimeFormatString?: string; } = {}) { super(); @@ -128,11 +157,11 @@ class DateTimeColumnFormatter extends TableColumnFormatter { ); } - getEffectiveFormatString(baseFormatString) { + getEffectiveFormatString(baseFormatString: string): string { return this.formatStringMap.get(baseFormatString) || baseFormatString; } - format(value, format) { + format(value: unknown, format?: TableColumnFormat): string { const baseFormatString = (format && format.formatString) || this.defaultDateTimeFormatString; const formatString = this.getEffectiveFormatString(baseFormatString); diff --git a/packages/iris-grid/src/formatters/TableColumnFormatter.js b/packages/iris-grid/src/formatters/TableColumnFormatter.js deleted file mode 100644 index bfd5ad85d4..0000000000 --- a/packages/iris-grid/src/formatters/TableColumnFormatter.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint class-methods-use-this: "off" */ -/* eslint no-unused-vars: "off" */ -/** - * Default column data formatter. Just interpolates the value as a string and returns. - * Extend this class and register with TableUtils to make use of it. - */ -class TableColumnFormatter { - static TYPE_GLOBAL = 'type-global'; - - static TYPE_CONTEXT_PRESET = 'type-context-preset'; - - static TYPE_CONTEXT_CUSTOM = 'type-context-custom'; - - /** - * Validates format object - * @param {Object} format Format object - * @returns {boolean} true for valid object - */ - static isValid(format) { - return true; - } - - /** - * Check if the given formats match - * @param {?Object} formatA format object to check - * @param {?Object} formatB format object to check - * @returns {boolean} True if the formats match - */ - static isSameFormat(formatA, formatB) { - throw new Error('isSameFormat not implemented'); - } - - /** - * Returns format object - */ - static makeFormat() { - return null; - } - - /** - * @param {Object} value The value to format - * @param {Object} format Optional format object with value transformation options - * @returns String the formatted text string of the value passed in. - */ - format(value, format) { - return ''; - } -} - -export default TableColumnFormatter; diff --git a/packages/iris-grid/src/formatters/TableColumnFormatter.ts b/packages/iris-grid/src/formatters/TableColumnFormatter.ts new file mode 100644 index 0000000000..fb9e2f7cc9 --- /dev/null +++ b/packages/iris-grid/src/formatters/TableColumnFormatter.ts @@ -0,0 +1,72 @@ +/* eslint class-methods-use-this: "off" */ +/** + * Default column data formatter. Just interpolates the value as a string and returns. + * Extend this class and register with TableUtils to make use of it. + */ + +export type TableColumnFormatType = + | 'type-global' + | 'type-context-preset' + | 'type-context-custom'; + +export type TableColumnFormat = { + label: string; + formatString: string; + type: TableColumnFormatType; +}; + +export class TableColumnFormatter { + static TYPE_GLOBAL: TableColumnFormatType = 'type-global'; + + static TYPE_CONTEXT_PRESET: TableColumnFormatType = 'type-context-preset'; + + static TYPE_CONTEXT_CUSTOM: TableColumnFormatType = 'type-context-custom'; + + /** + * Validates format object + * @param format Format object + * @returns true for valid object + */ + static isValid(format: TableColumnFormat): boolean { + return true; + } + + /** + * Check if the given formats match + * @param formatA format object to check + * @param formatB format object to check + * @returns True if the formats match + */ + static isSameFormat( + formatA?: TableColumnFormat, + formatB?: TableColumnFormat + ): boolean { + throw new Error('isSameFormat not implemented'); + } + + /** + * Create and return a Format object + * @param label The label of the format object + * @param formatString Format string to use for the format + * @param type The type of column to use for this format + * @returns A format object + */ + static makeFormat( + label: string, + formatString: string, + type: TableColumnFormatType + ): TableColumnFormat { + return { label, formatString, type }; + } + + /** + * @param value The value to format + * @param format Optional format object with value transformation options + * @returns String the formatted text string of the value passed in. + */ + format(value: T, format?: TableColumnFormat): string { + return ''; + } +} + +export default TableColumnFormatter;