Skip to content
This repository was archived by the owner on Nov 7, 2023. It is now read-only.

Commit 9268633

Browse files
authored
Data vault swap (#19)
* Create EditValueModal to handle editing of content - Handles single & multiline with custom inputs and event handlers - Additionally, add classNames to modal for better testing 'finds' - Can replace modal in EthrDid sections * Implement swap content by id/key. - Use EditValue modal to handle visuals - Add operations to interact with DV - Update value in redux on success * Update tests - Fix existing (delete) test that now found two buttons - Write test for upgrade state changes - Add missing reducer tests for types/payloads
1 parent c436234 commit 9268633

File tree

13 files changed

+321
-22
lines changed

13 files changed

+321
-22
lines changed

src/app/DataVault/DataVaultComponent.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@ interface DataVaultComponentProps {
99
declarativeDetails: DataVaultKey
1010
addDeclarativeDetail: (client: DataVaultWebClient, key: string, content: string) => any
1111
deleteValue: (client: DataVaultWebClient, key: string, id: string) => any
12+
swapValue: (client: DataVaultWebClient, key: string, content: string, id: string) => any
1213
}
1314

14-
const DataVaultComponent: React.FC<DataVaultComponentProps> = ({ addDeclarativeDetail, declarativeDetails, deleteValue }) => {
15+
const DataVaultComponent: React.FC<DataVaultComponentProps> = ({
16+
addDeclarativeDetail, declarativeDetails, deleteValue, swapValue
17+
}) => {
1518
const context = useContext(Web3ProviderContext)
1619

1720
const handleAdd = (key: string, content: string) =>
1821
context.dvClient && addDeclarativeDetail(context.dvClient, key, content)
1922
const handleDelete = (key: string, id: string) =>
2023
context.dvClient && deleteValue(context.dvClient, key, id)
24+
const handleSwap = (key: string, content: string, id: string) =>
25+
context.dvClient && swapValue(context.dvClient, key, content, id)
2126

2227
return (
2328
<div className="content data-vault">
@@ -30,7 +35,9 @@ const DataVaultComponent: React.FC<DataVaultComponentProps> = ({ addDeclarativeD
3035
<div className="column">
3136
<DeclarativeDetailsDisplay
3237
details={declarativeDetails}
33-
deleteValue={handleDelete} />
38+
deleteValue={handleDelete}
39+
swapValue={handleSwap}
40+
/>
3441
</div>
3542
</div>
3643
</div>

src/app/DataVault/DataVaultContainer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import DataVaultWebClient from '@rsksmart/ipfs-cpinner-client'
44
import { stateInterface } from '../state/configureStore'
55
import DataVaultComponent from './DataVaultComponent'
66
import { AnyAction } from 'redux'
7-
import { createDataVaultContent, deleteDataVaultContent } from '../state/operations/datavault'
7+
import { createDataVaultContent, deleteDataVaultContent, swapDataVaultContent } from '../state/operations/datavault'
88

99
const mapStateToProps = (state: stateInterface) => ({
1010
declarativeDetails: state.datavault.data
@@ -14,7 +14,9 @@ const mapDispatchToProps = (dispatch: ThunkDispatch<stateInterface, {}, AnyActio
1414
addDeclarativeDetail: (client: DataVaultWebClient, key: string, content: string) =>
1515
dispatch(createDataVaultContent(client, key, content)),
1616
deleteValue: (client: DataVaultWebClient, key: string, id: string) =>
17-
dispatch(deleteDataVaultContent(client, key, id))
17+
dispatch(deleteDataVaultContent(client, key, id)),
18+
swapValue: (client: DataVaultWebClient, key: string, content: string, id: string) =>
19+
dispatch(swapDataVaultContent(client, key, content, id))
1820
})
1921

2022
export default connect(mapStateToProps, mapDispatchToProps)(DataVaultComponent)
Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import { shallow, mount } from 'enzyme'
3+
import { act } from 'react-dom/test-utils'
34
import DeclarativeDetailsDisplay from './DeclarativeDetailsDisplay'
45
import { DataVaultKey } from '../../state/reducers/datavault'
56

@@ -9,13 +10,18 @@ describe('Component: DeclarativeDetailsDisplay', () => {
910
NAME: [{ id: '5', content: 'Jesse Clark' }]
1011
}
1112

13+
const mockedAttributes = {
14+
deleteValue: jest.fn(),
15+
swapValue: jest.fn()
16+
}
17+
1218
it('renders the component', () => {
13-
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} deleteValue={jest.fn()} />)
19+
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} {...mockedAttributes} />)
1420
expect(wrapper).toBeDefined()
1521
})
1622

1723
it('shows the content in a row', () => {
18-
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} deleteValue={jest.fn()} />)
24+
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} {...mockedAttributes} />)
1925
expect(wrapper.find('tbody').children()).toHaveLength(2)
2026

2127
expect(wrapper.find('tr').at(1).find('td').at(0).text()).toBe('EMAIL')
@@ -28,16 +34,46 @@ describe('Component: DeclarativeDetailsDisplay', () => {
2834
NAME: []
2935
}
3036

31-
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDetails} deleteValue={jest.fn()} />)
37+
const wrapper = shallow(<DeclarativeDetailsDisplay details={mockDetails} {...mockedAttributes} />)
3238
expect(wrapper.find('tbody').children()).toHaveLength(1)
3339
})
3440

