Skip to content

Commit

Permalink
Rewrite code to implement new ProposalActionsDecoder component
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth committed Nov 29, 2024
1 parent 7b98f87 commit 602237f
Show file tree
Hide file tree
Showing 35 changed files with 395 additions and 552 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ProposalActionsDecoder } from './proposalActionsDecoder';
export { ProposalActionsDecoderView, type IProposalActionsDecoderProps } from './proposalActionsDecoder.api';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type ComponentProps } from 'react';
import type { IProposalAction } from '../proposalActionsDefinitions';

export enum ProposalActionsDecoderView {
DECODED = 'DECODED',
RAW = 'RAW',
}

export enum ProposalActionsDecoderMode {
READ = 'READ',
EDIT = 'EDIT',
WATCH = 'WATCH',
}

export interface IProposalActionsDecoderProps extends ComponentProps<'div'> {
/**
* Action to display the values for.
*/
action: IProposalAction;
/**
* Prefix to be appended to all the action values on edit mode.
*/
formPrefix?: string;
/**
* Defines the behaviour of the decoder:
* - READ: Displays the values as disabled using the values on the action property;
* - EDIT: Displays the values as editable and updates the values on the form context;
* - WATCH: Displays the values as disabled but each value listens to the changes on the form context
* @default READ
*/
mode?: ProposalActionsDecoderMode;
/**
* Defines the action values to be displayed:
* - DECODED: Displays the parameters of the action and the value field if the function is payable
* - RAW: Only displays the base values of the action (value and data)
* @default RAW
*/
view?: ProposalActionsDecoderView;
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { DevTool } from '@hookform/devtools';
import type { Meta, StoryObj } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { generateProposalAction } from '../../proposalActionsTestUtils';
import {
type IProposalActionsItemDecodedViewProps,
ProposalActionsItemDecodedView,
} from './proposalActionsItemDecodedView';
import { generateProposalAction } from '../proposalActionsTestUtils';
import { ProposalActionsDecoder } from './proposalActionsDecoder';
import { ProposalActionsDecoderView, type IProposalActionsDecoderProps } from './proposalActionsDecoder.api';

const defaultRender = (props: IProposalActionsItemDecodedViewProps) => {
const defaultRender = (props: IProposalActionsDecoderProps) => {
const methods = useForm({ mode: 'onTouched', defaultValues: props.action });

return (
<FormProvider {...methods}>
<ProposalActionsItemDecodedView {...props} />
<ProposalActionsDecoder {...props} />
<DevTool control={methods.control} />
</FormProvider>
);
};

const meta: Meta<typeof ProposalActionsItemDecodedView> = {
title: 'Modules/Components/Proposal/ProposalActions/ProposalActions.Item/DecodedView',
component: ProposalActionsItemDecodedView,
const meta: Meta<typeof ProposalActionsDecoder> = {
title: 'Modules/Components/Proposal/ProposalActions/ProposalActions.Decoder',
component: ProposalActionsDecoder,
// Force component remount on edit-mode change to correctly register the form fields
decorators: [(Story, context) => <Story key={`story-${context.args.editMode?.toString() ?? '-'}`} />],
decorators: [(Story, context) => <Story key={`story-${context.args.mode ?? '-'}`} />],
parameters: {
design: {
type: 'figma',
Expand All @@ -31,14 +29,15 @@ const meta: Meta<typeof ProposalActionsItemDecodedView> = {
},
};

type Story = StoryObj<typeof ProposalActionsItemDecodedView>;
type Story = StoryObj<typeof ProposalActionsDecoder>;

/**
* Default usage example of the Decoded view from the ProposalActionsItem component.
*/
export const Default: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'vote',
Expand Down Expand Up @@ -74,6 +73,7 @@ export const Default: Story = {
*/
export const ReadOnly: Story = {
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'approve',
Expand Down Expand Up @@ -103,6 +103,7 @@ export const ReadOnly: Story = {
export const Payable: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'setBoostAmount',
Expand All @@ -126,6 +127,7 @@ export const Payable: Story = {
export const Tuple: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'initialize',
Expand Down Expand Up @@ -168,6 +170,7 @@ export const Tuple: Story = {
export const Array: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'addAddresses',
Expand Down Expand Up @@ -195,6 +198,7 @@ export const Array: Story = {
export const ArrayInsideTuple: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'applyInstallation',
Expand Down Expand Up @@ -231,6 +235,7 @@ export const ArrayInsideTuple: Story = {
export const TupleArray: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'createProposal',
Expand Down Expand Up @@ -270,6 +275,7 @@ export const TupleArray: Story = {
export const MultiDimentionalArray: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'setData',
Expand Down Expand Up @@ -300,6 +306,7 @@ export const MultiDimentionalArray: Story = {
export const NestedTuple: Story = {
render: defaultRender,
args: {
view: ProposalActionsDecoderView.DECODED,
action: generateProposalAction({
inputData: {
function: 'updateStages',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('<ProposalActionsDecoder /> component', () => {
it('', () => {
//
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import classNames from 'classnames';
import { useCallback, useEffect } from 'react';
import type { DeepPartial } from 'react-hook-form';
import { encodeFunctionData } from 'viem';
import { Button, clipboardUtils } from '../../../../../core';
import { useFormContext } from '../../../../hooks';
import { useGukModulesContext } from '../../../gukModulesProvider';
import type { IProposalAction } from '../proposalActionsDefinitions';
import {
ProposalActionsDecoderMode,
ProposalActionsDecoderView,
type IProposalActionsDecoderProps,
} from './proposalActionsDecoder.api';
import { ProposalActionsDecoderField } from './proposalActionsDecoderField/proposalActionsDecoderField';
import { ProposalActionsDecoderTextField } from './proposalActionsDecoderTextField';
import { proposalActionsDecoderUtils } from './proposalActionsDecoderUtils';

export const ProposalActionsDecoder: React.FC<IProposalActionsDecoderProps> = (props: IProposalActionsDecoderProps) => {
const {
action,
formPrefix,
mode = ProposalActionsDecoderMode.READ,
view = ProposalActionsDecoderView.RAW,
className,
...otherProps
} = props;
const { value, data, inputData } = action;

const { copy } = useGukModulesContext();

const { watch, setValue } = useFormContext<IProposalAction>(mode === ProposalActionsDecoderMode.EDIT);

const dataFieldName = proposalActionsDecoderUtils.getFieldName('data', formPrefix) as 'data';

const updateEncodedData = useCallback(
(formValues: DeepPartial<IProposalAction>) => {
const functionParameters = formValues.inputData?.parameters?.map((parameter) => parameter?.value);
const actionAbi = [{ type: 'function', name: inputData?.function, inputs: inputData?.parameters }];
let data = '0x';

try {
data = encodeFunctionData({ abi: actionAbi, args: functionParameters });
} finally {
// @ts-expect-error Limitation of react-hook-form, ignore error
setValue(dataFieldName, data);
}
},
[inputData, dataFieldName, setValue],
);

useEffect(() => {
if (mode !== ProposalActionsDecoderMode.EDIT || view !== ProposalActionsDecoderView.DECODED) {
return;
}

const { unsubscribe } = watch((formValues, { name }) =>
name === dataFieldName ? undefined : updateEncodedData(formValues),
);

return () => unsubscribe();
}, [mode, watch, setValue, updateEncodedData, dataFieldName, view]);

const handleCopyDataClick = () => clipboardUtils.copy(action.data);

return (
<div className={classNames('flex w-full flex-col gap-3', className)} {...otherProps}>
{(view === ProposalActionsDecoderView.RAW || action.inputData?.payable) && (
<ProposalActionsDecoderTextField
fieldName="value"
mode={mode}
formPrefix={formPrefix}
parameter={{
name: 'value',
notice: copy.proposalActionsItemDecodedView.valueHelper,
value: value,
type: 'uint',
}}
/>
)}
<ProposalActionsDecoderTextField
fieldName="data"
mode={mode}
formPrefix={formPrefix}
parameter={{ name: 'data', value: data, type: 'bytes' }}
className={view === ProposalActionsDecoderView.DECODED ? 'hidden' : undefined}
component="textarea"
/>
{view === ProposalActionsDecoderView.RAW && mode === ProposalActionsDecoderMode.READ && (
<Button variant="tertiary" size="md" onClick={handleCopyDataClick} className="self-end">
{copy.proposalActionsItemRawView.copyButton}
</Button>
)}
{view === ProposalActionsDecoderView.DECODED &&
inputData?.parameters.map((parameter, index) => (
<ProposalActionsDecoderField
key={parameter.name}
parameter={parameter}
mode={mode}
fieldName="value"
formPrefix={proposalActionsDecoderUtils.getFieldName(
`inputData.parameters.${index.toString()}`,
formPrefix,
)}
/>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('<proposalActionsDecoderField /> component', () => {
it('', () => {
//
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ import { useId, useState } from 'react';
import { Button, IconType, InputContainer } from '../../../../../../core';
import { useFormContext } from '../../../../../hooks';
import type { IProposalActionInputDataParameter } from '../../proposalActionsDefinitions';
import type { IProposalActionsItemProps } from '../proposalActionsItem.api';
import { ProposalActionsItemFormField, proposalActionsItemFormFieldUtils } from '../proposalActionsItemFormField';
import { type IProposalActionsDecoderProps, ProposalActionsDecoderMode } from '../proposalActionsDecoder.api';
import { ProposalActionsDecoderTextField } from '../proposalActionsDecoderTextField';
import { proposalActionsDecoderUtils } from '../proposalActionsDecoderUtils';

export interface IProposalActionsItemDecodedViewFieldProps extends Pick<IProposalActionsItemProps, 'editMode'> {
export interface IProposalActionsDecoderField extends Pick<IProposalActionsDecoderProps, 'mode' | 'formPrefix'> {
/**
* Parameter to be rendered.
*/
parameter: IProposalActionInputDataParameter;
/**
* Form prefix to be prepended to the form field.
*/
formPrefix: string;
/**
* Name of the form field.
*/
Expand All @@ -29,49 +26,49 @@ export interface IProposalActionsItemDecodedViewFieldProps extends Pick<IProposa
onDeleteClick?: () => void;
}

export const ProposalActionsItemDecodedViewField: React.FC<IProposalActionsItemDecodedViewFieldProps> = (props) => {
const { parameter, hideLabels, editMode = false, formPrefix, fieldName, onDeleteClick } = props;
export const ProposalActionsDecoderField: React.FC<IProposalActionsDecoderField> = (props) => {
const { parameter, hideLabels, formPrefix, fieldName, mode, onDeleteClick } = props;
const { notice, type, name } = parameter;

const inputId = useId();
const { setValue, getValues, unregister } = useFormContext(editMode);
const { setValue, getValues, unregister } = useFormContext(mode === ProposalActionsDecoderMode.EDIT);

const isArray = proposalActionsItemFormFieldUtils.isArrayType(type);
const isTuple = proposalActionsItemFormFieldUtils.isTupleType(type);
const isArray = proposalActionsDecoderUtils.isArrayType(type);
const isTuple = proposalActionsDecoderUtils.isTupleType(type);
const isNestedType = isTuple || isArray;

const initialParameters = proposalActionsItemFormFieldUtils.getNestedParameters(parameter);
const initialParameters = proposalActionsDecoderUtils.getNestedParameters(parameter);
const [nestedParameters, setNestedParameters] = useState<IProposalActionInputDataParameter[]>(initialParameters);

if (!isNestedType) {
return (
<div className="flex flex-row items-center gap-2">
<ProposalActionsItemFormField
<ProposalActionsDecoderTextField
parameter={parameter}
fieldName={fieldName}
hideLabels={hideLabels}
editMode={editMode}
mode={mode}
formPrefix={formPrefix}
/>
{onDeleteClick != null && editMode && (
{onDeleteClick != null && mode === ProposalActionsDecoderMode.EDIT && (
<Button iconLeft={IconType.CLOSE} size="lg" variant="tertiary" onClick={onDeleteClick} />
)}
</div>
);
}

const handleAddArrayItem = () => {
const defaultNestedParameter = proposalActionsItemFormFieldUtils.getDefaultNestedParameter(parameter);
const defaultNestedParameter = proposalActionsDecoderUtils.getDefaultNestedParameter(parameter);
const newNestedParameters = nestedParameters.concat(defaultNestedParameter);
setNestedParameters(newNestedParameters);
};

const handleRemoveArrayItem = (index: number) => () => {
const arrayFieldName = `${formPrefix}.${fieldName}`;
const arrayFieldName = proposalActionsDecoderUtils.getFieldName(fieldName, formPrefix);
const currentValues = getValues(arrayFieldName) as string[];
const newNestedParameters = nestedParameters.toSpliced(index, 1);
const newValues = currentValues.toSpliced(index, 1);
unregister(`${formPrefix}.${fieldName}.${(nestedParameters.length - 1).toString()}`);
unregister(proposalActionsDecoderUtils.getFieldName((nestedParameters.length - 1).toString(), arrayFieldName));
setValue(arrayFieldName, newValues);
setNestedParameters(newNestedParameters);
};
Expand All @@ -88,18 +85,18 @@ export const ProposalActionsItemDecodedViewField: React.FC<IProposalActionsItemD
<div className="flex grow flex-row gap-2">
<div className="flex grow flex-col gap-2">
{nestedParameters.map((parameter, index) => (
<ProposalActionsItemDecodedViewField
<ProposalActionsDecoderField
key={index}
parameter={parameter}
hideLabels={isArray}
editMode={editMode}
formPrefix={`${formPrefix}.${fieldName}`}
mode={mode}
formPrefix={proposalActionsDecoderUtils.getFieldName(fieldName, formPrefix)}
fieldName={index.toString()}
onDeleteClick={isArray ? handleRemoveArrayItem(index) : undefined}
/>
))}
</div>
{onDeleteClick != null && editMode && (
{onDeleteClick != null && mode === ProposalActionsDecoderMode.EDIT && (
<Button
iconLeft={IconType.CLOSE}
size="lg"
Expand All @@ -109,7 +106,7 @@ export const ProposalActionsItemDecodedViewField: React.FC<IProposalActionsItemD
/>
)}
</div>
{isArray && editMode && (
{isArray && mode === ProposalActionsDecoderMode.EDIT && (
<Button
iconLeft={IconType.PLUS}
size="md"
Expand Down
Loading

0 comments on commit 602237f

Please sign in to comment.