Skip to content

Commit

Permalink
Merge pull request #410 from Chia-Network/feat-address-book
Browse files Browse the repository at this point in the history
feat: integrate CRUD for address book & update create token modal
  • Loading branch information
wwills2 authored Jan 2, 2025
2 parents 8d5eb03 + 991ea94 commit 11847bf
Show file tree
Hide file tree
Showing 24 changed files with 761 additions and 101 deletions.
1 change: 1 addition & 0 deletions src/renderer/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './tokenization-engine/projects.api';
export * from './tokenization-engine/system.api';
export * from './tokenization-engine/units.api';
export * from './tokenization-engine/organizations.api';
export * from './tokenization-engine/address-book.api';
105 changes: 105 additions & 0 deletions src/renderer/api/tokenization-engine/address-book.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Address } from '@/schemas/Address.schema';
import { addressBookTag, RECORDS_PER_PAGE, tokenizationEngineApi } from './index';

interface GetAddressBookParams {
page?: number;
search?: string | null;
order?: string | null;
limit?: number;
}

interface GetAddressBookResponse {
page: number;
pageCount: number;
data: Address[];
}

interface GetAddressParams {
id: string;
}

interface CreateAddressParams {
name: string;
walletAddress?: string;
}

const addressBookApi = tokenizationEngineApi.injectEndpoints({
endpoints: (builder) => ({
getAddressBook: builder.query<GetAddressBookResponse, GetAddressBookParams>({
query: ({ page, search, order, limit }: GetAddressBookParams) => {
const params: GetAddressBookParams = { page, limit: limit || RECORDS_PER_PAGE };

if (search) {
params.search = search.replace(/[^a-zA-Z0-9 _.-]+/, '');
}

if (order) {
params.order = order;
}

return {
url: `/address-book`,
params,
method: 'GET',
};
},
providesTags: [addressBookTag],
}),

getAddress: builder.query<Address, GetAddressParams>({
query: ({ id }: GetAddressParams) => ({
url: `/address-book`,
params: { id },
method: 'GET',
}),
providesTags: (_response, _error, { id }) => [{ type: addressBookTag, id: id }],
}),

createAddress: builder.mutation<any, CreateAddressParams>({
query: (createAddressParams: CreateAddressParams) => ({
url: '/address-book',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: createAddressParams,
}),
invalidatesTags: [addressBookTag],
}),

deleteAddress: builder.mutation<any, { uuid: string }>({
query: ({ uuid }) => {
return {
url: `/address-book`,
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: { id: uuid },
};
},
invalidatesTags: [addressBookTag],
}),

editAddress: builder.mutation<any, { id: string; name?: string; walletAddress?: string }>({
query: (data) => {
const body: any = {};
if (data.id) body.id = data.id;
if (data.name) body.name = data.name;
if (data.walletAddress) body.walletAddress = data.walletAddress;
return {
url: `/address-book`,
method: 'PUT',
body,
};
},
invalidatesTags: [addressBookTag],
}),
}),
});

export const invalidateAddressBookApiTag = addressBookApi.util.invalidateTags;

export const {
useCreateAddressMutation,
useGetAddressQuery,
useGetAddressBookQuery,
useDeleteAddressMutation,
useEditAddressMutation,
} = addressBookApi;
3 changes: 2 additions & 1 deletion src/renderer/api/tokenization-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import initialState from '@/store/slices/app/app.initialstate';

export const RECORDS_PER_PAGE = 10;
export const projectsTag = 'projects';
export const addressBookTag = 'addressBook';
export const untokenizedUnitsTag = 'untokenizedUnits';
export const tokenizedUnitsTag = 'tokenizedUnits';
export const projectsByIdsTag = 'projectsByIds';
Expand Down Expand Up @@ -46,5 +47,5 @@ export const tokenizationEngineApi = createApi({
baseQuery: baseQueryWithDynamicHost,
reducerPath: 'tokenizationEngineApi',
endpoints: () => ({}),
tagTypes: [projectsTag, untokenizedUnitsTag, tokenizedUnitsTag, projectsByIdsTag],
tagTypes: [projectsTag, untokenizedUnitsTag, tokenizedUnitsTag, projectsByIdsTag, addressBookTag],
});
21 changes: 21 additions & 0 deletions src/renderer/components/blocks/buttons/AddWalletAddressButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { Button } from '@/components';
import { FormattedMessage } from 'react-intl';

interface AddWalletAddressButtonProps {
setActive: (active: boolean) => void;
}

const AddWalletAddressButton: React.FC<AddWalletAddressButtonProps> = ({ setActive }) => {
return (
<>
<Button onClick={() => setActive(true)}>
<p className="capitalize">
<FormattedMessage id="add-address" />
</p>
</Button>
</>
);
};

