From c64d00db7688a97ab1476789acbac7593f7f2f02 Mon Sep 17 00:00:00 2001 From: Maciej Rybaniec Date: Tue, 28 Jul 2020 12:40:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20properties=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/js/app/KeenExplorer.tsx | 18 +-- lib/js/app/components/Creator/Creator.tsx | 2 + lib/js/app/contexts/AppContext.ts | 2 + lib/js/app/queryCreator/QueryCreator.tsx | 9 +- .../components/Analysis/Analysis.styles.ts | 2 +- .../Analysis/components/ListItem.styles.ts | 2 +- .../EventCollection/EventCollection.styles.ts | 2 +- .../queryCreator/components/Portal/Portal.tsx | 33 ++++++ .../queryCreator/components/Portal/index.ts | 3 + .../PropertiesTree/PropertiesTree.test.tsx | 74 ++++++++++++ .../PropertiesTree/PropertiesTree.tsx | 20 ++-- .../components/TreeLeaf/TreeLeaf.tsx | 35 ------ .../components/TreeLeaf/index.ts | 3 - .../components/TreeLevel/TreeLevel.styles.ts | 7 ++ .../components/TreeLevel/TreeLevel.tsx | 38 +++---- .../PropertiesTree/components/index.ts | 3 +- .../utils/getPropertyPath.test.ts | 7 ++ .../PropertiesTree/utils/getPropertyPath.ts | 6 + .../utils/getPropertyType.test.ts | 7 ++ .../PropertiesTree/utils/getPropertyType.ts | 6 + .../components/PropertiesTree/utils/index.ts | 4 + .../PropertyTreeItem.styles.ts} | 30 +++++ .../PropertyTreeItem.test.tsx | 39 +++++++ .../PropertyTreeItem/PropertyTreeItem.tsx | 106 ++++++++++++++++++ .../components/PropertyTreeItem/constants.ts | 1 + .../components/PropertyTreeItem/index.ts | 3 + .../TargetProperty/TargetProperty.styles.ts | 2 +- .../TargetProperty/TargetProperty.test.tsx | 72 +++++++++++- .../TargetProperty/TargetProperty.tsx | 12 +- .../components/TargetProperty/constants.ts | 2 +- .../app/queryCreator/contexts/AppContext.ts | 9 ++ lib/js/app/queryCreator/contexts/index.ts | 3 + test/demo/index.html | 8 ++ 33 files changed, 481 insertions(+), 89 deletions(-) create mode 100644 lib/js/app/queryCreator/components/Portal/Portal.tsx create mode 100644 lib/js/app/queryCreator/components/Portal/index.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.test.tsx delete mode 100644 lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.tsx delete mode 100644 lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/index.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.test.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.test.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.ts create mode 100644 lib/js/app/queryCreator/components/PropertiesTree/utils/index.ts rename lib/js/app/queryCreator/components/{PropertiesTree/components/TreeLeaf/TreeLeaf.styles.ts => PropertyTreeItem/PropertyTreeItem.styles.ts} (54%) create mode 100644 lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.test.tsx create mode 100644 lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.tsx create mode 100644 lib/js/app/queryCreator/components/PropertyTreeItem/constants.ts create mode 100644 lib/js/app/queryCreator/components/PropertyTreeItem/index.ts create mode 100644 lib/js/app/queryCreator/contexts/AppContext.ts create mode 100644 lib/js/app/queryCreator/contexts/index.ts diff --git a/lib/js/app/KeenExplorer.tsx b/lib/js/app/KeenExplorer.tsx index 1c72a1e5b..2615f28eb 100644 --- a/lib/js/app/KeenExplorer.tsx +++ b/lib/js/app/KeenExplorer.tsx @@ -19,13 +19,6 @@ import { version } from '../../../package.json'; import App from './components/App'; import { AppContext } from './contexts'; -const defaultConfig = { - previewCollection: true, - saveStateToLocalStorage: { - eventCollection: true, - }, -}; - export let client; export let keenTrackingClient; @@ -56,13 +49,10 @@ export class KeenExplorer { ReactDOM.render( - - + + , document.querySelector(props.container) diff --git a/lib/js/app/components/Creator/Creator.tsx b/lib/js/app/components/Creator/Creator.tsx index 7f0abf7ff..0db87a72e 100644 --- a/lib/js/app/components/Creator/Creator.tsx +++ b/lib/js/app/components/Creator/Creator.tsx @@ -13,6 +13,7 @@ type Props = { const Creator: FC = ({ onUpdateQuery }) => { const dispatch = useDispatch(); const { + modalContainer, keenAnalysis: { config }, } = useContext(AppContext); @@ -22,6 +23,7 @@ const Creator: FC = ({ onUpdateQuery }) => { return ( ({ keenAnalysis: null, + modalContainer: null, }); export default AppContext; diff --git a/lib/js/app/queryCreator/QueryCreator.tsx b/lib/js/app/queryCreator/QueryCreator.tsx index 13e9ddcfe..672336370 100644 --- a/lib/js/app/queryCreator/QueryCreator.tsx +++ b/lib/js/app/queryCreator/QueryCreator.tsx @@ -8,6 +8,7 @@ import KeenAnalysis from 'keen-analysis'; import App from './App'; import rootSaga from './saga'; import rootReducer from './reducer'; +import { AppContext } from './contexts'; import { appStart } from './modules/app'; import { getQuery, setQuery, resetQuery } from './modules/query'; @@ -23,6 +24,8 @@ type Props = { readKey: string; /** Keen master access key */ masterKey: string; + /** Modal container selector */ + modalContainer: string; /** Update query event handler */ onUpdateQuery?: (query: Object) => void; }; @@ -100,7 +103,11 @@ class QueryCreator extends React.Component { render() { return ( - + + + ); } diff --git a/lib/js/app/queryCreator/components/Analysis/Analysis.styles.ts b/lib/js/app/queryCreator/components/Analysis/Analysis.styles.ts index 3a3fe1c36..006723cf4 100644 --- a/lib/js/app/queryCreator/components/Analysis/Analysis.styles.ts +++ b/lib/js/app/queryCreator/components/Analysis/Analysis.styles.ts @@ -18,6 +18,6 @@ export const Groups = styled.div` padding: 10px 0; ${List} + ${List} { - margin-top: 10px; + margin-top: 14px; } `; diff --git a/lib/js/app/queryCreator/components/Analysis/components/ListItem.styles.ts b/lib/js/app/queryCreator/components/Analysis/components/ListItem.styles.ts index b6ab4f9d1..e870866ae 100644 --- a/lib/js/app/queryCreator/components/Analysis/components/ListItem.styles.ts +++ b/lib/js/app/queryCreator/components/Analysis/components/ListItem.styles.ts @@ -6,7 +6,7 @@ import { colors } from '@keen.io/colors'; export const Container = styled.li<{ isActive: boolean; }>` - padding: 7px 10px; + padding: 8px 10px; display: flex; align-items: center; justify-content: space-between; diff --git a/lib/js/app/queryCreator/components/EventCollection/EventCollection.styles.ts b/lib/js/app/queryCreator/components/EventCollection/EventCollection.styles.ts index 19f73fc29..f3445596a 100644 --- a/lib/js/app/queryCreator/components/EventCollection/EventCollection.styles.ts +++ b/lib/js/app/queryCreator/components/EventCollection/EventCollection.styles.ts @@ -5,6 +5,6 @@ export const Container = styled.div` `; export const Collections = styled.div` - max-height: 140px; + max-height: 240px; overflow-y: scroll; `; diff --git a/lib/js/app/queryCreator/components/Portal/Portal.tsx b/lib/js/app/queryCreator/components/Portal/Portal.tsx new file mode 100644 index 000000000..28d39b7a1 --- /dev/null +++ b/lib/js/app/queryCreator/components/Portal/Portal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +type Props = { + /** Modal container selector */ + modalContainer: string; + /** Modal container selector */ + children: React.ReactNode; +}; + +export default class Portal extends React.Component { + element: HTMLDivElement; + + modalRoot: HTMLDivElement; + + constructor(props: Props) { + super(props); + this.element = document.createElement('div'); + this.modalRoot = document.querySelector(this.props.modalContainer); + } + + componentDidMount() { + this.modalRoot.appendChild(this.element); + } + + componentWillUnmount() { + this.modalRoot.removeChild(this.element); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.element); + } +} diff --git a/lib/js/app/queryCreator/components/Portal/index.ts b/lib/js/app/queryCreator/components/Portal/index.ts new file mode 100644 index 000000000..eb8f5d4da --- /dev/null +++ b/lib/js/app/queryCreator/components/Portal/index.ts @@ -0,0 +1,3 @@ +import Portal from './Portal'; + +export default Portal; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.test.tsx b/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.test.tsx new file mode 100644 index 000000000..8f24b18c6 --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render as rtlRender, fireEvent } from '@testing-library/react'; + +import PropertiesTree from './PropertiesTree'; + +const render = (overProps: any = {}) => { + const props = { + onClick: jest.fn(), + expanded: false, + ...overProps, + }; + + const wrapper = rtlRender( + + ); + + return { + props, + wrapper, + }; +}; + +test('allows user to select nested property', () => { + const properties = { + category: ['category', 'string'], + user: { + id: ['user.id', 'number'], + } + }; + + const { wrapper: { getByText }, props } = render({ properties }); + + const title = getByText('user'); + fireEvent.click(title); + + const property = getByText('id'); + fireEvent.click(property); + + expect(props.onClick.mock.calls[0][1]).toEqual('user.id'); +}); + +test('renders properties from all tree levels', () => { + const properties = { + category: ['category', 'string'], + user: { + details: { + name: ['user.details.name', 'string'], + } + } + }; + + const { wrapper: { getByText } } = render({ properties, expanded: true }); + const property = getByText('name'); + + expect(property).toBeInTheDocument(); +}); + +test('expands all properties tree levels', () => { + const properties = { + category: ['category', 'string'], + user: { + details: { + name: ['user.details.name', 'string'], + } + } + }; + + const { wrapper: { getByText, rerender }, props } = render({ properties }); + rerender() + + const property = getByText('name'); + + expect(property).toBeInTheDocument(); +}); diff --git a/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.tsx b/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.tsx index 925997e90..851595239 100644 --- a/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.tsx +++ b/lib/js/app/queryCreator/components/PropertiesTree/PropertiesTree.tsx @@ -1,6 +1,11 @@ import React, { FC } from 'react'; -import { TreeLevel, TreeLeaf } from './components'; +import { TreeLevel } from './components'; + +import PropertyTreeItem from '../PropertyTreeItem'; +import { getPropertyType, getPropertyPath } from './utils'; + +import { PADDING } from './constants'; type Props = { /** Properties tree */ @@ -9,8 +14,6 @@ type Props = { onClick: (e: React.MouseEvent, propertyPath: string) => void; /** Expand all tree levels */ expanded?: boolean; - /** Open indicator */ - isOpen?: boolean; }; const PropertiesTree: FC = ({ expanded, onClick, properties }) => { @@ -21,12 +24,13 @@ const PropertiesTree: FC = ({ expanded, onClick, properties }) => { {keys.map((key) => { if (Array.isArray(properties[key])) { return ( - onClick(e, properties[key][0])} + padding={PADDING} + propertyName={key} + propertyPath={getPropertyPath(properties[key] as string[])} + type={getPropertyType(properties[key] as string[])} + onClick={(e, propertyPath) => onClick(e, propertyPath)} /> ); } else { diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.tsx b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.tsx deleted file mode 100644 index 6a5f3a6aa..000000000 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { FC } from 'react'; - -import { Container, Type } from './TreeLeaf.styles'; - -import { PADDING } from '../../constants'; - -type Props = { - /** Property name */ - name: string; - /** Property type */ - type: string; - /** Padding space */ - padding: number; - /** Click event handler */ - onClick: (e: React.MouseEvent) => void; -}; - -const TreeLeaf: FC = ({ padding, onClick, name, type }) => { - return ( - - {name} - {type} - - ); -}; - -export default TreeLeaf; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/index.ts b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/index.ts deleted file mode 100644 index f9cea5c28..000000000 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import TreeLeaf from './TreeLeaf'; - -export default TreeLeaf; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.styles.ts b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.styles.ts index 5d19587b1..e01eac52b 100644 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.styles.ts +++ b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.styles.ts @@ -12,8 +12,15 @@ export const Header = styled.div` cursor: pointer; `; +export const Title = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + export const MotionIcon = styled(motion.div)` display: flex; + flex-shrink: 0; align-items: center; margin-left: 4px; `; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.tsx b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.tsx index 9f635e9d1..067109cc4 100644 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.tsx +++ b/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLevel/TreeLevel.tsx @@ -2,8 +2,10 @@ import React, { FC, useState, useEffect } from 'react'; import { Icon } from '@keen.io/icons'; import { colors } from '@keen.io/colors'; -import { Header, MotionIcon } from './TreeLevel.styles'; -import TreeLeaf from '../TreeLeaf'; +import { Header, Title, MotionIcon } from './TreeLevel.styles'; +import PropertyTreeItem from '../../../PropertyTreeItem'; + +import { getPropertyType, getPropertyPath } from '../../utils'; import { PADDING } from '../../constants'; @@ -39,9 +41,9 @@ const TreeLevel: FC = ({
setOpen(!isOpen)} - style={{ paddingLeft: level * PADDING }} + style={{ paddingLeft: level * PADDING, paddingRight: PADDING }} > - {header} + {header} = ({ keys.map((key) => { if (Array.isArray(properties[key])) { return ( - { - onClick(e, properties[key][0]); - }} + type={getPropertyType(properties[key] as string[])} + onClick={(e, propertyPath) => onClick(e, propertyPath)} /> ); } else { return ( -
- } - /> -
+ } + /> ); } })} diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/index.ts b/lib/js/app/queryCreator/components/PropertiesTree/components/index.ts index 48e68b103..bf48092c3 100644 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/index.ts +++ b/lib/js/app/queryCreator/components/PropertiesTree/components/index.ts @@ -1,4 +1,3 @@ import TreeLevel from './TreeLevel'; -import TreeLeaf from './TreeLeaf'; -export { TreeLevel, TreeLeaf }; +export { TreeLevel }; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.test.ts b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.test.ts new file mode 100644 index 000000000..493e32636 --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.test.ts @@ -0,0 +1,7 @@ +import getPropertyPath from './getPropertyPath'; + +test('extracts property path ', () => { + const property = ['user.id', 'string']; + + expect(getPropertyPath(property)).toEqual('user.id'); +}); diff --git a/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.ts b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.ts new file mode 100644 index 000000000..7191bde19 --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyPath.ts @@ -0,0 +1,6 @@ +const getPropertyPath = (property: string[]) => { + const [path] = property; + return path; +}; + +export default getPropertyPath; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.test.ts b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.test.ts new file mode 100644 index 000000000..ea178785e --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.test.ts @@ -0,0 +1,7 @@ +import getPropertyType from './getPropertyType'; + +test('extracts property type ', () => { + const property = ['id', 'string']; + + expect(getPropertyType(property)).toEqual('string'); +}); diff --git a/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.ts b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.ts new file mode 100644 index 000000000..d44d942a3 --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/utils/getPropertyType.ts @@ -0,0 +1,6 @@ +const getPropertyType = (property: string[]) => { + const [, type] = property; + return type; +}; + +export default getPropertyType; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/utils/index.ts b/lib/js/app/queryCreator/components/PropertiesTree/utils/index.ts new file mode 100644 index 000000000..762e542f7 --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertiesTree/utils/index.ts @@ -0,0 +1,4 @@ +import getPropertyType from './getPropertyType'; +import getPropertyPath from './getPropertyPath'; + +export { getPropertyType, getPropertyPath }; diff --git a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.styles.ts b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.styles.ts similarity index 54% rename from lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.styles.ts rename to lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.styles.ts index 2044d2078..2afc1a89b 100644 --- a/lib/js/app/queryCreator/components/PropertiesTree/components/TreeLeaf/TreeLeaf.styles.ts +++ b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.styles.ts @@ -4,12 +4,19 @@ import { colors } from '@keen.io/colors'; export const Container = styled.div<{ isActive?: boolean; + padding: number; }>` + position: relative; display: flex; font-family: Lato Regular, sans-serif; font-size: 14px; color: ${colors.blue[500]}; + padding-left: ${(props) => props.padding}px; + padding-right: 15px; + padding-top: 10px; + padding-bottom: 10px; + cursor: pointer; transition: background 0.2s linear; @@ -25,6 +32,29 @@ export const Container = styled.div<{ `; export const Type = styled.div` + padding-left: 5px; margin-left: auto; + flex-shrink: 0; color: ${transparentize(0.5, colors.black[500])}; `; + +export const Name = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const Path = styled.div<{ + isBold: boolean; +}>` + font-size: 14px; + font-family: Lato Regular, sans-serif; + line-height: 17px; + color: ${colors.white[500]}; + + ${(props) => + props.isBold && + css` + font-family: Lato Bold, sans-serif; + `} +`; diff --git a/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.test.tsx b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.test.tsx new file mode 100644 index 000000000..f4d88f43a --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render as rtlRender, fireEvent } from '@testing-library/react'; + +import PropertyTreeItem from './PropertyTreeItem'; + +const render = (overProps: any = {}) => { + const props = { + onClick: jest.fn(), + type: 'datetime', + padding: 15, + propertyName: 'clicks', + propertyPath: 'users.cliks', + ...overProps, + }; + + const wrapper = rtlRender( + + ); + + return { + props, + wrapper, + }; +}; + +test('shows the property type', () => { + const { wrapper: { getByText }, props } = render(); + + expect(getByText(props.type)).toBeInTheDocument(); +}); + +test('allows user to select property', () => { + const { wrapper: { getByText }, props } = render(); + + const element = getByText(props.propertyName); + fireEvent.click(element); + + expect(props.onClick.mock.calls[0][1]).toEqual(props.propertyPath); +}); diff --git a/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.tsx b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.tsx new file mode 100644 index 000000000..4f8ca7d0a --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertyTreeItem/PropertyTreeItem.tsx @@ -0,0 +1,106 @@ +import React, { FC, useState, useEffect, useRef, useContext } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Tooltip } from '@keen.io/ui-core'; + +import { Container, Type, Path, Name } from './PropertyTreeItem.styles'; +import Portal from '../Portal'; + +import { AppContext } from '../../contexts'; + +import { SEPARATOR } from './constants'; + +type Props = { + /** Property name */ + propertyName: string; + /** Property path */ + propertyPath: string; + /** Property type */ + type: string; + /** Left padding spacing */ + padding: number; + /** Click event handler */ + onClick: (e: React.MouseEvent, propertyPath: string) => void; +}; + +const tooltipMotion = { + transition: { duration: 0.3 }, + exit: { opacity: 0 }, +}; + +const PropertyTreeItem: FC = ({ + padding, + onClick, + propertyName, + type, + propertyPath, +}) => { + const { modalContainer } = useContext(AppContext); + const [isEllipsisActive, setEllipsis] = useState(false); + + const nameRef = useRef(null); + const [tooltip, setTooltip] = useState({ + visible: false, + x: 0, + y: 0, + }); + + useEffect(() => { + const element = nameRef.current; + const contentOverflow = element.offsetWidth < element.scrollWidth; + if (contentOverflow) { + setEllipsis(true); + } + }, []); + + return ( + { + if (isEllipsisActive) { + setTooltip({ x: e.pageX, y: e.pageY, visible: true }); + } + }} + onMouseLeave={() => { + if (isEllipsisActive) setTooltip({ x: 0, y: 0, visible: false }); + }} + padding={padding} + onClick={(e) => onClick(e, propertyPath)} + > + {propertyName} + {type} + + {tooltip.visible && ( + + + + {propertyPath + .split(SEPARATOR) + .map((path: string, idx: number, collection) => ( + + {idx > 0 ? '.' : ''} + {path} + + ))} + + + + )} + + + ); +}; + +export default PropertyTreeItem; diff --git a/lib/js/app/queryCreator/components/PropertyTreeItem/constants.ts b/lib/js/app/queryCreator/components/PropertyTreeItem/constants.ts new file mode 100644 index 000000000..c1cf17eee --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertyTreeItem/constants.ts @@ -0,0 +1 @@ +export const SEPARATOR = '.'; diff --git a/lib/js/app/queryCreator/components/PropertyTreeItem/index.ts b/lib/js/app/queryCreator/components/PropertyTreeItem/index.ts new file mode 100644 index 000000000..c4404e07f --- /dev/null +++ b/lib/js/app/queryCreator/components/PropertyTreeItem/index.ts @@ -0,0 +1,3 @@ +import PropertyTreeItem from './PropertyTreeItem'; + +export default PropertyTreeItem; diff --git a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.styles.ts b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.styles.ts index 066f2acbc..8bb77b5cd 100644 --- a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.styles.ts +++ b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.styles.ts @@ -11,5 +11,5 @@ export const PropertyOverflow = styled.div` export const TreeContainer = styled.div` padding: 10px 0; overflow-y: scroll; - max-height: 140px; + max-height: 400px; `; diff --git a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.test.tsx b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.test.tsx index cb2b2aa42..b92bfa23d 100644 --- a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.test.tsx +++ b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.test.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { render as rtlRender, fireEvent } from '@testing-library/react'; +import { render as rtlRender, fireEvent, act } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import TargetProperty from './TargetProperty'; +import text from './text.json'; import { createTree } from '../../utils/createTree'; import { createCollection } from '../../utils/createCollection'; @@ -31,6 +32,8 @@ const render = (storeState: any = {}, overProps: any = {}) => { }; }; + + test('allows user to select target property', async () => { const collectionSchema = { date: 'String', userId: 'String' }; const storeState = { @@ -94,6 +97,73 @@ test('allows user to search for target property', async () => { expect(getByText('industry')).toBeInTheDocument(); }); +test('expands search results', async () => { + jest.useFakeTimers(); + const collectionSchema = { 'category.geology.plants.flower': 'String' }; + + const storeState = { + query: { + targetProperty: undefined, + }, + events: { + schemas: { + purchases: { + schema: collectionSchema, + tree: createTree(collectionSchema), + list: createCollection(collectionSchema), + }, + }, + }, + }; + + const { + wrapper: { getByText, getByTestId }, + } = render(storeState, { collection: 'purchases' }); + + const propertyField = getByTestId('dropable-container'); + fireEvent.click(propertyField); + + const input = getByTestId('dropable-container-input'); + fireEvent.change(input, { target: { value: 'flower' } }); + + act(() => { + jest.runAllTimers(); + + }); + + expect(getByText('flower')).toBeInTheDocument(); +}); + +test('renders empty search results message', async () => { + const collectionSchema = {}; + const storeState = { + query: { + targetProperty: undefined, + }, + events: { + schemas: { + purchases: { + schema: collectionSchema, + tree: createTree(collectionSchema), + list: createCollection(collectionSchema), + }, + }, + }, + }; + + const { + wrapper: { getByText, getByTestId }, + } = render(storeState, { collection: 'purchases' }); + + const propertyField = getByTestId('dropable-container'); + fireEvent.click(propertyField); + + const input = getByTestId('dropable-container-input'); + fireEvent.change(input, { target: { value: 'industry' } }); + + expect(getByText(text.emptySearchResults)).toBeInTheDocument(); +}); + test('reset target property settings', async () => { const collectionSchema = { date: 'String', userId: 'String' }; const storeState = { diff --git a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.tsx b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.tsx index 4145e0911..61fab3ea1 100644 --- a/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.tsx +++ b/lib/js/app/queryCreator/components/TargetProperty/TargetProperty.tsx @@ -40,6 +40,9 @@ const TargetProperty: FC = ({ variant = 'primary', }) => { const [searchPhrase, setSearchPhrase] = useState(null); + const [expandTree, setTreeExpand] = useState(false); + const expandTrigger = useRef(null); + const { schema: collectionSchema, tree: schemaTree, @@ -62,6 +65,7 @@ const TargetProperty: FC = ({ }>( schemaList, (searchResult, phrase) => { + if (expandTrigger.current) clearTimeout(expandTrigger.current); if (phrase) { const searchTree = {}; searchResult.forEach(({ path, type }) => { @@ -69,7 +73,12 @@ const TargetProperty: FC = ({ }); setSearchPhrase(phrase); setPropertiesTree(createTree(searchTree)); + + expandTrigger.current = setTimeout(() => { + setTreeExpand(true); + }, EXPAND_TRESHOLD); } else { + setTreeExpand(false); setPropertiesTree(null); } }, @@ -88,6 +97,7 @@ const TargetProperty: FC = ({ useEffect(() => { if (!isOpen) { + setTreeExpand(false); setSearchPhrase(null); } }, [isOpen]); @@ -126,7 +136,7 @@ const TargetProperty: FC = ({ ) : ( EXPAND_TRESHOLD} + expanded={expandTree} onClick={(_e, property) => { setOpen(false); onChange(property); diff --git a/lib/js/app/queryCreator/components/TargetProperty/constants.ts b/lib/js/app/queryCreator/components/TargetProperty/constants.ts index 850d47d43..8a47c00b3 100644 --- a/lib/js/app/queryCreator/components/TargetProperty/constants.ts +++ b/lib/js/app/queryCreator/components/TargetProperty/constants.ts @@ -1,2 +1,2 @@ export const SEPARATOR = '.'; -export const EXPAND_TRESHOLD = 3; +export const EXPAND_TRESHOLD = 500; diff --git a/lib/js/app/queryCreator/contexts/AppContext.ts b/lib/js/app/queryCreator/contexts/AppContext.ts new file mode 100644 index 000000000..2eaed6f9c --- /dev/null +++ b/lib/js/app/queryCreator/contexts/AppContext.ts @@ -0,0 +1,9 @@ +import React from 'react'; + +const AppContext = React.createContext<{ + modalContainer: string; +}>({ + modalContainer: null, +}); + +export default AppContext; diff --git a/lib/js/app/queryCreator/contexts/index.ts b/lib/js/app/queryCreator/contexts/index.ts new file mode 100644 index 000000000..0c0168ab1 --- /dev/null +++ b/lib/js/app/queryCreator/contexts/index.ts @@ -0,0 +1,3 @@ +import AppContext from './AppContext'; + +export { AppContext }; diff --git a/test/demo/index.html b/test/demo/index.html index bedbf6228..284034e52 100644 --- a/test/demo/index.html +++ b/test/demo/index.html @@ -8,13 +8,21 @@ + +