4. Adding, Editing and Deleting shareholders - PolymathNetwork/whitelist-standalone GitHub Wiki
Now that we have loaded and displayed the shareholders list, we can add more interesting features. We're going to enable token issuer to add, edit or remove shareholders.
We'll use one form component for adding new shareholders as well as editing existing ones. The only different is that address
textfield is going to be disabled while editing a shareholder. That's because address
is the primary key.
First, we're going to add a couple of event handlers to App
component. You can add these functions anywhere in App function, before returning:
// App.js
function App() {
...
// Used for both adding a new shareholder and modifying an existing one.
async function modifyWhitelist(data) {
const queue = await tokens[selectedToken].shareholders.modifyData({
shareholderData: data
})
await queue.run()
dispatch({type:a.RELOAD_SHAREHOLDERS})
}
async function removeShareholders(addresses) {
dispatch({type: a.DELETING_SHAREHOLDER})
const queue = await tokens[selectedToken].shareholders.revokeKyc({
shareholderAddresses: addresses
})
await queue.run()
dispatch({type: a.SHAREHOLDER_DELETED})
dispatch({type:a.RELOAD_SHAREHOLDERS})
}
...
return (
...
{ selectedToken !== undefined &&
<Whitelist
shareholders={shareholders}
// Note these additional properties
modifyWhitelist={modifyWhitelist}
removeShareholders={removeShareholders}
/>
}
...
)
-
modifyWhitelist
: A submit handler for shareholder form. The handler receives aShareholder
object, whether it's being edited or inserted. Eventually, the function uses SDK'smodifyData
as follows:
const queue = await tokens[selectedToken].shareholders.modifyData({
// data is an array of shareholder objects.
shareholderData: data
})
await queue.run()
-
removeShareholders
which is the handler for thedelete
button. It will call SDK'srevokeKyc
to revoke KYC for passed addresses.
Note that we've previously dealt with similar async actions as an effect. We can still use the same setup here. For example. instead of calling removeShareholders
directly from the delete handler, we could just the records to delete into app state, and then have an effect pick them up for deletion, on the next App
component re-rendering. We've opted for calling removeShareholders
directly because it's less verbose.
How that we've got our ducks in a row, we'll go ahead and add the add/edit shareholder form. But first we'll create a wrapper component Whitelist
, which will wrap the form as well as the shareholders table we've created in previous steps.
// Whitelist.js
export default ({shareholders, modifyWhitelist, removeShareholders}) => {
const form = useForm()
// These functions are provided by rc-form-hooks library. See https://github.com/mushan0x0/rc-form-hooks
const { getFieldDecorator, setFieldsValue, resetFields, validateFields } = form
// Note this is a different reducer function from the one we created earlier. We're using this reducer particularly for form state.
const [state, dispatch] = useReducer(reducer, initialState)
const { visible, editIndex, ongoingTx } = state
const closeForm = () => {
dispatch({type: 'CLOSE_FORM'})
resetFields()
}
// Passing an index means that we're editing an existing item. Otherwise it's a create form.
const openForm = (index = '') => {
dispatch({ type: 'OPEN_FORM', payload: { editIndex: index } })
}
const submitForm = async () => {
const fields = ['address', 'canSendAfter', 'canReceiveAfter', 'kycExpiry', 'canBuyFromSto', 'isAccredited']
validateFields(fields, { force: true })
.then(async (values) => {
// The values below are instances of momentjs. Convert them to JS Date objects as the SDK expects.
values.canSendAfter = values.canSendAfter.toDate()
values.canReceiveAfter = values.canReceiveAfter.toDate()
values.kycExpiry = values.kycExpiry.toDate()
try {
dispatch({type: 'TX_SEND'})
// Call the helper function, which will the SDK in its turn.
await modifyWhitelist([values])
dispatch({ type: 'TX_RECEIPT'})
resetFields()
}
catch (error) {
// This could be transaction error, or a user error e.g user rejected transaction.
dispatch({ type: 'TX_ERROR',
payload: {error: error.message} })
message.error(error.message)
}
})
}
let editedRecord = shareholders.filter(shareholder => shareholder.address === editIndex)[0]
// This effect sets form initial values. In "edit" mode, initial values reflect the currently edited record.
useEffect(() => {
let initialValues = editedRecord || defaultShareholderValues
setFieldsValue(initialValues)
}, [editedRecord, setFieldsValue])
return (
<div style={{display: 'flex',
flexDirection: 'column'}}>
<Button onClick={openForm}>Add new</Button>
{/* This is the component from the previous guide */}
<ShareholdersTable shareholders={shareholders} removeShareholders={removeShareholders} openForm={openForm} />
<Modal
title={editedRecord ? 'Edit token holder' : 'Add a new token holder'}
closable={false}
visible={visible}
footer={null}
>
<Spin spinning={ongoingTx} size="large">
<Form {...formItemLayout}>
<Item name="address" label="Address">
// here we add a couple of address validators
// - The first make sure that the address is a valid ethereum address
// - The second make sure we're not adding an existing shareholder.
{getFieldDecorator('address', {
rules: [
{ required: true }, {
validator: (rule, value, callback) => {
if (!editedRecord && !web3Utils.isAddress(value)) {
callback('Address is invalid') return
}
callback() return
}
}, {
validator: (rule, value, callback) => {
const shareholderExists = (address) => {
const ret = shareholders.find((element) => element.address.toUpperCase() === address.toUpperCase())
!== undefined
return ret
}
if (!editedRecord && shareholderExists(value)) {
callback('Shareholder is already present in the whitelist') return
}
callback() return
}
}
],
// Disable address field in case of editing.
})(<Input disabled={!!editedRecord}/>)}
</Item>
<Item name="canSendAfter" label="Can Send after">
{getFieldDecorator('canSendAfter', {
rules: [{ required: true }],
})(<DatePicker />)}
</Item>
<Item name="canReceiveAfter" label="Can Receive After">
{getFieldDecorator('canReceiveAfter', {
rules: [{ required: true }],
})(<DatePicker />)}
</Item>
<Item name="kycExpiry" label="KYC Expiry">
{getFieldDecorator('kycExpiry', {
rules: [{ required: true }],
})(<DatePicker />)}
</Item>
<Item name="canBuyFromSto" label="Can Buy from STO">
{getFieldDecorator('canBuyFromSto', {
valuePropName: 'checked',
})(<Switch />)}
</Item>
<Item name="isAccredited" label="Accredited">
{getFieldDecorator('isAccredited', {
valuePropName: 'checked',
})(<Switch />)}
</Item>
<Item>
<Button onClick={closeForm}>cancel</Button>
<Button type="primary" onClick={submitForm}>save</Button>
</Item>
</Form>
</Spin>
</Modal>
</div>
)
}
Again, this is just a simple form wrapper by a component. It might look longer than needed, but that's only because field validation with Ant Design is a little awkward. Feel free to substitute for ant design with any other library you're familiar with.
This is a brand-new reducer function. We opted to use a separate reducer for form and transactions states in order not to muddy up our existing one.
const reducer = (state, action) => {
switch (action.type) {
case 'OPEN_FORM':
const { editIndex } = action.payload
return {
...state,
visible: true,
editIndex
}
case 'CLOSE_FORM':
return {
...state,
editIndex: '',
visible: false,
ongoingTx: false
}
case 'TX_SEND':
return {
...state,
ongoingTx: true
}
case 'TX_RECEIPT':
return {
...state,
ongoingTx: false,
visible: false,
error: '',
editIndex: ''
}
case 'TX_ERROR':
const { error } = action.payload
return {
...state,
error,
ongoingTx: false
}
default:
return state
}
}
The reducer above is responding to two kinds of actions:
- Form actions like opening or closing a form. In those cases we manipulate a few flags that determine whether the modal is open and whether a spinner is visible.
- Transaction state actions. These actions aim to display transaction status.