Adding
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import * as adalConfig from "../../adalConfig";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import AccountPageHeader from "./AccountPageHeader";
|
||||
|
||||
describe("AccountPageHeader", () => {
|
||||
var spy: jest.SpyInstance;
|
||||
let wrapper: ShallowWrapper;
|
||||
const selectedAccount: IRepoAccount = {
|
||||
repoId: 476,
|
||||
origAccountTypeCode: "",
|
||||
accountNo: "123456",
|
||||
status: "string",
|
||||
memberNo: 1453,
|
||||
loanNo: 0,
|
||||
accountTypeCode: "acct-type-code",
|
||||
branch: 0,
|
||||
balance: 0,
|
||||
dueToPayDate: moment("1941-12-07"),
|
||||
o4RDate: moment("1945-05-07"),
|
||||
dateFirstDelinquent: moment("1918-11-11"),
|
||||
delinquentAmount: 0,
|
||||
daysDelinquent: 101,
|
||||
paidSkipAmount: 0,
|
||||
subStatus: "string",
|
||||
requiredPaymentAmount: 3.14,
|
||||
currentDelinquentAmount: 19.45,
|
||||
lstPmtAmt: 1912.00000,
|
||||
origAmount: 42,
|
||||
lastPaymentDate: moment("1865-04-15"),
|
||||
dateFirstIntellect: moment("1963-11-22"),
|
||||
primaryIndividualId: "string",
|
||||
manualRedemptionDate: moment("1812-06-12"),
|
||||
borrowers: [{
|
||||
repoId: 1,
|
||||
individualId: "1",
|
||||
ownershipType: 6,
|
||||
individualName: "John Doe",
|
||||
}],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
};
|
||||
let updatedBackButtonValue: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
updatedBackButtonValue = false;
|
||||
wrapper = shallow(
|
||||
<AccountPageHeader
|
||||
accountNoHeader="abcd1234"
|
||||
selectedRepoAccount={selectedAccount}
|
||||
allowedStatusChanges={[]}
|
||||
onStatusChange={() => {}}
|
||||
onBackButtonClicked={() => updatedBackButtonValue = true}
|
||||
putRepoAccount={()=>{}}
|
||||
/>
|
||||
);
|
||||
spy = jest.spyOn(adalConfig, "hasAdminRole").mockReturnValueOnce(false).mockReturnValueOnce(true);
|
||||
});
|
||||
|
||||
it ("Renders Member Name", () => {
|
||||
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
|
||||
expect(memberName.length).toBe(1);
|
||||
expect(memberName.text()).toEqual("John Doe");
|
||||
});
|
||||
|
||||
it ("Renders Missing When There is No Selected Account", () => {
|
||||
wrapper.setProps({
|
||||
selectedRepoAccount: undefined,
|
||||
});
|
||||
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
|
||||
expect(memberName.length).toBe(1);
|
||||
expect(memberName.text()).toEqual("Missing");
|
||||
});
|
||||
|
||||
it ("Back Button Callback Works", () => {
|
||||
const backBtn = wrapper.find(`[data-test='account-page-header-back-btn']`);
|
||||
expect(backBtn.length).toBe(1);
|
||||
expect(updatedBackButtonValue).toBe(false);
|
||||
backBtn.simulate("click");
|
||||
expect(updatedBackButtonValue).toBe(true);
|
||||
});
|
||||
|
||||
it ("Does Not Render \"(Closed)\" Next to Name When Account is Open", () => {
|
||||
const closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
|
||||
expect(closedAccountLabel.length).toBe(0);
|
||||
});
|
||||
|
||||
it ("Renders \"(Closed)\" Next to Name When Account is Closed", () => {
|
||||
wrapper = wrapper.setProps({
|
||||
selectedRepoAccount: {
|
||||
...selectedAccount,
|
||||
dateClosed: moment("1941-12-07")
|
||||
}
|
||||
});
|
||||
|
||||
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
|
||||
expect(closedAccountLabel.length).toBe(1);
|
||||
|
||||
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
|
||||
expect(closedAccountLabel.length).toBe(0);
|
||||
});
|
||||
|
||||
it ("Renders \"Repopen\" button Next to Name When Account is Closed and user is Admin", () => {
|
||||
wrapper = wrapper.setProps({
|
||||
selectedRepoAccount: {
|
||||
...selectedAccount,
|
||||
dateClosed: moment("1941-12-07")
|
||||
}
|
||||
});
|
||||
|
||||
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
|
||||
expect(closedAccountLabel.length).toBe(1);
|
||||
|
||||
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
|
||||
expect(closedAccountLabel.length).toBe(1);
|
||||
|
||||
});
|
||||
});
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { IAllowedStatusChange } from '../../reducers/SettingsReducer';
|
||||
import StatusChangeSelector from '../status-change-selector/StatusChangeSelector';
|
||||
import RepoAccountService from '../../services/RepoAccountService';
|
||||
import { IRepoAccount } from '../../services/service-types/RepoAccountTypes';
|
||||
import * as adalConfig from "../../adalConfig";
|
||||
const cloneDeep = require("lodash.clonedeep");
|
||||
interface AccountPageHeaderProps {
|
||||
accountNoHeader: string;
|
||||
selectedRepoAccount?: IRepoAccount;
|
||||
allowedStatusChanges: IAllowedStatusChange[];
|
||||
onBackButtonClicked: () => any;
|
||||
onStatusChange: (status: string) => any;
|
||||
putRepoAccount: (repoAccount: IRepoAccount) => any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const AccountPageHeader: React.FC<AccountPageHeaderProps> = (props) => {
|
||||
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
|
||||
|
||||
const reopenAccountClicked = () => {
|
||||
const shouldReOpenAccount = window.confirm("Are you sure you want to reopen this account? This can have unexpected side effects. Use Caution with accounts that have already been refunded.");
|
||||
if(shouldReOpenAccount) {
|
||||
reopenClosedAccount();
|
||||
}
|
||||
}
|
||||
|
||||
const reopenClosedAccount = () => {
|
||||
if (!props.selectedRepoAccount) {
|
||||
console.warn("Cannot close account when the Selected Account is not loaded. Returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
const copiedRepoAccount = cloneDeep(props.selectedRepoAccount) as IRepoAccount;
|
||||
copiedRepoAccount.dateClosed = undefined;
|
||||
|
||||
props.putRepoAccount(copiedRepoAccount);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'back-navigation-header' + (isAccountClosed ? ' header-closed' : '')}>
|
||||
<div className="row form-inline">
|
||||
<div className="col-sm">
|
||||
<h5>
|
||||
<i className="fa fa-chevron-left text-primary" aria-hidden="true" onClick={props.onBackButtonClicked} data-test="account-page-header-back-btn"></i>
|
||||
Account: { props.accountNoHeader }
|
||||
{ isAccountClosed ?
|
||||
(
|
||||
<span className="text-danger" data-test="closed-account-label"> (CLOSED)</span>
|
||||
)
|
||||
: null}
|
||||
{ isAccountClosed && adalConfig.hasAdminRole() ?
|
||||
(
|
||||
<button className="form-input btn btn-danger btn-sm ml-2" data-test="reopen-button" onClick={() => reopenAccountClicked()}>Reopen This Account</button>
|
||||
)
|
||||
: null }
|
||||
</h5>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p><b data-test="account-page-header-member-name">{RepoAccountService.GetMemberName(props.selectedRepoAccount)}</b></p>
|
||||
</div>
|
||||
|
||||
<div className="col-sm">
|
||||
<StatusChangeSelector
|
||||
allowedStatusChanges={props.allowedStatusChanges}
|
||||
onStatusChange={props.onStatusChange}
|
||||
selectedRepoAccount={props.selectedRepoAccount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr></hr>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountPageHeader;
|
||||
@@ -0,0 +1,8 @@
|
||||
.alerts-list {
|
||||
max-height: 11em;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.alert-box .card-header {
|
||||
padding: .5em;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { IAlert } from "../../reducers/AlertReducer";
|
||||
import AlertBox, { groupAlerts, IAlertBoxProps } from "./AlertBox";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const div = document.createElement("div");
|
||||
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
|
||||
ReactDOM.render(<AlertBox {...props} />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
|
||||
const findByTestAttribute = (wrapper: ShallowWrapper, attr: string) => {
|
||||
const component = wrapper.find(`[data-test='${attr}']`);
|
||||
return component;
|
||||
};
|
||||
|
||||
describe("AlertBox Component", () => {
|
||||
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
|
||||
let wrapper = shallow(<AlertBox {...props} />);
|
||||
|
||||
const first: IAlert = { id: 1, repoId: 2, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Message", dayCount: 60 };
|
||||
const second: IAlert = { id: 2, repoId: 3, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Jane Sample", markedReadBy: "", message: "Message", dayCount: 60 };
|
||||
const third: IAlert = { id: 3, repoId: 4, accountNo: "3", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
|
||||
const fourth: IAlert = { id: 4, repoId: 5, accountNo: "4", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
|
||||
|
||||
it ("Should render without errors", () => {
|
||||
const component = findByTestAttribute(wrapper, "alertBoxComponent");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Should render a header", () => {
|
||||
const component = findByTestAttribute(wrapper, "alertBoxHeader");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Should render an alerts list", () => {
|
||||
const component = findByTestAttribute(wrapper, "alertsList");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it("groups an empty alert array", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it("groups one alert", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [first];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
const group = result[0];
|
||||
expect(group.message).toBe("Message");
|
||||
expect(group.repoIds[0]).toBe(2);
|
||||
});
|
||||
|
||||
it("groups two alerts of the same type", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [first, second];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
const group = result[0];
|
||||
expect(group.message).toBe("Message");
|
||||
expect(group.repoIds[0]).toBe(2);
|
||||
expect(group.repoIds[1]).toBe(3);
|
||||
});
|
||||
|
||||
it("groups two alerts of different types", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [first, third];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(2);
|
||||
|
||||
const messageGroup = result[0];
|
||||
expect(messageGroup.message).toBe("Message");
|
||||
expect(messageGroup.repoIds[0]).toBe(2);
|
||||
|
||||
const otherGroup = result[1];
|
||||
expect(otherGroup.message).toBe("Other Message");
|
||||
expect(otherGroup.repoIds[0]).toBe(4);
|
||||
});
|
||||
|
||||
it("discards duplicate alerts", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [first, second, first];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
const group = result[0];
|
||||
expect(group.message).toBe("Message");
|
||||
expect(group.repoIds[0]).toBe(2);
|
||||
expect(group.repoIds[1]).toBe(3);
|
||||
});
|
||||
|
||||
it("sorts alert groups in descending order of alert count", () => {
|
||||
// arrange
|
||||
const alerts: IAlert[] = [first, third, fourth];
|
||||
|
||||
// act
|
||||
const result = groupAlerts(alerts);
|
||||
|
||||
// assert
|
||||
expect(result.length).toBe(2);
|
||||
|
||||
const otherGroup = result[0];
|
||||
expect(otherGroup.message).toBe("Other Message");
|
||||
expect(otherGroup.repoIds[0]).toBe(4);
|
||||
expect(otherGroup.repoIds[1]).toBe(5);
|
||||
|
||||
const messageGroup = result[1];
|
||||
expect(messageGroup.message).toBe("Message");
|
||||
expect(messageGroup.repoIds[0]).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { IAlert } from "../../reducers/AlertReducer";
|
||||
import IAlertGroup from "../../shared/types/IAlertGroup";
|
||||
import AlertRow from "./../alert-row/AlertRow";
|
||||
import "./AlertBox.css";
|
||||
|
||||
export interface IAlertBoxProps {
|
||||
alerts: IAlert[];
|
||||
currentlySelectedAlertMessage?: string;
|
||||
title: string;
|
||||
alertRowClicked: (message: string, repoIds: number[]) => any;
|
||||
}
|
||||
|
||||
const AlertBox: React.FC<IAlertBoxProps> = (props: IAlertBoxProps) => {
|
||||
const groups = groupAlerts(props.alerts);
|
||||
|
||||
return (
|
||||
<div className="card h-100 alert-box" data-test="alertBoxComponent">
|
||||
<h6 className="card-header" data-test="alertBoxHeader">
|
||||
{totalAlertCount(groups)} {props.title}
|
||||
</h6>
|
||||
<div className="">
|
||||
<ul className="alerts-list list-group list-group-flush" data-test="alertsList">
|
||||
{groups.map((value, key) => {
|
||||
return (
|
||||
<AlertRow
|
||||
key={key}
|
||||
message={value.message}
|
||||
currentlySelectedAlertMessage={props.currentlySelectedAlertMessage}
|
||||
repoIds={value.repoIds}
|
||||
alertRowClicked={props.alertRowClicked}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const totalAlertCount = (groups: IAlertGroup[]) => {
|
||||
return groups.reduce((total, group) => total + group.repoIds.length, 0);
|
||||
};
|
||||
|
||||
export const groupAlerts = (alerts: IAlert[]) => {
|
||||
let groups: IAlertGroup[] = [];
|
||||
|
||||
for (let alert of alerts) {
|
||||
let found = groups.find(x => x.message === alert.message);
|
||||
|
||||
if (!found) {
|
||||
var group: IAlertGroup = {message: alert.message, repoIds: [alert.repoId]};
|
||||
groups.push(group);
|
||||
} else {
|
||||
if (!found.repoIds.includes(alert.repoId)){
|
||||
found.repoIds.push(alert.repoId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups.sort((a, b) => {
|
||||
return b.repoIds.length - a.repoIds.length;
|
||||
});
|
||||
};
|
||||
|
||||
export default AlertBox;
|
||||
@@ -0,0 +1,13 @@
|
||||
.alert-row {
|
||||
cursor: pointer;
|
||||
font-size: .9em;
|
||||
padding: .5em 1em;
|
||||
}
|
||||
|
||||
.alert-row.selected-row {
|
||||
background-color: #d9ebff;
|
||||
}
|
||||
|
||||
.alert-row:hover:not(.selected-row) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AssertionError } from "assert";
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import React, { Component } from "react";
|
||||
import AlertRow from "./AlertRow";
|
||||
|
||||
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
|
||||
const wrapper = component.find(`[data-test='${attr}']`);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
|
||||
describe("AlertRow", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AlertRow
|
||||
message="Cras justo odio"
|
||||
repoIds={[]}
|
||||
alertRowClicked={(_: string, __: number[]) => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it ("Should render without errors", () => {
|
||||
const component = findByTestAttribute(wrapper, "alertRowComponent");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Should render an alert type", () => {
|
||||
const component = findByTestAttribute(wrapper, "alertType");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Should render a type count", () => {
|
||||
const component = findByTestAttribute(wrapper, "typeCount");
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Should Render as badge-primary with count under 5", () => {
|
||||
var tempwrapper = shallow(
|
||||
<AlertRow
|
||||
message="Cras justo odio"
|
||||
repoIds={[1,2,3]}
|
||||
alertRowClicked={(_: string, __: number[]) => {}}
|
||||
/>);
|
||||
|
||||
const component = findByTestAttribute(tempwrapper, "typeCount");
|
||||
expect(component.hasClass("badge-primary")).toBe(true);
|
||||
});
|
||||
|
||||
it("Should Render as badge-warning with count > 6 and count < 20", () => {
|
||||
var tempwrapper = shallow(
|
||||
<AlertRow
|
||||
message="Cras justo odio"
|
||||
repoIds={[1,2,3,4,5,6,7,8]}
|
||||
alertRowClicked={(_: string, __: number[]) => {}}
|
||||
/>);
|
||||
|
||||
const component = findByTestAttribute(tempwrapper, "typeCount");
|
||||
expect(component.hasClass("badge-warning")).toBe(true);
|
||||
});
|
||||
|
||||
it("Should Render as badge-danger when count is greater than 20", () => {
|
||||
var tempwrapper = shallow(
|
||||
<AlertRow
|
||||
message="Cras justo odio"
|
||||
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
|
||||
alertRowClicked={(_: string, __: number[]) => {}}
|
||||
/>);
|
||||
|
||||
const component = findByTestAttribute(tempwrapper, "typeCount");
|
||||
expect(component.hasClass("badge-danger")).toBe(true);
|
||||
});
|
||||
|
||||
it("alert row properly handles click", () => {
|
||||
var clicked = false;
|
||||
var tempwrapper = shallow(
|
||||
<AlertRow
|
||||
message="Cras justo odio"
|
||||
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
|
||||
alertRowClicked={(_: string, __: number[]) => {clicked=true}}
|
||||
/>);
|
||||
|
||||
const component = findByTestAttribute(tempwrapper, "alertRowComponent");
|
||||
component.simulate("click");
|
||||
expect(clicked).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import "./AlertRow.css";
|
||||
|
||||
interface IAlertRowProps {
|
||||
message: string;
|
||||
repoIds: number[];
|
||||
currentlySelectedAlertMessage?: string;
|
||||
alertRowClicked: (message: string, repoIds: number[]) => any;
|
||||
}
|
||||
|
||||
const AlertRow: React.FC<IAlertRowProps> = (props: IAlertRowProps) => {
|
||||
|
||||
const rowClicked = () => {
|
||||
props.alertRowClicked(props.message, props.repoIds);
|
||||
};
|
||||
|
||||
const getCountClassStyle = (count: number): string => {
|
||||
if (count < 5) {
|
||||
return "badge-primary";
|
||||
}
|
||||
if (count < 20) {
|
||||
return "badge-warning";
|
||||
}
|
||||
return "badge-danger";
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
className={"alert-row list-group-item d-flex justify-content-between align-items-center " + (props.currentlySelectedAlertMessage === props.message ? " selected-row" : "")}
|
||||
data-test="alertRowComponent"
|
||||
onClick={rowClicked}
|
||||
>
|
||||
<span data-test="alertType">{props.message}</span>
|
||||
<span className={"badge badge-pill " + getCountClassStyle(props.repoIds.length)} data-test="typeCount">{props.repoIds.length}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertRow;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.auction-assignment-section input,
|
||||
.auction-assignment-section select,
|
||||
.auction-assignment-section textarea {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.auction-assignment-section p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
|
||||
import { ICleaningLevel } from "../../reducers/SettingsReducer";
|
||||
import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
|
||||
|
||||
// - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
|
||||
|
||||
describe("Auction Collateral Location Component", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const dummyAuctionAssignment: IUserSelectedAuctionAssignment = { repoId: 1 };
|
||||
const dummyCleaningLevels: ICleaningLevel[] = [
|
||||
{
|
||||
code: 1,
|
||||
description: "Super Clean",
|
||||
},
|
||||
{
|
||||
code: 2,
|
||||
description: "Dirty",
|
||||
},
|
||||
];
|
||||
const dummyAuctionHouses: IAuctionHouse[] = [
|
||||
{
|
||||
auctionID: "1060",
|
||||
name: "Dealers Auto Auction Southwest",
|
||||
active: "yes",
|
||||
address_Line_1: "1433 South 19th Avenue",
|
||||
address_Line_2: "-",
|
||||
city: "Phoenix",
|
||||
state: "AZ",
|
||||
zip: 85009,
|
||||
country: "USA",
|
||||
phone: "(602) 253-7766",
|
||||
fax: "(602) 253-4646",
|
||||
latitude: 33.4337272644043,
|
||||
longitude: -112.09916687011719
|
||||
},
|
||||
{
|
||||
auctionID: "1061",
|
||||
name: "ADESA Portland",
|
||||
active: "yes",
|
||||
address_Line_1: "23585 NE Sandy Blvd.",
|
||||
address_Line_2: "-",
|
||||
city: "Wood Village",
|
||||
state: "OR",
|
||||
zip: 97060,
|
||||
country: "USA",
|
||||
phone: "(800) 300-3200",
|
||||
fax: "(503) 661-7286",
|
||||
latitude: 45.545101165771484,
|
||||
longitude: -122.41893005371094
|
||||
},
|
||||
];
|
||||
|
||||
let dummyAuctionAssignmentToUpdate: IUserSelectedAuctionAssignment = { repoId: 1 };
|
||||
const dummyUpdateAuctionAssignment = (auctionAssignment: IUserSelectedAuctionAssignment) => {
|
||||
dummyAuctionAssignmentToUpdate = auctionAssignment;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AuctionAssignment
|
||||
auctionHouses={dummyAuctionHouses}
|
||||
cleaningLevels={dummyCleaningLevels}
|
||||
auctionAssignment={dummyAuctionAssignment}
|
||||
onAuctionAssignmentChange={dummyUpdateAuctionAssignment}
|
||||
collateralLocation={{} as any}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dummyAuctionAssignmentToUpdate = {
|
||||
repoId: 1
|
||||
};
|
||||
});
|
||||
|
||||
const getCardView = () => {
|
||||
return wrapper.find("CardView").dive();
|
||||
};
|
||||
|
||||
it("Renders & Updates Auction House", () => {
|
||||
// Inital value is present
|
||||
let auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
|
||||
let auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
|
||||
expect(auctionHouseSelection.length).toBe(1);
|
||||
expect(auctionHouseSelection.props().value).toBe(undefined);
|
||||
expect(auctionHouseAddress.length).toBe(1);
|
||||
expect(auctionHouseAddress.props().value).toEqual("Select Auction House for Address");
|
||||
|
||||
// Change triggers callback
|
||||
auctionHouseSelection.simulate("change", "1060");
|
||||
expect(dummyAuctionAssignmentToUpdate.auctionId).toEqual(dummyAuctionHouses[0].auctionID);
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
|
||||
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
auctionAssignment: {
|
||||
auctionId: dummyAuctionHouses[1].auctionID,
|
||||
},
|
||||
});
|
||||
auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
|
||||
auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
|
||||
expect(auctionHouseSelection.props().value).toBe("1061");
|
||||
expect(auctionHouseAddress.props().value).toEqual("23585 NE Sandy Blvd. -, Wood Village, OR, USA 97060");
|
||||
});
|
||||
|
||||
it("Renders & Updates Cleaning Level", () => {
|
||||
// Inital value is present
|
||||
let cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
|
||||
expect(cleaningLevelSelection.length).toBe(1);
|
||||
expect(cleaningLevelSelection.props().value).toBeUndefined();
|
||||
|
||||
// Change triggers callback
|
||||
cleaningLevelSelection.simulate("change", 1);
|
||||
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).not.toBeUndefined();
|
||||
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).toEqual(dummyCleaningLevels[0].code);
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
|
||||
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
auctionAssignment: {
|
||||
cleaningLevel: dummyCleaningLevels[1].code,
|
||||
},
|
||||
});
|
||||
cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
|
||||
expect(cleaningLevelSelection.props().value).toBe(2);
|
||||
});
|
||||
|
||||
it("Renders & Updates Auction Notes", () => {
|
||||
// Inital value is present
|
||||
let additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
|
||||
expect(additionalNotesInput.length).toBe(1);
|
||||
expect(additionalNotesInput.props().value).toBeUndefined();
|
||||
|
||||
// Change triggers callback
|
||||
additionalNotesInput.simulate("change", "Some New Notes");
|
||||
expect(dummyAuctionAssignmentToUpdate.auctionNotes).not.toBeUndefined();
|
||||
expect(dummyAuctionAssignmentToUpdate.auctionNotes).toEqual("Some New Notes");
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
|
||||
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
auctionAssignment: {
|
||||
auctionNotes: "Even newer Notes!",
|
||||
},
|
||||
});
|
||||
additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
|
||||
expect(additionalNotesInput.props().value).toBe("Even newer Notes!");
|
||||
});
|
||||
|
||||
it("Sets Default Legal Sale Date When Legal Sale Date is Undefined", () => {
|
||||
wrapper.setProps({
|
||||
defaultLegalSaleDate: moment("1969-07-20")
|
||||
});
|
||||
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(moment("1969-07-20"));
|
||||
});
|
||||
|
||||
it("Does Not Set Default Legal Sale Date When Legal Sale Date is Not Undefined", () => {
|
||||
wrapper.setProps({
|
||||
defaultLegalSaleDate: moment("1969-07-20"),
|
||||
auctionAssignment: {
|
||||
legalSaleDate: moment("1957-10-04")
|
||||
}
|
||||
});
|
||||
|
||||
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import moment, { Moment } from "moment";
|
||||
import React from "react";
|
||||
import { ICollateralLocation } from "../../containers/auction-collateral-location/AuctionCollateralLocation";
|
||||
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
|
||||
import { ICleaningLevel } from "../../reducers/SettingsReducer";
|
||||
import CardView from "../card-view/CardView";
|
||||
import FormInput, { FormInputType } from "../form-input/FormInput";
|
||||
import AuctionAssignmentMap from "./auction-assignment-map/AuctionAssignmentMap";
|
||||
import "./AuctionAssignment.css";
|
||||
|
||||
const cloneDeep = require("lodash.clonedeep");
|
||||
|
||||
export interface IUserSelectedAuctionAssignment {
|
||||
repoId: number;
|
||||
auctionId?: string;
|
||||
cleaningLevel?: number;
|
||||
availablePickupDate?: Moment;
|
||||
legalSaleDate?: Moment;
|
||||
auctionNotes?: string;
|
||||
}
|
||||
|
||||
export interface IAuctionAssignmentProps {
|
||||
collateralLocation: ICollateralLocation;
|
||||
auctionAssignment: IUserSelectedAuctionAssignment;
|
||||
auctionHouses: IAuctionHouse[];
|
||||
cleaningLevels: ICleaningLevel[];
|
||||
defaultLegalSaleDate?: Moment;
|
||||
onAuctionAssignmentChange: (auctionAssignment: IUserSelectedAuctionAssignment) => any;
|
||||
}
|
||||
|
||||
type IUserAuctionAssignmentUpdater = (x: IUserSelectedAuctionAssignment) => void;
|
||||
|
||||
const AuctionAssignment: React.FC<IAuctionAssignmentProps> = (props) => {
|
||||
|
||||
const applyChange = (updater: IUserAuctionAssignmentUpdater) => {
|
||||
const copiedAuctionassignment = cloneDeep(props.auctionAssignment);
|
||||
updater(copiedAuctionassignment);
|
||||
props.onAuctionAssignmentChange(copiedAuctionassignment);
|
||||
};
|
||||
|
||||
const getAuctionHouseAddressDisplay = (auctionId?: string) => {
|
||||
if (!auctionId) {
|
||||
return "Select Auction House for Address";
|
||||
}
|
||||
const auctionHouse = props.auctionHouses.find((auction) => auction.auctionID === auctionId );
|
||||
if (auctionHouse) {
|
||||
return `${auctionHouse.address_Line_1} ${auctionHouse.address_Line_2}, ${auctionHouse.city}, ${auctionHouse.state}, ${auctionHouse.country} ${auctionHouse.zip || ""}`;
|
||||
}
|
||||
};
|
||||
|
||||
const setDefaultLegalSaleValue = (defaultLegalSale?: Moment) => {
|
||||
if (!props.auctionAssignment.legalSaleDate && defaultLegalSale) {
|
||||
applyChange(auctionAssignment => auctionAssignment.legalSaleDate = defaultLegalSale);
|
||||
}
|
||||
};
|
||||
setDefaultLegalSaleValue(props.defaultLegalSaleDate);
|
||||
|
||||
return (
|
||||
<CardView
|
||||
header="Auction Assignment"
|
||||
rows={[
|
||||
<div key="auction-assignment-form" className="auction-assignment-section container">
|
||||
<div className="row">
|
||||
<div className="col-md">
|
||||
<FormInput
|
||||
label="Auction House"
|
||||
type={FormInputType.Selection}
|
||||
value={props.auctionAssignment.auctionId}
|
||||
onChange={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
|
||||
required={true}
|
||||
placeholder="Select Auction House"
|
||||
options={props.auctionHouses.map((auctionHouse) => {
|
||||
return {
|
||||
value: auctionHouse.auctionID,
|
||||
display: auctionHouse.name
|
||||
};
|
||||
})}
|
||||
data-test="auction-assignment-auction-house"
|
||||
/>
|
||||
<input
|
||||
className="form-control"
|
||||
value={getAuctionHouseAddressDisplay(props.auctionAssignment.auctionId)}
|
||||
data-test="auction-assignment-auction-house-address"
|
||||
disabled={true}
|
||||
/>
|
||||
<hr/>
|
||||
<FormInput
|
||||
label="Cleaning Level"
|
||||
type={FormInputType.Selection}
|
||||
value={props.auctionAssignment.cleaningLevel}
|
||||
onChange={(cleaningLevel) => applyChange(auctionAssignment => auctionAssignment.cleaningLevel = parseInt(cleaningLevel))}
|
||||
placeholder="Select Cleaning Level"
|
||||
required={true}
|
||||
options={props.cleaningLevels.map((cleaningLevel) => {
|
||||
return {
|
||||
value: cleaningLevel.code,
|
||||
display: cleaningLevel.description
|
||||
};
|
||||
})}
|
||||
data-test="auction-assignment-cleaning-level"
|
||||
/>
|
||||
<FormInput
|
||||
label="Available For Pickup"
|
||||
type={FormInputType.Datetime}
|
||||
minDate={moment()}
|
||||
value={props.auctionAssignment.availablePickupDate}
|
||||
required={true}
|
||||
onChange={(availablePickupDate) => applyChange(auctionAssignment => auctionAssignment.availablePickupDate = availablePickupDate)}
|
||||
data-test="auction-assignment-available-for-pickup"
|
||||
/>
|
||||
<FormInput
|
||||
label="Legal Sale"
|
||||
type={FormInputType.Datetime}
|
||||
minDate={moment()}
|
||||
value={props.auctionAssignment.legalSaleDate}
|
||||
required={true}
|
||||
onChange={(legalSaleDate) => applyChange(auctionAssignment => auctionAssignment.legalSaleDate = legalSaleDate)}
|
||||
data-test="auction-assignment-legal-sale"
|
||||
/>
|
||||
<FormInput
|
||||
label="Additional Pickup Notes"
|
||||
type={FormInputType.Textarea}
|
||||
value={props.auctionAssignment.auctionNotes}
|
||||
onChange={(auctionNotes) => applyChange(auctionAssignment => auctionAssignment.auctionNotes = auctionNotes)}
|
||||
data-test="auction-assignment-pickup-notes"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md">
|
||||
<AuctionAssignmentMap
|
||||
collateralLocation={props.collateralLocation}
|
||||
currentlySelectedAuctionId={props.auctionAssignment.auctionId}
|
||||
auctionHouses={props.auctionHouses}
|
||||
onPinSelected={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
|
||||
data-test=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuctionAssignment;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#auction-house-location-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
#collateral-center-btn {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { shallow, ShallowWrapper, mount} from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import AuctionAssignmentMap from "./AuctionAssignmentMap";
|
||||
//import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
|
||||
|
||||
//TODO: This one is hard, come back to this later
|
||||
|
||||
describe("Auction Assignment Map Component", () => {
|
||||
it("Does Nothing For Now", () => {});
|
||||
});
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import React from "react";
|
||||
import * as atlas from 'azure-maps-control';
|
||||
import { toast } from "react-toastify";
|
||||
import { ICollateralLocation } from "../../../containers/auction-collateral-location/AuctionCollateralLocation";
|
||||
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
|
||||
import { ILocationCoordinates } from "../../../services/service-types/LocationTypes";
|
||||
import NetworkService from "../../../services/NetworkService";
|
||||
import Configuration from "../../../shared/configuration/Configuration";
|
||||
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
|
||||
import "./AuctionAssignmentMap.css";
|
||||
|
||||
//const Microsoft = (window as any).Microsoft;
|
||||
var map: atlas.Map;
|
||||
|
||||
var popup = new atlas.Popup({
|
||||
pixelOffset: [0, -18],
|
||||
closeButton: false
|
||||
});
|
||||
|
||||
interface IAuctionAssignmentMapProps {
|
||||
collateralLocation: ICollateralLocation;
|
||||
auctionHouses: IAuctionHouse[];
|
||||
currentlySelectedAuctionId?: string;
|
||||
onPinSelected: (auctionId: string) => any;
|
||||
}
|
||||
|
||||
interface ICoordinates {
|
||||
lat: number;
|
||||
long: number;
|
||||
}
|
||||
|
||||
interface IAuctionAssignmentMapState {
|
||||
collateralCoordinates?: ICoordinates;
|
||||
}
|
||||
|
||||
class AuctionAssignmentMap extends React.Component<IAuctionAssignmentMapProps, IAuctionAssignmentMapState> {
|
||||
|
||||
constructor(props: IAuctionAssignmentMapProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
||||
this.LocationsApiRecieved = this.LocationsApiRecieved.bind(this);
|
||||
this.centerMap = this.centerMap.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.instantiateMap();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IAuctionAssignmentMapProps, prevState: IAuctionAssignmentMapState) {
|
||||
if (this.state.collateralCoordinates && prevState.collateralCoordinates !== this.state.collateralCoordinates) {
|
||||
this.updateMap(true);
|
||||
}
|
||||
if (prevProps.currentlySelectedAuctionId !== this.props.currentlySelectedAuctionId) {
|
||||
this.updateMap();
|
||||
}
|
||||
}
|
||||
|
||||
centerMap() {
|
||||
if (this.isCollateralLocationFilled()) {
|
||||
const addressParam = encodeURIComponent(this.props.collateralLocation.address);
|
||||
const zipParam = encodeURIComponent(this.props.collateralLocation.zip);
|
||||
const url = `${Configuration.Get.baseUrls.locationGeocode}api/Location/GetClosestPositionByAddress?Street=${addressParam}&ZipCode=${zipParam}`;
|
||||
this.sendLocationsApiRequest(url);
|
||||
}
|
||||
}
|
||||
|
||||
isCollateralLocationFilled() {
|
||||
return !!this.props.collateralLocation.address && !!this.props.collateralLocation.zip;
|
||||
}
|
||||
|
||||
sendLocationsApiRequest(url: string) {
|
||||
NetworkService.FetchLocationData(url)
|
||||
.then((data) => {
|
||||
this.LocationsApiRecieved(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = "Could not locate collateral address";
|
||||
window.console.warn(message, error);
|
||||
toast.warn(message, ToastDefaults.baseParams);
|
||||
});
|
||||
}
|
||||
|
||||
LocationsApiRecieved(data: ILocationCoordinates) {
|
||||
this.setState({
|
||||
collateralCoordinates: {
|
||||
lat: data.latitude,
|
||||
long: data.longitude
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAuctionHouse(auctionId?: string) {
|
||||
return this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === auctionId);
|
||||
}
|
||||
|
||||
|
||||
updateMap(switchViewToCollateral: boolean = false) {
|
||||
popup.close()
|
||||
|
||||
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
|
||||
|
||||
let viewPortLocation: any;
|
||||
if (this.state.collateralCoordinates && switchViewToCollateral) {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
|
||||
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
|
||||
} else {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(
|
||||
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
|
||||
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
|
||||
);
|
||||
}
|
||||
|
||||
map.markers.clear();
|
||||
|
||||
map.setCamera({
|
||||
center: viewPortLocation
|
||||
})
|
||||
|
||||
var validPins = [];
|
||||
|
||||
// Push Collateral & Selected Auction Pins First
|
||||
// This will ensure that they appear on top
|
||||
if (this.state.collateralCoordinates) {
|
||||
validPins.push(new atlas.HtmlMarker({
|
||||
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
|
||||
color: "green"
|
||||
}));
|
||||
}
|
||||
|
||||
if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
|
||||
const selectedAuctionHouse = this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === this.props.currentlySelectedAuctionId);
|
||||
if (selectedAuctionHouse) {
|
||||
const pushPin = new atlas.HtmlMarker({
|
||||
position: atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude || 0, selectedAuctionHouse.latitude || 0),//Check this Corey
|
||||
color: "red"
|
||||
});
|
||||
validPins.push(pushPin);
|
||||
map.events.add("mouseover", pushPin, () => this.openPopup(selectedAuctionHouse, popup, map));
|
||||
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.props.auctionHouses.length; i++) {
|
||||
const auctionHouse = this.props.auctionHouses[i];
|
||||
|
||||
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
|
||||
const pushPin = new atlas.HtmlMarker({
|
||||
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
|
||||
});
|
||||
validPins.push(pushPin);
|
||||
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
|
||||
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
|
||||
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
|
||||
}
|
||||
}
|
||||
|
||||
map.markers.add(validPins);
|
||||
}
|
||||
|
||||
instantiateMap(switchViewToCollateral: boolean = false) {
|
||||
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
|
||||
|
||||
let viewPortLocation: any;
|
||||
if (this.state.collateralCoordinates && switchViewToCollateral) {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
|
||||
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
|
||||
} else {
|
||||
viewPortLocation = atlas.data.Position.fromLatLng(
|
||||
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
|
||||
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
|
||||
);
|
||||
}
|
||||
|
||||
map = new atlas.Map("auction-house-location-map", {
|
||||
zoom: 9,
|
||||
center: viewPortLocation,
|
||||
authOptions: {
|
||||
authType: atlas.AuthenticationType.subscriptionKey,
|
||||
subscriptionKey: Configuration.Get.atlasCredentials
|
||||
}
|
||||
});
|
||||
|
||||
var validPins = [];
|
||||
|
||||
// Push Collateral & Selected Auction Pins First
|
||||
// This will ensure that they appear on top
|
||||
if (this.state.collateralCoordinates) {
|
||||
validPins.push(new atlas.HtmlMarker({
|
||||
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
|
||||
color: "green"
|
||||
}));
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.props.auctionHouses.length; i++) {
|
||||
const auctionHouse = this.props.auctionHouses[i];
|
||||
|
||||
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
|
||||
const pushPin = new atlas.HtmlMarker({
|
||||
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
|
||||
});
|
||||
validPins.push(pushPin);
|
||||
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
|
||||
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
|
||||
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
|
||||
}
|
||||
}
|
||||
|
||||
map.markers.add(validPins);
|
||||
}
|
||||
|
||||
openPopup(auctionHouse: IAuctionHouse, popup: atlas.Popup, map: atlas.Map) {
|
||||
console.log("Mouseover on pin for auction house:" + auctionHouse.name, auctionHouse.city, auctionHouse.longitude, auctionHouse.latitude);
|
||||
if (!map) {
|
||||
console.log("Map is not initialized, cannot open popup.")
|
||||
}
|
||||
popup.setOptions({
|
||||
content: "<div>" + auctionHouse.name + "<br>" + auctionHouse.city + "</div>",
|
||||
position: [auctionHouse.longitude||0, auctionHouse.latitude||0],
|
||||
})
|
||||
popup.open(map);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div id="auction-house-location-map" />
|
||||
<button
|
||||
id="collateral-center-btn"
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!this.isCollateralLocationFilled()}
|
||||
onClick={this.centerMap}
|
||||
>
|
||||
Center on Collateral
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuctionAssignmentMap;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
.auction-collateral-selection
|
||||
{
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.auction-collateral-selection
|
||||
.card-body:not(.bg-warning) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auction-collateral-selection
|
||||
.bg-warning {
|
||||
cursor: default;
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import AuctionCollateralSelection, { ICollateralSelection, IAuctionCollateralSelectionProps } from './AuctionCollateralSelection';
|
||||
|
||||
describe('Auction Collateral Selection Component', () => {
|
||||
let auctionCollateralSelection: AuctionCollateralSelection;
|
||||
let originalCollateralSelections: ICollateralSelection[];
|
||||
|
||||
let defaultProps: IAuctionCollateralSelectionProps;
|
||||
let updatedProps:IAuctionCollateralSelectionProps;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCollateralSelections = [
|
||||
{
|
||||
collateral: {
|
||||
collateralId: 1,
|
||||
repoId: 1,
|
||||
vin: '1234',
|
||||
make: 'ford',
|
||||
model: 'f150',
|
||||
year: 1996,
|
||||
repoCompanySelections: []
|
||||
|
||||
},
|
||||
isSelected: false,
|
||||
},
|
||||
{
|
||||
collateral: {
|
||||
collateralId: 2,
|
||||
repoId: 1,
|
||||
vin: 'abcd',
|
||||
make: 'dodge',
|
||||
model: 'ram',
|
||||
year: 2001,
|
||||
auctionAssignmentId: 1,
|
||||
repoCompanySelections: []
|
||||
},
|
||||
isSelected: false,
|
||||
},
|
||||
];
|
||||
defaultProps = {
|
||||
collateralSelections: originalCollateralSelections,
|
||||
vehicleTypes: [],
|
||||
collateralTypes: [],
|
||||
bodyStyles: [],
|
||||
collateralTypesToVehicleTypes: [],
|
||||
onCollateralSelectionsChanged: (cs) => updatedProps.collateralSelections = cs,
|
||||
onPrimaryCollateralChanged: (cid) => updatedProps.primaryCollateralId = cid,
|
||||
};
|
||||
updatedProps = Object.assign({}, defaultProps);
|
||||
auctionCollateralSelection = new AuctionCollateralSelection(defaultProps);
|
||||
});
|
||||
|
||||
it ('Renders AuctionCollateralSelectionRows', () => {
|
||||
let wrapper = shallow(
|
||||
<AuctionCollateralSelection
|
||||
collateralSelections={originalCollateralSelections}
|
||||
vehicleTypes={[]}
|
||||
collateralTypes={[]}
|
||||
bodyStyles={[]}
|
||||
collateralTypesToVehicleTypes={[]}
|
||||
onCollateralSelectionsChanged={() => {}}
|
||||
onPrimaryCollateralChanged={() => {}}
|
||||
/>
|
||||
);
|
||||
let selectionRow = wrapper.find(`[data-test='auction-collateral-selection-row']`);
|
||||
expect(selectionRow.length).toBe(2);
|
||||
});
|
||||
|
||||
it ('Updates Primary Collateral Id', () => {
|
||||
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[0]);
|
||||
expect(updatedProps.primaryCollateralId).toBe(1);
|
||||
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[1]);
|
||||
expect(updatedProps.primaryCollateralId).toBe(2);
|
||||
});
|
||||
|
||||
it ('Selecting Collateral Copies and Calls onCollateralSelectionsChanged()', () => {
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.collateralSelections).not.toBeUndefined();
|
||||
expect(updatedProps.collateralSelections).not.toBe(originalCollateralSelections);
|
||||
expect(updatedProps.collateralSelections[0].isSelected).toBe(true);
|
||||
});
|
||||
|
||||
it ('Selecting Collateral Without Primary Collateral Calls onPrimaryCollateralChanged()', () => {
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.primaryCollateralId).toBe(1);
|
||||
});
|
||||
|
||||
it ('Selecting Multiple Collaterals Does Not Continue Calling onPrimaryCollateralChanged()', () => {
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.primaryCollateralId).toBe(1);
|
||||
auctionCollateralSelection.toggleCollateralByIndex(1);
|
||||
expect(updatedProps.primaryCollateralId).toBe(1);
|
||||
});
|
||||
|
||||
it ('Unselecting All Collaterals Calls onPrimaryCollateralChanged() With Undefined', () => {
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.primaryCollateralId).toBe(1);
|
||||
|
||||
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
|
||||
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.primaryCollateralId).toBeUndefined();
|
||||
});
|
||||
|
||||
it ('Unselecting Primary Collateral With Another Selection Calls onPrimaryCollateralChanged() With Valid Id', () => {
|
||||
updatedProps.collateralSelections[0].isSelected = true;
|
||||
updatedProps.collateralSelections[1].isSelected = true;
|
||||
updatedProps.primaryCollateralId = 1;
|
||||
|
||||
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(updatedProps.primaryCollateralId).toBe(2);
|
||||
});
|
||||
|
||||
it ('Selecting Collateral With Auction Triggers Nothing', () => {
|
||||
let wasSelectionsUpdated = false;
|
||||
let wasPrimaryCollateralUpdated = false;
|
||||
|
||||
updatedProps.collateralSelections[0].collateral.auctionAssignmentId = 1;
|
||||
updatedProps.onCollateralSelectionsChanged = () => wasSelectionsUpdated = true;
|
||||
updatedProps.onPrimaryCollateralChanged = () => wasPrimaryCollateralUpdated = true;
|
||||
|
||||
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
|
||||
|
||||
auctionCollateralSelection.toggleCollateralByIndex(0);
|
||||
expect(wasSelectionsUpdated).toBe(false);
|
||||
expect(wasPrimaryCollateralUpdated).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { IVehicleType, ICollateralType, IBodyStyle, ICollateralTypesToVehicleType } from "../../reducers/SettingsReducer";
|
||||
import './AuctionCollateralSelection.css';
|
||||
import AuctionCollateralSelectionRow from './auction-collateral-selection-row/AuctionCollateralSelectionRow';
|
||||
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
|
||||
|
||||
const cloneDeep = require('lodash.clonedeep');
|
||||
|
||||
export interface ICollateralSelection {
|
||||
collateral: ICollateral;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export interface IAuctionCollateralSelectionProps {
|
||||
collateralSelections: ICollateralSelection[];
|
||||
vehicleTypes: IVehicleType[];
|
||||
collateralTypes: ICollateralType[];
|
||||
bodyStyles: IBodyStyle[];
|
||||
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
|
||||
primaryCollateralId?: number;
|
||||
onCollateralSelectionsChanged: (collateralSelections: ICollateralSelection[]) => any;
|
||||
onPrimaryCollateralChanged: (collateralId?: number) => any;
|
||||
}
|
||||
|
||||
class AuctionCollateralSelection extends React.Component<IAuctionCollateralSelectionProps> {
|
||||
|
||||
constructor(props: IAuctionCollateralSelectionProps) {
|
||||
super(props);
|
||||
this.isCollateralSelectionPrimary = this.isCollateralSelectionPrimary.bind(this);
|
||||
}
|
||||
|
||||
applyUpdate (index: number, updater: (x: ICollateralSelection) => void) {
|
||||
const copiedCollaterSpecSelection = cloneDeep(this.props.collateralSelections) as ICollateralSelection[];
|
||||
updater(copiedCollaterSpecSelection[index]);
|
||||
this.props.onCollateralSelectionsChanged(copiedCollaterSpecSelection);
|
||||
};
|
||||
|
||||
collateralUpdated(index: number, collateral: ICollateral) {
|
||||
this.applyUpdate(index, (collateralSelection) => {
|
||||
collateralSelection.collateral = collateral
|
||||
});
|
||||
};
|
||||
|
||||
toggleCollateralByIndex(index: number) {
|
||||
const collateralSelection = this.props.collateralSelections[index];
|
||||
|
||||
if (this.isCollateralSelectable(collateralSelection)) {
|
||||
|
||||
if (!this.props.primaryCollateralId) {
|
||||
// There is no primary collateral selected, default to this one
|
||||
this.props.onPrimaryCollateralChanged(collateralSelection.collateral.collateralId);
|
||||
} else if (collateralSelection.isSelected && this.isCollateralSelectionPrimary(collateralSelection)) {
|
||||
// The selection with the primary collateral is about to be closed.
|
||||
// Try and find a available collateral to be primary
|
||||
this.props.onPrimaryCollateralChanged(this.getNextPrimaryCollateralId());
|
||||
}
|
||||
|
||||
this.applyUpdate(index, (collateralSelection) => {
|
||||
collateralSelection.isSelected = !collateralSelection.isSelected;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getNextPrimaryCollateralId() {
|
||||
const nextCollateralSelection = this.props.collateralSelections.find(
|
||||
(collateralSelection) => collateralSelection.isSelected && !this.isCollateralSelectionPrimary(collateralSelection)
|
||||
);
|
||||
if (nextCollateralSelection) {
|
||||
return nextCollateralSelection.collateral.collateralId;
|
||||
}
|
||||
}
|
||||
|
||||
setPrimaryCollateral(collateralSelection: ICollateralSelection) {
|
||||
this.props.onPrimaryCollateralChanged(
|
||||
collateralSelection.collateral.collateralId,
|
||||
)
|
||||
}
|
||||
|
||||
isCollateralSelectable(collateralSelection: ICollateralSelection): boolean {
|
||||
return !collateralSelection.collateral.auctionAssignmentId;
|
||||
}
|
||||
|
||||
isCollateralSelectionPrimary(collateralSelection: ICollateralSelection) {
|
||||
return this.props.primaryCollateralId === collateralSelection.collateral.collateralId;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="card auction-collateral-selection">
|
||||
{this.props.collateralSelections.map((collateralSelection, index) =>
|
||||
<AuctionCollateralSelectionRow
|
||||
key={collateralSelection.collateral.collateralId}
|
||||
collateralSelection={collateralSelection}
|
||||
vehicleTypes={this.props.vehicleTypes}
|
||||
collateralTypes={this.props.collateralTypes}
|
||||
bodyStyles={this.props.bodyStyles}
|
||||
collateralTypesToVehicleTypes={this.props.collateralTypesToVehicleTypes}
|
||||
isCollateralPrimary={this.isCollateralSelectionPrimary(collateralSelection)}
|
||||
onCollateralChange={(collateral) => this.collateralUpdated(index, collateral)}
|
||||
onIsPrimaryClicked={() => this.setPrimaryCollateral(collateralSelection)}
|
||||
onRowClicked={() => this.toggleCollateralByIndex(index)}
|
||||
data-test="auction-collateral-selection-row"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export default AuctionCollateralSelection;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { ShallowWrapper, shallow } from 'enzyme';
|
||||
import AuctionCollateralSelectionRow from './AuctionCollateralSelectionRow';
|
||||
import { ICollateralSelection } from '../AuctionCollateralSelection';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('AuctionCollateralSelectionRow Component', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
let collateralSelection: ICollateralSelection;
|
||||
|
||||
beforeEach(() => {
|
||||
collateralSelection = {
|
||||
isSelected: false,
|
||||
collateral: {
|
||||
collateralId: 2,
|
||||
repoId: 1,
|
||||
vin: 'abcd',
|
||||
make: 'dodge',
|
||||
model: 'ram',
|
||||
year: 2001,
|
||||
repoCompanySelections: []
|
||||
},
|
||||
};
|
||||
wrapper = shallow(
|
||||
<AuctionCollateralSelectionRow
|
||||
collateralSelection={collateralSelection}
|
||||
vehicleTypes={[]}
|
||||
collateralTypes={[]}
|
||||
bodyStyles={[]}
|
||||
collateralTypesToVehicleTypes={[]}
|
||||
isCollateralPrimary={false}
|
||||
onCollateralChange={() => {}}
|
||||
onIsPrimaryClicked={() => {}}
|
||||
onRowClicked={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it ('Displays Collateral Correctly', () => {
|
||||
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
|
||||
expect(collateralDescription.length).toBe(1);
|
||||
expect(collateralDescription.text()).toEqual('2001 dodge ram');
|
||||
});
|
||||
|
||||
it ('Displays Collateral Selection Statuses Correctly', () => {
|
||||
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
|
||||
expect(collateralDescription.length).toBe(1);
|
||||
expect(collateralDescription.text()).toEqual('2001 dodge ram');
|
||||
|
||||
let selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
|
||||
expect(selectionStatus.length).toBe(1);
|
||||
expect(selectionStatus.text().trim()).toEqual('(Not Selected)');
|
||||
|
||||
wrapper.setProps({
|
||||
collateralSelection: {
|
||||
...collateralSelection,
|
||||
isSelected: true,
|
||||
},
|
||||
});
|
||||
selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
|
||||
expect(selectionStatus.length).toBe(1);
|
||||
expect(selectionStatus.text().trim()).toEqual('(Selected)');
|
||||
|
||||
// wrapper.setProps({
|
||||
// collateralSelection: {
|
||||
// collateral: {
|
||||
// ...collateralSelection.collateral,
|
||||
// auctionAssignmentId: 1,
|
||||
// },
|
||||
// isSelected: false,
|
||||
// },
|
||||
// });
|
||||
// selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
|
||||
// expect(selectionStatus.length).toBe(1);
|
||||
// expect(selectionStatus.text().trim()).toEqual('(Auction Already Created)');
|
||||
});
|
||||
|
||||
it ('Calls Callback on Click', () => {
|
||||
let wasCallTriggered = false;
|
||||
wrapper.setProps({
|
||||
onRowClicked: () => wasCallTriggered = true,
|
||||
});
|
||||
let rowHeader = wrapper.find(`[data-test='auction-collateral-selection-header']`);
|
||||
rowHeader.simulate('click');
|
||||
expect(wasCallTriggered).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import React from "react";
|
||||
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
|
||||
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
|
||||
import AuctionCollateralSpecs from "../auction-collateral-specs/AuctionCollateralSpecs";
|
||||
import { ICollateralSelection } from "../AuctionCollateralSelection";
|
||||
|
||||
interface IAuctionCollateralSelectionRowProps {
|
||||
collateralSelection: ICollateralSelection;
|
||||
vehicleTypes: IVehicleType[];
|
||||
collateralTypes: ICollateralType[];
|
||||
bodyStyles: IBodyStyle[];
|
||||
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
|
||||
isCollateralPrimary: boolean;
|
||||
onCollateralChange: (collateral: ICollateral) => any;
|
||||
onIsPrimaryClicked: () => any;
|
||||
onRowClicked: () => any;
|
||||
}
|
||||
|
||||
const AuctionCollateralSelectionRow: React.FC<IAuctionCollateralSelectionRowProps> = (props) => {
|
||||
|
||||
const getCollateralDisplayName = () => {
|
||||
const collateral = props.collateralSelection.collateral;
|
||||
return `${collateral.year} ${collateral.make} ${collateral.model}`;
|
||||
};
|
||||
|
||||
const getHeaderStatusText = () => {
|
||||
if (props.collateralSelection.isSelected) {
|
||||
return " (Selected)";
|
||||
}
|
||||
return " (Not Selected)";
|
||||
};
|
||||
|
||||
const getHeaderBGClass = () => {
|
||||
if (props.collateralSelection.isSelected) {
|
||||
return "bg-success";
|
||||
}
|
||||
return "bg-light";
|
||||
};
|
||||
|
||||
const getHeaderColorClass = () => {
|
||||
if (props.collateralSelection.isSelected) {
|
||||
return "text-white";
|
||||
}
|
||||
return "text-dark";
|
||||
};
|
||||
|
||||
const getFontAwesomeIconClass = () => {
|
||||
if (props.collateralSelection.isSelected) {
|
||||
return "fa-check-square-o";
|
||||
}
|
||||
return "fa-square-o";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auction-collateral-selection-row">
|
||||
<div
|
||||
className={"card-body " + getHeaderBGClass()}
|
||||
onClick={props.onRowClicked}
|
||||
data-test="auction-collateral-selection-header"
|
||||
>
|
||||
<h6 className={getHeaderColorClass()}>
|
||||
<i className={"fa " + getFontAwesomeIconClass()} aria-hidden="true"/>
|
||||
|
||||
<span data-test="auction-collateral-selection-collateral-description">
|
||||
{getCollateralDisplayName()}
|
||||
</span>
|
||||
<i data-test="auction-collateral-selection-header-status-text">
|
||||
{getHeaderStatusText()}
|
||||
</i>
|
||||
</h6>
|
||||
</div>
|
||||
{
|
||||
props.collateralSelection.isSelected
|
||||
?
|
||||
<ul className="list-group list-group-flush" data-test="auction-collateral-selection-opened-section">
|
||||
<li className="list-group-item">
|
||||
<AuctionCollateralSpecs
|
||||
collateral={props.collateralSelection.collateral}
|
||||
vehicleTypes={props.vehicleTypes}
|
||||
collateralTypes={props.collateralTypes}
|
||||
bodyStyles={props.bodyStyles}
|
||||
collateralTypesToVehicleTypes={props.collateralTypesToVehicleTypes}
|
||||
onCollateralChange={props.onCollateralChange}
|
||||
isCollateralPrimary={props.isCollateralPrimary}
|
||||
onIsPrimaryClicked={props.onIsPrimaryClicked}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuctionCollateralSelectionRow;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.is-collateral-primary-check,
|
||||
.is-collateral-primary-check * {
|
||||
cursor: pointer;
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import React from "react";
|
||||
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
|
||||
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
|
||||
import AuctionCollateralSpecs from "./AuctionCollateralSpecs";
|
||||
|
||||
describe("Auction Collateral Specs Component", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
let collateralTypes: ICollateralType[];
|
||||
let collateralTypeToVehicleTypes: ICollateralTypesToVehicleType[];
|
||||
let bodyStyles: IBodyStyle[];
|
||||
let vehicleTypes: IVehicleType[];
|
||||
|
||||
let originalCollateral: ICollateral;
|
||||
let updatedCollateral: ICollateral | undefined;
|
||||
|
||||
const collateralUpdated = (newCollateral: ICollateral) => {
|
||||
updatedCollateral = newCollateral;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
updatedCollateral = undefined;
|
||||
originalCollateral = {
|
||||
collateralId: 1,
|
||||
repoId: 1,
|
||||
vin: "1234",
|
||||
make: "ford",
|
||||
model: "f150",
|
||||
year: 1996,
|
||||
repoCompanySelections: []
|
||||
};
|
||||
collateralTypes = [
|
||||
{
|
||||
id: 1,
|
||||
description: "Collateral Type 1",
|
||||
lookupHandler: "",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
description: "Collateral Type 2",
|
||||
lookupHandler: "",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
description: "Collateral Type 3",
|
||||
lookupHandler: "",
|
||||
},
|
||||
];
|
||||
collateralTypeToVehicleTypes = [
|
||||
{
|
||||
id: 1,
|
||||
vehicleTypeId: 1,
|
||||
collateralTypeId: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
vehicleTypeId: 1,
|
||||
collateralTypeId: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
vehicleTypeId: 2,
|
||||
collateralTypeId: 3,
|
||||
},
|
||||
];
|
||||
bodyStyles = [
|
||||
{
|
||||
code: "body1",
|
||||
description: "Body Style #1",
|
||||
},
|
||||
{
|
||||
code: "body2",
|
||||
description: "Body Style #2",
|
||||
},
|
||||
];
|
||||
vehicleTypes = [
|
||||
{
|
||||
code: "1",
|
||||
description: "Vehicle Type #1",
|
||||
},
|
||||
{
|
||||
code: "2",
|
||||
description: "Vehicle Type #2",
|
||||
},
|
||||
{
|
||||
code: "3",
|
||||
description: "Vehicle Type #3",
|
||||
},
|
||||
];
|
||||
wrapper = shallow(
|
||||
<AuctionCollateralSpecs
|
||||
collateral={originalCollateral}
|
||||
vehicleTypes={vehicleTypes}
|
||||
collateralTypes={collateralTypes}
|
||||
bodyStyles={bodyStyles}
|
||||
collateralTypesToVehicleTypes={collateralTypeToVehicleTypes}
|
||||
onCollateralChange={collateralUpdated}
|
||||
isCollateralPrimary={false}
|
||||
onIsPrimaryClicked={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it("Renders & Updates Vehicle Type", () => {
|
||||
// Inital value is present
|
||||
let vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
|
||||
expect(vehicleTypeSelect.length).toBe(1);
|
||||
expect(vehicleTypeSelect.props().value).toBe(undefined);
|
||||
|
||||
// Change triggers callback
|
||||
vehicleTypeSelect.simulate("change", "1");
|
||||
expect(updatedCollateral).not.toBeUndefined();
|
||||
expect(updatedCollateral!.vehicleType).toEqual(parseInt(vehicleTypes[0].code));
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(originalCollateral).not.toBe(updatedCollateral);
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
collateral: {
|
||||
vehicleType: 2,
|
||||
},
|
||||
});
|
||||
vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
|
||||
expect(vehicleTypeSelect.props().value).toBe(2);
|
||||
});
|
||||
|
||||
it("Renders & Updates Body Style", () => {
|
||||
// Inital value is present
|
||||
let bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
|
||||
expect(bodyStyleSelect.length).toBe(1);
|
||||
expect(bodyStyleSelect.props().value).toBe(undefined);
|
||||
|
||||
// Change triggers callback
|
||||
bodyStyleSelect.simulate("change", "body1");
|
||||
expect(updatedCollateral).not.toBeUndefined();
|
||||
expect(updatedCollateral!.bodyStyle).toEqual(bodyStyles[0].code);
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(originalCollateral).not.toBe(updatedCollateral);
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
collateral: {
|
||||
bodyStyle: "body2",
|
||||
},
|
||||
});
|
||||
bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
|
||||
expect(bodyStyleSelect.props().value).toBe("body2");
|
||||
});
|
||||
|
||||
it("Renders & Updates Collateral Style", () => {
|
||||
// Inital value is present
|
||||
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
|
||||
expect(collateralTypeSelect.length).toBe(1);
|
||||
expect(collateralTypeSelect.props().value).toBe(undefined);
|
||||
|
||||
// Change triggers callback
|
||||
collateralTypeSelect.simulate("change", "3");
|
||||
expect(updatedCollateral).not.toBeUndefined();
|
||||
expect(updatedCollateral!.collateralTypeId).toEqual(collateralTypes[2].id);
|
||||
|
||||
// Change does not alter original prop object
|
||||
expect(originalCollateral).not.toBe(updatedCollateral);
|
||||
|
||||
// Prop changes update input
|
||||
wrapper = wrapper.setProps({
|
||||
collateral: {
|
||||
collateralTypeId: 2
|
||||
},
|
||||
});
|
||||
collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
|
||||
expect(collateralTypeSelect.props().value).toBe(2);
|
||||
});
|
||||
|
||||
it("Disabled Collateral Type Select when Vehicle Type is Undefined", () => {
|
||||
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
|
||||
expect(collateralTypeSelect.length).toBe(1);
|
||||
expect(collateralTypeSelect.props().value).toBe(undefined);
|
||||
expect(collateralTypeSelect.props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("No Collateral Type Options when Vehicle Type Does Not Match Collateral Types", () => {
|
||||
wrapper = wrapper.setProps({
|
||||
collateral: {
|
||||
vehicleType: 3,
|
||||
},
|
||||
});
|
||||
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
|
||||
expect(collateralTypeSelect.length).toBe(1);
|
||||
expect(collateralTypeSelect.props().value).toBe(undefined);
|
||||
expect(collateralTypeSelect.props().disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("Valid Collateral Types Options when Vehicle Type Matches Collateral Types", () => {
|
||||
wrapper = wrapper.setProps({
|
||||
collateral: {
|
||||
vehicleType: 1,
|
||||
},
|
||||
});
|
||||
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
|
||||
expect(collateralTypeSelect.length).toBe(1);
|
||||
expect(collateralTypeSelect.props().value).toBe(undefined);
|
||||
expect(collateralTypeSelect.props().disabled).toBe(false);
|
||||
});
|
||||
|
||||
it ("Calls onIsPrimaryClicked() When Checkbox is Clicked", () => {
|
||||
let wasCallTriggered = false;
|
||||
wrapper.setProps({
|
||||
onIsPrimaryClicked: () => wasCallTriggered = true,
|
||||
});
|
||||
let isPrimaryContainer = wrapper.find(`[data-test='auction-collateral-specs-is-primary-container']`);
|
||||
isPrimaryContainer.simulate("click");
|
||||
expect(wasCallTriggered).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import React from "react";
|
||||
import { getCollateralTypesFromVehicleType, IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
|
||||
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
|
||||
import FormInput, { FormInputType } from "../../form-input/FormInput";
|
||||
import "./AuctionCollateralSpecs.css";
|
||||
|
||||
const cloneDeep = require("lodash.clonedeep");
|
||||
|
||||
export interface IAuctionCollateralSpecsProps {
|
||||
collateral: ICollateral;
|
||||
vehicleTypes: IVehicleType[];
|
||||
collateralTypes: ICollateralType[];
|
||||
bodyStyles: IBodyStyle[];
|
||||
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
|
||||
isCollateralPrimary: boolean;
|
||||
onCollateralChange: (collateral: ICollateral) => any;
|
||||
onIsPrimaryClicked: () => any;
|
||||
}
|
||||
|
||||
type ICollateralUpdater = (collateral: ICollateral) => void;
|
||||
|
||||
const AuctionCollateralSpecs: React.FC<IAuctionCollateralSpecsProps> = (props) => {
|
||||
|
||||
const applyChange = function( updater: ICollateralUpdater) {
|
||||
const copiedCollateral = cloneDeep(props.collateral) as ICollateral;
|
||||
updater(copiedCollateral);
|
||||
props.onCollateralChange(copiedCollateral);
|
||||
};
|
||||
|
||||
const handlePrimaryCollateralClicked = () => {
|
||||
props.onIsPrimaryClicked();
|
||||
};
|
||||
|
||||
const getCollateralDescription = () => {
|
||||
return `${props.collateral.year} ${props.collateral.make} ${props.collateral.model} ${props.collateral.vin}`;
|
||||
};
|
||||
|
||||
const getCollateralTypes = (vehicleTypeCode?: number) => {
|
||||
return getCollateralTypesFromVehicleType(
|
||||
props.collateralTypesToVehicleTypes,
|
||||
props.collateralTypes,
|
||||
vehicleTypeCode
|
||||
);
|
||||
};
|
||||
|
||||
const handleVehicleTypeChange = (selection: string) => {
|
||||
applyChange((collateral) => {
|
||||
collateral.vehicleType = selection ? parseInt(selection) : undefined;
|
||||
const matchedCollateralTypes = getCollateralTypes(collateral.vehicleType);
|
||||
if (matchedCollateralTypes.length === 1) {
|
||||
collateral.collateralTypeId = matchedCollateralTypes[0].id;
|
||||
} else {
|
||||
collateral.collateralTypeId = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isVehicleTypeSelected = () => {
|
||||
return props.collateral.vehicleType !== undefined && props.collateral.vehicleType !== null;
|
||||
};
|
||||
|
||||
const currentlyMatchedCollateralTypes = getCollateralTypesFromVehicleType(
|
||||
props.collateralTypesToVehicleTypes,
|
||||
props.collateralTypes,
|
||||
props.collateral.vehicleType
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-sm">
|
||||
<div
|
||||
className="custom-control custom-checkbox is-collateral-primary-check"
|
||||
onClick={handlePrimaryCollateralClicked}
|
||||
data-test="auction-collateral-specs-is-primary-container"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
checked={props.isCollateralPrimary}
|
||||
readOnly={true}
|
||||
/>
|
||||
<label className="custom-control-label">Is Primary</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
{getCollateralDescription()}
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<FormInput
|
||||
label="Vehicle Type"
|
||||
required={true}
|
||||
type={FormInputType.Selection}
|
||||
value={props.collateral.vehicleType}
|
||||
onChange={(vehicleType) => handleVehicleTypeChange(vehicleType)}
|
||||
placeholder="Select Vehicle Type"
|
||||
options={props.vehicleTypes.map((vehicleType) => {
|
||||
return {
|
||||
value: vehicleType.code,
|
||||
display: vehicleType.description
|
||||
};
|
||||
})}
|
||||
data-test="auction-collateral-specs-vehicle-type"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<FormInput
|
||||
label="Body Style"
|
||||
required={true}
|
||||
type={FormInputType.Selection}
|
||||
value={props.collateral.bodyStyle}
|
||||
onChange={(bodyStyle) => applyChange((collateral) => collateral.bodyStyle = bodyStyle)}
|
||||
placeholder="Select Body Style"
|
||||
options={props.bodyStyles.map((bodyStyle) => {
|
||||
return {
|
||||
value: bodyStyle.code,
|
||||
display: bodyStyle.description
|
||||
};
|
||||
})}
|
||||
data-test="auction-collateral-specs-body-style"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<FormInput
|
||||
label="Collateral Type"
|
||||
required={true}
|
||||
type={FormInputType.Selection}
|
||||
value={props.collateral.collateralTypeId}
|
||||
onChange={(collateralType) => applyChange((collateral) => collateral.collateralTypeId = parseInt(collateralType))}
|
||||
placeholder="Select Collateral Type"
|
||||
disabled={!isVehicleTypeSelected()}
|
||||
options={currentlyMatchedCollateralTypes.map((collateralType) => {
|
||||
return {
|
||||
value: collateralType.id,
|
||||
display: collateralType.description
|
||||
};
|
||||
})}
|
||||
data-test="auction-collateral-specs-collateral-type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuctionCollateralSpecs;
|
||||
@@ -0,0 +1,16 @@
|
||||
.card-view-main {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.card-view-main .card-body {
|
||||
padding: .25em;
|
||||
}
|
||||
|
||||
.card-view-main h6 {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.card-view-main .list-group-item {
|
||||
padding: .25em 1em;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { shallow } from "enzyme";
|
||||
import CardView from './CardView';
|
||||
|
||||
describe('CardView Component', () => {
|
||||
|
||||
it('CardView Displays No Header', () => {
|
||||
const cardView = shallow(<CardView rows={[]} />);
|
||||
const header = cardView.find(`[data-test='card-view-header']`);
|
||||
expect(header.length).toBe(0);
|
||||
});
|
||||
|
||||
it('CardView Displays No Rows', () => {
|
||||
const cardView = shallow(<CardView rows={[]} />);
|
||||
const rows = cardView.find(`[data-test='card-view-row-parent']`);
|
||||
expect(rows.length).toBe(0);
|
||||
});
|
||||
|
||||
it('CardView Displays Header', () => {
|
||||
const cardView = shallow(<CardView header="Header Test" rows={[]} />);
|
||||
const header = cardView.find(`[data-test='card-view-header']`);
|
||||
expect(header.length).toBe(1);
|
||||
expect(header.text()).toEqual('Header Test');
|
||||
});
|
||||
|
||||
it('CardView Displays Valid Text Rows', () => {
|
||||
const cardView = shallow(<CardView rows={[
|
||||
'Row 1',
|
||||
'Row 2',
|
||||
]} />);
|
||||
const rows = cardView.find(`[data-test='card-view-row-parent']`);
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.at(0).text()).toEqual('Row 1');
|
||||
expect(rows.at(1).text()).toEqual('Row 2');
|
||||
});
|
||||
|
||||
it('CardView Displays Valid HTML Rows', () => {
|
||||
const cardView = shallow(<CardView rows={[
|
||||
<p data-test='card-view-test-html-child'>Child Text</p>,
|
||||
]} />);
|
||||
const pChild = cardView.find(`[data-test='card-view-test-html-child']`);
|
||||
expect(pChild.length).toBe(1);
|
||||
expect(pChild.text()).toEqual('Child Text');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import './CardView.css';
|
||||
|
||||
interface ICardViewProps {
|
||||
header?: any;
|
||||
rows: any[];
|
||||
}
|
||||
|
||||
const CardView: React.FC<ICardViewProps> = (props) => {
|
||||
|
||||
const tryDisplayHeader = () => {
|
||||
if (props.header) {
|
||||
return (
|
||||
<div className="card-body bg-secondary">
|
||||
<h6 className="text-white" data-test="card-view-header">{props.header}</h6>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-view-main">
|
||||
{tryDisplayHeader()}
|
||||
<ul className="list-group list-group-flush">
|
||||
{
|
||||
props.rows.map((item, index) => {
|
||||
return (
|
||||
<li key={index} className="list-group-item" data-test="card-view-row-parent">
|
||||
{item}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardView;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.collateral-view-row p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import CollateralDisplay, { ICollateralDisplay } from './CollateralDisplay';
|
||||
import moment from 'moment';
|
||||
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
|
||||
|
||||
describe('CollateralDisplay Component', () => {
|
||||
let collateralDisplay: ShallowWrapper;
|
||||
let retitleClickValue = 0;
|
||||
let refreshCollateralValue = false;
|
||||
|
||||
const retitledClicked = (collateralId: number) => {
|
||||
retitleClickValue = collateralId;
|
||||
};
|
||||
|
||||
const refreshCollateralClicked = () => {
|
||||
refreshCollateralValue = true;
|
||||
}
|
||||
|
||||
let dummyCollateral1: ICollateralDisplay = {
|
||||
collateral: {
|
||||
vin: '1234abcd',
|
||||
make: 'ford',
|
||||
model: 'fiesta',
|
||||
year: 1865,
|
||||
collateralId: 1970,
|
||||
} as ICollateral,
|
||||
refreshCollateral: refreshCollateralClicked,
|
||||
}
|
||||
|
||||
let dummyCollateral2: ICollateralDisplay = {
|
||||
collateral: {
|
||||
vin: 'abcd1234',
|
||||
make: 'ferrari',
|
||||
model: 'enzo',
|
||||
year: 1916,
|
||||
collateralId: 1968,
|
||||
refCollateralId: '1998',
|
||||
retitledInLandmarkName: moment('1914-06-28'),
|
||||
} as ICollateral,
|
||||
refreshCollateral: () => {},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
collateralDisplay = shallow(
|
||||
<CollateralDisplay
|
||||
isAccountClosed={false}
|
||||
collateral={[dummyCollateral1, dummyCollateral2]}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
retitleClickValue = 0;
|
||||
refreshCollateralValue = false;
|
||||
})
|
||||
|
||||
const getChildCardView = () => {
|
||||
return collateralDisplay.find('CardView').dive();
|
||||
}
|
||||
|
||||
it('CollateralDisplay Renders Year', () => {
|
||||
const collateralYear = getChildCardView().find(`[data-test='collateral-display-year']`);
|
||||
expect(collateralYear.length).toBe(2);
|
||||
expect(collateralYear.at(0).text()).toEqual('1865');
|
||||
expect(collateralYear.at(1).text()).toEqual('1916');
|
||||
});
|
||||
|
||||
it('CollateralDisplay Renders Make', () => {
|
||||
const collateralMake = getChildCardView().find(`[data-test='collateral-display-make']`);
|
||||
expect(collateralMake.length).toBe(2);
|
||||
expect(collateralMake.at(0).text()).toEqual('ford');
|
||||
expect(collateralMake.at(1).text()).toEqual('ferrari');
|
||||
});
|
||||
|
||||
it('CollateralDisplay Renders Model', () => {
|
||||
const collateralModel = getChildCardView().find(`[data-test='collateral-display-model']`);
|
||||
expect(collateralModel.length).toBe(2);
|
||||
expect(collateralModel.at(0).text()).toEqual('fiesta');
|
||||
expect(collateralModel.at(1).text()).toEqual('enzo');
|
||||
});
|
||||
|
||||
it('CollateralDisplay Renders Vin', () => {
|
||||
const collateralVin = getChildCardView().find(`[data-test='collateral-display-vin']`);
|
||||
expect(collateralVin.length).toBe(2);
|
||||
expect(collateralVin.at(0).text()).toEqual('1234abcd');
|
||||
expect(collateralVin.at(1).text()).toEqual('abcd1234');
|
||||
});
|
||||
|
||||
it('CollateralDisplay Collateral Refresh Button is Disabled', () => {
|
||||
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
|
||||
expect(collateralRefreshBtn.length).toBe(2);
|
||||
expect(collateralRefreshBtn.at(0).props().disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('CollateralDisplay Collateral Refresh Button is Enabled', () => {
|
||||
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
|
||||
expect(collateralRefreshBtn.length).toBe(2);
|
||||
expect(collateralRefreshBtn.at(1).props().disabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('CollateralDisplay Calls Collateral Refresh Callback', () => {
|
||||
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
|
||||
expect(collateralRefreshBtn.length).toBe(2);
|
||||
collateralRefreshBtn.at(0).simulate('click');
|
||||
expect(refreshCollateralValue).toEqual(true);
|
||||
});
|
||||
|
||||
it('CollateralDisplay Displays Retitled Date', () => {
|
||||
const collateralRetitleDate = getChildCardView().find(`[data-test='collateral-display-retitle-date']`);
|
||||
expect(collateralRetitleDate.length).toBe(2);
|
||||
expect(collateralRetitleDate.last().text()).toBe('1914-06-28');
|
||||
expect(collateralRetitleDate.first().text()).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { ICollateral } from "../../services/service-types/RepoAccountTypes";
|
||||
import CardView from "../card-view/CardView";
|
||||
import DateFormatting from "./../../shared/formatting/DateFormatting";
|
||||
import "./CollateralDisplay.css";
|
||||
|
||||
export interface ICollateralDisplay {
|
||||
collateral: ICollateral;
|
||||
refreshCollateral: () => any;
|
||||
}
|
||||
|
||||
export interface ICollateralDisplayProps {
|
||||
isAccountClosed: boolean;
|
||||
collateral: ICollateralDisplay[];
|
||||
}
|
||||
|
||||
const CollateralDisplay: React.FC<ICollateralDisplayProps> = (props: ICollateralDisplayProps) => {
|
||||
return (
|
||||
<CardView
|
||||
rows={props.collateral.map((collateralDisplay) => (
|
||||
<div key={collateralDisplay.collateral.collateralId} className="row collateral-view-row">
|
||||
<div className="col-sm">
|
||||
<p>Year:</p>
|
||||
<p><b data-test="collateral-display-year">{collateralDisplay.collateral.year}</b></p>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Make:</p>
|
||||
<p><b data-test="collateral-display-make">{collateralDisplay.collateral.make}</b></p>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Model:</p>
|
||||
<p><b data-test="collateral-display-model">{collateralDisplay.collateral.model}</b></p>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>VIN:</p>
|
||||
<p><b data-test="collateral-display-vin">{collateralDisplay.collateral.vin}</b></p>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Retitled Date:</p>
|
||||
<p><b data-test="collateral-display-retitle-date">{DateFormatting.FormatYYYYMMDD(collateralDisplay.collateral.retitledInLandmarkName)}</b></p>
|
||||
</div>
|
||||
<div className="col-sm text-center my-auto">
|
||||
<button
|
||||
className={"btn " + (props.isAccountClosed ? "btn-secondary" : "btn-info")}
|
||||
onClick={() => collateralDisplay.refreshCollateral()}
|
||||
disabled={!collateralDisplay.collateral.refCollateralId || props.isAccountClosed}
|
||||
data-test="collateral-display-refresh-btn"
|
||||
>
|
||||
Collateral Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollateralDisplay;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.vin-disp {
|
||||
font-size: smaller;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import {shallow, ShallowWrapper} from 'enzyme';
|
||||
import CollateralTextSummary, {ICollateralSummaryInfo} from "./CollateralTextSummary";
|
||||
|
||||
describe('CollateralTextSummary Component', () => {
|
||||
let collateralDisplay: ShallowWrapper;
|
||||
|
||||
let dummyCollateral1: ICollateralSummaryInfo = {
|
||||
vin: '1234abcd',
|
||||
make: 'ford',
|
||||
model: 'FIESTA',
|
||||
year: 1865,
|
||||
collateralId: 1970
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
collateralDisplay = shallow(
|
||||
<CollateralTextSummary collateral={dummyCollateral1}/>
|
||||
)
|
||||
});
|
||||
|
||||
const getView = () => {
|
||||
return collateralDisplay.find('span');
|
||||
};
|
||||
|
||||
it('CollateralTextSummary Renders ', () => {
|
||||
const collateral = getView();
|
||||
expect(collateral.first().text()).toEqual('1865 Ford FIESTA 1234abcd');
|
||||
});
|
||||
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import "./CollateralTextSummary.css";
|
||||
import StringFormatting from "../../../shared/formatting/StringFormatting";
|
||||
|
||||
export interface ICollateralSummaryInfo {
|
||||
vin: string;
|
||||
make: string;
|
||||
model: string;
|
||||
year: number;
|
||||
collateralId: number;
|
||||
}
|
||||
|
||||
export interface ICollateralTextSummaryProps {
|
||||
collateral: ICollateralSummaryInfo;
|
||||
}
|
||||
|
||||
const CollateralTextSummary: React.FC<ICollateralTextSummaryProps> = (props: ICollateralTextSummaryProps) => {
|
||||
return (
|
||||
<span>{props.collateral.year} {StringFormatting.ToTitleCase(props.collateral.make)} {props.collateral.model} <span className='vin-disp'>{props.collateral.vin}</span></span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollateralTextSummary;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import {shallow} from "enzyme";
|
||||
import {_AuctionDisplay} from "./AuctionDisplay";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
|
||||
describe('AuctionDisplay Component', () => {
|
||||
|
||||
it('Auction looks up company by id', () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IAuctionHouse[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
|
||||
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
|
||||
expect(aucDisplay.length).toBe(1);
|
||||
expect(aucDisplay.text()).toBe("mmaa");
|
||||
});
|
||||
|
||||
it('Load if status is not success', () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IAuctionHouse[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Loading
|
||||
};
|
||||
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
|
||||
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
|
||||
expect(aucDisplay.length).toBe(1);
|
||||
expect(aucDisplay.text()).toBe("Loading...")
|
||||
});
|
||||
|
||||
it('Loads Auction Houses When Inactive', () => {
|
||||
let didLoad = false;
|
||||
const loader = () => {didLoad = !didLoad};
|
||||
const state:IAsyncStateContainer<IAuctionHouse[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Inactive
|
||||
};
|
||||
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
|
||||
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
|
||||
expect(didLoad).toBe(true);
|
||||
});
|
||||
|
||||
it("Display Auction House Name when Auction House Name Found", () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IAuctionHouse[]> = {
|
||||
data: [{auctionID: "mmaa",
|
||||
name: "name",
|
||||
active: "yes",
|
||||
address_Line_1: "11",
|
||||
address_Line_2: "11",
|
||||
state: "WI",
|
||||
city: "Eagle Creek",
|
||||
phone: "13131",
|
||||
fax: "123213",
|
||||
zip: 55555,
|
||||
country: "USA" }
|
||||
],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
|
||||
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
|
||||
expect(aucDisplay.text()).toBe("name");
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {getAllAuctionHouses} from "../../actions/AuctionHouseActions";
|
||||
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {IAppState} from "../../store/Store";
|
||||
|
||||
interface IAuctionDisplayProps {
|
||||
auctionId: string;
|
||||
}
|
||||
|
||||
interface IPropsFromState {
|
||||
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
auctionHouses: state.auctionHouseState.auctionHouses,
|
||||
};
|
||||
};
|
||||
|
||||
interface IPropsFromDispatch {
|
||||
loadAuctionHouses: () => any;
|
||||
}
|
||||
|
||||
const mapDispatchToProps: IPropsFromDispatch = {
|
||||
loadAuctionHouses: getAllAuctionHouses,
|
||||
};
|
||||
|
||||
type Props = IAuctionDisplayProps & IPropsFromState & IPropsFromDispatch;
|
||||
|
||||
const AuctionDisplay: React.FC<Props> = (props) => {
|
||||
|
||||
const auctionLookup = (auctionId: string): string => {
|
||||
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
|
||||
if (props.auctionHouses.state === AsyncState.Inactive) {
|
||||
props.loadAuctionHouses();
|
||||
}
|
||||
|
||||
if (props.auctionHouses && props.auctionHouses.state === AsyncState.Success) {;
|
||||
const auctionHouseSet = props.auctionHouses.data as IAuctionHouse[];
|
||||
const auctionHouse = auctionHouseSet.find((house: IAuctionHouse) => {
|
||||
return house.auctionID.toLowerCase() === props.auctionId.toLowerCase();
|
||||
});
|
||||
return auctionHouse ? auctionHouse.name : props.auctionId;
|
||||
} else {
|
||||
return "Loading..."; // @TODO switch to showing notification.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span data-test={props.auctionId + "-auc-display"}>{auctionLookup(props.auctionId)}</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const _AuctionDisplay = AuctionDisplay;
|
||||
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AuctionDisplay);
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import {shallow} from "enzyme";
|
||||
import {_RepoCompanyDisplay} from "./RepoCompanyDisplay";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
|
||||
|
||||
|
||||
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
|
||||
const wrapper = component.find(`[data-test='${attr}']`);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe('Repo Company Display Component', () => {
|
||||
|
||||
it('looks up company by id', () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IRepoCompany[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
|
||||
const aucDisplay = component.find(`[data-test='23-rc-display']`);
|
||||
expect(aucDisplay.length).toBe(1);
|
||||
});
|
||||
|
||||
it('displays loading if status is not success', () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IRepoCompany[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Loading
|
||||
};
|
||||
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
|
||||
const aucDisplay = component.find(`[data-test='23-rc-display']`);
|
||||
expect(aucDisplay.length).toBe(1);
|
||||
expect(aucDisplay.text()).toBe("Loading...")
|
||||
});
|
||||
|
||||
it('Calls Load repo Companies when inactive',() => {
|
||||
var didLoad = false;
|
||||
const loader = () => {didLoad = !didLoad};
|
||||
const state:IAsyncStateContainer<IRepoCompany[]> = {
|
||||
data: [],
|
||||
state: AsyncState.Inactive
|
||||
};
|
||||
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
|
||||
const aucDisplay = component.find(`[data-test='23-rc-display']`);
|
||||
expect(didLoad).toBe(true);
|
||||
});
|
||||
|
||||
it('Displays Repo Company Name when Repo Company Name Is Found', () => {
|
||||
const loader = () => {};
|
||||
const state:IAsyncStateContainer<IRepoCompany[]> = {
|
||||
data: [
|
||||
{id: 23,
|
||||
name: "company"
|
||||
}
|
||||
],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
|
||||
const aucDisplay = component.find(`[data-test='23-rc-display']`);
|
||||
expect(aucDisplay.text()).toBe("company")
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {IAppState} from "../../store/Store";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import {connect} from "react-redux";
|
||||
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
|
||||
import {getAllRepoCompanies} from "../../actions/RepoCompanyActions";
|
||||
|
||||
interface IRepoCompanyDisplayProps {
|
||||
repoCompanyId: number;
|
||||
}
|
||||
|
||||
interface IPropsFromState {
|
||||
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
repoCompanies: state.repoCompanyState.repoCompanies,
|
||||
};
|
||||
};
|
||||
|
||||
interface IPropsFromDispatch {
|
||||
loadRepoCompanies: () => any;
|
||||
}
|
||||
|
||||
const mapDispatchToProps: IPropsFromDispatch = {
|
||||
loadRepoCompanies: getAllRepoCompanies,
|
||||
};
|
||||
|
||||
type Props = IRepoCompanyDisplayProps & IPropsFromState & IPropsFromDispatch;
|
||||
|
||||
const RepoCompanyDisplay: React.FC<Props> = (props) => {
|
||||
|
||||
const RepoCompanyLookup = (repoCompanyId: number): string => {
|
||||
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
|
||||
if (props.repoCompanies.state === AsyncState.Inactive) {
|
||||
props.loadRepoCompanies();
|
||||
}
|
||||
|
||||
if (props.repoCompanies && props.repoCompanies.state === AsyncState.Success) {
|
||||
const repoCompanieSet = props.repoCompanies.data as IRepoCompany[];
|
||||
const repoCompany = repoCompanieSet.find((repo: IRepoCompany) => {
|
||||
return repo.id === repoCompanyId;
|
||||
});
|
||||
return repoCompany ? repoCompany.name : (props.repoCompanyId + '');
|
||||
} else {
|
||||
return "Loading...";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span data-test={props.repoCompanyId + '-rc-display'}>{RepoCompanyLookup(props.repoCompanyId)}</span>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export const _RepoCompanyDisplay = RepoCompanyDisplay;
|
||||
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(RepoCompanyDisplay);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Footer from "./Footer";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<Footer />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import "./Footer.css";
|
||||
|
||||
import {BUILD_STAMP, VERSION} from "../../environments/version";
|
||||
|
||||
import React from "react";
|
||||
import watermark from "../../assets/light-house-corner-watermark.png";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const versionHint = VERSION.version + "-" + BUILD_STAMP;
|
||||
const currentYear = new Date().getFullYear();
|
||||
return (
|
||||
<div>
|
||||
<div className="watermark-outer">
|
||||
<div className="watermark-inner">
|
||||
<img src={watermark} className="watermark-primary" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<footer className="py-3 bg-dark">
|
||||
<div className="container">
|
||||
<p
|
||||
className="m-0 text-center text-white"
|
||||
title={versionHint}>Copyright © {currentYear} Landmark Credit Union
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,15 @@
|
||||
.form-input-label {
|
||||
margin-bottom: 0;
|
||||
font-size: 0.95em;
|
||||
color: #555;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.small-checkbox{
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
align-self:center;
|
||||
margin-top: .25em;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { shallow, mount } from "enzyme";
|
||||
import React from "react";
|
||||
import FormInput, { FormInputType } from "./FormInput";
|
||||
|
||||
describe("FormInput Component", () => {
|
||||
|
||||
it ("Renders Label", () => {
|
||||
const wrapper = shallow(
|
||||
<FormInput
|
||||
label="The Label"
|
||||
type={FormInputType.Selection}
|
||||
value={1}
|
||||
options={[]}
|
||||
onChange={() => {}}
|
||||
placeholder="Option"
|
||||
/>
|
||||
);
|
||||
const selectionInput = wrapper.find(`[data-test='label-text']`);
|
||||
expect(selectionInput.length).toBe(1);
|
||||
expect(wrapper.find(`[data-test='error-message']`).length).toBe(0);
|
||||
});
|
||||
|
||||
it ("Passes Proper Error State To FormInputselector", () => {
|
||||
const wrapper = mount(
|
||||
<FormInput
|
||||
label="The Label"
|
||||
type={FormInputType.Selection}
|
||||
value={1}
|
||||
options={[]}
|
||||
onChange={() => {}}
|
||||
placeholder="Option"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.state("isInputInErrorState")).toBe(false);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it ("Loads Required asterisk", () => {
|
||||
const wrapper = mount(
|
||||
<FormInput
|
||||
required = {true}
|
||||
label="The Label"
|
||||
type={FormInputType.Selection}
|
||||
value={undefined}
|
||||
options={[]}
|
||||
onChange={() => {}}
|
||||
placeholder="Option"
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find(`[data-test='required-asterisk']`).length).toBe(1);
|
||||
|
||||
expect(wrapper.state("isInputInErrorState")).toBe(false);
|
||||
|
||||
});
|
||||
|
||||
it ("Ignores labels if there is no label", () => {
|
||||
const wrapper = mount(
|
||||
<FormInput
|
||||
required = {true}
|
||||
type={FormInputType.Selection}
|
||||
value={undefined}
|
||||
options={[]}
|
||||
onChange={() => {}}
|
||||
placeholder="Option"
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find(`[data-test='form-input-label']`).length).toBe(0);
|
||||
|
||||
expect(wrapper.state("isInputInErrorState")).toBe(false);
|
||||
|
||||
});
|
||||
|
||||
it ("Turns red when hasError is set to true", () => {
|
||||
const mountedWrapper = mount(
|
||||
<FormInput
|
||||
required={true}
|
||||
type={FormInputType.Selection}
|
||||
value={"undefined"}
|
||||
options={[]}
|
||||
onChange={() => {}}
|
||||
placeholder="Option"
|
||||
hasError={true}
|
||||
/>
|
||||
);
|
||||
mountedWrapper.setProps(
|
||||
{
|
||||
value: "TEst"
|
||||
}
|
||||
);
|
||||
mountedWrapper.update();
|
||||
expect(mountedWrapper.state("isInputInErrorState")).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Moment } from "moment";
|
||||
import React from "react";
|
||||
import FormInputSelector from "./form-input-selector/FormInputSelector";
|
||||
import "./FormInput.css";
|
||||
|
||||
export enum FormInputType {
|
||||
Selection,
|
||||
Datetime,
|
||||
Text,
|
||||
Textarea,
|
||||
Number,
|
||||
Checkbox
|
||||
}
|
||||
|
||||
export enum FormInputNumericalType {
|
||||
Monetary,
|
||||
Percentage
|
||||
}
|
||||
|
||||
export interface IFormInputSelectOption {
|
||||
value: string | number;
|
||||
display: string | number;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
interface IFormInputWrapper<T1, T2> {
|
||||
type: FormInputType;
|
||||
value: T1;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: (newValue: T2) => any;
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
||||
export interface FormInputText extends IFormInputWrapper<string | number | undefined, string> {
|
||||
type: FormInputType.Text;
|
||||
}
|
||||
|
||||
export interface FormInputTextArea extends IFormInputWrapper<string | number | undefined, string> {
|
||||
type: FormInputType.Textarea;
|
||||
}
|
||||
|
||||
export interface FormInputDate extends IFormInputWrapper<Moment | undefined, Moment | undefined> {
|
||||
type: FormInputType.Datetime;
|
||||
minDate?: Moment;
|
||||
maxDate?: Moment;
|
||||
}
|
||||
|
||||
export interface FormInputNumber extends IFormInputWrapper<number | undefined, number | undefined> {
|
||||
type: FormInputType.Number;
|
||||
precision?: number;
|
||||
displayType?: FormInputNumericalType;
|
||||
}
|
||||
|
||||
export interface FormInputSelection extends IFormInputWrapper<string | number | undefined, string> {
|
||||
type: FormInputType.Selection;
|
||||
options: IFormInputSelectOption[];
|
||||
}
|
||||
|
||||
export interface FormInputCheckbox extends IFormInputWrapper<string | undefined, boolean> {
|
||||
type: FormInputType.Checkbox;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export type FormInputSpecs =
|
||||
FormInputText |
|
||||
FormInputTextArea |
|
||||
FormInputDate |
|
||||
FormInputSelection |
|
||||
FormInputNumber |
|
||||
FormInputCheckbox;
|
||||
|
||||
interface IFormInputState {
|
||||
isInputInErrorState: boolean;
|
||||
}
|
||||
|
||||
class FormInput extends React.Component<FormInputSpecs, IFormInputState> {
|
||||
|
||||
constructor(props: FormInputSpecs) {
|
||||
super(props);
|
||||
this.state = { isInputInErrorState: false };
|
||||
|
||||
this.setInputValidity = this.setInputValidity.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: FormInputSpecs) {
|
||||
if (prevProps.value !== this.props.value) {
|
||||
this.setInputValidity();
|
||||
}
|
||||
if(prevProps.hasError !== this.props.hasError){
|
||||
this.setInputValidity();
|
||||
}
|
||||
}
|
||||
|
||||
setInputValidity() {
|
||||
this.setState({
|
||||
isInputInErrorState:
|
||||
this.props.hasError === true ||
|
||||
(!!this.props.required &&
|
||||
(
|
||||
this.props.value === null ||
|
||||
this.props.value === undefined ||
|
||||
this.props.value === ""
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{ this.props.label ?
|
||||
<p className={"form-input-label " + (this.state.isInputInErrorState ? "text-danger" : "")}>
|
||||
<span data-test="label-text">{this.props.label}</span>
|
||||
{ this.props.required ?
|
||||
<span className="text-danger"> <b data-test="required-asterisk">*</b></span>
|
||||
: null}
|
||||
</p>
|
||||
: null}
|
||||
<FormInputSelector inputSpecs={this.props} isError={this.state.isInputInErrorState} onBlur={this.setInputValidity} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FormInput;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
@import "node_modules/bootstrap/scss/functions";
|
||||
@import "node_modules/bootstrap/scss/variables";
|
||||
@import "node_modules/bootstrap/scss/mixins";
|
||||
|
||||
.form-input-is-error {
|
||||
border-width: 1px;
|
||||
border-color: lighten($danger, 15%);
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment, { Moment } from "moment";
|
||||
import React from "react";
|
||||
import DateFormatting from "../../../shared/formatting/DateFormatting";
|
||||
import { FormInputDate, FormInputSelection, FormInputText, FormInputTextArea, FormInputType, FormInputCheckbox, FormInputNumber } from "../FormInput";
|
||||
import FormInputSelector from "./FormInputSelector";
|
||||
|
||||
describe("FormInputSelector Component", () => {
|
||||
|
||||
it ("Renders and Updates Selection Input", () => {
|
||||
const inputSpecs: FormInputSelection = {
|
||||
type: FormInputType.Selection,
|
||||
value: 1,
|
||||
options: [
|
||||
{ value: 1, display: "Option #1" },
|
||||
{ value: 2, display: "Option #2" },
|
||||
{ value: 3, display: "Option #3" }
|
||||
],
|
||||
onChange: (value) => callbackValue = value,
|
||||
placeholder: "Select an Option"
|
||||
};
|
||||
|
||||
let callbackValue: string | undefined;
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let selectionInput = wrapper.find(`[data-test='select-input']`);
|
||||
expect(selectionInput.length).toBe(1);
|
||||
|
||||
const selectionPlaceholderOption = wrapper.find(`[data-test='select-input-placeholder-option']`);
|
||||
expect(selectionPlaceholderOption.length).toBe(1);
|
||||
expect(selectionPlaceholderOption.text()).toEqual("Select an Option");
|
||||
|
||||
const selectionInputOptions = wrapper.find(`[data-test='select-input-option']`);
|
||||
expect(selectionInputOptions.length).toBe(3);
|
||||
expect(selectionInputOptions.at(0).text()).toEqual("Option #1");
|
||||
expect(selectionInputOptions.at(1).text()).toEqual("Option #2");
|
||||
expect(selectionInputOptions.at(2).text()).toEqual("Option #3");
|
||||
|
||||
expect(callbackValue).toBeUndefined();
|
||||
selectionInput.simulate("change", { target: { value: "2" }});
|
||||
expect(callbackValue).toEqual("2");
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
value: 3
|
||||
},
|
||||
});
|
||||
selectionInput = wrapper.find(`[data-test='select-input']`);
|
||||
expect(selectionInput.props().value).toBe(3);
|
||||
});
|
||||
|
||||
it ("Renders and Updates Text Input", () => {
|
||||
const inputSpecs: FormInputText = {
|
||||
type: FormInputType.Text,
|
||||
value: undefined,
|
||||
onChange: (value) => callbackValue = value
|
||||
};
|
||||
|
||||
let callbackValue: string | undefined;
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let textInput = wrapper.find(`[data-test='text-input']`);
|
||||
expect(textInput.length).toBe(1);
|
||||
|
||||
expect(callbackValue).toBeUndefined();
|
||||
textInput.simulate("change", { target: { value: "Different Text" }});
|
||||
expect(callbackValue).toEqual("Different Text");
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
value: "Even Newer Text"
|
||||
},
|
||||
});
|
||||
textInput = wrapper.find(`[data-test='text-input']`);
|
||||
expect(textInput.props().value).toBe("Even Newer Text");
|
||||
});
|
||||
|
||||
it ("Renders and Updates Text Area Input", () => {
|
||||
const inputSpecs: FormInputTextArea = {
|
||||
type: FormInputType.Textarea,
|
||||
value: undefined,
|
||||
onChange: (value) => callbackValue = value
|
||||
};
|
||||
|
||||
let callbackValue: string | undefined;
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let textAreaInput = wrapper.find(`[data-test='textarea-input']`);
|
||||
expect(textAreaInput.length).toBe(1);
|
||||
|
||||
expect(callbackValue).toBeUndefined();
|
||||
textAreaInput.simulate("change", { target: { value: "Different Text" }});
|
||||
expect(callbackValue).toEqual("Different Text");
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
value: "Even Newer Text"
|
||||
},
|
||||
});
|
||||
textAreaInput = wrapper.find(`[data-test='textarea-input']`);
|
||||
expect(textAreaInput.props().value).toBe("Even Newer Text");
|
||||
});
|
||||
|
||||
it ("Renders and Updates Date Input", () => {
|
||||
const inputSpecs: FormInputDate = {
|
||||
type: FormInputType.Datetime,
|
||||
value: undefined,
|
||||
onChange: (value) => callbackValue = value
|
||||
};
|
||||
|
||||
let callbackValue: Moment | undefined;
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let dateInput = wrapper.find(`[data-test='date-input']`);
|
||||
expect(dateInput.length).toBe(1);
|
||||
|
||||
expect(callbackValue).toBeUndefined();
|
||||
dateInput.simulate("change", new Date(2019, 1, 7));
|
||||
expect(callbackValue).not.toBeUndefined();
|
||||
expect(DateFormatting.FormatMMDDYYYY(callbackValue)).toEqual("02/07/2019");
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
value: moment("2015-08-24")
|
||||
},
|
||||
});
|
||||
dateInput = wrapper.find(`[data-test='date-input']`);
|
||||
expect(dateInput.props().selected).toEqual(moment("2015-08-24").toDate());
|
||||
});
|
||||
it("Renders and Updates Checkbox Input", () => {
|
||||
let callbackValue : boolean | undefined;
|
||||
const inputSpecs : FormInputCheckbox = {
|
||||
type: FormInputType.Checkbox,
|
||||
value: undefined,
|
||||
checked: false,
|
||||
onChange: (value) => callbackValue = value
|
||||
}
|
||||
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
|
||||
checkboxinput.simulate("change", {target : {checked: true}});
|
||||
expect(callbackValue).toBe(true);
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
checked: true
|
||||
},
|
||||
});
|
||||
checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
|
||||
expect(checkboxinput.props().value).toBe("true");
|
||||
expect(checkboxinput.props().checked).toBe(true)
|
||||
|
||||
});
|
||||
|
||||
it ("Renders and Updates Numerical Input", () => {
|
||||
const inputSpecs: FormInputNumber = {
|
||||
type: FormInputType.Number,
|
||||
value: undefined,
|
||||
onChange: (value) => callbackValue = value
|
||||
};
|
||||
|
||||
let callbackValue: number | undefined;
|
||||
let wrapper = shallow(
|
||||
<FormInputSelector
|
||||
inputSpecs={inputSpecs}
|
||||
isError={false}
|
||||
/>
|
||||
);
|
||||
|
||||
let textInput = wrapper.find(`[data-test='number-input']`);
|
||||
expect(textInput.length).toBe(1);
|
||||
|
||||
expect(callbackValue).toBeUndefined();
|
||||
textInput.simulate("change", { target: { value: 5 }});
|
||||
expect(callbackValue).toEqual(5);
|
||||
|
||||
wrapper.setProps({
|
||||
inputSpecs: {
|
||||
...inputSpecs,
|
||||
value: 6
|
||||
},
|
||||
});
|
||||
textInput = wrapper.find(`[data-test='number-input']`);
|
||||
expect(textInput.props().value).toBe(6);
|
||||
});
|
||||
});
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { Moment } from "moment";
|
||||
import React from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import DateConversions from "../../../shared/conversions/DateConversions";
|
||||
import NumericalFormatting from "../../../shared/formatting/NumericalFormatting";
|
||||
import StringFormatting from "../../../shared/formatting/StringFormatting";
|
||||
import { FormInputNumericalType, FormInputSpecs, FormInputType } from "../FormInput";
|
||||
import "./FormInputSelector.scss";
|
||||
|
||||
interface IFormInputSelectorProps {
|
||||
inputSpecs: FormInputSpecs;
|
||||
isError: boolean;
|
||||
onBlur?: () => any;
|
||||
onFocus?: () => any;
|
||||
}
|
||||
|
||||
const FormInputSelector: React.FC<IFormInputSelectorProps> = (props) => {
|
||||
|
||||
const handleBasicInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement>, callback: (data: string) => any) => {
|
||||
callback(StringFormatting.TrimLeft(event.target.value));
|
||||
};
|
||||
|
||||
const handleDateInputChange = (date: Date | null, callback: (data: Moment | undefined) => any) => {
|
||||
const momentDate = DateConversions.dateToMoment(date);
|
||||
callback(momentDate);
|
||||
};
|
||||
|
||||
const numericalInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: number | undefined) => any, precision?: number) => {
|
||||
const value = event.target.value;
|
||||
|
||||
if (!value) {
|
||||
callback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
let floatValue = parseFloat(value);
|
||||
if (precision) {
|
||||
floatValue = NumericalFormatting.FormatPreciseFlooredNumber(floatValue, precision);
|
||||
}
|
||||
|
||||
callback(floatValue);
|
||||
};
|
||||
|
||||
const handleCheckboxInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: boolean) => any) => {
|
||||
callback(event.target.checked);
|
||||
}
|
||||
|
||||
const getInputClassName = () => {
|
||||
let classes = "form-control";
|
||||
if (props.isError) {
|
||||
classes += " form-input-is-error";
|
||||
}
|
||||
return classes;
|
||||
};
|
||||
|
||||
const getAdjustedInputValue = (value: string | number | undefined) => {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
switch(props.inputSpecs.type) {
|
||||
case FormInputType.Selection:
|
||||
const selectionOnChange = props.inputSpecs.onChange;
|
||||
return (
|
||||
<select
|
||||
className={getInputClassName()}
|
||||
value={getAdjustedInputValue(props.inputSpecs.value)}
|
||||
onChange={(event) => handleBasicInputChange(event, selectionOnChange)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
data-test="select-input"
|
||||
>
|
||||
{ props.inputSpecs.placeholder ?
|
||||
<option value="" disabled={true} data-test="select-input-placeholder-option">{props.inputSpecs.placeholder}</option>
|
||||
: null}
|
||||
{
|
||||
props.inputSpecs.options.map((option, index) =>
|
||||
<option
|
||||
value={option.value}
|
||||
key={index}
|
||||
data-test="select-input-option"
|
||||
>
|
||||
{option.display}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
</select>
|
||||
);
|
||||
case FormInputType.Textarea:
|
||||
const textAreaOnChange = props.inputSpecs.onChange;
|
||||
return (
|
||||
<textarea
|
||||
className={getInputClassName()}
|
||||
placeholder={props.inputSpecs.placeholder}
|
||||
value={props.inputSpecs.value}
|
||||
onChange={(event) => handleBasicInputChange(event, textAreaOnChange)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
data-test="textarea-input"
|
||||
/>
|
||||
);
|
||||
case FormInputType.Text:
|
||||
const textOnChange = props.inputSpecs.onChange;
|
||||
return (
|
||||
<input
|
||||
className={getInputClassName()}
|
||||
placeholder={props.inputSpecs.placeholder}
|
||||
value={props.inputSpecs.value}
|
||||
onChange={(event) => handleBasicInputChange(event, textOnChange)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
data-test="text-input"
|
||||
/>
|
||||
);
|
||||
case FormInputType.Datetime:
|
||||
const datetimeOnChange = props.inputSpecs.onChange;
|
||||
return (
|
||||
<DatePicker
|
||||
className={getInputClassName()}
|
||||
placeholderText={props.inputSpecs.placeholder ? props.inputSpecs.placeholder : "mm/dd/yyyy"}
|
||||
minDate={props.inputSpecs.minDate ? props.inputSpecs.minDate.toDate() : undefined}
|
||||
selected={DateConversions.momentToDate(props.inputSpecs.value)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onChange={(date) => handleDateInputChange(date, datetimeOnChange)}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
data-test="date-input"
|
||||
/>
|
||||
);
|
||||
case FormInputType.Number:
|
||||
const numberOnChange = props.inputSpecs.onChange;
|
||||
const precision = props.inputSpecs.precision;
|
||||
const step = precision ? Math.pow(10, - Math.floor(precision)).toString() : undefined;
|
||||
return (
|
||||
<div className="input-group">
|
||||
{ props.inputSpecs.displayType === FormInputNumericalType.Monetary ?
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">$</span>
|
||||
</div>
|
||||
: null}
|
||||
<input
|
||||
type="number"
|
||||
step={step}
|
||||
className={getInputClassName()}
|
||||
placeholder={props.inputSpecs.placeholder}
|
||||
value={props.inputSpecs.value}
|
||||
onChange={(event) => numericalInputChange(event, numberOnChange, precision)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
data-test="number-input"
|
||||
/>
|
||||
{ props.inputSpecs.displayType === FormInputNumericalType.Percentage ?
|
||||
<div className="input-group-append">
|
||||
<span className="input-group-text">%</span>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
case FormInputType.Checkbox:
|
||||
const checkboxValueOnChange = props.inputSpecs.onChange;
|
||||
return (
|
||||
<input
|
||||
value = {props.inputSpecs.checked? "true" : "false"}
|
||||
className="pl-5 small-checkbox"
|
||||
placeholder={props.inputSpecs.placeholder}
|
||||
checked = {(props.inputSpecs.checked)}
|
||||
onChange={(event) => handleCheckboxInputChange(event, checkboxValueOnChange)}
|
||||
disabled={props.inputSpecs.disabled}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
type = "checkbox"
|
||||
data-test="checkbox-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FormInputSelector;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
.notification-bar {
|
||||
.fa {
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
.notification-bar-button-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.notification-bar-sub-text-container {
|
||||
margin-left: 1.75em;
|
||||
|
||||
hr {
|
||||
margin: 2px;
|
||||
width: 67%;
|
||||
}
|
||||
p {
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import NotificationBar, { NotificationBarType } from './NotificationBar';
|
||||
|
||||
describe('NotificationBar', () => {
|
||||
|
||||
it ('Renders text', () => {
|
||||
let messageText = "Text to display."
|
||||
let wrapper = shallow(<NotificationBar text={messageText} type={NotificationBarType.Danger} />);
|
||||
const component = wrapper.find(`[data-test='notification-bar']`)
|
||||
expect(component.length).toBe(1);
|
||||
expect(component.text()).toMatch(messageText);
|
||||
});
|
||||
|
||||
it ('Renders Loading alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Loading} />);
|
||||
const component = wrapper.find(`[data-test='loading-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Danger alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Danger} />);
|
||||
const component = wrapper.find(`[data-test='danger-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Warning alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Warning} />);
|
||||
const component = wrapper.find(`[data-test='warning-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Success alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} />);
|
||||
const component = wrapper.find(`[data-test='success-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Info alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Info} />);
|
||||
const component = wrapper.find(`[data-test='info-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Diabled alert', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Disabled} />);
|
||||
const component = wrapper.find(`[data-test='exclamation-icon']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Renders Dismiss Button', () => {
|
||||
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} isButtonPresent={true} />);
|
||||
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Dismiss Button Triggers Callback', () => {
|
||||
let valToUpdate = false;
|
||||
let wrapper = shallow(
|
||||
<NotificationBar
|
||||
text=""
|
||||
type={NotificationBarType.Success}
|
||||
isButtonPresent={true}
|
||||
onButtonClick={() => valToUpdate = true}
|
||||
/>
|
||||
);
|
||||
expect(valToUpdate).toBe(false);
|
||||
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
|
||||
expect(component.length).toBe(1);
|
||||
component.simulate('click');
|
||||
expect(valToUpdate).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import './NotificationBar.scss';
|
||||
|
||||
export enum NotificationBarType {
|
||||
Success = 'success',
|
||||
Warning = 'warning',
|
||||
Danger = 'danger',
|
||||
Loading = 'primary',
|
||||
Info = 'info',
|
||||
Disabled = 'secondary'
|
||||
}
|
||||
|
||||
interface INotificationBarProps {
|
||||
type: NotificationBarType;
|
||||
text: string;
|
||||
subText?: string;
|
||||
isButtonPresent?: boolean;
|
||||
buttonText?: string;
|
||||
fontAwesomeIcon?: string;
|
||||
onButtonClick?: () => any;
|
||||
}
|
||||
|
||||
const getNotificationIcon = (alertType: NotificationBarType, fontAwesomeIcon?: string) => {
|
||||
if (fontAwesomeIcon) {
|
||||
return <i className={'fa ' + fontAwesomeIcon} data-test="misc-icon"></i>
|
||||
}
|
||||
switch (alertType) {
|
||||
case NotificationBarType.Loading:
|
||||
return (
|
||||
<i className="fa fa-spinner fa-spin" data-test="loading-icon"></i>
|
||||
);
|
||||
case NotificationBarType.Danger:
|
||||
return (
|
||||
<i className="fa fa-times-circle" data-test="danger-icon"></i>
|
||||
);
|
||||
case NotificationBarType.Warning:
|
||||
return (
|
||||
<i className="fa fa-exclamation-triangle" data-test="warning-icon"></i>
|
||||
);
|
||||
case NotificationBarType.Success:
|
||||
return (
|
||||
<i className="fa fa-check-circle" data-test="success-icon"></i>
|
||||
);
|
||||
case NotificationBarType.Info:
|
||||
return (
|
||||
<i className="fa fa-info-circle" data-test="info-icon"></i>
|
||||
);
|
||||
case NotificationBarType.Disabled:
|
||||
return (
|
||||
<i className="fa fa-exclamation-circle" data-test="exclamation-icon"></i>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationBar: React.FC<INotificationBarProps> = (props) => {
|
||||
|
||||
const callDismissCallback = () => {
|
||||
if (props.onButtonClick) {
|
||||
props.onButtonClick();
|
||||
}
|
||||
};
|
||||
|
||||
const getbuttonText = () => {
|
||||
if (props.isButtonPresent) {
|
||||
return props.buttonText || 'Dismiss';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'notification-bar alert alert-' + props.type} role="alert" data-test="notification-bar">
|
||||
{getNotificationIcon(props.type, props.fontAwesomeIcon)}
|
||||
{props.text}
|
||||
{props.isButtonPresent ?
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
data-dismiss="alert"
|
||||
aria-label="Close"
|
||||
onClick={callDismissCallback}
|
||||
data-test="notification-bar-dismiss-button"
|
||||
>
|
||||
<span className="notification-bar-button-text">{getbuttonText()}</span>
|
||||
</button>
|
||||
: null}
|
||||
{props.subText ?
|
||||
<div className="notification-bar-sub-text-container">
|
||||
<hr></hr>
|
||||
<p><b>{props.subText}</b></p>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default NotificationBar;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IAllowedStatusChange } from "../../reducers/SettingsReducer";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import StatusChangeSelector from "./StatusChangeSelector";
|
||||
|
||||
describe("AccountPageHeader", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const selectedAccount: IRepoAccount = {
|
||||
repoId: 476,
|
||||
accountNo: "123456",
|
||||
status: "Original Status",
|
||||
memberNo: 1453,
|
||||
loanNo: 0,
|
||||
accountTypeCode: "acct-type-code",
|
||||
branch: 0,
|
||||
origAccountTypeCode: "",
|
||||
balance: 0,
|
||||
dueToPayDate: moment("1941-12-07"),
|
||||
o4RDate: moment("1945-05-07"),
|
||||
dateFirstDelinquent: moment("1918-11-11"),
|
||||
delinquentAmount: 0,
|
||||
daysDelinquent: 101,
|
||||
paidSkipAmount: 0,
|
||||
subStatus: "string",
|
||||
requiredPaymentAmount: 3.14,
|
||||
currentDelinquentAmount: 19.45,
|
||||
lstPmtAmt: 1912.00000,
|
||||
origAmount: 42,
|
||||
lastPaymentDate: moment("1865-04-15"),
|
||||
dateFirstIntellect: moment("1963-11-22"),
|
||||
primaryIndividualId: "string",
|
||||
manualRedemptionDate: moment("1812-06-12"),
|
||||
borrowers: [{
|
||||
repoId: 1,
|
||||
individualId: "1",
|
||||
ownershipType: 6,
|
||||
individualName: "John Doe",
|
||||
}],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
};
|
||||
const allowedStatusChanges: IAllowedStatusChange[] = [
|
||||
{
|
||||
id: 1,
|
||||
currentStatus: "Original Status",
|
||||
allowedChangeStatus: "Secondary Status",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
currentStatus: "Original Status",
|
||||
allowedChangeStatus: "Secondary Status (Alternative)",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
currentStatus: "Separated Status",
|
||||
allowedChangeStatus: "Final Status",
|
||||
},
|
||||
];
|
||||
let statusToUpdate: string;
|
||||
|
||||
beforeEach(() => {
|
||||
statusToUpdate = "";
|
||||
wrapper = shallow(
|
||||
<StatusChangeSelector
|
||||
selectedRepoAccount={selectedAccount}
|
||||
allowedStatusChanges={allowedStatusChanges}
|
||||
onStatusChange={(status) => statusToUpdate = status}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it ("Renders Correct Status Options", () => {
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
expect(statusChangeSelect.length).toBe(1);
|
||||
expect(statusChangeSelect.props().value).toBe("Original Status");
|
||||
});
|
||||
|
||||
it ("Renders Correct Status Options on Prop Change", () => {
|
||||
wrapper.setProps({
|
||||
selectedRepoAccount: {
|
||||
...selectedAccount,
|
||||
status: "Separated Status",
|
||||
},
|
||||
});
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
|
||||
expect(statusChangeSelect.length).toBe(1);
|
||||
expect(statusChangeSelect.props().value).toBe("Separated Status");
|
||||
});
|
||||
|
||||
it ("Renders Empty Options on Missing Account", () => {
|
||||
wrapper.setProps({
|
||||
selectedRepoAccount: undefined,
|
||||
});
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
|
||||
expect(statusChangeSelect.length).toBe(1);
|
||||
expect(statusChangeSelect.props().value).toBe("");
|
||||
});
|
||||
|
||||
it ("Renders Single Unknown Status", () => {
|
||||
wrapper.setProps({
|
||||
selectedRepoAccount: {
|
||||
...selectedAccount,
|
||||
status: "Non-Existant Status",
|
||||
},
|
||||
});
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
|
||||
expect(statusChangeSelect.length).toBe(1);
|
||||
expect(statusChangeSelect.props().value).toBe("Non-Existant Status");
|
||||
});
|
||||
|
||||
it ("Select Callback Called", () => {
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
|
||||
expect(statusToUpdate).toEqual("");
|
||||
statusChangeSelect.simulate("change", "Secondary Status");
|
||||
expect(statusToUpdate).toEqual("Secondary Status");
|
||||
});
|
||||
|
||||
it ("Does Not Render The Status Selector as Disabled When Account is Open", () => {
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
expect(statusChangeSelect.props().disabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it ("Renders Status Selector as Disabled When Account is Closed", () => {
|
||||
wrapper = wrapper.setProps({
|
||||
selectedRepoAccount: {
|
||||
...selectedAccount,
|
||||
dateClosed: moment("1916-04-29")
|
||||
}
|
||||
});
|
||||
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
|
||||
expect(statusChangeSelect.props().disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { getViableStatuses, IAllowedStatusChange } from "../../reducers/SettingsReducer";
|
||||
import RepoAccountService from "../../services/RepoAccountService";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import FormInput, { FormInputType } from "../form-input/FormInput";
|
||||
|
||||
interface IStatusChangeSelectorProps {
|
||||
selectedRepoAccount?: IRepoAccount;
|
||||
allowedStatusChanges: IAllowedStatusChange[];
|
||||
onStatusChange: (newStatus: string) => any;
|
||||
}
|
||||
|
||||
const StatusChangeSelector: React.FC<IStatusChangeSelectorProps> = (props) => {
|
||||
|
||||
const getAllowedStatusChanges = (): string[] => {
|
||||
if (!props.selectedRepoAccount) {
|
||||
return [];
|
||||
}
|
||||
return getViableStatuses(props.selectedRepoAccount.status, props.allowedStatusChanges);
|
||||
};
|
||||
|
||||
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
|
||||
return (
|
||||
<FormInput
|
||||
type={FormInputType.Selection}
|
||||
value={props.selectedRepoAccount ? props.selectedRepoAccount.status : ""}
|
||||
onChange={props.onStatusChange}
|
||||
disabled={!props.selectedRepoAccount || isAccountClosed || props.selectedRepoAccount.status.toUpperCase() === "REDEEMED"}
|
||||
options={getAllowedStatusChanges().map((status) => {
|
||||
return {
|
||||
value: status,
|
||||
display: status
|
||||
};
|
||||
})}
|
||||
data-test="status-change-selector"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusChangeSelector;
|
||||
@@ -0,0 +1,7 @@
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { ShallowWrapper, shallow } from "enzyme";
|
||||
import TabView, { ITabViewPage as ITabViewPage } from "./TabView";
|
||||
|
||||
describe('TabView component', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const pages: ITabViewPage[] = [
|
||||
{
|
||||
header: 'Page 1',
|
||||
element: <div data-test="page-0-element"></div>,
|
||||
},
|
||||
{
|
||||
header: 'Page 1',
|
||||
element: <div data-test="page-1-element"></div>,
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<TabView pages={pages}/>);
|
||||
})
|
||||
|
||||
it ('Renders initial page only', () => {
|
||||
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
|
||||
expect(pageZeroComponent.length).toBe(1);
|
||||
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
|
||||
expect(pageOneComponent.length).toBe(0);
|
||||
})
|
||||
|
||||
it ('Renders secondary page only on click', () => {
|
||||
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
|
||||
navLinkComponent.simulate('click');
|
||||
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
|
||||
expect(pageOneComponent.length).toBe(1);
|
||||
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
|
||||
expect(pageZeroComponent.length).toBe(0);
|
||||
})
|
||||
|
||||
it ('Calls Tab Callback on Tab Change', () => {
|
||||
let tabOneValue = 0;
|
||||
let tabTwoValue = 5;
|
||||
|
||||
const tabViewPages: ITabViewPage[] = [
|
||||
{
|
||||
header: 'Page 1',
|
||||
element: <div data-test="page-0-element"></div>,
|
||||
tabOpenCallback: () => {tabOneValue = 42},
|
||||
},
|
||||
{
|
||||
header: 'Page 1',
|
||||
element: <div data-test="page-1-element"></div>,
|
||||
tabOpenCallback: () => {tabTwoValue = 1945}
|
||||
},
|
||||
];
|
||||
wrapper = shallow(<TabView pages={tabViewPages} />);
|
||||
expect(tabOneValue).toBe(42);
|
||||
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
|
||||
navLinkComponent.simulate('click');
|
||||
expect(tabTwoValue).toBe(1945);
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import "./TabView.css";
|
||||
|
||||
export interface ITabViewPage {
|
||||
header: string;
|
||||
element: ReactElement;
|
||||
tabOpenCallback?: () => any;
|
||||
}
|
||||
|
||||
export interface TabViewProps {
|
||||
pages: ITabViewPage[];
|
||||
}
|
||||
|
||||
interface TabViewState {
|
||||
currentPageIndex: number;
|
||||
}
|
||||
|
||||
class TabView extends React.Component<TabViewProps, TabViewState> {
|
||||
|
||||
constructor(props: TabViewProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentPageIndex: 0,
|
||||
};
|
||||
this.callPageCallback(0);
|
||||
}
|
||||
|
||||
private getClassStyle(index: number): string {
|
||||
return index === this.state.currentPageIndex ? "active ": "text-primary ";
|
||||
}
|
||||
|
||||
private setCurrentPage(index: number): void {
|
||||
const newState: TabViewState = Object.assign({}, this.state);
|
||||
newState.currentPageIndex = index;
|
||||
this.setState(newState);
|
||||
this.callPageCallback(index);
|
||||
}
|
||||
|
||||
private getCurrentPageElement(): ReactElement {
|
||||
const currentPage = this.props.pages[this.state.currentPageIndex];
|
||||
if (currentPage) {
|
||||
return currentPage.element;
|
||||
}
|
||||
return (<h3>Missing</h3>);
|
||||
}
|
||||
|
||||
private callPageCallback(index: number) {
|
||||
if (index < this.props.pages.length) {
|
||||
const tabPageCallback = this.props.pages[index].tabOpenCallback;
|
||||
if (tabPageCallback !== undefined) {
|
||||
tabPageCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<ul className="nav nav-tabs">
|
||||
{
|
||||
this.props.pages.map((page, index) => {
|
||||
return (
|
||||
<li key={index} className="nav-item">
|
||||
<p className={"nav-link " + this.getClassStyle(index)} onClick={() => this.setCurrentPage(index)} data-test={"nav-link-" + index}>{page.header}</p>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
{this.getCurrentPageElement()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TabView;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
.table-pagination p {
|
||||
color: white;
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
|
||||
.table-pagination .table-chev:not(.table-chev-disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-pagination .table-chev:hover:not(.table-chev-disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.table-pagination .table-chev-disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import './TablePagination.css';
|
||||
import ITablePagination from "./TablePagination";
|
||||
import { shallow } from "enzyme";
|
||||
import TablePagination from './TablePagination';
|
||||
import { isExpressionWithTypeArguments } from 'typescript';
|
||||
|
||||
describe("FormInput Component", () => {
|
||||
|
||||
it ("Only Allows Next/Prev page when enabled 1", () => {
|
||||
let nextPage = false;
|
||||
let prevPage = false;
|
||||
let wrapper = shallow(<TablePagination
|
||||
totalItems = {10}
|
||||
itemsPerPage = {2}
|
||||
totalPages = {5}
|
||||
currentPage = {0}
|
||||
isPrevDisabled = {true}
|
||||
isNextDisabled = {false}
|
||||
nextPageCallback = {() => {nextPage = !nextPage}}
|
||||
prevPageCallback = {() => {prevPage = !prevPage}}
|
||||
/>)
|
||||
|
||||
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
|
||||
expect(prevlink.hasClass('table-chev-disabled')).toBe(true);
|
||||
prevlink.simulate("click")
|
||||
expect(prevPage).toBe(false)
|
||||
|
||||
const nextLink = wrapper.find(`[data-test='next-page-link']`);
|
||||
expect(nextLink.hasClass('table-chev-disabled')).toBe(false);
|
||||
nextLink.simulate("click")
|
||||
expect(nextPage).toBe(true)
|
||||
});
|
||||
|
||||
it ("Only Allows Next/Prev page when enabled 2", () => {
|
||||
let nextPage = false;
|
||||
let prevPage = false;
|
||||
let wrapper = shallow(<TablePagination
|
||||
totalItems = {10}
|
||||
itemsPerPage = {2}
|
||||
totalPages = {5}
|
||||
currentPage = {2}
|
||||
isPrevDisabled = {false}
|
||||
isNextDisabled = {true}
|
||||
nextPageCallback = {() => {nextPage = !nextPage}}
|
||||
prevPageCallback = {() => {prevPage = !prevPage}}
|
||||
/>)
|
||||
|
||||
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
|
||||
expect(prevlink.hasClass('table-chev-disabled')).toBe(false);
|
||||
prevlink.simulate("click")
|
||||
expect(prevPage).toBe(true)
|
||||
|
||||
const nextLink = wrapper.find(`[data-test='next-page-link']`);
|
||||
expect(nextLink.hasClass('table-chev-disabled')).toBe(true);
|
||||
nextLink.simulate("click")
|
||||
expect(nextPage).toBe(false)
|
||||
});
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import './TablePagination.css';
|
||||
|
||||
interface ITablePaginationProps {
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
isPrevDisabled: boolean;
|
||||
isNextDisabled: boolean;
|
||||
nextPageCallback: () => any;
|
||||
prevPageCallback: () => any;
|
||||
}
|
||||
|
||||
const TablePagination: React.FC<ITablePaginationProps> = (props) => {
|
||||
const disabledChevClassName = 'table-chev-disabled';
|
||||
|
||||
return (
|
||||
<div className="table-pagination bg-secondary">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<p
|
||||
data-test='prev-page-link'
|
||||
className={'table-chev table-chev-left ' + (props.isPrevDisabled ? disabledChevClassName : '')}
|
||||
onClick={() => !props.isPrevDisabled ? props.prevPageCallback() : null}
|
||||
>
|
||||
<i className="fa fa-chevron-left" aria-hidden="true"></i>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col">
|
||||
<p className="text-center">
|
||||
Page <b>{props.currentPage}</b> of <b>{props.totalPages}</b>.
|
||||
|
||||
Showing <b>{props.itemsPerPage}</b> of <b>{props.totalItems}</b>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col">
|
||||
<p
|
||||
data-test='next-page-link'
|
||||
className={'text-right table-chev table-chev-right ' + (props.isNextDisabled ? disabledChevClassName : '')}
|
||||
onClick={() => !props.isNextDisabled ? props.nextPageCallback() : null}
|
||||
>
|
||||
<i className="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TablePagination;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.warning-info-page {
|
||||
padding-top: 15em;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
p {
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import "./WarningInfoPage.scss";
|
||||
|
||||
interface IWarningInfoPageProps {
|
||||
title: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
const WarningInfoPage: React.FC<IWarningInfoPageProps> = (props) => {
|
||||
return (
|
||||
<div className="warning-info-page">
|
||||
<h1 aria-hidden="true"><i className="fa fa-exclamation-triangle" /></h1>
|
||||
<h1>{props.title}</h1>
|
||||
<p>{props.body}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarningInfoPage;
|
||||
Reference in New Issue
Block a user