Merge branch 'develop' of https://github.com/dternyak/zcash-grant-system into fix-edit-profile

This commit is contained in:
Daniel Ternyak 2019-02-17 13:56:09 -06:00
commit 5fb6f4c36a
No known key found for this signature in database
GPG Key ID: DF212D2DC5D0E245
27 changed files with 308 additions and 75 deletions

View File

@ -46,7 +46,7 @@ class ArbiterControlNaked extends React.Component<Props, State> {
{/* CONTROL */}
<Button
className="ArbiterControl-control"
loading={store.proposalDetailApproving}
loading={store.arbiterSaving}
icon="crown"
type="primary"
onClick={this.handleShowSearch}
@ -146,15 +146,13 @@ class ArbiterControlNaked extends React.Component<Props, State> {
private handleSelect = async (user: User) => {
this.setState({ showSearch: false });
store.searchArbitersClear();
try {
await store.setArbiter(this.props.proposalId, user.userid);
await store.setArbiter(this.props.proposalId, user.userid);
if (store.arbiterSaved) {
message.success(
<>
Arbiter nominated for <b>{this.props.title}</b>
<b>{user.displayName}</b> nominated as arbiter of <b>{this.props.title}</b>
</>,
);
} catch (e) {
message.error(`Could not set arbiter: ${e}`);
}
};

View File

@ -17,8 +17,13 @@ import {
} from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import store from 'src/store';
import { formatDateSeconds } from 'util/time';
import { PROPOSAL_STATUS, PROPOSAL_ARBITER_STATUS, MILESTONE_STAGE } from 'src/types';
import { formatDateSeconds, formatDurationSeconds } from 'util/time';
import {
PROPOSAL_STATUS,
PROPOSAL_ARBITER_STATUS,
MILESTONE_STAGE,
PROPOSAL_STAGE,
} from 'src/types';
import { Link } from 'react-router-dom';
import Back from 'components/Back';
import Info from 'components/Info';
@ -52,6 +57,11 @@ class ProposalDetailNaked extends React.Component<Props, State> {
return 'loading proposal...';
}
const needsArbiter =
PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status &&
p.status === PROPOSAL_STATUS.LIVE &&
!p.isFailed;
const renderDeleteControl = () => (
<Popconfirm
onConfirm={this.handleDelete}
@ -72,7 +82,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
type: 'default',
className: 'ProposalDetail-controls-control',
block: true,
disabled: p.status !== PROPOSAL_STATUS.LIVE,
disabled: p.status !== PROPOSAL_STATUS.LIVE || p.isFailed,
}}
/>
);
@ -98,7 +108,14 @@ class ProposalDetailNaked extends React.Component<Props, State> {
okText="ok"
cancelText="cancel"
>
<Switch checked={p.contributionMatching === 1} loading={false} />{' '}
<Switch
checked={p.contributionMatching === 1}
loading={false}
disabled={
p.isFailed ||
[PROPOSAL_STAGE.WIP, PROPOSAL_STAGE.COMPLETED].includes(p.stage)
}
/>{' '}
</Popconfirm>
<span>
matching{' '}
@ -108,6 +125,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<span>
<b>Contribution matching</b>
<br /> Funded amount will be multiplied by 2.
<br /> <i>Disabled after proposal is fully-funded.</i>
</span>
}
/>
@ -215,8 +233,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
);
const renderNominateArbiter = () =>
PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status &&
p.status === PROPOSAL_STATUS.LIVE && (
needsArbiter && (
<Alert
showIcon
type="warning"
@ -297,6 +314,21 @@ class ProposalDetailNaked extends React.Component<Props, State> {
);
};
const renderFailed = () =>
p.isFailed && (
<Alert
showIcon
type="error"
message="Funding failed"
description={
<>
This proposal failed to reach its funding goal of <b>{p.target} ZEC</b> by{' '}
<b>{formatDateSeconds(p.datePublished + p.deadlineDuration)}</b>.
</>
}
/>
);
const renderDeetItem = (name: string, val: any) => (
<div className="ProposalDetail-deet">
<span>{name}</span>
@ -317,6 +349,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{renderNominateArbiter()}
{renderNominatedArbiter()}
{renderMilestoneAccepted()}
{renderFailed()}
<Collapse defaultActiveKey={['brief', 'content']}>
<Collapse.Panel key="brief" header="brief">
{p.brief}
@ -347,6 +380,17 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Card title="Details" size="small">
{renderDeetItem('id', p.proposalId)}
{renderDeetItem('created', formatDateSeconds(p.dateCreated))}
{renderDeetItem('published', formatDateSeconds(p.datePublished))}
{renderDeetItem(
'deadlineDuration',
formatDurationSeconds(p.deadlineDuration),
)}
{p.datePublished &&
renderDeetItem(
'(deadline)',
formatDateSeconds(p.datePublished + p.deadlineDuration),
)}
{renderDeetItem('isFailed', JSON.stringify(p.isFailed))}
{renderDeetItem('status', p.status)}
{renderDeetItem('stage', p.stage)}
{renderDeetItem('category', p.category)}

View File

@ -181,6 +181,9 @@ const app = store({
userDeleting: false,
userDeleted: false,
arbiterSaving: false,
arbiterSaved: false,
arbitersSearch: {
search: '',
results: [] as User[],
@ -385,10 +388,17 @@ const app = store({
},
async setArbiter(proposalId: number, userId: number) {
// let component handle errors for this one
const { proposal, user } = await setArbiter(proposalId, userId);
this.updateProposalInStore(proposal);
this.updateUserInStore(user);
app.arbiterSaving = true;
app.arbiterSaved = false;
try {
const { proposal, user } = await setArbiter(proposalId, userId);
this.updateProposalInStore(proposal);
this.updateUserInStore(user);
app.arbiterSaved = true;
} catch (e) {
handleApiError(e);
}
app.arbiterSaving = false;
},
// Proposals

View File

@ -79,6 +79,13 @@ export enum PROPOSAL_STATUS {
DELETED = 'DELETED',
STAKING = 'STAKING',
}
// NOTE: sync with backend/grant/utils/enums.py ProposalStage
export enum PROPOSAL_STAGE {
PREVIEW = 'PREVIEW',
FUNDING_REQUIRED = 'FUNDING_REQUIRED',
WIP = 'WIP',
COMPLETED = 'COMPLETED',
}
export interface Proposal {
proposalId: number;
brief: string;
@ -87,9 +94,11 @@ export interface Proposal {
dateCreated: number;
dateApproved: number;
datePublished: number;
deadlineDuration: number;
isFailed: boolean;
title: string;
content: string;
stage: string;
stage: PROPOSAL_STAGE;
category: string;
milestones: Milestone[];
currentMilestone?: Milestone;

View File

@ -6,6 +6,14 @@ export const formatDateSeconds = (s: number) => {
return moment(s * 1000).format(DATE_FMT_STRING);
};
export const formatDateSecondsFromNow = (s: number) => {
return moment(s * 1000).fromNow();
};
export const formatDateMs = (s: number) => {
return moment(s).format(DATE_FMT_STRING);
};
export const formatDurationSeconds = (s: number) => {
return moment.duration(s, 'seconds').humanize();
};

View File

@ -209,6 +209,13 @@ def set_arbiter(proposal_id, user_id):
if not proposal:
return {"message": "Proposal not found"}, 404
for member in proposal.team:
if member.id == user_id:
return {"message": "Cannot set proposal team member as arbiter"}, 400
if proposal.is_failed:
return {"message": "Cannot set arbiter on failed proposal"}, 400
user = User.query.filter(User.id == user_id).first()
if not user:
return {"message": "User not found"}, 404
@ -281,13 +288,7 @@ def update_proposal(id, contribution_matching):
proposal = Proposal.query.filter(Proposal.id == id).first()
if proposal:
if contribution_matching is not None:
# enforce 1 or 0 for now
if contribution_matching == 0.0 or contribution_matching == 1.0:
proposal.contribution_matching = contribution_matching
# TODO: trigger check if funding target reached OR make sure
# job schedule checks for funding completion include matching funds
else:
return {"message": f"Bad value for contributionMatching: {contribution_matching}"}, 400
proposal.set_contribution_matching(contribution_matching)
db.session.commit()
return proposal_schema.dump(proposal)
@ -499,6 +500,11 @@ def create_contribution(proposal_id, user_id, status, amount, tx_id):
contribution.tx_id = tx_id
db.session.add(contribution)
db.session.flush()
contribution.proposal.set_pending_when_ready()
contribution.proposal.set_funded_when_ready()
db.session.commit()
return proposal_contribution_schema.dump(contribution), 200
@ -528,6 +534,10 @@ def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_
if not contribution:
return {"message": "No contribution matching that id"}, 404
# do not allow editing contributions once a proposal has become funded
if contribution.proposal.is_funded:
return {"message": "Cannot edit contributions to fully-funded proposals"}, 400
print((contribution_id, proposal_id, user_id, status, amount, tx_id))
# Proposal ID (must belong to an existing proposal)
@ -558,5 +568,10 @@ def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_
contribution.tx_id = tx_id
db.session.add(contribution)
db.session.flush()
contribution.proposal.set_pending_when_ready()
contribution.proposal.set_funded_when_ready()
db.session.commit()
return proposal_contribution_schema.dump(contribution), 200

View File

@ -9,7 +9,7 @@ from grant import commands, proposal, user, comment, milestone, admin, email, bl
from grant.extensions import bcrypt, migrate, db, ma, security
from grant.settings import SENTRY_RELEASE, ENV
from sentry_sdk.integrations.flask import FlaskIntegration
from grant.utils.auth import AuthException, handle_auth_error
from grant.utils.auth import AuthException, handle_auth_error, get_authed_user
def create_app(config_objects=["grant.settings"]):
@ -33,6 +33,11 @@ def create_app(config_objects=["grant.settings"]):
# NOTE: testing mode does not honor this handler, and instead returns the generic 500 response
app.register_error_handler(AuthException, handle_auth_error)
@app.after_request
def grantio_authed(response):
response.headers["X-Grantio-Authed"] = 'yes' if get_authed_user() else 'no'
return response
return app
@ -46,7 +51,7 @@ def register_extensions(app):
security.init_app(app, datastore=user_datastore, register_blueprint=False)
# supports_credentials for session cookies
CORS(app, supports_credentials=True)
CORS(app, supports_credentials=True, expose_headers='X-Grantio-Authed')
SSLify(app)
return None

View File

@ -82,6 +82,14 @@ class Milestone(db.Model):
self.reject_reason = reason
self.reject_arbiter_id = arbiter_id
def accept_immediate(self):
if self.immediate_payout and self.index == 0:
self.date_requested = datetime.datetime.now()
self.stage = MilestoneStage.ACCEPTED
self.date_accepted = datetime.datetime.now()
db.session.add(self)
db.session.flush()
def accept_request(self, arbiter_id: int):
if self.stage != MilestoneStage.REQUESTED:
raise MilestoneException(f'Cannot accept payout request for milestone at {self.stage} stage')

View File

@ -353,6 +353,7 @@ class Proposal(db.Model):
return contribution
# state: status (DRAFT || REJECTED) -> (PENDING || STAKING)
def submit_for_approval(self):
self.validate_publishable()
allowed_statuses = [ProposalStatus.DRAFT, ProposalStatus.REJECTED]
@ -365,6 +366,21 @@ class Proposal(db.Model):
else:
self.status = ProposalStatus.STAKING
def set_pending_when_ready(self):
if self.status == ProposalStatus.STAKING and self.is_staked:
self.set_pending()
# state: status STAKING -> PENDING
def set_pending(self):
if self.status != ProposalStatus.STAKING:
raise ValidationException(f"Proposal status must be staking in order to be set to pending")
if not self.is_staked:
raise ValidationException(f"Proposal is not fully staked, cannot set to pending")
self.status = ProposalStatus.PENDING
db.session.add(self)
db.session.flush()
# state: status PENDING -> (APPROVED || REJECTED)
def approve_pending(self, is_approve, reject_reason=None):
self.validate_publishable()
# specific validation
@ -394,15 +410,48 @@ class Proposal(db.Model):
'admin_note': reject_reason
})
# state: status APPROVE -> LIVE, stage PREVIEW -> FUNDING_REQUIRED
def publish(self):
self.validate_publishable()
# specific validation
if not self.status == ProposalStatus.APPROVED:
raise ValidationException(f"Proposal status must be approved")
self.date_published = datetime.datetime.now()
self.status = ProposalStatus.LIVE
self.stage = ProposalStage.FUNDING_REQUIRED
# If we had a bounty that pushed us into funding, skip straight into WIP
self.set_funded_when_ready()
def set_funded_when_ready(self):
if self.status == ProposalStatus.LIVE and self.is_funded:
self.set_funded()
# state: stage FUNDING_REQUIRED -> WIP
def set_funded(self):
if self.status != ProposalStatus.LIVE:
raise ValidationException(f"Proposal status must be live in order transition to funded state")
if self.stage != ProposalStage.FUNDING_REQUIRED:
raise ValidationException(f"Proposal stage must be funding_required in order transition to funded state")
if not self.is_funded:
raise ValidationException(f"Proposal is not fully funded, cannot set to funded state")
self.stage = ProposalStage.WIP
db.session.add(self)
db.session.flush()
# check the first step, if immediate payout bump it to accepted
self.current_milestone.accept_immediate()
def set_contribution_matching(self, matching: float):
# do not allow on funded/WIP proposals
if self.is_funded:
raise ValidationException("Cannot set contribution matching on fully-funded proposal")
# enforce 1 or 0 for now
if matching == 0.0 or matching == 1.0:
self.contribution_matching = matching
db.session.add(self)
db.session.flush()
self.set_funded_when_ready()
else:
raise ValidationException("Bad value for contribution_matching, must be 1 or 0")
@hybrid_property
def contributed(self):
@ -417,6 +466,9 @@ class Proposal(db.Model):
target = Decimal(self.target)
# apply matching multiplier
funded = Decimal(self.contributed) * Decimal(1 + self.contribution_matching)
# apply bounty, if available
if self.rfp:
funded = funded + Decimal(self.rfp.bounty)
# if funded > target, just set as target
if funded > target:
return str(target)
@ -429,7 +481,15 @@ class Proposal(db.Model):
@hybrid_property
def is_funded(self):
return Decimal(self.contributed) >= Decimal(self.target)
return Decimal(self.funded) >= Decimal(self.target)
@hybrid_property
def is_failed(self):
if not self.status == ProposalStatus.LIVE or not self.date_published:
return False
deadline = self.date_published + datetime.timedelta(seconds=self.deadline_duration)
passed = deadline < datetime.datetime.now()
return passed and not self.is_funded
@hybrid_property
def current_milestone(self):
@ -458,6 +518,7 @@ class ProposalSchema(ma.Schema):
"target",
"contributed",
"is_staked",
"is_failed",
"funded",
"content",
"comments",

View File

@ -489,11 +489,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
db.session.flush()
if contribution.proposal.status == ProposalStatus.STAKING:
# fully staked, set status PENDING
if contribution.proposal.is_staked: # Decimal(contribution.proposal.contributed) >= PROPOSAL_STAKING_AMOUNT:
contribution.proposal.status = ProposalStatus.PENDING
db.session.add(contribution.proposal)
db.session.flush()
contribution.proposal.set_pending_when_ready()
# email progress of staking, partial or complete
send_email(contribution.user.email_address, 'staking_contribution_confirmed', {
@ -524,12 +520,9 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
})
# TODO: Once we have a task queuer in place, queue emails to everyone
# on funding target reached.
if contribution.proposal.status == ProposalStatus.LIVE:
if contribution.proposal.is_funded:
contribution.proposal.stage = ProposalStage.WIP
db.session.add(contribution.proposal)
db.session.flush()
contribution.proposal.set_funded_when_ready()
db.session.commit()
return None, 200

View File

@ -1,8 +1,8 @@
<p style="margin: 0 0 20px;">
The proposal milestone
<a href="{{ args.proposal_milestones_url }}" target="_blank">
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
</a>
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}</a
>
payout of <b>{{ args.amount }} ZEC</b> has been approved.
</p>

View File

@ -1,7 +1,7 @@
<p style="margin: 0 0 20px;">
Hooray! <b>{{ args.amount }} ZEC</b> has been paid out for
<a href="{{ args.proposal_milestones_url }}" target="_blank">
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }} </a
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}</a
>! You can view the transaction below:
</p>

View File

@ -1,8 +1,8 @@
<p style="margin: 0 0 20px;">
The payout request for proposal milestone
<a href="{{ args.proposal_milestones_url }}" target="_blank">
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
</a>
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}</a
>
has been rejected.
</p>

View File

@ -1,8 +1,8 @@
<p style="margin: 0 0 20px;">
A payout request for the proposal milestone
<a href="{{ args.proposal_milestones_url }}" target="_blank">
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}
</a>
{{ args.proposal.title }} - {{ args.proposal.current_milestone.title }}</a
>
has been made. As arbiter, you are responsible for reviewing this request.
</p>

