Skip to content

Commit

Permalink
Merge pull request #773 from ensdomains/feat/FET-1271-add-disabled-pr…
Browse files Browse the repository at this point in the history
…ofile-editing-button-when-owner-but-not-manager

Feat/fet 1271 add disabled profile editing button when owner but not manager
  • Loading branch information
LeonmanRolls authored Aug 2, 2024
2 parents fba88fb + 2d2293f commit feeabdc
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 11 deletions.
81 changes: 81 additions & 0 deletions e2e/specs/stateless/profileEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,3 +914,84 @@ test.describe('subgraph errors', () => {
await page.getByTestId('subgraph-network-error').uncheck()
})
})

test('Should show edit profile button if user is manager', async ({
login,
makeName,
makePageObject,
}) => {
const name = await makeName({
label: 'unwrapped',
type: 'legacy',
owner: 'user2',
manager: 'user',
})

const profilePage = makePageObject('ProfilePage')

await profilePage.goto(name)
await login.connect()

await expect(profilePage.editProfileButton).toBeVisible()
})

test('Should show disabled edit profile button if user is owner but not manager', async ({
page,
login,
makeName,
makePageObject,
}) => {
const name = await makeName({
label: 'unwrapped',
type: 'legacy',
owner: 'user',
manager: 'user2',
})

const profilePage = makePageObject('ProfilePage')

await profilePage.goto(name)
await login.connect()

await expect(page.getByTestId('disabled-profile-action-Edit profile')).toBeVisible()
})

test('Should show disabled edit profile button if name is wrapped but use for edit resolver is burned and resolver is unauthorised', async ({
page,
login,
makeName,
makePageObject,
}) => {
const name = await makeName({
label: 'wrapped',
type: 'wrapped',
owner: 'user',
})

const UNAUTHORISED_RESOLVER = '0xd7a4F6473f32aC2Af804B3686AE8F1932bC35750'
const profilePage = makePageObject('ProfilePage')
const morePage = makePageObject('MorePage')
const permissionsPage = makePageObject('PermissionsPage')
const transactionModal = makePageObject('TransactionModal')

await profilePage.goto(name)
await login.connect()

// Set resolver to unauthorized resolver
await morePage.goto(name)
await morePage.editResolverButton.click()
await page.getByTestId('custom-resolver-radio').check()
await page.getByTestId('dogfood').fill(UNAUTHORISED_RESOLVER)
await page.getByTestId('update-button').click()
await transactionModal.autoComplete()

// Burn CSR fuse
await permissionsPage.goto(name)
await permissionsPage.burnChildPermissions(['CANNOT_UNWRAP', 'CANNOT_SET_RESOLVER'], name)
await transactionModal.autoComplete()

// Validate that the edit profile button is disabled
await profilePage.goto(name)

await expect(page.getByTestId('disabled-profile-action-Edit profile')).toBeVisible()
})
4 changes: 2 additions & 2 deletions playwright/pageObjects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { PermissionsPage } from './permissionsPage'
import { ProfilePage } from './profilePage'
import { RecordsPage } from './recordsPage'
import { RegistrationPage } from './registrationPage'
import { SelectPrimaryNameModal } from './selectPrimaryNameModal'
import { SendNameModal } from './sendNameModal'
import {SettingsPage} from './settingsPage'
import {SelectPrimaryNameModal} from './selectPrimaryNameModal'
import { SettingsPage } from './settingsPage'
import { SubnamesPage } from './subnamePage'
import { TransactionModal } from './transactionModal'

Expand Down
5 changes: 3 additions & 2 deletions public/locales/en/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
"grace-period": {
"title": "Grace period ends",
"tooltip": "A 90 day grace window after expiration, when the name can still be extended but not re-registered."

},
"registration": {
"title": "Registered"
Expand Down Expand Up @@ -453,6 +452,8 @@
"permissionRevoked": "This name has revoked the permissions needed to perform this action.",
"gracePeriod": "This cannot be done because the name has expired",
"default": "This action is not available",
"invalidJSON": "Invalid JSON"
"invalidJSON": "Invalid JSON",
"isOwnerCannotEdit": "You must be the manager in order to edit the profile",
"cannotEdit": "You do not have permission to update to an authorised resolver"
}
}
74 changes: 74 additions & 0 deletions src/hooks/useProfileActions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mockFunction, renderHook, waitFor } from '@app/test-utils'

import { makeMockUseAbilitiesData } from '@root/test/mock/makeMockUseAbilitiesData'
import { labelhash } from 'viem'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -538,4 +539,77 @@ describe('useProfileActions', () => {
).toEqual(1)
})
})

