diff --git a/src/routes/Apps/App/Actions.tsx b/src/routes/Apps/App/Actions.tsx index 51bdeb3a..4868be6f 100644 --- a/src/routes/Apps/App/Actions.tsx +++ b/src/routes/Apps/App/Actions.tsx @@ -12,10 +12,17 @@ import FormattedMessageId from '../../../components/FormattedMessageId' import { returntypeof } from 'react-redux-typescript' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { RouteComponentProps } from 'react-router' import { ActionCreatorEditor } from '../../../components/modals' import { State } from '../../../types' import { FM } from '../../../react-intl-messages' import { injectIntl, InjectedIntlProps } from 'react-intl' +import * as queryString from 'query-string' + +const queryParams = { + selectedActionId: 'selectedActionId' +} interface ComponentState { actionSelected: CLM.ActionBase | null @@ -46,15 +53,71 @@ class Actions extends React.Component { componentDidMount() { this.focusNewActionButton() + this.handleQueryParameters(this.props.location.search) + } + + componentWillReceiveProps(nextProps: Props) { + this.handleQueryParameters(nextProps.location.search, this.props.location.search) + } + + + @OF.autobind + async handleQueryParameters(newSearch: string, oldSearch?: string): Promise { + if (this.props.actions.length === 0) { + return + } + + const searchParams = new URLSearchParams(newSearch) + const selectedActionId = searchParams.get(queryParams.selectedActionId) + + if (oldSearch) { + const searchParamsPrev = new URLSearchParams(oldSearch) + const selectedActionIdPrev = searchParamsPrev.get(queryParams.selectedActionId) + // If query parameter hasn't changed, no action to take + if (selectedActionId === selectedActionIdPrev) { + return + } + } + + /** + * If there a id in URL, and action is not already open or is different than open item, open the item from url. + */ + if (selectedActionId + && (!this.state.actionSelected + || (selectedActionId !== this.state.actionSelected.actionId)) + ) { + const action = this.props.actions.find(a => a.actionId === selectedActionId) + if (!action) { + // Invalid action id, go back to list + this.props.history.replace(this.props.match.path, { app: this.props.app }) + return + } + + this.openAction(action) + } } onSelectAction(action: CLM.ActionBase) { - if (this.props.editingPackageId === this.props.app.devPackageId) { - this.setState({ - actionSelected: action, - isActionEditorOpen: true - }) + const isEditingDevPackage = this.props.editingPackageId === this.props.app.devPackageId + if (!isEditingDevPackage) { + return } + + this.openAction(action) + } + + private async openAction(action: CLM.ActionBase) { + const queryObject = { + [queryParams.selectedActionId]: action.actionId + } + const query = queryString.stringify(queryObject) + const url = `${this.props.match.path}?${query}` + this.props.history.push(url, { app: this.props.app }) + + this.setState({ + actionSelected: action, + isActionEditorOpen: true + }) } onClickOpenActionEditor() { @@ -66,6 +129,13 @@ class Actions extends React.Component { @OF.autobind onClickCancelActionEditor() { + // Remove selection from query parameter + const searchParams = new URLSearchParams(this.props.location.search) + const selectedActionId = searchParams.get(queryParams.selectedActionId) + if (selectedActionId) { + this.props.history.replace(this.props.match.path, { app: this.props.app }) + } + this.setState({ isActionEditorOpen: false, actionSelected: null @@ -75,14 +145,9 @@ class Actions extends React.Component { } @OF.autobind - async onClickDeleteActionEditor(action: CLM.ActionBase, removeFromDialogs: boolean) { - await Utils.setStateAsync(this, { - isActionEditorOpen: false, - actionSelected: null - }) - + onClickDeleteActionEditor(action: CLM.ActionBase, removeFromDialogs: boolean) { + this.onClickCancelActionEditor() this.props.deleteActionThunkAsync(this.props.app.appId, action.actionId, removeFromDialogs) - setTimeout(() => this.focusNewActionButton(), 1000) } @OF.autobind @@ -236,6 +301,7 @@ export interface ReceivedProps { // Props types inferred from mapStateToProps & dispatchToProps const stateProps = returntypeof(mapStateToProps); const dispatchProps = returntypeof(mapDispatchToProps); -type Props = typeof stateProps & typeof dispatchProps & ReceivedProps & InjectedIntlProps; +type Props = typeof stateProps & typeof dispatchProps & ReceivedProps & InjectedIntlProps & RouteComponentProps -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Actions) as any) +// TODO: Why use 'as any' hack? This component is almost same as Entities component. +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(injectIntl(Actions) as any) as any) diff --git a/src/routes/Apps/App/Entities.tsx b/src/routes/Apps/App/Entities.tsx index bf783123..4b0d2cfd 100644 --- a/src/routes/Apps/App/Entities.tsx +++ b/src/routes/Apps/App/Entities.tsx @@ -6,6 +6,8 @@ import * as React from 'react' import { returntypeof } from 'react-redux-typescript' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { RouteComponentProps } from 'react-router' import * as OF from 'office-ui-fabric-react' import { EntityCreatorEditor } from '../../../components/modals' import actions from '../../../actions' @@ -17,6 +19,7 @@ import FormattedMessageId from '../../../components/FormattedMessageId' import { injectIntl, InjectedIntl, InjectedIntlProps } from 'react-intl' import * as Util from '../../../Utils/util' import * as moment from 'moment' +import * as queryString from 'query-string' interface IRenderableColumn extends OF.IColumn { render: (entity: EntityBase, component: Entities) => JSX.Element | JSX.Element[] @@ -136,6 +139,10 @@ function getColumns(intl: InjectedIntl): IRenderableColumn[] { ] } +const queryParams = { + selectedEntityId: 'selectedEntityId' +} + interface ComponentState { searchValue: string createEditModalOpen: boolean @@ -145,8 +152,7 @@ interface ComponentState { } class Entities extends React.Component { - newEntityButtonRef = React.createRef() - state: ComponentState + private newEntityButtonRef = React.createRef() constructor(props: Props) { super(props) @@ -177,16 +183,53 @@ class Entities extends React.Component { componentDidMount() { this.focusNewEntityButton() + this.handleQueryParameters(this.props.location.search) + } + + componentWillReceiveProps(nextProps: Props) { + this.handleQueryParameters(nextProps.location.search, this.props.location.search) + } + + @OF.autobind + async handleQueryParameters(newSearch: string, oldSearch?: string): Promise { + if (this.props.entities.length === 0) { + return + } + + const searchParams = new URLSearchParams(newSearch) + const selectedEntityId = searchParams.get(queryParams.selectedEntityId) + + if (oldSearch) { + const searchParamsPrev = new URLSearchParams(oldSearch) + const selectedEntityIdPrev = searchParamsPrev.get(queryParams.selectedEntityId) + // If query parameter hasn't changed, no action to take + if (selectedEntityId === selectedEntityIdPrev) { + return + } + } + + /** + * If there a id in URL, and entity is not already open or is different than open item, open the item from url. + */ + if (selectedEntityId + && (!this.state.entitySelected + || (selectedEntityId !== this.state.entitySelected.entityId)) + ) { + const entity = this.props.entities.find(e => e.entityId === selectedEntityId) + if (!entity) { + // Invalid entity id, go back to list + this.props.history.replace(this.props.match.path, { app: this.props.app }) + return + } + + this.openEntity(entity) + } } @OF.autobind handleDelete(entity: EntityBase) { - this.setState({ - createEditModalOpen: false, - entitySelected: null - }) + this.handleCloseCreateModal() this.props.deleteEntityThunkAsync(this.props.app.appId, entity) - setTimeout(() => this.focusNewEntityButton(), 1000) } @OF.autobind @@ -199,6 +242,13 @@ class Entities extends React.Component { @OF.autobind handleCloseCreateModal() { + // Remove selection from query parameter + const searchParams = new URLSearchParams(this.props.location.search) + const selectedEntityId = searchParams.get(queryParams.selectedEntityId) + if (selectedEntityId) { + this.props.history.replace(this.props.match.path, { app: this.props.app }) + } + this.setState({ createEditModalOpen: false, entitySelected: null @@ -209,12 +259,12 @@ class Entities extends React.Component { } onSelectEntity(entity: EntityBase) { - if (this.props.editingPackageId === this.props.app.devPackageId) { - this.setState({ - entitySelected: entity, - createEditModalOpen: true - }) + const isEditingDevPackage = this.props.editingPackageId === this.props.app.devPackageId + if (!isEditingDevPackage) { + return } + + this.openEntity(entity) } @OF.autobind @@ -368,6 +418,20 @@ class Entities extends React.Component { ); } + private async openEntity(entity: EntityBase) { + const queryObject = { + [queryParams.selectedEntityId]: entity.entityId + } + const query = queryString.stringify(queryObject) + const url = `${this.props.match.path}?${query}` + this.props.history.push(url, { app: this.props.app }) + + this.setState({ + entitySelected: entity, + createEditModalOpen: true + }) + } + private focusNewEntityButton() { if (this.newEntityButtonRef.current) { this.newEntityButtonRef.current.focus() @@ -394,6 +458,6 @@ export interface ReceivedProps { // Props types inferred from mapStateToProps & dispatchToProps const stateProps = returntypeof(mapStateToProps); const dispatchProps = returntypeof(mapDispatchToProps); -type Props = typeof stateProps & typeof dispatchProps & ReceivedProps & InjectedIntlProps +type Props = typeof stateProps & typeof dispatchProps & ReceivedProps & InjectedIntlProps & RouteComponentProps -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Entities)) +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(injectIntl(Entities)))