Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i586 - on click download button it will be disabled for 5 seconds #107

Merged
merged 6 commits into from
Dec 4, 2017
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface ModellingGroupsProps extends RemoteContent {
groups: ModellingGroup[]
}

export class ModellingGroupsListComponent extends RemoteContentComponent<ModellingGroupsProps> {
export class ModellingGroupsListComponent extends RemoteContentComponent<ModellingGroupsProps, undefined> {
static getStores() {
return [ groupStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface Props extends RemoteContent {
isAdmin: boolean;
}

export class GroupAdminContentComponent extends RemoteContentComponent<Props> {
export class GroupAdminContentComponent extends RemoteContentComponent<Props, undefined> {
static getStores() {
return [ groupStore, userStore, adminAuthStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface Props extends RemoteContent {
users: User[];
}

class ModellingGroupDetailsContentComponent extends RemoteContentComponent<Props> {
class ModellingGroupDetailsContentComponent extends RemoteContentComponent<Props, undefined> {
static getStores() {
return [ groupStore, userStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface GroupTitleProps extends RemoteContent {
group: ModellingGroup;
}

export abstract class ModellingGroupTitle extends RemoteContentComponent<GroupTitleProps> {
export abstract class ModellingGroupTitle extends RemoteContentComponent<GroupTitleProps, undefined> {
static getStores() {
return [ groupStore ];
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/admin/components/Users/List/UsersList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface UserProps extends RemoteContent {
users: User[]
}

export class UsersListComponent extends RemoteContentComponent<UserProps> {
export class UsersListComponent extends RemoteContentComponent<UserProps, undefined> {
static getStores() {
return [userStore];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Props extends RemoteContent {

const commonStyles = require("../../../../shared/styles/common.css");

export class UserDetailsContentComponent extends RemoteContentComponent<Props> {
export class UserDetailsContentComponent extends RemoteContentComponent<Props, undefined> {
static getStores() {
return [userStore];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface UserTitleProps extends RemoteContent {
user: User;
}

export abstract class UserTitle extends RemoteContentComponent<UserTitleProps> {
export abstract class UserTitle extends RemoteContentComponent<UserTitleProps, undefined> {
static getStores() {
return [ userStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ChooseActionContentProps extends RemoteContent {
group: ModellingGroup
}

export class ChooseActionContentComponent extends RemoteContentComponent<ChooseActionContentProps> {
export class ChooseActionContentComponent extends RemoteContentComponent<ChooseActionContentProps, undefined> {
static getStores() {
return [ responsibilityStore, contribAuthStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ChooseGroupProps extends RemoteContent {
groups: ModellingGroup[];
}

export class ChooseGroupContentComponent extends RemoteContentComponent<ChooseGroupProps> {
export class ChooseGroupContentComponent extends RemoteContentComponent<ChooseGroupProps, undefined> {
static getStores() {
return [ responsibilityStore, contribAuthStore ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface UploadBurdenEstimatesContentComponentProps extends RemoteConten
};
}

export class UploadBurdenEstimatesContentComponent extends RemoteContentComponent<UploadBurdenEstimatesContentComponentProps> {
export class UploadBurdenEstimatesContentComponent extends RemoteContentComponent<UploadBurdenEstimatesContentComponentProps, undefined> {

static getStores() {
return [responsibilityStore];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ interface Props extends HasFormatOption {
coverageToken: string;
}

export class DownloadCoverageContentComponent extends RemoteContentComponent<DownloadCoverageComponentProps> {
interface DownloadState {
downloadButtonEnabled: boolean;
}

export class DownloadCoverageContentComponent extends RemoteContentComponent<DownloadCoverageComponentProps, DownloadState> {
constructor() {
super();
this.state = {
downloadButtonEnabled: true
}
this.onDownloadClicked = this.onDownloadClicked.bind(this);
}

static getStores() {
return [responsibilityStore];
}
Expand All @@ -43,7 +55,7 @@ export class DownloadCoverageContentComponent extends RemoteContentComponent<Dow
scenario: r.scenario,
coverageSets: r.coverageSets,
coverageToken: state.coverageOneTimeToken,
selectedFormat: state.selectedFormat
selectedFormat: state.selectedFormat,
}
};
} else {
Expand All @@ -54,7 +66,6 @@ export class DownloadCoverageContentComponent extends RemoteContentComponent<Dow
}
}


onSelectFormat(format: string) {
coverageSetActions.selectFormat(format);
responsibilityStore.fetchOneTimeCoverageToken().catch(doNothing);
Expand All @@ -65,6 +76,19 @@ export class DownloadCoverageContentComponent extends RemoteContentComponent<Dow
responsibilityStore.fetchOneTimeCoverageToken();
}

onDownloadClicked() {
setTimeout(() => {
this.setState({
downloadButtonEnabled: false,
})
}, 50)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong about this, but I'll just check: I think the 50 ms delay is unnecessary. If you leave the timing argument off I would expect the browser to finish resolving the user button action, and only when the stack was empty would this callback be invoked.

setTimeout(() => {
this.setState({
downloadButtonEnabled: true,
})
}, 5000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe more readable as

setTimeout(() => this.setState({
    downloadButtonEnabled: true
}), 5000);

But that's personal preference. Equally the other statement. Also, it might be nice to pull the 5000ms delay into a named static const?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although see below about idea to parameterize delay.

}

renderContent(props: DownloadCoverageComponentProps) {
const data = props.props;
return <div>
Expand Down Expand Up @@ -132,7 +156,12 @@ export class DownloadCoverageContentComponent extends RemoteContentComponent<Dow
</div>
</div>
<div className="mt-4">
<OneTimeButton token={data.coverageToken} refreshToken={this.refreshToken}>
<OneTimeButton
token={data.coverageToken}
refreshToken={this.refreshToken}
enabled={this.state.downloadButtonEnabled}
onClickOuterEvent={this.onDownloadClicked}
>
Download combined coverage set data in CSV format
</OneTimeButton>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface DownloadDemographicsContentProps extends RemoteContent {
token: string;
}

export class DownloadDemographicsContentComponent extends RemoteContentComponent<DownloadDemographicsContentProps> {
export class DownloadDemographicsContentComponent extends RemoteContentComponent<DownloadDemographicsContentProps, undefined> {
static getStores() {
return [demographicStore, responsibilityStore];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ResponsibilityOverviewComponentProps extends RemoteContent {
modellingGroup: ModellingGroup;
}

export class ResponsibilityOverviewContentComponent extends RemoteContentComponent<ResponsibilityOverviewComponentProps> {
export class ResponsibilityOverviewContentComponent extends RemoteContentComponent<ResponsibilityOverviewComponentProps, undefined> {
static getStores() {
return [responsibilityStore];
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/report/components/Reports/ReportDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface ReportDetailsProps extends RemoteContent, PublicProps {
allVersions: string[];
}

export class ReportDetailsComponent extends RemoteContentComponent<ReportDetailsProps> {
export class ReportDetailsComponent extends RemoteContentComponent<ReportDetailsProps, undefined> {
static getStores() {
return [reportStore];
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/report/components/Reports/ReportList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface ReportProps extends RemoteContent {
reports: Report[]
}

export class ReportListComponent extends RemoteContentComponent<ReportProps> {
export class ReportListComponent extends RemoteContentComponent<ReportProps, undefined> {
static getStores() {
return [reportStore];
}
Expand Down
20 changes: 15 additions & 5 deletions app/src/main/shared/components/OneTimeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ interface Props {
token: string
enabled?: boolean;
refreshToken: () => void;
onClickOuterEvent?: () =>void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could just be called onClick. Better to have it easy to understand for components using the OneTimeButton. If you'd like to keep things unambiguous, maybe rename the internal method to something like internalOnClickHandler.

}

export class OneTimeButton extends React.Component<Props, undefined> {
export class OneTimeButton extends React.Component<Props, any> {
static defaultProps: Partial<Props> = {
enabled: true
};

constructor() {
super();
this.refreshToken = this.refreshToken.bind(this);
this.onClick = this.onClick.bind(this);
}

refreshToken() {
Expand All @@ -32,14 +33,23 @@ export class OneTimeButton extends React.Component<Props, undefined> {
}
}

onClick() {
this.refreshToken();
if (typeof this.props.onClickOuterEvent === 'function') {
this.props.onClickOuterEvent();
}
}

render() {
const props = this.props;
const url = fetcher.fetcher.buildOneTimeLink(props.token);
const enabled = props.enabled && props.token != null;
return <form action={ url }>
<button onClick={ this.refreshToken }
disabled={ !enabled }
type="submit">
<button
onClick={ this.onClick }
disabled={ !enabled }
type="submit"
>
{ this.props.children }
</button>
{ this.renderAnimation() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RemoteContent } from "../../models/RemoteContent";
const spinner = require("./spinner.gif");
const messageStyles = require("../../styles/messages.css");

export abstract class RemoteContentComponent<TProps extends RemoteContent> extends React.Component<TProps, undefined> {
export abstract class RemoteContentComponent<TProps extends RemoteContent, TState> extends React.Component<TProps, TState> {
abstract renderContent(content: TProps): JSX.Element;

render() {
Expand Down
8 changes: 8 additions & 0 deletions app/src/test/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ export class Sandbox {
stubFetch(obj: any, method: string): sinon.SinonStub {
return this.sinon.stub(obj, method).returns(Promise.resolve(true));
}

setSpy(obj: any, method: string): sinon.SinonSpy {
return this.sinon.spy(obj, method);
}

createSpy(): sinon.SinonSpy {
return this.sinon.spy();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { RemoteContentComponent } from "../../../main/shared/components/RemoteCo

const spinner = require("../../../main/shared/components/RemoteContentComponent/spinner.gif");

class DummyComponent extends RemoteContentComponent<RemoteContent> {
class DummyComponent extends RemoteContentComponent<RemoteContent, undefined> {
renderContent() {
return <span>Content</span>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { expect } from "chai";
import { mockCoverageSet, mockScenario, mockTouchstone } from "../../../mocks/mockModels";
import { shallow, ShallowWrapper } from "enzyme";
import { shallow, ShallowWrapper, mount } from "enzyme";

import {
DownloadCoverageContentComponent,
Expand Down Expand Up @@ -45,6 +45,7 @@ describe("DownloadCoverageContentComponent", () => {
expect(rendered.find(OneTimeButton).props()).to.eql({
token: "TOKEN",
refreshToken: (rendered.instance() as DownloadCoverageContentComponent).refreshToken,
onClickOuterEvent: (rendered.instance() as DownloadCoverageContentComponent).onDownloadClicked,
enabled: true,
children: "Download combined coverage set data in CSV format"
});
Expand All @@ -58,6 +59,22 @@ describe("DownloadCoverageContentComponent", () => {
expect(fetchNewToken.called).to.be.true;
});

it("calling meth onDownloadClicked sets state prop downloadButtonEnabled to false for 5 seconds", function(done: DoneCallback) {
this.timeout(5020);
const props = makeProps({ coverageToken: "TOKEN" });
const component = shallow(<DownloadCoverageContentComponent {...props} />);
const instance = component.instance() as DownloadCoverageContentComponent;
expect(component.state().downloadButtonEnabled).to.be.equal(true)
instance.onDownloadClicked();
setTimeout(() => {
expect(component.state().downloadButtonEnabled).to.be.equal(false)
},70);
setTimeout(() => {
expect(component.state().downloadButtonEnabled).to.be.equal(true)
done();
},5010);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this makes this test take 5 seconds to run which is a bit long. Maybe parameterize the delay (via a React prop) so you can test this more rapidly?


it("renders format control", () => {
const props = makeProps({ selectedFormat: "x" });
const rendered = shallow(<DownloadCoverageContentComponent {...props} />);
Expand Down
21 changes: 20 additions & 1 deletion app/src/test/shared/components/OneTimeButtonTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import { shallow } from "enzyme";
import { OneTimeButton } from "../../../main/shared/components/OneTimeButton";
import { doNothing } from "../../../main/shared/Helpers";
import fetcher from "../../../main/shared/sources/Fetcher";
import { Sandbox } from "../../Sandbox";

describe("OneTimeButton", () => {
const sandbox = new Sandbox();

afterEach(() => {
sandbox.restore();
});

it("renders form with onetime URL", () => {
const rendered = shallow(<OneTimeButton token="TOKEN" refreshToken={doNothing} />);
expect(rendered.find("form").prop("action")).to.equal(fetcher.fetcher.buildURL("/onetime_link/TOKEN/"));
Expand Down Expand Up @@ -36,7 +43,7 @@ describe("OneTimeButton", () => {
checkImage("TOKEN", true, false);
});

it("clicking button triggers callback after timeout", (done: DoneCallback) => {
it("clicking button triggers refresh token callback after timeout", (done: DoneCallback) => {
let tracker = false;
const callback = () => tracker = true;

Expand All @@ -51,4 +58,16 @@ describe("OneTimeButton", () => {
done();
});
});

it("clicking button triggers outer callback", () => {
const outerCallbackSpy = sandbox.createSpy();
const rendered = shallow(<OneTimeButton
token="TOKEN"
refreshToken={()=>{}}
onClickOuterEvent={outerCallbackSpy}
/>);
const button = rendered.find("form").find("button");
button.simulate("click");
expect(outerCallbackSpy.calledOnce).to.equal(true);
});
});