-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #410 from Chia-Network/feat-address-book
feat: integrate CRUD for address book & update create token modal
- Loading branch information
Showing
24 changed files
with
761 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
src/renderer/api/tokenization-engine/address-book.api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/renderer/components/blocks/buttons/AddWalletAddressButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
128
src/renderer/components/blocks/forms/CreateTokenForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.