Skip to content
Snippets Groups Projects
Commit b0b7e1d4 authored by Kyle Doherty's avatar Kyle Doherty Committed by GitHub
Browse files

Allow users to navigate sequentially between object details (#5832)

* intitial version of increment and decrement object detail

* add tests

* add keyboard shortcuts and styling

* create standardized back / forward button

* fix object detail unit test

* add tests for keyboard shortcuts
parent c3f6955d
No related branches found
No related tags found
No related merge requests found
import React from 'react'
import Icon from 'metabase/components/Icon'
const DirectionalButton = ({ direction = "back", onClick }) =>
<div
className="shadowed cursor-pointer text-grey-4 flex align-center circle p2 bg-white transition-background transition-color"
onClick={onClick}
style={{
border: "1px solid #DCE1E4",
boxShadow: "0 2px 4px 0 #DCE1E4"
}}
>
<Icon name={`${direction}Arrow`} />
</div>
export default DirectionalButton
......@@ -5,6 +5,7 @@ import _ from "underscore";
import cx from "classnames";
import SearchHeader from "metabase/components/SearchHeader";
import DirectionalButton from "metabase/components/DirectionalButton";
import { caseInsensitiveSearch } from "metabase/lib/string";
import Icon from "metabase/components/Icon";
......@@ -169,15 +170,9 @@ export default class EntitySearch extends Component {
<div className="bg-slate-extra-light full Entity-search">
<div className="wrapper wrapper--small pt4 pb4">
<div className="flex mb4 align-center" style={{ height: "50px" }}>
<Icon
className="Entity-search-back-button shadowed cursor-pointer text-grey-4 mr2 flex align-center circle p2 bg-white transition-background transition-color"
style={{
border: "1px solid #DCE1E4",
boxShadow: "0 2px 4px 0 #DCE1E4"
}}
name="backArrow"
onClick={ () => backButtonUrl ? onChangeLocation(backButtonUrl) : window.history.back() }
/>
<div className="Entity-search-back-button mr2" onClick={ () => backButtonUrl ? onChangeLocation(backButtonUrl) : window.history.back() }>
<DirectionalButton direction="back" />
</div>
<div className="text-centered flex-full">
<h2>{title}</h2>
</div>
......
......@@ -233,6 +233,14 @@ ICON_PATHS["horizontal_bar"] = {
}
};
// $FlowFixMe
ICON_PATHS["forwardArrow"] = {
path: ICON_PATHS["backArrow"],
attrs: {
style: { transform: "rotate(-180deg)" }
}
};
// $FlowFixMe
ICON_PATHS["scalar"] = ICON_PATHS["number"];
......
......@@ -1172,7 +1172,47 @@ export const archiveQuestion = createThunkAction(ARCHIVE_QUESTION, (questionId,
}
)
export const VIEW_NEXT_OBJECT_DETAIL = 'metabase/qb/VIEW_NEXT_OBJECT_DETAIL'
export const viewNextObjectDetail = () => {
return (dispatch, getState) => {
const question = getQuestion(getState());
let filter = question.query().filters()[0]
let newFilter = ["=", filter[1], filter[2] + 1]
dispatch.action(VIEW_NEXT_OBJECT_DETAIL)
dispatch(updateQuestion(
question.query().updateFilter(0, newFilter).question()
))
dispatch(runQuestionQuery());
}
}
export const VIEW_PREVIOUS_OBJECT_DETAIL = 'metabase/qb/VIEW_PREVIOUS_OBJECT_DETAIL'
export const viewPreviousObjectDetail = () => {
return (dispatch, getState) => {
const question = getQuestion(getState());
let filter = question.query().filters()[0]
if(filter[2] === 1) {
return false
}
let newFilter = ["=", filter[1], filter[2] - 1]
dispatch.action(VIEW_PREVIOUS_OBJECT_DETAIL)
dispatch(updateQuestion(
question.query().updateFilter(0, newFilter).question()
))
dispatch(runQuestionQuery());
}
}
// these are just temporary mappings to appease the existing QB code and it's naming prefs
export const toggleDataReferenceFn = toggleDataReference;
......
/* @flow weak */
import React, { Component } from "react";
import { connect } from 'react-redux';
import DirectionalButton from 'metabase/components/DirectionalButton';
import ExpandableString from 'metabase/query_builder/components/ExpandableString.jsx';
import Icon from 'metabase/components/Icon.jsx';
import IconBorder from 'metabase/components/IconBorder.jsx';
......@@ -13,15 +15,27 @@ import { singularize, inflect } from 'inflection';
import { formatValue, formatColumn } from "metabase/lib/formatting";
import { isQueryable } from "metabase/lib/table";
import { viewPreviousObjectDetail, viewNextObjectDetail } from 'metabase/query_builder/actions'
import cx from "classnames";
import _ from "underscore";
import type { VisualizationProps } from "metabase/meta/types/Visualization";
type Props = VisualizationProps;
type Props = VisualizationProps & {
viewNextObjectDetail: () => void,
viewPreviousObjectDetail: () => void
}
const mapStateToProps = () => ({})
const mapDispatchToProps = {
viewPreviousObjectDetail,
viewNextObjectDetail
}
export default class ObjectDetail extends Component {
props: Props;
export class ObjectDetail extends Component {
props: Props
static uiName = "Object Detail";
static identifier = "object";
......@@ -33,6 +47,11 @@ export default class ObjectDetail extends Component {
componentDidMount() {
// load up FK references
this.props.loadObjectDetailFKReferences();
window.addEventListener('keydown', this.onKeyDown, true)
}
componentWillUnMount() {
window.removeEventListener('keydown', this.onKeyDown)
}
componentWillReceiveProps(nextProps) {
......@@ -200,6 +219,15 @@ export default class ObjectDetail extends Component {
);
}
onKeyDown = (event) => {
if(event.key === 'ArrowLeft') {
this.props.viewPreviousObjectDetail()
}
if(event.key === 'ArrowRight') {
this.props.viewNextObjectDetail()
}
}
render() {
if(!this.props.data) {
return false;
......@@ -231,7 +259,27 @@ export default class ObjectDetail extends Component {
<div className="Grid-cell ObjectDetail-infoMain p4">{this.renderDetailsTable()}</div>
<div className="Grid-cell Cell--1of3 bg-alt">{this.renderRelationships()}</div>
</div>
<div
className={cx("fixed left cursor-pointer text-brand-hover lg-ml2", { "disabled": idValue <= 1 })}
style={{ top: '50%', left: '1em', transform: 'translate(0, -50%)' }}
>
<DirectionalButton
direction="back"
onClick={this.props.viewPreviousObjectDetail}
/>
</div>
<div
className="fixed right cursor-pointer text-brand-hover lg-ml2"
style={{ top: '50%', right: '1em', transform: 'translate(0, -50%)' }}
>
<DirectionalButton
direction="forward"
onClick={this.props.viewNextObjectDetail}
/>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ObjectDetail)
import {
login,
createSavedQuestion,
createTestStore
} from "__support__/integrated_tests";
import {
click,
dispatchBrowserEvent
} from "__support__/enzyme_utils"
import { mount } from 'enzyme'
import {
INITIALIZE_QB,
QUERY_COMPLETED,
} from "metabase/query_builder/actions";
import Question from "metabase-lib/lib/Question"
import { getMetadata } from "metabase/selectors/metadata";
describe('ObjectDetail', () => {
beforeAll(async () => {
await login()
})
describe('Increment and Decrement', () => {
it('should properly increment and decrement object deteail', async () => {
const store = await createTestStore()
const newQuestion = Question.create({databaseId: 1, tableId: 1, metadata: getMetadata(store.getState())})
.query()
.addFilter(["=", ["field-id", 2], 2])
.question()
.setDisplayName('Object Detail')
const savedQuestion = await createSavedQuestion(newQuestion);
store.pushPath(savedQuestion.getUrl());
const app = mount(store.getAppContainer());
await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
expect(app.find('.ObjectDetail h1').text()).toEqual("2")
const previousObjectTrigger = app.find('.Icon.Icon-backArrow')
click(previousObjectTrigger)
await store.waitForActions([QUERY_COMPLETED]);
expect(app.find('.ObjectDetail h1').text()).toEqual("1")
const nextObjectTrigger = app.find('.Icon.Icon-forwardArrow')
click(nextObjectTrigger)
await store.waitForActions([QUERY_COMPLETED]);
expect(app.find('.ObjectDetail h1').text()).toEqual("2")
// test keyboard shortcuts
// left arrow
dispatchBrowserEvent('keydown', { key: 'ArrowLeft' })
await store.waitForActions([QUERY_COMPLETED]);
expect(app.find('.ObjectDetail h1').text()).toEqual("1")
// left arrow
dispatchBrowserEvent('keydown', { key: 'ArrowRight' })
await store.waitForActions([QUERY_COMPLETED]);
expect(app.find('.ObjectDetail h1').text()).toEqual("2")
})
})
})
import React from 'react'
import { mount } from 'enzyme'
import ObjectDetail from 'metabase/visualizations/visualizations/ObjectDetail'
import { ObjectDetail } from 'metabase/visualizations/visualizations/ObjectDetail'
import { TYPE } from "metabase/lib/types";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment