Skip to content

Commit

Permalink
feat: added cancel with reputation
Browse files Browse the repository at this point in the history
fix after rebase

fix: translations, duplicated steps

fix: cancel step

fixes after rebase
  • Loading branch information
CzarekDryl committed Nov 29, 2024
1 parent e83c2b6 commit d01637e
Show file tree
Hide file tree
Showing 27 changed files with 956 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ const Motions: FC<MotionsProps> = ({ transactionId }) => {
>
<Stepper<Steps>
activeStepKey={activeStepKey}
setActiveStepKey={setActiveStepKey}
setActiveStepKey={(key: Steps) => setActiveStepKey(key)}
items={items}
/>
</MotionProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const MultiSigWidget: FC<MultiSigWidgetProps> = ({
<Stepper<MultiSigState>
items={items}
activeStepKey={activeStepKey}
setActiveStepKey={setActiveStepKey}
setActiveStepKey={(key: MultiSigState) => setActiveStepKey(key)}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const PaymentBuilder = ({ action }: PaymentBuilderProps) => {
expenditure={expenditure}
onClose={toggleOffCancelModal}
refetchExpenditure={refetchExpenditure}
isActionStaked={expenditure.isStaked}
/>
</>
);
Expand Down Expand Up @@ -281,6 +282,7 @@ const PaymentBuilder = ({ action }: PaymentBuilderProps) => {
expenditure={expenditure}
onClose={toggleOffCancelModal}
refetchExpenditure={refetchExpenditure}
isActionStaked={expenditure.isStaked}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,25 @@
import { isToday, isYesterday } from 'date-fns';
import React, { type FC } from 'react';
import { FormattedDate, defineMessages } from 'react-intl';

import PermissionRow from '~frame/v5/pages/VerifiedPage/partials/PermissionRow/index.ts';
import { getFormattedDateFrom } from '~utils/getFormattedDateFrom.ts';
import { formatText } from '~utils/intl.ts';
import MenuWithStatusText from '~v5/shared/MenuWithStatusText/index.ts';
import { StatusTypes } from '~v5/shared/StatusText/consts.ts';
import StatusText from '~v5/shared/StatusText/StatusText.tsx';
import UserPopover from '~v5/shared/UserPopover/UserPopover.tsx';

import { type ActionWithPermissionsInfoProps } from './types.ts';

const displayName =
'v5.common.CompletedAction.partials.ActionWithPermissionsInfoProps';

const MSG = defineMessages({
todayAt: {
id: `${displayName}.todayAt`,
defaultMessage: 'Today at',
},
yestardayAt: {
id: `${displayName}.yestardayAt`,
defaultMessage: 'Yesterday at',
},
at: {
id: `${displayName}.at`,
defaultMessage: 'at',
},
});

const formatDate = (value: string | undefined) => {
if (!value) {
return undefined;
}

const date = new Date(value);

if (isToday(date)) {
return (
<>
{formatText(MSG.todayAt)}{' '}
<FormattedDate value={date} hour="numeric" minute="numeric" />
</>
);
}
import FormatDate from '../FormatDate/FormatDate.tsx';

if (isYesterday(date)) {
return (
<>
{formatText(MSG.yestardayAt)}{' '}
<FormattedDate value={date} hour="numeric" minute="numeric" />
</>
);
}

return (
<>
{getFormattedDateFrom(value)} {formatText(MSG.at)}{' '}
<FormattedDate value={date} hour="numeric" minute="numeric" />
</>
);
};
import { type ActionWithPermissionsInfoProps } from './types.ts';

const ActionWithPermissionsInfo: FC<ActionWithPermissionsInfoProps> = ({
action,
title,
}) => {
if (!action) {
return null;
}

const { createdAt, initiatorAddress } = action ?? {};
const formattedDate = formatDate(createdAt);

return (
<MenuWithStatusText
Expand All @@ -83,9 +31,10 @@ const ActionWithPermissionsInfo: FC<ActionWithPermissionsInfoProps> = ({
iconSize={16}
iconClassName="text-gray-500"
>
{formatText({
id: 'action.executed.permissions.description',
})}
{title ||
formatText({
id: 'action.executed.permissions.description',
})}
</StatusText>
}
sections={[
Expand Down Expand Up @@ -131,7 +80,9 @@ const ActionWithPermissionsInfo: FC<ActionWithPermissionsInfoProps> = ({
id: 'action.executed.permissions.date',
})}
</span>
<span className="text-sm text-gray-900">{formattedDate}</span>
<span className="text-sm text-gray-900">
<FormatDate value={createdAt} />
</span>
</div>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { type ExpenditureAction } from '~types/graphql.ts';

export interface ActionWithPermissionsInfoProps {
action?: ExpenditureAction | null;
title?: string;
}
Original file line number Diff line number Diff line change
@@ -1,69 +1,153 @@
import { Prohibit, SpinnerGap } from '@phosphor-icons/react';
import { Id } from '@colony/colony-js';
import {
CheckCircle,
Prohibit,
SpinnerGap,
WarningCircle,
} from '@phosphor-icons/react';
import React, { useState, type FC } from 'react';
import { toast } from 'react-toastify';

import { useAppContext } from '~context/AppContext/AppContext.ts';
import { useColonyContext } from '~context/ColonyContext/ColonyContext.ts';
import { usePaymentBuilderContext } from '~context/PaymentBuilderContext/PaymentBuilderContext.ts';
import useAsyncFunction from '~hooks/useAsyncFunction.ts';
import useEnabledExtensions from '~hooks/useEnabledExtensions.ts';
import useExpenditureStaking from '~hooks/useExpenditureStaking.ts';
import { ActionTypes } from '~redux';
import { type CancelExpenditurePayload } from '~redux/types/actions/expenditures.ts';
import {
type CancelStakedExpenditurePayload,
type CancelExpenditurePayload,
} from '~redux/types/actions/expenditures.ts';
import {
type StakedExpenditureCancelMotionPayload,
type ExpenditureCancelMotionPayload,
} from '~redux/types/actions/motion.ts';
import Toast from '~shared/Extensions/Toast/index.ts';
import { Form } from '~shared/Fields/index.ts';
import SpinnerLoader from '~shared/Preloaders/SpinnerLoader.tsx';
import { DecisionMethod } from '~types/actions.ts';
import { formatText } from '~utils/intl.ts';
import IconButton from '~v5/shared/Button/IconButton.tsx';
import Button, { ActionButton } from '~v5/shared/Button/index.ts';
import { LoadingBehavior } from '~v5/shared/Button/types.ts';
import Modal from '~v5/shared/Modal/index.ts';

import DecisionMethodSelect from '../DecisionMethodSelect/DecisionMethodSelect.tsx';
import AmountField from '../PaymentBuilderTable/partials/AmountField/AmountField.tsx';
import { ExpenditureStep } from '../PaymentBuilderWidget/types.ts';

import {
cancelDecisionMethodDescriptions,
cancelDecisionMethodItems,
stakedValidationSchema,
validationSchema,
} from './consts.ts';
import { useCancelingDecisionMethods } from './hooks.ts';
import RadioButtons from './partials/RadioButtons.tsx';
import { PenaliseOptions } from './partials/types.ts';
import { type CancelModalProps } from './types.ts';

const CancelModal: FC<CancelModalProps> = ({
isOpen,
onClose,
refetchExpenditure,
isActionStaked,
expenditure,
...rest
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const { user } = useAppContext();
const { colony } = useColonyContext();
const { setExpectedStepKey } = usePaymentBuilderContext();
const { votingReputationAddress } = useEnabledExtensions();
const { nativeToken } = colony;
const { tokenAddress } = nativeToken;

const payload: CancelExpenditurePayload = {
colonyAddress: colony.colonyAddress,
expenditure,
userAddress: user?.walletAddress ?? '',
};
const motionPayload: ExpenditureCancelMotionPayload = {
colony,
expenditure,
userAddress: user?.walletAddress ?? '',
motionDomainId: Id.RootDomain,
votingReputationAddress: votingReputationAddress ?? '',
};

const {
stakeAmount = '0',
isLoading,
stakedExpenditureAddress = '',
} = useExpenditureStaking();

const cancelExpenditure = useAsyncFunction({
submit: ActionTypes.EXPENDITURE_CANCEL,
error: ActionTypes.EXPENDITURE_CANCEL_ERROR,
success: ActionTypes.EXPENDITURE_CANCEL_SUCCESS,
});
const cancelExpenditureViaMotion = useAsyncFunction({
submit: ActionTypes.MOTION_EXPENDITURE_CANCEL,
error: ActionTypes.MOTION_EXPENDITURE_CANCEL_ERROR,
success: ActionTypes.MOTION_EXPENDITURE_CANCEL_SUCCESS,
});
const cancelStakedExpenditure = useAsyncFunction({
submit: ActionTypes.STAKED_EXPENDITURE_CANCEL,
error: ActionTypes.STAKED_EXPENDITURE_CANCEL_ERROR,
success: ActionTypes.STAKED_EXPENDITURE_CANCEL_SUCCESS,
});
const cancelStakedExpenditureViaMotion = useAsyncFunction({
submit: ActionTypes.MOTION_STAKED_EXPENDITURE_CANCEL,
error: ActionTypes.MOTION_STAKED_EXPENDITURE_CANCEL_ERROR,
success: ActionTypes.MOTION_STAKED_EXPENDITURE_CANCEL_SUCCESS,
});

const handleFundExpenditure = async () => {
const handleFundExpenditure = async ({ decisionMethod, penalise }) => {
setIsSubmitting(true);
try {
if (!expenditure) {
return;
}

await cancelExpenditure(payload);
const stakedPayload: CancelStakedExpenditurePayload = {
colonyAddress: colony.colonyAddress,
expenditure,
stakedExpenditureAddress,
shouldPunish: penalise === PenaliseOptions.Yes,
};
const stakedMotionPayload: StakedExpenditureCancelMotionPayload = {
colonyAddress: colony.colonyAddress,
colonyName: colony.name,
expenditure,
motionDomainId: Id.RootDomain,
stakedExpenditureAddress,
shouldPunish: penalise === PenaliseOptions.Yes,
};

if (
decisionMethod &&
decisionMethod.value === DecisionMethod.Reputation
) {
if (penalise) {
await cancelStakedExpenditureViaMotion(stakedMotionPayload);
} else {
await cancelExpenditureViaMotion(motionPayload);
}
} else if (penalise) {
cancelStakedExpenditure(stakedPayload);
} else {
await cancelExpenditure(payload);
}

await refetchExpenditure({
expenditureId: expenditure.id,
});

setIsSubmitting(false);
setExpectedStepKey(ExpenditureStep.Cancel);
setExpectedStepKey(
isActionStaked ? ExpenditureStep.Reclaim : ExpenditureStep.Cancel,
);
onClose();
} catch (err) {
setIsSubmitting(false);
Expand All @@ -75,6 +159,8 @@ const CancelModal: FC<CancelModalProps> = ({
expenditure.lockingActions?.items &&
expenditure.lockingActions.items.length > 0;

const cancelDecisionMethodItems = useCancelingDecisionMethods();

return (
<Modal
isOpen={isOpen}
Expand All @@ -101,15 +187,66 @@ const CancelModal: FC<CancelModalProps> = ({
<Form
className="flex flex-grow flex-col"
onSubmit={handleFundExpenditure}
validationSchema={validationSchema}
defaultValues={{ decisionMethod: {} }}
validationSchema={
isActionStaked ? stakedValidationSchema : validationSchema
}
defaultValues={{ decisionMethod: {}, penalise: '' }}
>
{({ watch }) => {
const method = watch('decisionMethod');
const penalise = watch('penalise');

return (
<>
<div className="mb-8">
{isActionStaked && (
<>
<div className="mb-4 flex items-center justify-between rounded bg-gray-50 p-3 text-gray-900">
<p className="text-1">
{formatText({ id: 'cancelModal.creatorStake' })}
</p>
{isLoading ? (
<SpinnerLoader appearance={{ size: 'small' }} />
) : (
<AmountField
amount={stakeAmount || '0'}
tokenAddress={tokenAddress}
/>
)}
</div>
<h5 className="mb-4 text-gray-900 text-1">
{formatText({ id: 'cancelModal.penaliseTitle' })}
</h5>
<div className="mb-4">
<RadioButtons />
</div>
{penalise && penalise === PenaliseOptions.No && (
<div className="mb-4 flex gap-2 rounded-lg border border-success-200 bg-success-100 px-[1.125rem] py-3">
<CheckCircle
size={18}
className="shrink-0 text-success-400"
/>
<p className="text-md">
The payment creator will keep their full stake and
reputation
</p>
</div>
)}
{penalise && penalise === PenaliseOptions.Yes && (
<div className="mb-4 flex gap-2 rounded-lg border border-negative-200 bg-negative-100 px-[1.125rem] py-3">
<WarningCircle
size={18}
className="shrink-0 text-negative-400"
/>
<p className="text-md">
The payment creator will lose their full stake and
the relative amount of reputation. Penalised funds
are burned.
</p>
</div>
)}
</>
)}
<DecisionMethodSelect
options={cancelDecisionMethodItems}
name="decisionMethod"
Expand Down
Loading

0 comments on commit d01637e

Please sign in to comment.