35-
it('handles delete click', () => {
36-
const deleteValue = jest.fn()
37-
const wrapper = mount(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} deleteValue={deleteValue} />)
41+
it('handles delete click', async () => {
42+
const deleteFunction = jest.fn()
43+
const deleteValue = (key: string, id: string) => new Promise((resolve) => resolve(deleteFunction(key, id)))
44+
const wrapper = mount(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} deleteValue={deleteValue} swapValue={jest.fn()} />)
45+
46+
wrapper.find('.content-row').at(0).find('button.delete').simulate('click')
47+
48+
await act(async () => {
49+
await wrapper.find('.delete-modal').find('.column').at(1).find('button').simulate('click')
50+
51+
expect(deleteFunction).toBeCalledTimes(1)
52+
expect(deleteFunction).toBeCalledWith('EMAIL', '1')
53+
})
54+
})
55+
56+
it('handles swap click', async () => {
57+
const editFunction = jest.fn()
58+
const swapValue = (key:string, content: string, id: string) => new Promise((resolve) => resolve(editFunction(key, content, id)))
59+
const wrapper = mount(<DeclarativeDetailsDisplay details={mockDeclarativeDetials} deleteValue={jest.fn()} swapValue={swapValue} />)
60+
61+
wrapper.find('.content-row').at(0).find('button.edit').simulate('click')
62+
expect(wrapper.find('textarea').props().value).toBe('[email protected]')
63+
64+
await act(async () => {
65+
await wrapper.find('.edit-modal').find('button.submit').simulate('click')
66+
wrapper.update()
67+
expect(wrapper.find('.modal-content').find('div.alert.error').text()).toBe('New value is the same as the old.')
68+
69+
wrapper.find('textarea.line').simulate('change', { target: { value: '[email protected]' } })
70+
expect(wrapper.find('textarea').props().value).toBe('[email protected]')
71+
72+
await wrapper.find('.edit-modal').find('button.submit').simulate('click')
73+
wrapper.update()
3874

39-
wrapper.find('.content-row').at(0).find('button').simulate('click')
40-
wrapper.find('.delete-modal').find('.column').at(1).find('button').simulate('click')
41-
expect(deleteValue).toBeCalledWith('EMAIL', '1')
75+
expect(editFunction).toBeCalledTimes(1)
76+
expect(editFunction).toBeCalledWith('EMAIL', '[email protected]', '1')
77+
})
4278
})
4379
})

src/app/DataVault/panels/DeclarativeDetailsDisplay.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,51 @@ import React, { useState } from 'react'
22
import Panel from '../../../components/Panel/Panel'
33
import declarativeIcon from '../../../assets/images/icons/declarative-details.svg'
44
import trashIcon from '../../../assets/images/icons/trash.svg'
5+
import pencilIcon from '../../../assets/images/icons/pencil.svg'
56
import { DataVaultContent, DataVaultKey } from '../../state/reducers/datavault'
67
import BinaryModal from '../../../components/Modal/BinaryModal'
8+
import EditValueModal from '../../../components/Modal/EditValueModal'
79

810
interface DeclarativeDetailsDisplayInterface {
911
deleteValue: (key: string, id: string) => Promise<any>
12+
swapValue: (key: string, content: string, id: string) => Promise<any>
1013
details: DataVaultKey
1114
}
1215