describe('edit profile button', () => {
it('Should show edit profile button if user is manager', () => {
mockUseAbilities.mockReturnValue({
data: makeMockUseAbilitiesData('eth-unwrapped-2ld:manager'),
isLoading: false,
})

const { result } = renderHook(() => useProfileActions({ name: 'test.eth' }))
expect(result.current.profileActions).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: 'tabs.profile.actions.editProfile.label',
tooltipContent: undefined,
}),
]),
)
})

it('Should show disabled profile button if there is a graph error', () => {
mockUseHasGraphError.mockReturnValue({
data: true,
isLoading: false,
})

const { result } = renderHook(() => useProfileActions({ name: 'test.eth' }))
expect(result.current.profileActions).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: 'tabs.profile.actions.editProfile.label',
tooltipContent: 'errors.networkError.blurb',
}),
]),
)
})

it('Should show disabled edit profile button if user is owner but not manager', () => {
mockUseAbilities.mockReturnValue({
data: makeMockUseAbilitiesData('eth-unwrapped-2ld:owner'),
isLoading: false,
})

const { result } = renderHook(() => useProfileActions({ name: 'test.eth' }))
expect(result.current.profileActions).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: 'tabs.profile.actions.editProfile.label',
tooltipContent: 'errors.isOwnerCannotEdit',
}),
]),
)
})

it('Should show disabled edit profile button if name is wrapped but fuse for edit resolver is burned and resolver is unauthorised', () => {
mockUseAbilities.mockReturnValue({
data: {
...makeMockUseAbilitiesData('eth-burnt-2ld'),
canEditRecords: false,
},
isLoading: false,
})

const { result } = renderHook(() => useProfileActions({ name: 'test.eth' }))
expect(result.current.profileActions).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: 'tabs.profile.actions.editProfile.label',
tooltipContent: 'errors.cannotEdit',
}),
]),
)
})
})
})
32 changes: 26 additions & 6 deletions src/hooks/useProfileActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { TFunction, useTranslation } from 'react-i18next'

import { checkIsDecrypted } from '@ensdomains/ensjs/utils'

Expand All @@ -11,7 +11,7 @@ import { checkAvailablePrimaryName } from '@app/utils/checkAvailablePrimaryName'
import { nameParts } from '@app/utils/name'
import { useHasGraphError } from '@app/utils/SyncProvider/SyncProvider'

import { useAbilities } from './abilities/useAbilities'
import { Abilities, useAbilities } from './abilities/useAbilities'
import { useAccountSafely } from './account/useAccountSafely'
import { useExpiry } from './ensjs/public/useExpiry'
import { useOwner } from './ensjs/public/useOwner'
Expand Down Expand Up @@ -39,6 +39,21 @@ type Props = {
enabled?: boolean
}

const editButtonTooltip = ({
hasGraphError,
abilities,
t,
}: {
hasGraphError: boolean
abilities: Abilities
t: TFunction
}) => {
if (hasGraphError) return t('errors.networkError.blurb', { ns: 'common' })
if (!abilities.canEdit) return t('errors.isOwnerCannotEdit')
if (!abilities.canEditRecords && !abilities.canEditResolver) return t('errors.cannotEdit')
return undefined
}

export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => {
const { t } = useTranslation('profile')
const { createTransactionFlow, usePreparedDataInput } = useTransactionFlow()
Expand Down Expand Up @@ -140,12 +155,15 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) =>
})
}

if (abilities.canEdit && (abilities.canEditRecords || abilities.canEditResolver)) {
const isOwnerOrManager = address === ownerData?.owner || ownerData?.registrant === address
if (isOwnerOrManager) {
actions.push({
label: t('tabs.profile.actions.editProfile.label'),
tooltipContent: hasGraphError
? t('errors.networkError.blurb', { ns: 'common' })
: undefined,
tooltipContent: editButtonTooltip({
hasGraphError: !!hasGraphError,
abilities,
t,
}),
tooltipPlacement: 'left',
loading: hasGraphErrorLoading,
onClick: () =>
Expand Down Expand Up @@ -271,6 +289,8 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) =>
getPrimaryNameTransactionFlowItem,
name,
isAvailablePrimaryName,
ownerData?.owner,
ownerData?.registrant,
abilities.canEdit,
abilities.canEditRecords,
abilities.canEditResolver,
Expand Down
4 changes: 3 additions & 1 deletion vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export default defineConfig({
globals: false,
environment: 'jsdom',
alias: {
// eslint-disable-next-line @typescript-eslint/naming-convention
/* eslint-disable @typescript-eslint/naming-convention */
'@app/': new URL('./src/', import.meta.url).pathname,
'@root/': new URL('./', import.meta.url).pathname,
/* eslint-enable @typescript-eslint/naming-convention */
},
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
setupFiles: ['./test/textencoder-setup.mts', './test/websocket-setup.mts'],
Expand Down

0 comments on commit feeabdc

Please sign in to comment.