|
@@ -2,9 +2,9 @@ import {ClassNames} from '@emotion/core';
|
|
|
import PropTypes from 'prop-types';
|
|
|
import React from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
+import $ from 'jquery';
|
|
|
import createReactClass from 'create-react-class';
|
|
|
import Reflux from 'reflux';
|
|
|
-import * as Sentry from '@sentry/browser';
|
|
|
|
|
|
import theme from 'app/utils/theme';
|
|
|
import {
|
|
@@ -15,94 +15,78 @@ import {
|
|
|
recordFinish,
|
|
|
dismissGuide,
|
|
|
} from 'app/actionCreators/guides';
|
|
|
-import {CloseIcon} from 'app/components/assistant/styles';
|
|
|
-import {Guide} from 'app/components/assistant/types';
|
|
|
-import {t} from 'app/locale';
|
|
|
import GuideStore from 'app/stores/guideStore';
|
|
|
import Hovercard from 'app/components/hovercard';
|
|
|
import Button from 'app/components/button';
|
|
|
import space from 'app/styles/space';
|
|
|
+import {t} from 'app/locale';
|
|
|
+import {CloseIcon} from 'app/components/assistant/styles';
|
|
|
|
|
|
-type Props = {
|
|
|
- target: string;
|
|
|
- position?: string;
|
|
|
-};
|
|
|
-
|
|
|
-type State = {
|
|
|
- active: boolean;
|
|
|
- orgId: string | null;
|
|
|
- currentGuide?: Guide;
|
|
|
- step?: number;
|
|
|
-};
|
|
|
-
|
|
|
-/**
|
|
|
- * A GuideAnchor puts an informative hovercard around an element.
|
|
|
- * Guide anchors register with the GuideStore, which uses registrations
|
|
|
- * from one or more anchors on the page to determine which guides can
|
|
|
- * be shown on the page.
|
|
|
- */
|
|
|
-const GuideAnchor = createReactClass<Props, State>({
|
|
|
+// A GuideAnchor puts an informative hovercard around an element.
|
|
|
+// Guide anchors register with the GuideStore, which uses registrations
|
|
|
+// from one or more anchors on the page to determine which guides can
|
|
|
+// be shown on the page.
|
|
|
+const GuideAnchor = createReactClass({
|
|
|
propTypes: {
|
|
|
target: PropTypes.string.isRequired,
|
|
|
position: PropTypes.string,
|
|
|
},
|
|
|
|
|
|
- mixins: [Reflux.listenTo(GuideStore, 'onGuideStateChange') as any],
|
|
|
+ mixins: [Reflux.listenTo(GuideStore, 'onGuideStateChange')],
|
|
|
|
|
|
getInitialState() {
|
|
|
return {
|
|
|
active: false,
|
|
|
- orgId: null,
|
|
|
};
|
|
|
},
|
|
|
|
|
|
componentDidMount() {
|
|
|
- const {target} = this.props;
|
|
|
- target && registerAnchor(target);
|
|
|
+ registerAnchor(this);
|
|
|
},
|
|
|
|
|
|
componentDidUpdate(_prevProps, prevState) {
|
|
|
if (this.containerElement && !prevState.active && this.state.active) {
|
|
|
- try {
|
|
|
- const {top} = this.containerElement.getBoundingClientRect();
|
|
|
- const scrollTop = window.pageYOffset;
|
|
|
- const centerElement = top + scrollTop - window.innerHeight / 2;
|
|
|
- window.scrollTo({top: centerElement});
|
|
|
- } catch (err) {
|
|
|
- Sentry.captureException(err);
|
|
|
- }
|
|
|
+ const windowHeight = $(window).height();
|
|
|
+ $('html,body').animate({
|
|
|
+ scrollTop: $(this.containerElement).offset().top - windowHeight / 2,
|
|
|
+ });
|
|
|
}
|
|
|
},
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
- const {target} = this.props;
|
|
|
- target && unregisterAnchor(target);
|
|
|
+ unregisterAnchor(this);
|
|
|
},
|
|
|
|
|
|
onGuideStateChange(data) {
|
|
|
const active =
|
|
|
data.currentGuide &&
|
|
|
data.currentGuide.steps[data.currentStep].target === this.props.target;
|
|
|
-
|
|
|
this.setState({
|
|
|
active,
|
|
|
- currentGuide: data.currentGuide,
|
|
|
+ guide: data.currentGuide,
|
|
|
step: data.currentStep,
|
|
|
- orgId: data.orgId,
|
|
|
+ org: data.org,
|
|
|
+ messageVariables: {
|
|
|
+ orgSlug: data.org && data.org.slug,
|
|
|
+ projectSlug: data.project && data.project.slug,
|
|
|
+ },
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * Terminology:
|
|
|
- *
|
|
|
- * - A guide can be FINISHED by clicking one of the buttons in the last step
|
|
|
- * - A guide can be DISMISSED by x-ing out of it at any step except the last (where there is no x)
|
|
|
- * - In both cases we consider it CLOSED
|
|
|
- */
|
|
|
+ interpolate(template, variables) {
|
|
|
+ const regex = /\${([^{]+)}/g;
|
|
|
+ return template.replace(regex, (_match, g1) => variables[g1.trim()]);
|
|
|
+ },
|
|
|
+
|
|
|
+ /* Terminology:
|
|
|
+ - A guide can be FINISHED by clicking one of the buttons in the last step.
|
|
|
+ - A guide can be DISMISSED by x-ing out of it at any step except the last (where there is no x).
|
|
|
+ - In both cases we consider it CLOSED.
|
|
|
+ */
|
|
|
handleFinish(e) {
|
|
|
e.stopPropagation();
|
|
|
- const {currentGuide, orgId} = this.state;
|
|
|
- recordFinish(currentGuide.guide, orgId);
|
|
|
+ const {guide, org} = this.state;
|
|
|
+ recordFinish(guide.id, org);
|
|
|
closeGuide();
|
|
|
},
|
|
|
|
|
@@ -113,12 +97,12 @@ const GuideAnchor = createReactClass<Props, State>({
|
|
|
|
|
|
handleDismiss(e) {
|
|
|
e.stopPropagation();
|
|
|
- const {currentGuide, step, orgId} = this.state;
|
|
|
- dismissGuide(currentGuide.guide, step, orgId);
|
|
|
+ const {guide, step, org} = this.state;
|
|
|
+ dismissGuide(guide.id, step, org);
|
|
|
},
|
|
|
|
|
|
render() {
|
|
|
- const {active, currentGuide, step} = this.state;
|
|
|
+ const {active, guide, step, messageVariables} = this.state;
|
|
|
if (!active) {
|
|
|
return this.props.children ? this.props.children : null;
|
|
|
}
|
|
@@ -126,24 +110,28 @@ const GuideAnchor = createReactClass<Props, State>({
|
|
|
const body = (
|
|
|
<GuideContainer>
|
|
|
<GuideInputRow>
|
|
|
- <StyledTitle>{currentGuide.steps[step].title}</StyledTitle>
|
|
|
- {step < currentGuide.steps.length - 1 && (
|
|
|
+ <StyledTitle>{guide.steps[step].title}</StyledTitle>
|
|
|
+ {step < guide.steps.length - 1 && (
|
|
|
<CloseLink onClick={this.handleDismiss} href="#" data-test-id="close-button">
|
|
|
<CloseIcon />
|
|
|
</CloseLink>
|
|
|
)}
|
|
|
</GuideInputRow>
|
|
|
<StyledContent>
|
|
|
- <div>{currentGuide.steps[step].description}</div>
|
|
|
+ <div
|
|
|
+ dangerouslySetInnerHTML={{
|
|
|
+ __html: this.interpolate(guide.steps[step].message, messageVariables),
|
|
|
+ }}
|
|
|
+ />
|
|
|
<Actions>
|
|
|
<div>
|
|
|
- {step < currentGuide.steps.length - 1 ? (
|
|
|
+ {step < guide.steps.length - 1 ? (
|
|
|
<Button priority="success" size="small" onClick={this.handleNextStep}>
|
|
|
- {t('Next')}
|
|
|
+ {t('Next')} →
|
|
|
</Button>
|
|
|
) : (
|
|
|
<Button priority="success" size="small" onClick={this.handleFinish}>
|
|
|
- {t(currentGuide.steps.length === 1 ? 'Got It' : 'Done')}
|
|
|
+ {t(guide.steps.length === 1 ? 'Got It' : 'Done')}
|
|
|
</Button>
|
|
|
)}
|
|
|
</div>
|