View File

@ -1,7 +1,7 @@
<p style="margin: 0 0 20px;">
You have been nominated for arbiter of
<a href="{{ args.proposal_url }}" target="_blank">
{{ args.proposal.title }} </a
{{ args.proposal.title }}</a
>. You will be responsible for reviewing milestone payout requests should you
choose to accept.
</p>

View File

@ -18,7 +18,7 @@ def handle_auth_error(e):
def get_authed_user():
return current_user if current_user.is_authenticated else None
return current_user if current_user.is_authenticated and not current_user.banned else None
def throw_on_banned(user):

View File

@ -97,7 +97,8 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def test_update_proposal_bad_matching(self):
self.login_admin()
resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 2})
self.assert400(resp)
self.assert500(resp)
self.assertIn('Bad value', resp.json['data'])
@patch('requests.get', side_effect=mock_blockchain_api_requests)
def test_approve_proposal(self, mock_get):
@ -140,7 +141,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
"/api/v1/admin/arbiters",
data={
'proposalId': self.proposal.id,
'userId': self.user.id
'userId': self.other_user.id
}
)
self.assert200(resp)

View File

@ -1,4 +1,6 @@
import axios from 'axios';
import { getStoreRef } from 'store/configure';
import { checkUser } from 'modules/auth/actions';
const instance = axios.create({
baseURL: process.env.BACKEND_URL,
@ -7,9 +9,24 @@ const instance = axios.create({
withCredentials: true,
});
let lastAuthed = null as null | string;
instance.interceptors.response.use(
// Do nothing to responses
res => res,
// - watch for changes to auth header and trigger checkUser action if it changes
// - this allows for external authorization events to be registered in this context
// - external auth events include login/logout in another tab, or
// the user getting banned
res => {
const authed = res.headers['x-grantio-authed'];
if (lastAuthed !== null && lastAuthed !== authed) {
const store = getStoreRef();
if (store) {
store.dispatch<any>(checkUser());
}
}
lastAuthed = authed;
return res;
},
// Try to parse error message if possible
err => {
if (err.response && err.response.data) {

View File

@ -61,6 +61,14 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
const remainingTargetNum = parseFloat(fromZat(target.sub(funded)));
// Get bounty from RFP. If it exceeds proposal target, show bounty as full amount
let bounty;
if (proposal.rfp && proposal.rfp.bounty) {
bounty = proposal.rfp.bounty.gt(proposal.target)
? proposal.target
: proposal.rfp.bounty;
}
content = (
<React.Fragment>
{isLive && (
@ -96,6 +104,12 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
</div>
</div>
{bounty && (
<div className="ProposalCampaignBlock-bounty">
Awarded with <UnitDisplay value={bounty} symbol="ZEC" /> bounty
</div>
)}
{proposal.contributionMatching > 0 && (
<div className="ProposalCampaignBlock-matching">
<span>Funds are being matched x{proposal.contributionMatching + 1}</span>

View File

@ -32,11 +32,11 @@
}
}
&-bounty,
&-matching {
margin: 0.5rem -1.5rem;
padding: 0.75rem 1.5rem;
padding: 0.75rem 1rem;
text-align: center;
background: @info-color;
color: #FFF;
font-size: 1rem;
@ -45,6 +45,19 @@
}
}
&-bounty {
background: @primary-color;
}
&-matching {
background: @info-color;
}
&-bounty + &-matching {
margin-top: 0;
}
&-popover {
&-overlay {
max-width: 400px;

View File

@ -546,20 +546,25 @@ const MilestoneAction: React.SFC<MilestoneProps> = p => {
}
// special warning if no arbiter is set for team members
if (!p.hasArbiter && p.isTeamMember) {
if (!p.hasArbiter && p.isTeamMember && p.stage === MILESTONE_STAGE.IDLE) {
content = (
<Alert
type="error"
type="info"
message="Arbiter not assigned"
description={
<p>
We are sorry for the inconvenience, but in order to have milestone payouts
reviewed an arbiter must be assigned. Please{' '}
<Link target="_blank" to="/contact">
contact support
</Link>{' '}
for help.
</p>
<>
<p>
Arbiters are users who review requests for payment. When they have approved
a payment the grant administrators are then notified to make payment.
</p>
<p>
It typically takes a couple of days to have an arbiter assigned. Please{' '}
<Link target="_blank" to="/contact">
contact support
</Link>{' '}
if you have waited longer than three days.
</p>
</>
}
/>
);

View File

@ -8,8 +8,11 @@ import {
authUser as apiAuthUser,
logoutUser,
} from 'api/api';
import { AppState } from 'store/reducers';
import { User } from 'types';
type GetState = () => AppState;
function setSentryScope(user: User) {
Sentry.configureScope(scope => {
scope.setUser({
@ -20,7 +23,17 @@ function setSentryScope(user: User) {
// check if user has authenticated session
export function checkUser() {
return async (dispatch: Dispatch<any>) => {
return async (dispatch: Dispatch<any>, getState: GetState) => {
const state = getState();
if (state.auth.isAuthingUser || state.auth.isLoggingOut) {
// this happens when axios calls checkUser upon seeing a change in the
// custom auth-header, this call will not be ignored on other tabs not
// initiating the authentication related behaviors
console.info(
'ignoring checkUser action b/c we are currently authing or logging out',
);
return;
}
dispatch({ type: types.CHECK_USER_PENDING });
try {
const res = await checkUserAuth();

View File

@ -13,6 +13,8 @@ export interface AuthState {
isCheckingUser: boolean;
hasCheckedUser: boolean;
isLoggingOut: boolean;
isCreatingUser: boolean;
createUserError: string | null;
@ -35,6 +37,8 @@ export const INITIAL_STATE: AuthState = {
isCheckingUser: false,
hasCheckedUser: false,
isLoggingOut: false,
authSignature: null,
authSignatureAddress: null,
isSigningAuth: false,
@ -81,6 +85,7 @@ export default function createReducer(
case types.CHECK_USER_REJECTED:
return {
...state,
user: null,
isCheckingUser: false,
hasCheckedUser: true,
};
@ -135,9 +140,22 @@ export default function createReducer(
signAuthError: action.payload,
};
case types.LOGOUT_PENDING:
return {
...state,
isLoggingOut: true,
user: null,
};
case types.LOGOUT_FULFILLED:
return {
...state,
isLoggingOut: false,
user: null,
};
case types.LOGOUT_REJECTED:
return {
...state,
isLoggingOut: false,
user: null,
};

View File

@ -23,6 +23,11 @@ const bindMiddleware = (middleware: MiddleWare[]) => {
return composeWithDevTools(applyMiddleware(...middleware));
};
let storeRef = null as null | Store<AppState>;
export function getStoreRef() {
return storeRef;
}
export function configureStore(initialState: Partial<AppState> = combineInitialState) {
const store: Store<AppState> = createStore(
rootReducer,
@ -44,6 +49,6 @@ export function configureStore(initialState: Partial<AppState> = combineInitialS
);
}
}
storeRef = store;
return { store };
}

View File

@ -97,6 +97,9 @@ export function formatProposalFromGet(p: any): Proposal {
proposal.milestones = proposal.milestones.map(msToFe);
proposal.currentMilestone = msToFe(proposal.currentMilestone);
}
if (proposal.rfp) {
proposal.rfp = formatRFPFromGet(proposal.rfp);
}
return proposal;
}
@ -104,7 +107,9 @@ export function formatRFPFromGet(rfp: RFP): RFP {
if (rfp.bounty) {
rfp.bounty = toZat(rfp.bounty as any);
}
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
if (rfp.acceptedProposals) {
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
}
return rfp;
}

View File

@ -5,7 +5,6 @@ import { ChunkExtractor } from '@loadable/server';
export interface Props {
children: any;
css: string[];
scripts: string[];
linkTags: Array<React.LinkHTMLAttributes<HTMLLinkElement>>;
metaTags: Array<React.MetaHTMLAttributes<HTMLMetaElement>>;
state: string;
@ -15,7 +14,6 @@ export interface Props {
const HTML: React.SFC<Props> = ({
children,
scripts,
css,
state,
i18n,
@ -78,9 +76,6 @@ const HTML: React.SFC<Props> = ({
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
{scripts.map(src => {
return <script key={src} src={src} />;
})}
</body>
</html>
);

View File

@ -53,9 +53,6 @@ const serverRenderer = async (req: Request, res: Response) => {
const cssFiles = ['bundle.css', 'vendor.css']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const jsFiles = ['vendor.js', 'bundle.js']
.map(f => res.locals.assetPath(f))
.filter(Boolean);
const mappedLinkTags = linkTags
.map(l => ({ ...l, href: res.locals.assetPath(l.href) }))
.filter(l => !!l.href);
@ -66,7 +63,6 @@ const serverRenderer = async (req: Request, res: Response) => {
const html = renderToString(
<Html
css={cssFiles}
scripts={jsFiles}
linkTags={mappedLinkTags}
metaTags={mappedMetaTags}
state={state}