diff --git a/e2e/specs/stateless/advancedEditor.spec.ts b/e2e/specs/stateless/advancedEditor.spec.ts
new file mode 100644
index 000000000..1f41c780c
--- /dev/null
+++ b/e2e/specs/stateless/advancedEditor.spec.ts
@@ -0,0 +1,79 @@
+import { expect } from '@playwright/test'
+import { test } from '@root/playwright'
+
+test('should be able to maintain state when returning from transaction modal to advanced editor', async ({
+ login,
+ makeName,
+ makePageObject,
+}) => {
+ const name = await makeName({
+ label: 'profile',
+ type: 'legacy',
+ records: {
+ texts: [{ key: 'text', value: 'text' }],
+ coinTypes: [{ key: 'SOL', value: 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH' }],
+ contentHash: 'ipfs://bafybeico3uuyj3vphxpvbowchdwjlrlrh62awxscrnii7w7flu5z6fk77y',
+ abi: {
+ contentType: 1,
+ data: '{"test":"test"}',
+ },
+ },
+ })
+
+ const recordsPage = makePageObject('RecordsPage')
+ const advancedEditor = makePageObject('AdvancedEditorModal')
+ const transactionModal = makePageObject('TransactionModal')
+
+ await recordsPage.goto(name)
+ await login.connect()
+
+ // Validate records
+ await expect(recordsPage.getRecordValue('text', 'text')).toHaveText('text')
+ await expect(recordsPage.getRecordValue('address', 'sol')).toHaveText(
+ 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
+ )
+ await expect(recordsPage.getRecordValue('contentHash')).toHaveText(
+ 'ipfs://bafybeico3uuyj3vphxpvbowchdwjlrlrh62awxscrnii7w7flu5z6fk77y',
+ )
+ await expect(recordsPage.getRecordValue('abi')).toHaveText('"{\\"test\\":\\"test\\"}"')
+
+ await recordsPage.editRecordsButton.click()
+
+ // Validate advanced editor
+ await expect(await advancedEditor.recordInput('text', 'text')).toHaveValue('text')
+ await expect(await advancedEditor.recordInput('address', 'SOL')).toHaveValue(
+ 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
+ )
+ await expect(await advancedEditor.recordInput('contentHash')).toHaveValue(
+ 'ipfs://bafybeico3uuyj3vphxpvbowchdwjlrlrh62awxscrnii7w7flu5z6fk77y',
+ )
+ await expect(await advancedEditor.recordInput('abi')).toHaveValue('"{\\"test\\":\\"test\\"}"')
+
+ await advancedEditor.recordClearButton('text', 'text').then((button) => button.click())
+ await advancedEditor.recordClearButton('address', 'SOL').then((button) => button.click())
+ await advancedEditor.recordInput('contentHash').then((input) => input.fill(''))
+ await advancedEditor.recordInput('abi').then((input) => input.fill(''))
+
+ await advancedEditor.saveButton.click()
+
+ // Validate transaction display item
+ await expect(transactionModal.displayItem('update')).toHaveText('4 records')
+
+ await transactionModal.backButton.click()
+
+ // Validate inputs have been rebuilt correctly
+ await expect(await advancedEditor.recordComponent('text', 'text')).toHaveCount(0)
+ await expect(await advancedEditor.recordComponent('address', 'SOL')).toHaveCount(0)
+ await expect(await advancedEditor.recordInput('contentHash')).toHaveValue('')
+ await expect(await advancedEditor.recordInput('abi')).toHaveValue('')
+
+ await advancedEditor.saveButton.click()
+
+ await transactionModal.autoComplete()
+
+ // Validate change in records
+ await expect(recordsPage.getRecordButton('text', 'text')).toHaveCount(0)
+ await expect(recordsPage.getRecordButton('address', 'sol')).toHaveCount(0)
+ await expect(recordsPage.getRecordButton('contentHash')).toHaveCount(0)
+ await expect(recordsPage.getRecordButton('abi')).toHaveCount(0)
+})
diff --git a/playwright/pageObjects/advancedEditorModal.ts b/playwright/pageObjects/advancedEditorModal.ts
new file mode 100644
index 000000000..6e150163d
--- /dev/null
+++ b/playwright/pageObjects/advancedEditorModal.ts
@@ -0,0 +1,37 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { Locator, Page } from '@playwright/test'
+
+export class AdvancedEditorModal {
+ readonly page: Page
+
+ readonly saveButton: Locator
+
+ constructor(page: Page) {
+ this.page = page
+ this.saveButton = this.page.getByTestId('advanced-editor').getByRole('button', { name: 'Save' })
+ }
+
+ tab(tab: 'text' | 'address' | 'other') {
+ return this.page.getByTestId(`${tab}-tab`)
+ }
+
+ async recordComponent(type: 'text' | 'address' | 'contentHash' | 'abi', key?: string) {
+ if (['text', 'address'].includes(type)) {
+ await this.tab(type as 'text' | 'other').click()
+ return this.page.getByTestId(`record-input-${key}`)
+ }
+ await this.tab('other').click()
+ const _key = type === 'contentHash' ? 'Content Hash' : 'ABI'
+ return this.page.getByTestId(`record-input-${_key}`)
+ }
+
+ async recordInput(type: 'text' | 'address' | 'contentHash' | 'abi', key?: string) {
+ const component = await this.recordComponent(type, key)
+ return component.locator('input')
+ }
+
+ async recordClearButton(type: 'text' | 'address', key: string) {
+ const component = await this.recordComponent(type, key)
+ return component.locator('button')
+ }
+}
diff --git a/playwright/pageObjects/index.ts b/playwright/pageObjects/index.ts
index f163b8895..8692e8f23 100644
--- a/playwright/pageObjects/index.ts
+++ b/playwright/pageObjects/index.ts
@@ -3,6 +3,7 @@ import { Page } from '@playwright/test'
import { Web3ProviderBackend } from 'headless-web3-provider'
import { AddressPage } from './addressPage'
+import { AdvancedEditorModal } from './advancedEditorModal'
import { EditRolesModal } from './editRolesModal'
import { ExtendNamesModal } from './extendNamesModal'
import { HomePage } from './homePage'
@@ -10,6 +11,7 @@ import { MorePage } from './morePage'
import { OwnershipPage } from './ownershipPage'
import { PermissionsPage } from './permissionsPage'
import { ProfilePage } from './profilePage'
+import { RecordsPage } from './recordsPage'
import { RegistrationPage } from './registrationPage'
import { SendNameModal } from './sendNameModal'
import { SubnamesPage } from './subnamePage'
@@ -30,6 +32,8 @@ const pageObjects = {
SendNameModal,
SubnamesPage,
TransactionModal,
+ RecordsPage,
+ AdvancedEditorModal,
}
type PageObjects = typeof pageObjects
diff --git a/playwright/pageObjects/recordsPage.ts b/playwright/pageObjects/recordsPage.ts
new file mode 100644
index 000000000..513f7e2de
--- /dev/null
+++ b/playwright/pageObjects/recordsPage.ts
@@ -0,0 +1,26 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { Locator, Page } from '@playwright/test'
+
+export class RecordsPage {
+ readonly page: Page
+
+ readonly editRecordsButton: Locator
+
+ constructor(page: Page) {
+ this.page = page
+ this.editRecordsButton = this.page.getByRole('button', { name: 'Edit records' })
+ }
+
+ async goto(name: string) {
+ await this.page.goto(`/${name}?tab=records`)
+ }
+
+ getRecordButton(type: 'text' | 'address' | 'contentHash' | 'abi', key?: string) {
+ const testId = key ? `name-details-${type}-${key.toLowerCase()}` : `name-details-${type}`
+ return this.page.getByTestId(testId)
+ }
+
+ getRecordValue(type: 'text' | 'address' | 'contentHash' | 'abi', key?: string) {
+ return this.getRecordButton(type, key).locator('> div').last()
+ }
+}
diff --git a/playwright/pageObjects/transactionModal.ts b/playwright/pageObjects/transactionModal.ts
index b3bda4e31..862a3289b 100644
--- a/playwright/pageObjects/transactionModal.ts
+++ b/playwright/pageObjects/transactionModal.ts
@@ -17,6 +17,8 @@ export class TransactionModal {
readonly transactionModal: Locator
+ readonly backButton: Locator
+
constructor(page: Page, wallet: Web3ProviderBackend) {
this.page = page
this.wallet = wallet
@@ -25,6 +27,7 @@ export class TransactionModal {
this.completeButton = this.page.getByTestId('transaction-modal-complete-button')
this.closeButton = this.page.getByTestId('close-icon')
this.transactionModal = this.page.getByTestId('transaction-modal-inner')
+ this.backButton = this.page.locator('body .modal').getByRole('button', { name: 'Back' })
}
async authorize() {
@@ -53,4 +56,8 @@ export class TransactionModal {
} while (isModalVisible)
/* eslint-enable no-await-in-loop */
}
+
+ displayItem(key: string, option = 'normal') {
+ return this.page.getByTestId(`display-item-${key}-${option}`).locator('> div').last()
+ }
}
diff --git a/src/components/@molecules/AdvancedEditor/AdvancedEditorTabContent.tsx b/src/components/@molecules/AdvancedEditor/AdvancedEditorTabContent.tsx
index 940e1c2d9..290d8db3c 100644
--- a/src/components/@molecules/AdvancedEditor/AdvancedEditorTabContent.tsx
+++ b/src/components/@molecules/AdvancedEditor/AdvancedEditorTabContent.tsx
@@ -25,6 +25,7 @@ const TabContentContainer = styled.div(
display: flex;
flex-direction: column;
gap: ${theme.space['3']};
+ padding-right: ${theme.space['1']};
overflow: hidden;
flex: 1;
`,
diff --git a/src/components/RecordItem.tsx b/src/components/RecordItem.tsx
index 880ef89fb..e0dd5d851 100644
--- a/src/components/RecordItem.tsx
+++ b/src/components/RecordItem.tsx
@@ -11,7 +11,7 @@ const RecordItem = ({
itemKey?: string
value: string
showLegacy?: boolean
- type: 'text' | 'address' | 'contentHash'
+ type: 'text' | 'address' | 'contentHash' | 'abi'
}) => {
const breakpoint = useBreakpoint()
const keyLabel = showLegacy && itemKey ? itemKey?.replace('_LEGACY', '') : itemKey
diff --git a/src/components/pages/profile/[name]/tabs/RecordsTab.tsx b/src/components/pages/profile/[name]/tabs/RecordsTab.tsx
index 2256c7031..b266009f3 100644
--- a/src/components/pages/profile/[name]/tabs/RecordsTab.tsx
+++ b/src/components/pages/profile/[name]/tabs/RecordsTab.tsx
@@ -261,7 +261,7 @@ export const RecordsTab = ({
)}
- {abi && }
+ {abi && }
{canEdit && resolverAddress !== emptyAddress && (
diff --git a/src/hooks/useAdvancedEditor.test.ts b/src/hooks/useAdvancedEditor.test.ts
new file mode 100644
index 000000000..87b402b2d
--- /dev/null
+++ b/src/hooks/useAdvancedEditor.test.ts
@@ -0,0 +1,54 @@
+import { normalizeAbi } from './useAdvancedEditor'
+
+describe('normalizeAbi', () => {
+ it('should normalize abi that is a string', () => {
+ expect(normalizeAbi("test")).toEqual({ contentType: 1, data: 'test' })
+ })
+
+ it('should normalize abi that is an empty string', () => {
+ expect(normalizeAbi("")).toEqual({ contentType: 1, data: '' })
+ })
+
+ it('should normalize abi with a string for data', () => {
+ expect(normalizeAbi({ data: 'test' })).toEqual({ contentType: 1, data: 'test' })
+ })
+
+ it('should normalize abi with an empty string for data', () => {
+ expect(normalizeAbi({ data: '' })).toEqual({ contentType: 1, data: '' })
+ })
+
+ it('should normalize abi with an object for data', () => {
+ expect(normalizeAbi({ data: {test: 'test'} })).toEqual({ contentType: 1, data: '{"test":"test"}' })
+ })
+ it('should normalize abi with an array for data', () => {
+ expect(normalizeAbi({ data: ['test'] })).toEqual({ contentType: 1, data: '["test"]' })
+ })
+
+ it('should normalize abi with an object for data', () => {
+ expect(normalizeAbi({ data: {test: 'test'} })).toEqual({ contentType: 1, data: '{"test":"test"}' })
+ })
+
+ it('should NOT normalize abi that is a number', () => {
+ expect(normalizeAbi(5 as any)).toBeUndefined()
+ })
+
+ it('should NOT normalize abi that is null', () => {
+ expect(normalizeAbi(null as any)).toBeUndefined()
+ })
+
+ it('should NOT normalize abi that is undefined', () => {
+ expect(normalizeAbi(undefined as any)).toBeUndefined()
+ })
+
+ it('should NOT normalize abi with null for data', () => {
+ expect(normalizeAbi({ data: null as any})).toBeUndefined()
+ })
+
+ it('should NOT normalize abi with undefined for data', () => {
+ expect(normalizeAbi({ data: undefined as any})).toBeUndefined()
+ })
+
+ it('should NOT normalize abi with a number for data', () => {
+ expect(normalizeAbi({ data: 5 as any})).toBeUndefined()
+ })
+})
\ No newline at end of file
diff --git a/src/hooks/useAdvancedEditor.ts b/src/hooks/useAdvancedEditor.ts
index 69cc06f15..1771fc2b9 100644
--- a/src/hooks/useAdvancedEditor.ts
+++ b/src/hooks/useAdvancedEditor.ts
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
-import { Pattern, match } from 'ts-pattern'
+import { P, Pattern, match } from 'ts-pattern'
import { RecordOptions } from '@ensdomains/ensjs/utils/recordHelpers'
@@ -11,7 +11,6 @@ import useExpandableRecordsGroup from '@app/hooks/useExpandableRecordsGroup'
import { DetailedProfile } from '@app/hooks/useNameDetails'
import { useProfile } from '@app/hooks/useProfile'
import { useResolverHasInterfaces } from '@app/hooks/useResolverHasInterfaces'
-import { emptyAddress } from '@app/utils/constants'
import {
convertFormSafeKey,
convertProfileToProfileFormObject,
@@ -35,6 +34,15 @@ const getFieldsByType = (type: 'text' | 'addr' | 'contentHash', data: AdvancedEd
return Object.fromEntries(entries)
}
+type NormalizedAbi = { contentType: number | undefined; data: string }
+export const normalizeAbi = (abi: RecordOptions['abi'] | string): NormalizedAbi | undefined => {
+ return match(abi)
+ .with(P.string, (data) => ({ contentType: 1, data }))
+ .with({ data: P.string }, ({ data }) => ({ contentType: 1, data }))
+ .with({ data: {} }, ({ data }) => ({ contentType: 1, data: JSON.stringify(data) }))
+ .otherwise(() => undefined)
+}
+
export type AdvancedEditorType = {
text: {
[key: string]: string
@@ -166,6 +174,7 @@ const useAdvancedEditor = ({ profile, loading, overwrites, callback }: Props) =>
}
})()
+ const [shouldRunOverwritesScript, setShouldRunOverwritesScript] = useState(false)
useEffect(() => {
if (profile) {
const formObject = convertProfileToProfileFormObject(profile)
@@ -189,33 +198,60 @@ const useAdvancedEditor = ({ profile, loading, overwrites, callback }: Props) =>
address: Object.keys(newDefaultValues.address) || [],
text: Object.keys(newDefaultValues.text) || [],
}
+ setExistingRecords(newExistingRecords)
+ setShouldRunOverwritesScript(true)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [profile])
+ useEffect(() => {
+ if (shouldRunOverwritesScript) {
overwrites?.texts?.forEach((text) => {
const { key, value } = text
const formKey = formSafeKey(key)
- setValue(`text.${formKey}`, value, { shouldDirty: true })
- if (!newExistingRecords.text.includes(formKey)) {
- newExistingRecords.text.push(formKey)
+ const isExisting = existingRecords.text.includes(formKey)
+ if (value && isExisting) {
+ setValue(`text.${formKey}`, value, { shouldDirty: true })
+ } else if (value && !isExisting) {
+ addTextKey(formKey)
+ setValue(`text.${formKey}`, value, { shouldDirty: true })
+ } else if (!value && isExisting) {
+ removeTextKey(formKey, false)
+ } else if (!value && !isExisting) {
+ removeTextKey(formKey, true)
}
})
overwrites?.coinTypes?.forEach((coinType) => {
const { key, value } = coinType
const formKey = formSafeKey(key)
- setValue(`address.${formKey}`, value, { shouldDirty: true })
- if (!newExistingRecords.address.includes(formKey)) {
- newExistingRecords.address.push(formKey)
+ const isExisting = existingRecords.address.includes(formKey)
+ if (value && isExisting) {
+ setValue(`address.${formKey}`, value, { shouldDirty: true })
+ } else if (value && !isExisting) {
+ addAddressKey(formKey)
+ setValue(`address.${formKey}`, value, { shouldDirty: true })
+ } else if (!value && isExisting) {
+ removeAddressKey(formKey, false)
+ } else if (!value && !isExisting) {
+ removeAddressKey(formKey, true)
}
})
- if (overwrites?.contentHash) {
+ if (typeof overwrites?.contentHash === 'string') {
setValue('other.contentHash', overwrites.contentHash, { shouldDirty: true })
}
- setExistingRecords(newExistingRecords)
+ const abi = normalizeAbi(overwrites?.abi)
+ if (abi) {
+ setValue('other.abi', abi, { shouldDirty: true })
+ }
+
+ setShouldRunOverwritesScript(false)
}
+
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [profile])
+ }, [existingRecords, shouldRunOverwritesScript])
const { hasInterface: hasABIInterface, isLoading: isLoadingABIInterface } =
useResolverHasInterfaces(['IABIResolver'], profile?.resolverAddress, loading)
@@ -236,11 +272,6 @@ const useAdvancedEditor = ({ profile, loading, overwrites, callback }: Props) =>
value,
})) as { key: string; value: string }[]
- const coinTypesWithZeroAddressses = coinTypes.map((coinType) => {
- if (coinType.value) return coinType
- return { key: coinType.key, value: emptyAddress }
- })
-
const contentHash = dirtyFields.other?.contentHash
const abi = match(dirtyFields.other?.abi?.data)
@@ -250,7 +281,7 @@ const useAdvancedEditor = ({ profile, loading, overwrites, callback }: Props) =>
const records = {
texts,
- coinTypes: coinTypesWithZeroAddressses,
+ coinTypes,
contentHash,
abi,
}
@@ -276,7 +307,6 @@ const useAdvancedEditor = ({ profile, loading, overwrites, callback }: Props) =>
handleTabClick,
hasErrors,
existingRecords,
- setExistingRecords,
existingTextKeys,
newTextKeys,
addTextKey,
diff --git a/src/utils/records.ts b/src/utils/records.ts
index 214c13def..9e1e944b8 100644
--- a/src/utils/records.ts
+++ b/src/utils/records.ts
@@ -1,3 +1,6 @@
+import { BigNumberish } from '@ethersproject/bignumber'
+import { P, match as _match } from 'ts-pattern'
+
import { RecordOptions } from '@ensdomains/ensjs/utils/recordHelpers'
import { ContentHash, Profile, RecordItem } from '@app/types'
@@ -11,12 +14,27 @@ const contentHashTouple = (contentHash?: string, deleteLabel = 'delete'): [strin
return [['contenthash', contentHash]]
}
+const abiTouple = (
+ abi?: { contentType?: BigNumberish; data: string | object } | string,
+ deleteLabel = 'delete',
+): [string, string][] => {
+ const abiStr = _match(abi)
+ .with(P.string, (_abi) => _abi)
+ .with({ data: P.string }, ({ data }) => data)
+ .with({ data: {} }, ({ data }) => JSON.stringify(data))
+ .otherwise(() => undefined)
+ if (abiStr === '') return [[deleteLabel, 'abi']]
+ if (!abiStr) return []
+ return [['abi', abiStr]]
+}
+
export const recordOptionsToToupleList = (
records?: RecordOptions,
deleteLabel = 'delete',
): [string, string][] => {
return [
...contentHashTouple(records?.contentHash, deleteLabel),
+ ...abiTouple(records?.abi, deleteLabel),
...(records?.texts?.map(({ key, value }) => [key, value]) || []),
...(records?.coinTypes?.map(({ key, value }) => [key, shortenAddress(value)]) || []),
].map(([key, value]) => (value ? [key, value] : [deleteLabel, key]))