Skip to content

Commit

Permalink
Feature: APP-2264 - Action Builder & Execution Widget (#911)
Browse files Browse the repository at this point in the history
* update type of ActionWC to include the raw action

* new data type for input form

* WalletConnectAction Card

* walletConnect action in action builder

* action listener finalized

* wallet connect action card into actionbuilder

* added wc action card to execution widget

* update create proposal with wc action

* natspec comments

* new component for type for uneditable fields

* decode action

* time sensitive warning

* minor styling update for action card in execution widget

* lint

* moved search header

* search header references update

* formless component for type and time sensitive action refactor

* waiting for inputs to be decoded

* optimization comment

* comment fix

* fix multiple actions in review proposal

* spacing fix and capitalization

* lint and Crowdin todos
  • Loading branch information
Fabricevladimir authored Jul 10, 2023
1 parent 0ba64fa commit 37e1709
Show file tree
Hide file tree
Showing 15 changed files with 559 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {AlertCard, Label} from '@aragon/ui-components';
import React, {useMemo} from 'react';
import {useTranslation} from 'react-i18next';
import styled from 'styled-components';

import {AccordionMethod, AccordionMethodType} from 'components/accordionMethod';
import {FormlessComponentForType} from 'containers/smartContractComposer/components/inputForm';
import {POTENTIALLY_TIME_SENSITIVE_FIELDS} from 'utils/constants/misc';
import {capitalizeFirstLetter, shortenAddress} from 'utils/library';
import {ActionWC, Input} from 'utils/types';

type WCActionCardActionCardProps = Pick<AccordionMethodType, 'type'> & {
action: ActionWC;
methodActions?: Array<{
component: React.ReactNode;
callback: () => void;
}>;
};

export const WCActionCard: React.FC<WCActionCardActionCardProps> = ({
action,
methodActions,
type,
}) => {
const {t} = useTranslation();

const showTimeSensitiveWarning = useMemo(() => {
// Note: need to check whether the inputs exist because the decoding
// and form setting might take a while
if (action.inputs) {
for (const i of action.inputs) {
if (POTENTIALLY_TIME_SENSITIVE_FIELDS.has(i.name.toLowerCase()))
return true;
}
}
return false;
}, [action.inputs]);

return (
<AccordionMethod
type={type}
methodName={action.functionName}
dropdownItems={methodActions}
smartContractName={shortenAddress(action.contractName)}
verified={!!action.verified}
methodDescription={action.notice}
>
<Content type={type}>
{action.inputs?.length > 0 ? (
<FormGroup>
{action.inputs.map(input => {
if (!input.name) return null;
return (
<FormItem key={input.name}>
<Label
label={capitalizeFirstLetter(input.name)}
helpText={input.notice}
/>
<FormlessComponentForType
disabled
key={input.name}
input={input as Input}
/>
</FormItem>
);
})}
</FormGroup>
) : null}
{!action.decoded && (
<AlertCard
title={t('newProposal.configureActions.actionAlertWarning.title')}
helpText={t('newProposal.configureActions.actionAlertWarning.desc')}
mode="warning"
/>
)}
{showTimeSensitiveWarning && (
<AlertCard
title={t('newProposal.configureActions.actionAlertCritical.title')}
helpText={t(
'newProposal.configureActions.actionAlertCritical.desc'
)}
mode="critical"
/>
)}
</Content>
</AccordionMethod>
);
};

type ContentProps = Pick<WCActionCardActionCardProps, 'type'>;

const Content = styled.div.attrs(({type}: ContentProps) => ({
className: `px-2 desktop:px-3 p-3 border border-ui-100 border-t-0 space-y-2 desktop:space-y-3 rounded-b-xl ${
type === 'action-builder' ? 'bg-ui-0' : 'bg-ui-50'
}`,
}))<ContentProps>``;

const FormGroup = styled.div.attrs({
className: 'space-y-2 desktop:space-y-3',
})``;

const FormItem = styled.div.attrs({
className: 'space-y-1.5',
})``;
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {ModifyMetadataCard} from './actions/modifyMetadataCard';
import {ModifyMultisigSettingsCard} from './actions/modifyMultisigSettingsCard';
import {ModifyMvSettingsCard} from './actions/modifySettingsCard';
import {RemoveAddressCard} from './actions/removeAddressCard';
import {WithdrawCard} from './actions/withdrawCard';
import {SCCExecutionCard} from './actions/sccExecutionWidget';
import {WCActionCard} from './actions/walletConnectActionCard';
import {WithdrawCard} from './actions/withdrawCard';

type ActionsFilterProps = {
action: Action;
Expand Down Expand Up @@ -38,6 +39,8 @@ export const ActionsFilter: React.FC<ActionsFilterProps> = ({action}) => {
return <ModifyMultisigSettingsCard action={action} />;
case 'external_contract_action':
return <SCCExecutionCard action={action} />;
case 'wallet_connect_action':
return <WCActionCard action={action} type="execution-widget" />;
default:
return <></>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ import {
IconType,
} from '@aragon/ui-components';
import React from 'react';
import {useFormContext} from 'react-hook-form';
import {useTranslation} from 'react-i18next';
import styled from 'styled-components';

type DesktopModalHeaderProps = {
type SearchHeader = {
onClose?: () => void;
selectedContract?: string; // Note: may come from form, not set in stone
selectedValue?: string;
onSearch?: (search: string) => void;
buttonIcon?: React.FunctionComponentElement<IconType>;
onHomeButtonClick?: () => void;
};

const DesktopModalHeader: React.FC<DesktopModalHeaderProps> = props => {
const SearchHeader: React.FC<SearchHeader> = props => {
const {t} = useTranslation();
const {setValue} = useFormContext();

return (
<Container>
Expand All @@ -28,15 +27,12 @@ const DesktopModalHeader: React.FC<DesktopModalHeaderProps> = props => {
icon={props.buttonIcon || <IconHome />}
mode="secondary"
bgWhite
onClick={() => {
setValue('selectedSC', null);
setValue('selectedAction', null);
}}
onClick={props.onHomeButtonClick}
/>
<IconChevronRight />
{props.selectedContract && (
{props.selectedValue && (
<>
<SelectedContract>{props.selectedContract}</SelectedContract>
<SelectedValue>{props.selectedValue}</SelectedValue>
<IconChevronRight />
</>
)}
Expand All @@ -57,7 +53,7 @@ const DesktopModalHeader: React.FC<DesktopModalHeaderProps> = props => {
);
};

export default DesktopModalHeader;
export default SearchHeader;

const Container = styled.div.attrs({
className:
Expand All @@ -68,7 +64,7 @@ const LeftContent = styled.div.attrs({
className: 'flex gap-x-1 items-center text-ui-300 ft-text-base',
})``;

const SelectedContract = styled.p.attrs({
const SelectedValue = styled.p.attrs({
className: 'font-bold text-ui-600 ft-text-base',
})``;

Expand Down
9 changes: 7 additions & 2 deletions packages/web-app/src/containers/actionBuilder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import MintTokens from './mintTokens';
import RemoveAddresses from './removeAddresses';
import SCCAction from './scc';
import UpdateMinimumApproval from './updateMinimumApproval';
import WalletConnectAction from './walletConnect';
import WithdrawAction from './withdraw/withdrawAction';

/**
Expand Down Expand Up @@ -99,8 +100,12 @@ const Action: React.FC<ActionsComponentProps> = ({
case 'wallet_connect_modal':
return <WalletConnect actionIndex={actionIndex} />;
case 'wallet_connect_action':
//TODO: Create a separate action-builder accordion for Wallet Connect Actions to handle the non-decodable flow and AlertCards
return <SCCAction actionIndex={actionIndex} allowRemove={allowRemove} />;
return (
<WalletConnectAction
actionIndex={actionIndex}
allowRemove={allowRemove}
/>
);
default:
throw Error('Action not found');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {ListItemAction} from '@aragon/ui-components';
import React from 'react';
import {useWatch} from 'react-hook-form';
import {useTranslation} from 'react-i18next';

import {WCActionCard} from 'components/executionWidget/actions/walletConnectActionCard';
import {useActionsContext} from 'context/actions';
import {useAlertContext} from 'context/alert';
import {ActionIndex} from 'utils/types';

const WalletConnectAction: React.FC<ActionIndex & {allowRemove?: boolean}> = ({
actionIndex,
allowRemove = true,
}) => {
const {t} = useTranslation();
const {alert} = useAlertContext();

const [actionData] = useWatch({name: [`actions.${actionIndex}`]});
const {removeAction} = useActionsContext();

const methodActions = (() => {
const result = [];

if (allowRemove) {
result.push({
component: (
<ListItemAction title={t('labels.removeEntireAction')} bgWhite />
),
callback: () => {
removeAction(actionIndex);
alert(t('alert.chip.removedAction'));
},
});
}

return result;
})();

if (actionData) {
return (
<>
<WCActionCard
type="action-builder"
action={actionData}
methodActions={methodActions}
/>
</>
);
}

return null;
};

export default WalletConnectAction;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ButtonText,
IconSuccess,
CheckboxListItem,
IconSuccess,
NumberInput,
TextInput,
WalletInputLegacy,
Expand Down Expand Up @@ -299,6 +299,7 @@ export const ComponentForType: React.FC<ComponentForTypeProps> = ({
// Check if we need to add "index" kind of variable to the "name"
switch (classifyInputType(input.type)) {
case 'address':
case 'encodedData':
return (
<Controller
defaultValue=""
Expand Down Expand Up @@ -418,9 +419,88 @@ export const ComponentForType: React.FC<ComponentForTypeProps> = ({
}
};

export const ComponentForTypeWithFormProvider: React.FC<
ComponentForTypeProps
> = ({input, functionName, formHandleName, defaultValue, disabled = false}) => {
/** This version of the component returns uncontrolled inputs */
type FormlessComponentForTypeProps = {
input: Input;
disabled?: boolean;
};

export function FormlessComponentForType({
input,
disabled,
}: FormlessComponentForTypeProps) {
const {alert} = useAlertContext();

// Check if we need to add "index" kind of variable to the "name"
switch (classifyInputType(input.type)) {
case 'address':
case 'encodedData': // custom type for the data field which is encoded bytes
return (
<WalletInputLegacy
name={input.name}
value={input.value}
onChange={() => {}}
placeholder="0x"
adornmentText={t('labels.copy')}
disabledFilled={disabled}
onAdornmentClick={() =>
handleClipboardActions(input.value as string, () => {}, alert)
}
/>
);

case 'int':
case 'uint8':
case 'int8':
case 'uint32':
case 'int32':
case 'uint256':
return (
<NumberInput
name={input.name}
placeholder="0"
includeDecimal
disabled={disabled}
value={input.value as string}
/>
);

case 'tuple':
return (
<>
{input.components?.map(component => (
<div key={component.name}>
<div className="mb-1.5 text-base font-bold text-ui-800 capitalize">
{input.name}
</div>
<FormlessComponentForType
key={component.name}
input={component}
disabled={disabled}
/>
</div>
))}
</>
);
default:
return (
<TextInput
name={input.name}
placeholder={`${input.name} (${input.type})`}
value={input.value}
disabled={disabled}
/>
);
}
}

export function ComponentForTypeWithFormProvider({
input,
functionName,
formHandleName,
defaultValue,
disabled = false,
}: ComponentForTypeProps) {
const methods = useForm({mode: 'onChange'});

return (
Expand All @@ -435,7 +515,7 @@ export const ComponentForTypeWithFormProvider: React.FC<
/>
</FormProvider>
);
};
}

const ActionName = styled.p.attrs({
className: 'text-lg font-bold text-ui-800 capitalize truncate',
Expand Down
Loading

0 comments on commit 37e1709

Please sign in to comment.