export { AddWalletAddressButton };
1 change: 1 addition & 0 deletions src/renderer/components/blocks/buttons/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ConnectButton';
export * from './FormButton';
export * from './DetokenizeUnitButton';
export * from './AddWalletAddressButton';
128 changes: 38 additions & 90 deletions src/renderer/components/blocks/forms/CreateTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,65 @@
import React, { useCallback } from 'react';
import { noop } from 'lodash';
import { ErrorMessage, Field, Form, Formik } from 'formik';
import * as yup from 'yup';
import { TestContext, ValidationError } from 'yup';
import { FloatingLabel, FormButton, HelperText, Spacer } from '@/components';
import React, { forwardRef, useImperativeHandle } from 'react';
import { Field, HelperText, Spacer } from '@/components';
import { FormattedMessage, IntlShape, useIntl } from 'react-intl';
import { Form, Formik, FormikProps } from 'formik';
import * as yup from 'yup';
import { Address } from '@/schemas/Address.schema';

interface FormProps {
onSubmit: (walletAddress: string) => Promise<void>;
onClearError?: () => void;
interface FormProps extends React.RefAttributes<CreateTokenFormRef> {
data?: Address[];
}

const validateWalletAddress = (value: string, context: TestContext, intl: IntlShape): ValidationError | true => {
if (!value) return true; // If empty, required will handle it

if (value.startsWith('xch')) {
if (/^xch[a-zA-Z0-9]{59}$/.test(value)) {
return true;
} else {
return context.createError({
message: intl.formatMessage({
id: 'wallet-addresses-start-with-xch-and-are-62-characters-long',
}),
});
}
} else if (value.startsWith('txch')) {
if (/^txch[a-zA-Z0-9]{59}$/.test(value)) {
return true;
} else {
return context.createError({
message: intl.formatMessage({
id: 'testnet-wallet-addresses-start-with-txch-and-are-63-characters-long',
}),
});
}
} else {
return context.createError({
message: intl.formatMessage({
id: 'wallet-address-must-start-with-xch-or-txch',
}),
});
}
};
export interface CreateTokenFormRef {
submitForm: () => Promise<any>;
}

const CreateTokenForm: React.FC<FormProps> = ({ onSubmit, onClearError = noop }) => {
const CreateTokenForm: React.FC<FormProps> = forwardRef<CreateTokenFormRef, FormProps>(({ data }, ref) => {
const intl: IntlShape = useIntl();
const formikRef = React.useRef<FormikProps<any>>(null);

const validationSchema = yup.object({
walletAddress: yup
.string()
.required(intl.formatMessage({ id: 'wallet-address-is-required' }))
.test('validate-wallet-address', function (value) {
return validateWalletAddress(value, this, intl);
}),
walletAddress: yup.string().required(intl.formatMessage({ id: 'wallet-address-is-required' })),
});

const handleSubmit = useCallback(
async (values: { walletAddress: string }, { setSubmitting }) => {
await onSubmit(values.walletAddress);
setSubmitting(false);
},
[onSubmit],
);
useImperativeHandle(ref, () => ({
submitForm: async () => {
if (formikRef.current) {
const formik = formikRef.current;
if (formik) {
const errors = await formik.validateForm(formik.values);
formik.setTouched(Object.keys(errors).reduce((acc, key) => ({ ...acc, [key]: true }), {}));

const handleChange = useCallback(
(event, field) => {
onClearError();
field.onChange(event); // Call Formik's original onChange
return [errors, formik.values];
}
}
},
[onClearError],
);
}));

return (
<Formik<{ walletAddress: string }>
initialValues={{ walletAddress: '' }}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isSubmitting }) => (
<Formik innerRef={formikRef} initialValues={data} validationSchema={validationSchema} onSubmit={() => {}}>
{() => (
<Form>
<div className="mb-4">
<HelperText className="text-gray-400 sentence-case">
<FormattedMessage id="carbon-token-recipient" />
</HelperText>
<Spacer size={5} />
<Field name="walletAddress">
{({ field }) => (
<FloatingLabel
id="walletAddress"
disabled={isSubmitting}
label={intl.formatMessage({ id: 'wallet-address' })}
color={errors.walletAddress && touched.walletAddress && 'error'}
variant="outlined"
required
type="text"
{...field}
onChange={(event) => handleChange(event, field)}
/>
)}
</Field>
{touched.walletAddress && <ErrorMessage name="walletAddress" component="div" className="text-red-600" />}
</div>
<div className="flex gap-4">
<FormButton isSubmitting={isSubmitting} formikErrors={errors}>
<p className="capitalize">
<FormattedMessage id="create-token" />
</p>
</FormButton>
<Field
name="walletAddress"
type="picklist"
options={
data?.map((d) => ({
label: d.name || '',
value: d.walletAddress || '',
})) || []
}
freeform={true}
/>
</div>
</Form>
)}
</Formik>
);
};
});

export { CreateTokenForm };
Loading

0 comments on commit 11847bf

Please sign in to comment.