13-
const DeclarativeDetailsDisplay: React.FC<DeclarativeDetailsDisplayInterface> = ({ details, deleteValue }) => {
16+
const DeclarativeDetailsDisplay: React.FC<DeclarativeDetailsDisplayInterface> = ({ details, deleteValue, swapValue }) => {
1417
interface DeleteItemI { key: string; id: string }
18+
interface EditItemI { key: string; item: DataVaultContent }
19+
1520
const [isLoading, setIsLoading] = useState<boolean>(false)
21+
const [isError, setIsError] = useState<null | string>(null)
1622
const [isDeleting, setIsDeleting] = useState<null | DeleteItemI>(null)
23+
const [isEditing, setIsEditing] = useState<null | EditItemI>(null)
1724

18-
const handleDeleteItem = async (item: DeleteItemI) => {
25+
const handleDeleteItem = (item: DeleteItemI) => {
1926
setIsLoading(true)
27+
setIsError(null)
28+
2029
deleteValue(item.key, item.id)
30+
.then(() => setIsDeleting(null))
31+
.catch((err: Error) => setIsError(err.message))
32+
.finally(() => setIsLoading(false))
33+
}
34+
35+
const handleEditItem = (newValue: string, existingItem: EditItemI) => {
36+
if (newValue === existingItem.item.content) {
37+
return setIsError('New value is the same as the old.')
38+
}
39+
40+
setIsLoading(true)
41+
setIsError(null)
42+
43+
swapValue(existingItem.key, newValue, existingItem.item.id)
2144
.then(() => {
22-
setIsDeleting(null)
2345
setIsLoading(false)
46+
setIsEditing(null)
2447
})
48+
.catch((err: Error) => setIsError(err.message))
49+
.finally(() => setIsLoading(false))
2550
}
2651

2752
return (
@@ -48,7 +73,14 @@ const DeclarativeDetailsDisplay: React.FC<DeclarativeDetailsDisplayInterface> =
4873
<div className="options">
4974
<button
5075
disabled={isLoading}
51-
className="icon"
76+
className="icon edit"
77+
onClick={() => { setIsError(null); setIsEditing({ key, item }) }}
78+
>
79+
<img src={pencilIcon} alt="Edit item" />
80+
</button>
81+
<button
82+
disabled={isLoading}
83+
className="icon delete"
5284
onClick={() => setIsDeleting({ key, id: item.id })}>
5385
<img src={trashIcon} alt="Delete Item" />
5486
</button>
@@ -64,6 +96,18 @@ const DeclarativeDetailsDisplay: React.FC<DeclarativeDetailsDisplayInterface> =
6496
</tbody>
6597
</table>
6698

99+
<EditValueModal
100+
show={isEditing !== null}
101+
onClose={() => setIsEditing(null)}
102+
onConfirm={(value: string) => isEditing && handleEditItem(value, isEditing)}
103+
disabled={isLoading}
104+
strings={{ title: 'Edit value in the DataVault', label: 'New value', submit: 'Update' }}
105+
className="edit-modal"
106+
initValue={isEditing ? isEditing.item.content : ''}
107+
inputType="textarea"
108+
error={isError}
109+
/>
110+
67111
<BinaryModal
68112
show={isDeleting !== null}
69113
onClose={() => setIsDeleting(null)}

src/app/state/operations/datavault.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Dispatch } from 'react'
22
import DataVaultWebClient from '@rsksmart/ipfs-cpinner-client'
33
import { createDidFormat } from '../../../formatters'
4-
import { addContentToKey, DataVaultContent, receiveKeyData, removeContentfromKey } from '../reducers/datavault'
4+
import { addContentToKey, DataVaultContent, receiveKeyData, removeContentfromKey, swapContentById } from '../reducers/datavault'
55
import { getDataVault } from '../../../config/getConfig'
66
import { CreateContentResponse } from '@rsksmart/ipfs-cpinner-client/lib/types'
77

@@ -59,3 +59,7 @@ export const createDataVaultContent = (client: DataVaultWebClient, key: string,
5959
export const deleteDataVaultContent = (client: DataVaultWebClient, key: string, id: string) => (dispatch: Dispatch<any>) =>
6060
client.delete({ key, id })
6161
.then(() => dispatch(removeContentfromKey({ key, id })))
62+
63+
export const swapDataVaultContent = (client: DataVaultWebClient, key: string, content: string, id: string) => (dispatch: Dispatch<any>) =>
64+
client.swap({ key, content, id })
65+
.then(() => dispatch(swapContentById({ key, id, content })))

src/app/state/reducers/datavault.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { configureStore, Store, AnyAction } from '@reduxjs/toolkit'
2-
import dataVaultSlice, { DataVaultState, receiveKeyData, initialState, addContentToKey, removeContentfromKey } from './datavault'
2+
import dataVaultSlice, { DataVaultState, receiveKeyData, initialState, addContentToKey, removeContentfromKey, swapContentById, DataVaultContent } from './datavault'
33

44
describe('dataVault slice', () => {
55
describe('action creators', () => {
@@ -8,6 +8,20 @@ describe('dataVault slice', () => {
88
expect(receiveKeyData({ key: 'KEY', content }))
99
.toEqual({ type: receiveKeyData.type, payload: { key: 'KEY', content } })
1010
})
11+
12+
test('addContentToKey', () => {
13+
const content = { key: 'KEY', content: { id: '1', content: 'hello' } }
14+
expect(addContentToKey(content)).toEqual({ type: addContentToKey.type, payload: content })
15+
})
16+
17+
test('removeContentfromKey', () => {
18+
expect(removeContentfromKey({ key: 'KEY', id: '2' })).toEqual({ type: removeContentfromKey.type, payload: { key: 'KEY', id: '2' } })
19+
})
20+
21+
test('swapContentById', () => {
22+
const content = { key: 'KEY', id: '2', content: 'new' }
23+
expect(swapContentById(content)).toEqual({ type: swapContentById.type, payload: content })
24+
})
1125
})
1226

1327
describe('reducer', () => {
@@ -76,5 +90,25 @@ describe('dataVault slice', () => {
7690
expect(store.getState().data).toEqual({ MY_KEY: [] })
7791
})
7892
})
93+
94+
describe('swapContentById', () => {
95+
const initContent = [{ id: '1', content: 'hello' }, { id: '2', content: 'hello' }]
96+
97+
beforeEach(() => {
98+
store.dispatch(receiveKeyData({ key: 'MY_KEY', content: initContent }))
99+
})
100+
101+
test('it swaps content of existing key', () => {
102+
store.dispatch(swapContentById({ id: '1', content: 'newContent', key: 'MY_KEY' }))
103+
104+
const id1 = store.getState().data.MY_KEY.filter((item: DataVaultContent) => item.id === '1')[0]
105+
expect(id1.content).toBe('newContent')
106+
})
107+
108+
test('it keeps content when id is invalid', () => {
109+
store.dispatch(swapContentById({ id: '15', content: 'newContent', key: 'MY_KEY' }))
110+
expect(store.getState().data.MY_KEY).toMatchObject(initContent)
111+
})
112+
})
79113
})
80114
})

src/app/state/reducers/datavault.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ interface ReceivePayLoad {
1414
content: DataVaultContent[]
1515
}
1616

17+
interface SwapPayLoad {
18+
key: string,
19+
id: string,
20+
content: string
21+
}
22+
1723
export interface DataVaultState {
1824
data: DataVaultKey
1925
}
@@ -34,10 +40,13 @@ const dataVaultSlice = createSlice({
3440
},
3541
removeContentfromKey (state: DataVaultState, { payload: { key, id } }: PayloadAction<{ key: string, id: string }>) {
3642
state.data[key] = state.data[key].filter((item: DataVaultContent) => item.id !== id)
43+
},
44+
swapContentById (state: DataVaultState, { payload: { key, id, content } }: PayloadAction<SwapPayLoad>) {
45+
state.data[key] = state.data[key].map((item: DataVaultContent) => item.id === id ? { ...item, content } : item)
3746
}
3847
}
3948
})
4049

41-
export const { receiveKeyData, addContentToKey, removeContentfromKey } = dataVaultSlice.actions
50+
export const { receiveKeyData, addContentToKey, removeContentfromKey, swapContentById } = dataVaultSlice.actions
4251

4352
export default dataVaultSlice.reducer

src/assets/images/icons/pencil.svg

Lines changed: 4 additions & 0 deletions
Loading

src/assets/scss/screens/_data-vault.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@
5555
flex: 1;
5656
}
5757
}
58+
59+
.edit-modal {
60+
label {
61+
display: block;
62+
font-weight: bold;
63+
padding-bottom: 5px;
64+
}
65+
66+
textarea {
67+
width: 100%;
68+
min-height: 35px;
69+
border: 1px solid $lightGray;
70+
padding: 5px;
71+
}
72+
}
5873
}
5974
}
6075

0 commit comments

Comments
 (0)