Skip to content

Commit

Permalink
feat: add contribute tab (#3102)
Browse files Browse the repository at this point in the history
  • Loading branch information
Melisa Anabella Rossi authored May 20, 2024
1 parent 47bc85e commit f1266d7
Show file tree
Hide file tree
Showing 45 changed files with 1,359 additions and 176 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"decentraland-ecs": "6.12.4-7784644013.commit-f770b3e",
"decentraland-experiments": "^1.0.2",
"decentraland-transactions": "^2.6.1",
"decentraland-ui": "^5.23.2",
"decentraland-ui": "^5.23.3",
"ethers": "^5.6.8",
"file-saver": "^2.0.1",
"graphql": "^15.8.0",
Expand Down
10 changes: 9 additions & 1 deletion src/components/WorldListPage/NameTabs/NameTabs.container.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { connect } from 'react-redux'
import { push } from 'connected-react-router'
import { getIsWorldContributorEnabled } from 'modules/features/selectors'
import { RootState } from 'modules/common/types'
import NameTabs from './NameTabs'
import { MapDispatch, MapDispatchProps } from './NameTabs.types'

const mapState = (state: RootState) => {
return {
isWorldContributorEnabled: getIsWorldContributorEnabled(state)
}
}

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => {
return {
onNavigate: to => dispatch(push(to))
}
}

export default connect(null, mapDispatch)(NameTabs)
export default connect(mapState, mapDispatch)(NameTabs)
80 changes: 67 additions & 13 deletions src/components/WorldListPage/NameTabs/NameTabs.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const mockUseCurrentlySelectedTab = useCurrentlySelectedTab as jest.Mock
const mockMobile = Mobile as jest.Mock
const mockNotMobile = NotMobile as jest.Mock

const dclTabText = 'Decentraland NAMEs'
const ensTabText = 'ENS Domains'
const contributorTabText = 'Contributor'

describe('when rendering the name tabs component', () => {
let useCurrentlySelectedTabResult: UseCurrentlySelectedTabResult
let onNavigate: jest.Mock
Expand All @@ -33,7 +37,7 @@ describe('when rendering the name tabs component', () => {
})

it('should call the on navigate prop with the pathname, url search params and an added tab query param with dcl as value', () => {
render(<NameTabs onNavigate={onNavigate} />)
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)
expect(onNavigate).toHaveBeenCalledWith(
`${useCurrentlySelectedTabResult.pathname}?${useCurrentlySelectedTabResult.urlSearchParams.toString()}&${TAB_QUERY_PARAM_KEY}=${
TabType.DCL
Expand All @@ -42,24 +46,20 @@ describe('when rendering the name tabs component', () => {
})
})

describe('when the tab param is returned as dcl or ens bu the currently selected tab hook', () => {
let dclTabText: string
let ensTabText: string

describe('when the tab param is returned as dcl, ens or contributor by the currently selected tab hook', () => {
beforeEach(() => {
useCurrentlySelectedTabResult.tab = TabType.DCL
dclTabText = 'Decentraland NAMEs'
ensTabText = 'ENS Domains'

mockMobile.mockImplementation(({ children }) => children as ReactNode)
mockNotMobile.mockImplementation(() => null)
})

it('should render both the ens and dcl tabs name tabs', () => {
render(<NameTabs onNavigate={onNavigate} />)
it('should render both the ens, dcl and contributor tabs name tabs', () => {
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

expect(screen.getByText(dclTabText)).toBeInTheDocument()
expect(screen.getByText(ensTabText)).toBeInTheDocument()
expect(screen.getByText(contributorTabText)).toBeInTheDocument()
})

describe('when the tab param is ens', () => {
Expand All @@ -68,9 +68,10 @@ describe('when rendering the name tabs component', () => {
})

it('should add the active css class to the ens tab', () => {
render(<NameTabs onNavigate={onNavigate} />)
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

expect(screen.getByText(ensTabText)).toHaveClass('active')
expect(screen.getByText(contributorTabText)).not.toHaveClass('active')
expect(screen.getByText(dclTabText)).not.toHaveClass('active')
})
})
Expand All @@ -81,16 +82,31 @@ describe('when rendering the name tabs component', () => {
})

it('should add the active css class to the dcl tab', () => {
render(<NameTabs onNavigate={onNavigate} />)
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

expect(screen.getByText(ensTabText)).not.toHaveClass('active')
expect(screen.getByText(contributorTabText)).not.toHaveClass('active')
expect(screen.getByText(dclTabText)).toHaveClass('active')
})
})

describe('when the tab param is contributor', () => {
beforeEach(() => {
useCurrentlySelectedTabResult.tab = TabType.CONTRIBUTOR
})

it('should add the active css class to the contributor tab', () => {
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

expect(screen.getByText(ensTabText)).not.toHaveClass('active')
expect(screen.getByText(contributorTabText)).toHaveClass('active')
expect(screen.getByText(dclTabText)).not.toHaveClass('active')
})
})

describe('when the dcl tab is clicked', () => {
it('should call the on navigate prop with the current pathname + the tab query param with dcl as value', () => {
render(<NameTabs onNavigate={onNavigate} />)
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

screen.getByText(dclTabText).click()

Expand All @@ -104,7 +120,7 @@ describe('when rendering the name tabs component', () => {

describe('when the ens tab is clicked', () => {
it('should call the on navigate prop with the current pathname + the tab query param with ens as value', () => {
render(<NameTabs onNavigate={onNavigate} />)
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

screen.getByText(ensTabText).click()

Expand All @@ -115,5 +131,43 @@ describe('when rendering the name tabs component', () => {
)
})
})

describe('when the contributor tab is clicked', () => {
it('should call the on navigate prop with the current pathname + the tab query param with contributor as value', () => {
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled />)

screen.getByText(contributorTabText).click()

expect(onNavigate).toHaveBeenCalledWith(
`${useCurrentlySelectedTabResult.pathname}?${useCurrentlySelectedTabResult.urlSearchParams.toString()}&${TAB_QUERY_PARAM_KEY}=${
TabType.CONTRIBUTOR
}`
)
})
})
})
})

describe('when isWorldContributorEnabled is false', () => {
let useCurrentlySelectedTabResult: UseCurrentlySelectedTabResult
let onNavigate: jest.Mock

beforeEach(() => {
useCurrentlySelectedTabResult = {
tab: TabType.DCL,
pathname: '/pathname',
urlSearchParams: new URLSearchParams('?foo=bar')
} as UseCurrentlySelectedTabResult

mockUseCurrentlySelectedTab.mockReturnValueOnce(useCurrentlySelectedTabResult)

onNavigate = jest.fn()
})

it('should not show the contributor tab', () => {
render(<NameTabs onNavigate={onNavigate} isWorldContributorEnabled={false} />)
expect(screen.getByText(dclTabText)).toBeInTheDocument()
expect(screen.getByText(ensTabText)).toBeInTheDocument()
expect(screen.queryByText(contributorTabText)).not.toBeInTheDocument()
})
})
9 changes: 7 additions & 2 deletions src/components/WorldListPage/NameTabs/NameTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Props } from './NameTabs.types'
import { TAB_QUERY_PARAM_KEY, TabType, useCurrentlySelectedTab } from '../hooks'

const NameTabs = ({ onNavigate }: Props) => {
const NameTabs = ({ isWorldContributorEnabled, onNavigate }: Props) => {
const { tab, pathname, urlSearchParams } = useCurrentlySelectedTab()

const navigateToTab = (tab: TabType) => {
Expand All @@ -13,7 +13,7 @@ const NameTabs = ({ onNavigate }: Props) => {
onNavigate(`${pathname}?${urlSearchParamsCopy.toString()}`)
}

if (!tab) {
if (!tab || (tab === TabType.CONTRIBUTOR && !isWorldContributorEnabled)) {
navigateToTab(TabType.DCL)
return null
}
Expand All @@ -26,6 +26,11 @@ const NameTabs = ({ onNavigate }: Props) => {
<Tabs.Tab active={tab === TabType.ENS} onClick={() => navigateToTab(TabType.ENS)}>
{t('worlds_list_page.name_tabs.ens_names')}
</Tabs.Tab>
{isWorldContributorEnabled && (
<Tabs.Tab active={tab === TabType.CONTRIBUTOR} onClick={() => navigateToTab(TabType.CONTRIBUTOR)}>
{t('worlds_list_page.name_tabs.contributor_names')}
</Tabs.Tab>
)}
</Tabs>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/WorldListPage/NameTabs/NameTabs.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { CallHistoryMethodAction } from 'connected-react-router'
import { Dispatch } from 'react'

export type Props = {
isWorldContributorEnabled: boolean
onNavigate: (to: string) => void
}

export type MapStateProps = Pick<Props, 'isWorldContributorEnabled'>
export type MapDispatchProps = Pick<Props, 'onNavigate'>
export type MapDispatch = Dispatch<CallHistoryMethodAction>
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { t } from 'decentraland-dapps/dist/modules/translation'
import { ENS } from 'modules/ens/types'
import { Deployment } from 'modules/deployment/types'
import { Project } from 'modules/project/types'
import { Props } from './PublishSceneButton.types'
import PublishSceneButton from './PublishSceneButton'

const ens = {
name: 'test',
subdomain: 'test',
content: '',
ensOwnerAddress: '0xtest1',
nftOwnerAddress: '0xtest1',
resolver: '0xtest3',
tokenId: '',
ensAddressRecord: '',
worldStatus: {
healthy: true
}
} as ENS

let deploymentsByWorlds: Record<string, Deployment>
let projects: Project[]

function renderPublishSceneButton(props: Partial<Props>) {
return render(
<PublishSceneButton
deploymentsByWorlds={{}}
ens={ens}
projects={[]}
onEditScene={jest.fn()}
onPublishScene={jest.fn()}
onUnpublishScene={jest.fn()}
{...props}
/>
)
}

describe('when the world has a scene deployed', () => {
beforeEach(() => {
deploymentsByWorlds = {
[ens.subdomain]: {
projectId: '1',
name: 'Deployment'
} as Deployment
}
})

describe('and the user has access to the deployed project', () => {
beforeEach(() => {
projects = [{ id: '1' } as Project]
})

it('should show the edit scene button', () => {
const screen = renderPublishSceneButton({ projects, deploymentsByWorlds })
expect(screen.getByRole('button', { name: t('worlds_list_page.table.edit_scene') })).toBeInTheDocument()
})

describe('when the editScene button is clicked', () => {
it('should trigger onEditScene callback action', () => {
const onEditScene = jest.fn()
const screen = renderPublishSceneButton({ onEditScene, projects, deploymentsByWorlds })
const editSceneButton = screen.getByRole('button', { name: t('worlds_list_page.table.edit_scene') })
userEvent.click(editSceneButton)
expect(onEditScene).toHaveBeenCalled()
})
})
})

describe("and the user doesn't have access to the deployed project", () => {
beforeEach(() => {
projects = []
})

it('should show the unpublish scene button', () => {
const screen = renderPublishSceneButton({ projects, deploymentsByWorlds })
expect(screen.getByRole('button', { name: t('worlds_list_page.table.unpublish_scene') })).toBeInTheDocument()
})

describe('when the unpublish button is clicked', () => {
it('should trigger onUnpublish callback action', () => {
const onUnpublishScene = jest.fn()
const screen = renderPublishSceneButton({ onUnpublishScene, projects, deploymentsByWorlds })
const unpublishSceneButton = screen.getByRole('button', { name: t('worlds_list_page.table.unpublish_scene') })
userEvent.click(unpublishSceneButton)
expect(onUnpublishScene).toHaveBeenCalled()
})
})
})
})

describe('when the world has no scene deployed', () => {
it('should show the publish scene button', () => {
const screen = renderPublishSceneButton({})
expect(screen.getByRole('button', { name: t('worlds_list_page.table.publish_scene') })).toBeInTheDocument()
})

describe('when the publish button is clicked', () => {
it('should trigger onPublish callback action', () => {
const onPublishScene = jest.fn()
const screen = renderPublishSceneButton({ onPublishScene })
const publishButton = screen.getByRole('button', { name: t('worlds_list_page.table.publish_scene') })
userEvent.click(publishButton)
expect(onPublishScene).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Button, Popup } from 'decentraland-ui'
import { Props } from './PublishSceneButton.types'
import { isWorldDeployed } from '../utils'
import { t } from 'decentraland-dapps/dist/modules/translation'

export default function PublishSceneButton({
deploymentsByWorlds,
ens,
projects,
onEditScene,
onUnpublishScene,
onPublishScene
}: Props): JSX.Element {
const deployment = deploymentsByWorlds[ens.subdomain]
return isWorldDeployed(deploymentsByWorlds, ens) ? (
<div className="publish-scene">
<Popup content={deployment?.name} on="hover" trigger={<span>{deployment?.name}</span>} />
{projects.find(project => project.id === deployment?.projectId)
? onEditScene && (
<Button inverted size="small" onClick={() => onEditScene(ens)}>
{t('worlds_list_page.table.edit_scene')}
</Button>
)
: onUnpublishScene && (
<Popup
content={t('worlds_list_page.table.scene_published_outside_builder')}
on="hover"
position="top center"
trigger={
<Button inverted size="small" onClick={() => onUnpublishScene(ens)}>
{t('worlds_list_page.table.unpublish_scene')}
</Button>
}
/>
)}
</div>
) : (
<div className="publish-scene">
<span>-</span>
{onPublishScene && (
<Button primary size="small" onClick={onPublishScene}>
{t('worlds_list_page.table.publish_scene')}
</Button>
)}
</div>
)
}
Loading

0 comments on commit f1266d7

Please sign in to comment.