EditDonationPane works in React
This commit is contained in:
parent
6f94e3c724
commit
bb67341f37
15 changed files with 784 additions and 208 deletions
|
@ -1,97 +1,9 @@
|
|||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
||||
<!-- partial: donations/edit_modal -->
|
||||
|
||||
<div class="modal" id='editDonationModal'>
|
||||
<!--= scope 'payment_details.data' -->
|
||||
|
||||
<%= render 'common/modal_header', title: 'Edit Donation' %>
|
||||
|
||||
<div class='modal-body'>
|
||||
<%= render 'payment_info' %>
|
||||
|
||||
<form class='u-marginTop--20' parsley-validate>
|
||||
<!--= on 'submit' (update_donation form_object) -->
|
||||
|
||||
|
||||
<div>
|
||||
<!--= show_if (eq this.kind 'OffsitePayment') -->
|
||||
<div class='layout--two'>
|
||||
<fieldset>
|
||||
<label>Gross Amount<br><small>This amount should be more than zero</small></label>
|
||||
<input required type='number' min='0.01' name='gross_amount' step='0.01'>
|
||||
<!--= set_value (remove_commas (cents_to_dollars this.gross_amount -->
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Processing Fees <small>(optional)</small><br><small>This amount should be 0 or negative</small></label>
|
||||
<input required type='number' name='fee_total' max='0' step='0.01'>
|
||||
<!--= set_value (remove_commas (cents_to_dollars this.fee_total -->
|
||||
</fieldset>
|
||||
</div>
|
||||
<div id='EditPaymentPaneElement'/>
|
||||
|
||||
<fieldset>
|
||||
<label>Date</label>
|
||||
<input required type='text' name='date'>
|
||||
<!--= set_value (readable_date_time this.date) -->
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>Check or Payment Number/ID</label>
|
||||
<input type='text' name='check_number'>
|
||||
<!--= set_value (this.offsite_payment.check_number) -->
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<label>Campaign</label>
|
||||
<select name='campaign_id'>
|
||||
<option>
|
||||
<!--= repeat campaigns.data -->
|
||||
<!--= set_attr_if (eq payment_details.data.donation.campaign.name this.name) 'selected' true -->
|
||||
<!--= set_value this.id -->
|
||||
<!--= put this.name -->
|
||||
</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>Event</label>
|
||||
<select name='event_id'>
|
||||
<option>
|
||||
<!--= repeat events.data -->
|
||||
<!--= set_attr_if (eq payment_details.data.donation.event.name this.name) 'selected' true -->
|
||||
<!--= set_value this.id -->
|
||||
<!--= put this.name -->
|
||||
</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<input type='hidden' name='id'>
|
||||
<!--= set_value this.donation.id -->
|
||||
|
||||
<div class='layout--two'>
|
||||
<fieldset>
|
||||
<label>Dedication <small> (optional)</small></label>
|
||||
<textarea rows='3' name='dedication' placeholder='Dedication'></textarea>
|
||||
<!--= set_value this.donation.dedication -->
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>Designation<small> (optional)</small></label>
|
||||
<textarea rows='3' name='designation' placeholder='Designation'></textarea>
|
||||
<!--= set_value this.donation.designation -->
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<label>Notes <small> (optional) </small> </label>
|
||||
<textarea name='comment' rows='3' placeholder='Notes'></textarea>
|
||||
<!--= set_value this.donation.comment -->
|
||||
</fieldset>
|
||||
|
||||
<%= render 'components/forms/submit_button', button_text: 'Save', loading_text: 'Updating...' %>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- end partial: donations/edit_modal -->
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
||||
|
||||
<!-- partial nonprofits/payments/_side_panel -->
|
||||
|
||||
<div class='sidePanel'>
|
||||
<!--= add_class_if loading 'u-halfOpacity' -->
|
||||
|
||||
|
@ -25,7 +24,7 @@
|
|||
<div class='u-marginTop--20'>
|
||||
<a class="button--tiny edit">
|
||||
<!--= show_if (all (payment_details.data.donation) (not payment_details.data.dispute)) -->
|
||||
<!--= on 'click' (open_modal 'editDonationModal') -->
|
||||
<!--= on 'click' (open_donation_modal payment_details) -->
|
||||
<i class='fa fa-pencil'></i> Edit Donation
|
||||
</a>
|
||||
|
||||
|
|
|
@ -4,13 +4,43 @@
|
|||
|
||||
<% content_for :stylesheets do %>
|
||||
<%= stylesheet_link_tag 'nonprofits/payments/index/page' %>
|
||||
<%= IncludeAsset.css '/client/css/bootstrap.css' %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :javascripts do %>
|
||||
|
||||
<script>
|
||||
appl.def('has_bank', <%= !!@nonprofit.bank_account %>)
|
||||
</script>
|
||||
<%= IncludeAsset.js '/client/js/nonprofits/payments/index/page.js' %>
|
||||
<%= IncludeAsset.js '/app/react.js' %>
|
||||
<%= IncludeAsset.js '/app/react-dom.js' %>
|
||||
<%= IncludeAsset.js '/app/vendor.js' %>
|
||||
<%= IncludeAsset.js '/app/edit_payment_panex.js' %>
|
||||
|
||||
<script>
|
||||
appl.def('open_donation_modal', function(payment_details) {
|
||||
$('.modal').removeClass('inView')
|
||||
|
||||
function SetupLoadReactEditPaymentPane(modalActive){
|
||||
LoadReactEditPaymentPane(document.getElementById('EditPaymentPaneElement'),
|
||||
payment_details.data,
|
||||
appl.campaigns.data,
|
||||
appl.events.data,
|
||||
() => {SetupLoadReactEditPaymentPane(false)},
|
||||
modalActive,
|
||||
appl.start_loading,
|
||||
appl.update_donation__success,
|
||||
ENV.nonprofitTimezone)
|
||||
}
|
||||
|
||||
SetupLoadReactEditPaymentPane(true)
|
||||
|
||||
|
||||
return appl
|
||||
})
|
||||
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %>
|
||||
|
|
|
@ -19,13 +19,13 @@ child :donation, object_root: false do
|
|||
|
||||
|
||||
child :campaign, object_root: false do
|
||||
attributes :name, :url
|
||||
attributes :name, :url, :id
|
||||
end
|
||||
|
||||
node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}}
|
||||
|
||||
child :event, object_root: false do
|
||||
attributes :name, :url
|
||||
attributes :name, :url, :id
|
||||
end
|
||||
|
||||
child :recurring_donation, object_root: false do
|
||||
|
@ -49,7 +49,7 @@ end
|
|||
node(:ticket) do |payment|
|
||||
event = GetData.obj(payment.tickets.last, :event)
|
||||
h = {
|
||||
event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url)},
|
||||
event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url), id: GetData.obj(event, :id)},
|
||||
levels: payment.tickets.map{|t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)"}.join(", "),
|
||||
discount: payment.tickets.map{|t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil}.compact.join(", ")
|
||||
}
|
||||
|
@ -68,3 +68,7 @@ child :supporter do
|
|||
attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country
|
||||
|
||||
end
|
||||
|
||||
child :nonprofit do
|
||||
attributes :id
|
||||
end
|
||||
|
|
|
@ -112,6 +112,16 @@ appl.def('update_donation', function(donation) {
|
|||
})
|
||||
})
|
||||
|
||||
appl.def('start_loading', function(){
|
||||
appl.def('loading', true)
|
||||
})
|
||||
|
||||
appl.def('update_donation__success', function() {
|
||||
appl.ajax_payment_details.fetch(appl.payment_details.data.id)
|
||||
appl.def('loading', false)
|
||||
appl.notify('Donation successfully updated!')
|
||||
})
|
||||
|
||||
appl.def('delete_offline_donation', function() {
|
||||
var payment = appl.payment_details.data
|
||||
request
|
||||
|
|
27
javascripts/app/edit_payment_pane.tsx
Normal file
27
javascripts/app/edit_payment_pane.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
// require a root component here. This will be treated as the root of a webpack package
|
||||
import Root from "../src/components/common/Root"
|
||||
import EditPaymentPane, {FundraiserInfo} from "../src/components/edit_payment_pane/EditPaymentPane"
|
||||
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import * as React from 'react'
|
||||
|
||||
function LoadReactPage(element:HTMLElement, data:any, campaigns:FundraiserInfo[],
|
||||
events:FundraiserInfo[],
|
||||
onClose:() => void,
|
||||
modalActive:boolean,
|
||||
preupdateDonationAction: () => void,
|
||||
postUpdateSuccess: () => void,
|
||||
nonprofitTimezone?:string
|
||||
|
||||
) {
|
||||
ReactDOM.render(<Root><EditPaymentPane data={data} campaigns={campaigns}
|
||||
events={events} onClose={onClose}
|
||||
modalActive={modalActive} nonprofitTimezone={nonprofitTimezone}
|
||||
postUpdateSuccess={postUpdateSuccess}
|
||||
preupdateDonationAction={preupdateDonationAction}
|
||||
/></Root>, element)
|
||||
}
|
||||
|
||||
|
||||
(window as any).LoadReactEditPaymentPane = LoadReactPage
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
exports[`StandardFieldComponent sets error message properly 1`] = `
|
||||
<div>
|
||||
<input
|
||||
className="form-control"
|
||||
key=".0"
|
||||
/>
|
||||
<input />
|
||||
<div
|
||||
className="help-block"
|
||||
role="alert"
|
||||
|
@ -17,10 +14,7 @@ exports[`StandardFieldComponent sets error message properly 1`] = `
|
|||
|
||||
exports[`StandardFieldComponent works with a child 1`] = `
|
||||
<div>
|
||||
<input
|
||||
className="form-control"
|
||||
key=".0"
|
||||
/>
|
||||
<input />
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
@ -6,11 +6,9 @@ import {Form} from "mobx-react-form";
|
|||
import {mount} from 'enzyme';
|
||||
import {toJS, observable, action, runInAction} from 'mobx';
|
||||
import {observer} from 'mobx-react';
|
||||
import {InputHTMLAttributes} from 'react';
|
||||
import {ReactForm} from "./ReactForm";
|
||||
|
||||
|
||||
|
||||
@observer
|
||||
class TestChange extends React.Component {
|
||||
@observable
|
||||
|
@ -20,21 +18,24 @@ class TestChange extends React.Component{
|
|||
|
||||
@action.bound
|
||||
componentWillMount() {
|
||||
this.form = new Form({fields:[{
|
||||
this.form = new Form({
|
||||
fields: [{
|
||||
name: 'name',
|
||||
extra: null}
|
||||
]})
|
||||
extra: null
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@action.bound
|
||||
onClick() {
|
||||
this.remove = true
|
||||
}
|
||||
|
||||
render() {
|
||||
let reactInput = !this.remove ? <ReactInput field={this.form.$('name')} label={'label1'} placeholder={"holder"}>
|
||||
{this.props.children}
|
||||
|
||||
</ReactInput> : undefined
|
||||
|
||||
return <ReactForm form={this.form}>
|
||||
|
@ -45,18 +46,6 @@ class TestChange extends React.Component{
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class WrappedInput extends React.Component<InputHTMLAttributes<HTMLInputElement>>{
|
||||
|
||||
render(){
|
||||
let notChildren = {...this.props}
|
||||
delete notChildren.children
|
||||
return <div>
|
||||
<input {...notChildren} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
describe('ReactInput', () => {
|
||||
|
||||
let form: Form
|
||||
|
@ -71,7 +60,6 @@ describe('ReactInput', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('no children passed in', () => {
|
||||
test('gets added properly', () => {
|
||||
let res = mount(<ReactForm form={form}>
|
||||
<ReactInput field={form.$('name')} label={"label"}
|
||||
|
@ -117,48 +105,3 @@ describe('ReactInput', () => {
|
|||
expect(f.form.$('name').placeholder).toEqual('holder')
|
||||
})
|
||||
})
|
||||
|
||||
describe('children passed in', () => {
|
||||
test('gets added properly', () => {
|
||||
let res = mount(<ReactForm form={form}>
|
||||
<ReactInput field={form.$('name')} label={"label"}
|
||||
placeholder={"holder"} value={'snapshot'} aria-required={true}>
|
||||
<WrappedInput/>
|
||||
</ReactInput>
|
||||
|
||||
</ReactForm>)
|
||||
|
||||
//Did the attributes settings work as expected back to the objects
|
||||
expect(form.$('name').label).toEqual('label')
|
||||
expect(form.$('name').placeholder).toEqual('holder')
|
||||
expect(form.$('name').value).toEqual('')
|
||||
|
||||
//is the aria attribute passted through to the input
|
||||
let input = res.find('input')
|
||||
expect(input.prop('aria-required')).toEqual(true)
|
||||
|
||||
|
||||
// is the input properly bound?
|
||||
input.simulate('change', {target: { value: 'something' } })
|
||||
expect(form.$('name').value).toEqual('something')
|
||||
})
|
||||
|
||||
test('gets removed properly', () => {
|
||||
|
||||
let res = mount(<TestChange>
|
||||
<WrappedInput/>
|
||||
</TestChange>)
|
||||
let f = res.find('ReactForm').instance() as any as ReactForm
|
||||
res.find('input').simulate('change', {target: { value: 'something' } })
|
||||
|
||||
expect(f.form.$('name').value).toEqual('something')
|
||||
expect(f.form.size).toEqual(1)
|
||||
res.find('button').simulate('click')
|
||||
expect(f.form.size).toEqual(1)
|
||||
|
||||
expect(f.form.$('name').label).toEqual('label1')
|
||||
expect(f.form.$('name').placeholder).toEqual('holder')
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -1,10 +1,112 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import * as React from 'react';
|
||||
import 'jest';
|
||||
import ReactSelect from './ReactSelect'
|
||||
import {Form} from "mobx-react-form";
|
||||
import ReactInput from "./ReactInput";
|
||||
import {ReactForm} from "./ReactForm";
|
||||
import {action, observable, toJS} from 'mobx';
|
||||
import ReactTextarea from './ReactTextarea';
|
||||
import {observer} from 'mobx-react';
|
||||
import {mount} from 'enzyme';
|
||||
import ReactSelect from './ReactSelect';
|
||||
|
||||
|
||||
@observer
|
||||
class TestChange extends React.Component{
|
||||
@observable
|
||||
remove:boolean
|
||||
@observable
|
||||
form: Form
|
||||
|
||||
@action.bound
|
||||
componentWillMount(){
|
||||
this.form = new Form({fields:[{
|
||||
name: 'name',
|
||||
extra: null}
|
||||
]})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@action.bound
|
||||
onClick(){
|
||||
this.remove = true
|
||||
}
|
||||
render() {
|
||||
let reactInput = !this.remove ? <ReactSelect field={this.form.$('name')} label={'label1'} placeholder={"holder"} options={[{id: null, name:null},
|
||||
{id: 'something', name: "Something"},
|
||||
{id: 'another_value', name: "aonther value"}
|
||||
]}>
|
||||
|
||||
</ReactSelect> : undefined
|
||||
|
||||
return <ReactForm form={this.form}>
|
||||
|
||||
{reactInput}
|
||||
<button onClick={() => this.onClick()}/>
|
||||
</ReactForm>
|
||||
}
|
||||
}
|
||||
|
||||
describe('ReactSelect', () => {
|
||||
test('your test here', () => {
|
||||
expect(false).toBe(true)
|
||||
let form: Form
|
||||
beforeEach(() => {
|
||||
form = new Form({
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
extra: null
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('gets added properly', () => {
|
||||
let res = mount(<ReactForm form={form}>
|
||||
<ReactSelect field={form.$('name')} label={"label"}
|
||||
placeholder={"holder"} value={'snapshot'} aria-required={true}options={[{id: null, name:null},
|
||||
{id: 'something', name: "Something"},
|
||||
{id: 'another_value', name: "aonther value"}
|
||||
]}/>
|
||||
|
||||
</ReactForm>)
|
||||
|
||||
|
||||
//Did the attributes settings work as expected back to the objects
|
||||
expect(form.$('name').label).toEqual('label')
|
||||
expect(form.$('name').placeholder).toEqual('holder')
|
||||
expect(form.$('name').value).toEqual('')
|
||||
|
||||
//is the aria attribute passted through to the input
|
||||
let input = res.find('select')
|
||||
let options = input.find('option')
|
||||
expect(options.getElements().length).toBe(3)
|
||||
|
||||
|
||||
// is the input properly bound?
|
||||
input.simulate('change', {target: {value: 'something'}})
|
||||
expect(form.$('name').value).toEqual('something')
|
||||
})
|
||||
|
||||
test('gets removed properly', () => {
|
||||
|
||||
let res = mount(<TestChange/>)
|
||||
|
||||
// The two casts are needed because Typescript was going blowing up without the 'any' first.
|
||||
// Why was it? *shrugs*
|
||||
let f = res.find('ReactForm').instance() as any as ReactForm
|
||||
expect(f.form.size).toEqual(1)
|
||||
|
||||
res.find('select').simulate('change', {target: {value: 'something'}})
|
||||
|
||||
expect(f.form.$('name').value).toEqual('something')
|
||||
|
||||
res.find('button').simulate('click')
|
||||
expect(f.form.size).toEqual(1)
|
||||
|
||||
expect(toJS(res.find('form'))).toMatchSnapshot()
|
||||
|
||||
expect(f.form.$('name').label).toEqual('label1')
|
||||
expect(f.form.$('name').placeholder).toEqual('holder')
|
||||
})
|
||||
})
|
|
@ -1,10 +1,102 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import * as React from 'react';
|
||||
import 'jest';
|
||||
import ReactSelect from './ReactSelect'
|
||||
import {Form} from "mobx-react-form";
|
||||
import {ReactForm} from "./ReactForm";
|
||||
import {action, observable, toJS} from 'mobx';
|
||||
import ReactTextarea from './ReactTextarea';
|
||||
import {observer} from 'mobx-react';
|
||||
import {mount} from 'enzyme';
|
||||
|
||||
describe('ReactSelect', () => {
|
||||
test('your test here', () => {
|
||||
expect(false).toBe(true)
|
||||
|
||||
@observer
|
||||
class TestChange extends React.Component{
|
||||
@observable
|
||||
remove:boolean
|
||||
@observable
|
||||
form: Form
|
||||
|
||||
@action.bound
|
||||
componentWillMount(){
|
||||
this.form = new Form({fields:[{
|
||||
name: 'name',
|
||||
extra: null}
|
||||
]})
|
||||
}
|
||||
|
||||
@action.bound
|
||||
onClick(){
|
||||
this.remove = true
|
||||
}
|
||||
render() {
|
||||
let reactInput = !this.remove ? <ReactTextarea field={this.form.$('name')} label={'label1'} placeholder={"holder"} rows={3}>
|
||||
|
||||
</ReactTextarea> : undefined
|
||||
|
||||
return <ReactForm form={this.form}>
|
||||
|
||||
{reactInput}
|
||||
<button onClick={() => this.onClick()}/>
|
||||
</ReactForm>
|
||||
}
|
||||
}
|
||||
|
||||
describe('ReactTextarea', () => {
|
||||
let form: Form
|
||||
beforeEach(() => {
|
||||
form = new Form({
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
extra: null
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('gets added properly', () => {
|
||||
let res = mount(<ReactForm form={form}>
|
||||
<ReactTextarea field={form.$('name')} label={"label"}
|
||||
placeholder={"holder"} value={'snapshot'} aria-required={true} rows={3}/>
|
||||
|
||||
</ReactForm>)
|
||||
|
||||
|
||||
//Did the attributes settings work as expected back to the objects
|
||||
expect(form.$('name').label).toEqual('label')
|
||||
expect(form.$('name').placeholder).toEqual('holder')
|
||||
expect(form.$('name').value).toEqual('')
|
||||
|
||||
//is the aria attribute passted through to the input
|
||||
let input = res.find('textarea')
|
||||
expect(input.prop('aria-required')).toEqual(true)
|
||||
expect(input.prop('rows')).toEqual(3)
|
||||
|
||||
|
||||
// is the input properly bound?
|
||||
input.simulate('change', {target: {value: 'something'}})
|
||||
expect(form.$('name').value).toEqual('something')
|
||||
})
|
||||
|
||||
test('gets removed properly', () => {
|
||||
|
||||
let res = mount(<TestChange/>)
|
||||
|
||||
// The two casts are needed because Typescript was going blowing up without the 'any' first.
|
||||
// Why was it? *shrugs*
|
||||
let f = res.find('ReactForm').instance() as any as ReactForm
|
||||
expect(f.form.size).toEqual(1)
|
||||
|
||||
res.find('textarea').simulate('change', {target: {value: 'something'}})
|
||||
|
||||
expect(f.form.$('name').value).toEqual('something')
|
||||
|
||||
res.find('button').simulate('click')
|
||||
expect(f.form.size).toEqual(1)
|
||||
|
||||
expect(toJS(res.find('form'))).toMatchSnapshot()
|
||||
|
||||
expect(f.form.$('name').label).toEqual('label1')
|
||||
expect(f.form.$('name').placeholder).toEqual('holder')
|
||||
})
|
||||
})
|
|
@ -1,5 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactInput gets removed properly 1`] = `
|
||||
<form>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`ReactInput no children passed in gets removed properly 1`] = `
|
||||
<form>
|
||||
<button
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactSelect gets removed properly 1`] = `
|
||||
<form>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</form>
|
||||
`;
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactTextarea gets removed properly 1`] = `
|
||||
<form>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</form>
|
||||
`;
|
437
javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx
Normal file
437
javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx
Normal file
|
@ -0,0 +1,437 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import * as React from 'react';
|
||||
import {observer} from 'mobx-react';
|
||||
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||
import {action, computed, observable, runInAction} from "mobx";
|
||||
import {Field, FieldDefinition, Form} from "../../../../types/mobx-react-form";
|
||||
import {HoudiniForm} from "../../lib/houdini_form";
|
||||
import ProgressableButton from "../common/ProgressableButton";
|
||||
import AriaModal = require('react-aria-modal');
|
||||
import {centsToDollars, dollarsToCents, readableInterval, readableKind} from "../../lib/format";
|
||||
import {NonprofitTimezonedDates, readable_date} from '../../lib/date';
|
||||
import {UpdateDonationModel, PutDonation} from "../../lib/api/put_donation";
|
||||
import {ApiManager} from "../../lib/api_manager";
|
||||
import * as CustomAPIS from "../../lib/apis";
|
||||
import {CSRFInterceptor} from "../../lib/csrf_interceptor";
|
||||
import {BasicField, SelectField, TextareaField} from '../common/fields';
|
||||
import ReactTextarea from "../common/form/ReactTextarea";
|
||||
import {TwoColumnFields} from "../common/layout";
|
||||
import {Validations} from "../../lib/vjf_rules";
|
||||
import _ = require("lodash");
|
||||
import {Dedication, parseDedication, serializeDedication} from '../../lib/dedication';
|
||||
|
||||
interface Charge {
|
||||
status: string
|
||||
}
|
||||
|
||||
|
||||
interface RecurringDonation {
|
||||
interval?: number
|
||||
time_unit?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface Donation {
|
||||
designation?: string
|
||||
comment?: string
|
||||
event?: { id: number }
|
||||
campaign?: { id: number }
|
||||
dedication?: string
|
||||
recurring_donation?: RecurringDonation
|
||||
id:number
|
||||
}
|
||||
|
||||
interface PaymentData {
|
||||
gross_amount: number
|
||||
fee_total: number
|
||||
date: string
|
||||
offsite_payment: OffsitePayment
|
||||
donation: Donation
|
||||
kind: string
|
||||
id: string
|
||||
refund_total: number
|
||||
net_amount: number
|
||||
origin_url?: string
|
||||
charge?: Charge,
|
||||
nonprofit: {id:number}
|
||||
}
|
||||
|
||||
interface OffsitePayment {
|
||||
check_number: string
|
||||
kind: string
|
||||
}
|
||||
|
||||
export interface EditPaymentPaneProps {
|
||||
data: PaymentData
|
||||
events: FundraiserInfo[]
|
||||
campaigns: FundraiserInfo[]
|
||||
onClose: () => void
|
||||
modalActive: boolean
|
||||
nonprofitTimezone?: string
|
||||
preupdateDonationAction:() => void
|
||||
postUpdateSuccess: () => void
|
||||
}
|
||||
|
||||
export interface FundraiserInfo {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
class EditPaymentPanelForm extends HoudiniForm {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@observer
|
||||
class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedIntlProps, {}> {
|
||||
|
||||
constructor(props: EditPaymentPaneProps & InjectedIntlProps) {
|
||||
super(props);
|
||||
this.putDonation = new ApiManager(CustomAPIS.APIS as Array<any>, CSRFInterceptor).get(PutDonation)
|
||||
|
||||
this.loadFormFromData()
|
||||
|
||||
}
|
||||
|
||||
@computed
|
||||
get nonprofitTimezonedDates():NonprofitTimezonedDates {
|
||||
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||
}
|
||||
|
||||
@computed
|
||||
get dedication(): Dedication|null {
|
||||
return parseDedication(this.props.data.donation && this.props.data.donation.dedication)
|
||||
}
|
||||
|
||||
putDonation : PutDonation
|
||||
|
||||
@action.bound
|
||||
async updateDonation() {
|
||||
if (this.props.preupdateDonationAction) {
|
||||
this.props.preupdateDonationAction()
|
||||
}
|
||||
|
||||
|
||||
let updateData:UpdateDonationModel = {
|
||||
id: Number(this.props.data.donation.id),
|
||||
donation: {
|
||||
designation: this.form.$('designation').value,
|
||||
comment: this.form.$('comment').value,
|
||||
campaign_id: this.form.$('campaign').value,
|
||||
event_id: this.form.$('event').value,
|
||||
gross_amount: dollarsToCents(this.form.$('gross_amount').get('value')),
|
||||
fee_total: dollarsToCents(this.form.$('fee_total').get('value')),
|
||||
|
||||
date: this.form.$('date').get('value')
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.$('dedication.type').get('value') != '') {
|
||||
updateData.donation.dedication = serializeDedication({
|
||||
type: this.form.$('dedication.type').get('value'),
|
||||
supporter_id: this.form.$('dedication.supporter_id').get('value'),
|
||||
name: this.form.$('dedication.name').get('value'),
|
||||
contact:this.form.$('dedication.contact').get('value'),
|
||||
note: this.form.$('dedication.note').get('value')
|
||||
})
|
||||
}
|
||||
else {
|
||||
updateData.donation.dedication = ""
|
||||
}
|
||||
|
||||
if (this.form.has('check_number'))
|
||||
{
|
||||
updateData.donation.check_number = this.form.$('check_number').value
|
||||
}
|
||||
|
||||
|
||||
await this.putDonation.putDonation(updateData, this.props.data.nonprofit.id)
|
||||
|
||||
|
||||
if(this.props.postUpdateSuccess){
|
||||
try {
|
||||
this.props.postUpdateSuccess()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
this.props.onClose()
|
||||
|
||||
}
|
||||
|
||||
createDefinition<TInputType>(fieldDef:FieldDefinition<TInputType>) : FieldDefinition<TInputType> {
|
||||
return fieldDef;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
loadFormFromData() {
|
||||
const eventId = this.props.data.donation.event && this.props.data.donation.event.id;
|
||||
const campaignId = this.props.data.donation.campaign && this.props.data.donation.campaign.id;
|
||||
let params: {[name:string]:FieldDefinition} = {
|
||||
'event': {name: 'event', label: 'Event', value: eventId},
|
||||
'campaign': {name: 'campaign', label: 'Campaign', value: campaignId},
|
||||
'gross_amount': {name: 'gross_amount', label: 'Gross Amount', value: centsToDollars(this.props.data.gross_amount)},
|
||||
'fee_total': {name: 'fee_total', label: 'Fees', value: centsToDollars(this.props.data.fee_total)},
|
||||
'date': this.createDefinition({name: 'date', label: 'Date',
|
||||
value: this.props.data.date,
|
||||
input: (isoTime:string) => this.nonprofitTimezonedDates.readable_date(isoTime),
|
||||
output:(date:string) => this.nonprofitTimezonedDates.readable_date_time_to_iso(date)}),
|
||||
'dedication': {name: 'dedication', label: 'Dedication', fields: [
|
||||
this.createDefinition({name:'type', label: 'Dedication Type', value: this.dedication.type}),
|
||||
this.createDefinition({name: 'supporter_id', type: 'hidden', value: this.dedication.supporter_id}),
|
||||
this.createDefinition({name:'name', label:'Person dedicated for', value: this.dedication.name}),
|
||||
this.createDefinition({name: 'contact', type: 'hidden', value: this.dedication.contact}),
|
||||
this.createDefinition({name: 'note', value: this.dedication.note})
|
||||
]},
|
||||
'designation': {name: 'designation', label: 'Designation', value: this.props.data.donation.designation},
|
||||
'comment': {name: 'comment', label: 'Note', value: this.props.data.donation.comment}
|
||||
};
|
||||
|
||||
|
||||
if (this.props.data.kind == 'OffsitePayment') {
|
||||
params.check_number = {name: 'check_number', label: 'Check Number', value: this.props.data.offsite_payment.check_number}
|
||||
|
||||
params.date.validators = [Validations.isDate('MM/DD/YYYY')]
|
||||
|
||||
params.gross_amount.validators = [Validations.isGreaterThanOrEqualTo(0.01)];
|
||||
params.fee_total.validators = [Validations.optional(Validations.isLessThanOrEqualTo(0))];
|
||||
|
||||
}
|
||||
|
||||
return new EditPaymentPanelForm({fields: _.values(params)}, {
|
||||
hooks: {
|
||||
onSubmit: async (e: Field) => {
|
||||
await this.updateDonation()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@computed get form(): EditPaymentPanelForm {
|
||||
//add this.props because we need to reload on prop change
|
||||
return this.props && this.loadFormFromData()
|
||||
}
|
||||
|
||||
@computed get dateFormatter(): NonprofitTimezonedDates {
|
||||
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let rd = this.props.data.donation && this.props.data.donation.recurring_donation;
|
||||
let initialTable = <table className='table--small u-marginBottom--10'>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Payment Info</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>
|
||||
{this.dateFormatter.readable_date(this.props.data.date)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
{readableKind(this.props.data.kind)}
|
||||
|
||||
{
|
||||
this.props.data.offsite_payment && this.props.data.offsite_payment ?
|
||||
|
||||
<span>
|
||||
|
||||
({this.props.data.offsite_payment.kind})
|
||||
</span> : false
|
||||
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{
|
||||
this.props.data.kind === 'RecurringDonation' ?
|
||||
<tr>
|
||||
<td>Recurring</td>
|
||||
<td>
|
||||
{rd ? readableInterval(rd.interval, rd.time_unit) : false}
|
||||
since
|
||||
{rd ? this.dateFormatter.readable_date(rd.created_at) : false}
|
||||
</td>
|
||||
</tr> : false
|
||||
}
|
||||
|
||||
<tr className='test-grossAmount'>
|
||||
<td>Gross Amount</td>
|
||||
<td>
|
||||
$ {centsToDollars(this.props.data.gross_amount)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Processing Fees</td>
|
||||
<td>
|
||||
${centsToDollars(this.props.data.fee_total)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{
|
||||
this.props.data.refund_total && this.props.data.refund_total > 0 ?
|
||||
<tr>
|
||||
|
||||
<td>Total Refunds</td>
|
||||
<td>
|
||||
${centsToDollars(this.props.data.fee_total)}
|
||||
</td>
|
||||
</tr> : false
|
||||
}
|
||||
<tr>
|
||||
<td>Net Amount</td>
|
||||
<td>
|
||||
${centsToDollars(this.props.data.net_amount)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{
|
||||
this.props.data.origin_url ?
|
||||
<tr>
|
||||
|
||||
<td>Origin</td>
|
||||
<td>
|
||||
<a target='_blank' href={this.props.data.origin_url}>
|
||||
{this.props.data.origin_url}
|
||||
</a>
|
||||
</td>
|
||||
</tr> : false
|
||||
}
|
||||
|
||||
{
|
||||
this.props.data.charge ?
|
||||
<tr>
|
||||
|
||||
<td>Status</td>
|
||||
<td>{this.props.data.charge.status}</td>
|
||||
</tr> : false
|
||||
}
|
||||
|
||||
{this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ?
|
||||
<tr>
|
||||
|
||||
<td>Check #</td>
|
||||
<td>
|
||||
{this.props.data.offsite_payment.check_number}
|
||||
</td>
|
||||
</tr> : false
|
||||
|
||||
}
|
||||
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{this.props.data.id}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>;
|
||||
|
||||
|
||||
let checkNumber = this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ? <fieldset>
|
||||
<label>Check or Payment Number/ID</label>
|
||||
<input {...this.form.$('check_number').bind()}/>
|
||||
</fieldset> : false;
|
||||
|
||||
let offsitePayment = this.props.data.kind === "OffsitePayment" ? (<div>
|
||||
|
||||
<TwoColumnFields>
|
||||
<BasicField field={this.form.$('gross_amount')} label={"Gross Amount"}/>
|
||||
<BasicField field={this.form.$('fee_total')} label={"Processing Fees"}/>
|
||||
|
||||
</TwoColumnFields>
|
||||
|
||||
<BasicField field={this.form.$('date')} label={"Date"} />
|
||||
|
||||
|
||||
{checkNumber}
|
||||
</div>) : undefined;
|
||||
|
||||
const modal = this.props.modalActive ?
|
||||
<AriaModal mounted={this.props.modalActive} titleText={'Edit Donation'} focusDialog={true}
|
||||
onExit={this.props.onClose} dialogStyle={{minWidth:'768px'}}>
|
||||
<header className='modal-header'>
|
||||
<h4 className='modal-header-title'>Edit Donation</h4>
|
||||
</header>
|
||||
<div className="modal-body">
|
||||
<div className={"tw-bs"}>
|
||||
<div>
|
||||
{initialTable}
|
||||
|
||||
<form className='u-marginTop--20'>
|
||||
|
||||
{offsitePayment}
|
||||
|
||||
|
||||
<SelectField field={this.form.$('campaign')}
|
||||
label={"Campaign"}
|
||||
options={this.props.campaigns}/>
|
||||
|
||||
|
||||
<SelectField field={this.form.$('event')}
|
||||
label={"Event"}
|
||||
options={this.props.events} />
|
||||
|
||||
|
||||
|
||||
<TextareaField field={this.form.$('designation')} label={"Designation"} rows={3} />
|
||||
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading"><label>Dedication <small> (optional)</small></label></div>
|
||||
<div className="panel-body">
|
||||
<SelectField field={this.form.$('dedication.type')} label={"Dedication Type"} options={[{id: null, name: ''}, {id: 'honor', name: 'In honor of'}, {id:'memory', name: 'In memory of'}]} />
|
||||
|
||||
{ this.form.$('dedication.type').get('value') != '' ? <div> <div className={"panel panel-default"}>
|
||||
<div className="panel-heading"><label>Dedicated to:</label></div>
|
||||
<div className={'panel-body'}>
|
||||
<table className='table--small u-marginBottom--10'>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td><input {...this.form.$('dedication.name').bind()}/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th > Supporter
|
||||
ID</th>
|
||||
<td>{this.dedication.supporter_id}<input {...this.form.$('dedication.supporter_id').bind()}/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<TextareaField rows={3} placeholder={"Dedication"} field={this.form.$('dedication.note')} label={"Dedication Note"}/></div>
|
||||
: undefined }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<TextareaField field={this.form.$('comment')} label={"Notes"} rows={3} />
|
||||
|
||||
|
||||
<ProgressableButton buttonText={'Save'}
|
||||
buttonTextOnProgress={'Updating...'} className={'button'}
|
||||
inProgress={this.form.submitting}
|
||||
disabled={!this.form.isValid}
|
||||
disableOnProgress={true} onClick={this.form.onSubmit}/>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AriaModal> : false;
|
||||
|
||||
return (<div>{modal}</div>)
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(observer(EditPaymentPane))
|
Loading…
Reference in a new issue