Skip to content

Commit

Permalink
Merge branch 'dev' into fix-4484
Browse files Browse the repository at this point in the history
  • Loading branch information
mkbeefcake committed Oct 7, 2023
2 parents 4de58ad + d2fba2f commit 1bb1095
Show file tree
Hide file tree
Showing 23 changed files with 1,095 additions and 145 deletions.
4 changes: 4 additions & 0 deletions packages/ui/src/app/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { VoteRationale } from '@/proposals/modals/VoteRationale/VoteRationale'
import { ApplicationDetailsModal, ApplicationDetailsModalCall } from '@/working-groups/modals/ApplicationDetailsModal'
import { ApplyForRoleModal, ApplyForRoleModalCall } from '@/working-groups/modals/ApplyForRoleModal'
import { ChangeAccountModal, ChangeAccountModalCall } from '@/working-groups/modals/ChangeAccountModal'
import { CreateOpeningModal, CreateOpeningModalCall } from '@/working-groups/modals/CreateOpening'
import {
IncreaseWorkerStakeModal,
IncreaseWorkerStakeModalCall,
Expand All @@ -87,6 +88,7 @@ export type ModalNames =
| ModalName<MoveFundsModalCall>
| ModalName<AddNewProposalModalCall>
| ModalName<VoteRationaleModalCall>
| ModalName<CreateOpeningModalCall>
| ModalName<CreateThreadModalCall>
| ModalName<DeleteThreadModalCall>
| ModalName<DeletePostModalCall>
Expand Down Expand Up @@ -131,6 +133,7 @@ const modals: Record<ModalNames, ReactElement> = {
ApplyForRoleModal: <ApplyForRoleModal />,
ApplicationDetails: <ApplicationDetailsModal />,
SwitchMember: <SwitchMemberModal />,
CreateOpening: <CreateOpeningModal />,
LeaveRole: <LeaveRoleModal />,
ChangeAccountModal: <ChangeAccountModal />,
MoveFundsModal: <MoveFundsModal />,
Expand Down Expand Up @@ -193,6 +196,7 @@ const GUEST_ACCESSIBLE_MODALS: ModalNames[] = [
export const MODAL_WITH_CLOSE_CONFIRMATION: ModalNames[] = [
'AddNewProposalModal',
'AnnounceCandidateModal',
'CreateOpening',
'CreatePost',
'CreateThreadModal',
'ApplyForRoleModal',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { OpeningMetadata } from '@joystream/metadata-protobuf'
import { expect } from '@storybook/jest'
import { Meta, StoryContext, StoryObj } from '@storybook/react'
import { userEvent, waitFor, within } from '@storybook/testing-library'
import BN from 'bn.js'
import { FC } from 'react'

import { metadataFromBytes } from '@/common/model/JoystreamNode'
import { member } from '@/mocks/data/members'
import { joy } from '@/mocks/helpers'
import { getButtonByText, getEditorByLabel, joy, withinModal } from '@/mocks/helpers'
import { MocksParameters } from '@/mocks/providers'
import { GetWorkersDocument, GetWorkingGroupDocument } from '@/working-groups/queries'

import { WorkingGroup } from './WorkingGroup'

type Args = {
isLead: boolean
isLoggedIn: boolean
onCreateOpening: jest.Mock
}

type Story = StoryObj<FC<Args>>
Expand All @@ -20,12 +25,41 @@ const WG_DATA = {
name: 'membership',
}

const WG_OPENING_METADATA = {
title: 'Membership worker role',
shortDescription: 'Lorem Ipsum...',
description: 'Bigger Lorem ipsum...',
applicationDetails: 'Application process default',
applicationFormQuestions: [
{ question: '🐁?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXT },
{ question: '🐘?', type: OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA },
],
hiringLimit: 5,
expectedEndingTimestamp: 2000,
}

const WG_JSON_OPENING = {
...WG_OPENING_METADATA,
applicationFormQuestions: [
{ question: '🐁?', type: 'TEXT' },
{ question: '🐘?', type: 'TEXTAREA' },
],
rewardPerBlock: 20_0000000000,
stakingPolicy: {
unstakingPeriod: 200,
amount: 200_0000000000,
},
}

export default {
title: 'Pages/Working Group/WorkingGroup',
component: WorkingGroup,

argTypes: {
onCreateOpening: { action: 'MembershipWorkingGroup.OpeningCreated' },
},

args: {
isLoggedIn: true,
isLead: true,
},

Expand All @@ -47,9 +81,25 @@ export default {
],
})
return {
accounts: parameters.isLoggedIn ? { active: { member: alice } } : { list: [{ member: alice }] },
accounts: { active: { member: alice } },

// chain: undefined,
chain: {
tx: {
membershipWorkingGroup: {
addOpening: {
event: 'OpeningCreated',
onSend: args.onCreateOpening,
failure: parameters.createOpeningFailure,
},
},
},
consts: {
membershipWorkingGroup: {
minimumApplicationStake: joy(10),
minUnstakingPeriodLimit: 100,
},
},
},

queryNode: [
{
Expand All @@ -60,7 +110,7 @@ export default {
name: WG_DATA.name,
budget: joy(200),
workers: [],
leader: { membershipId: alice.id, isActive: true },
leader: { membershipId: alice.id, isActive: args.isLead },
},
},
},
Expand Down Expand Up @@ -96,3 +146,144 @@ export default {
} satisfies Meta<Args>

export const Default: Story = {}

export const CreateOpening: Story = {
play: async ({ args, canvasElement, step }) => {
const screen = within(canvasElement)
const modal = withinModal(canvasElement)

await userEvent.click(screen.getByText('Add opening'))
expect(modal.getByText('Create Opening'))
const nextButton = getButtonByText(modal, 'Next step')

await step('Working group & description', async () => {
const openingTitleField = await modal.findByLabelText('Opening title')
const shortDescriptionField = modal.getByLabelText('Short description')

await waitFor(() => expect(nextButton).toBeDisabled())

await userEvent.type(openingTitleField, 'Membership worker role')
await userEvent.type(shortDescriptionField, 'Lorem Ipsum...')
;(await getEditorByLabel(modal, 'Description')).setData('Bigger Lorem ipsum...')
await waitFor(() => expect(nextButton).toBeEnabled())
await userEvent.click(nextButton)
})
await step('Duration & Process', async () => {
await waitFor(() => expect(nextButton).toBeDisabled())
;(await getEditorByLabel(modal, 'Application process')).setData('Application process default')
await waitFor(() => expect(nextButton).toBeEnabled())
const hiringTargetField = modal.getByLabelText('Hiring Target')
await userEvent.clear(hiringTargetField)
await userEvent.type(hiringTargetField, '5')
await userEvent.click(modal.getByText('Limited'))
const expectedLengthField = modal.getByLabelText('Expected length of the application period')
await userEvent.clear(expectedLengthField)
await userEvent.type(expectedLengthField, '2000')
await waitFor(() => expect(nextButton).toBeEnabled())
await userEvent.click(nextButton)
})
await step('Application Form', async () => {
await waitFor(() => expect(nextButton).toBeDisabled())
await userEvent.type(modal.getByRole('textbox'), '🐁?')
await waitFor(() => expect(nextButton).toBeEnabled())
await userEvent.click(modal.getByText('Add new question'))
await waitFor(() => expect(nextButton).toBeDisabled())
await userEvent.click(modal.getAllByText('Long answer')[1])
await userEvent.type(modal.getAllByRole('textbox')[1], '🐘?')
await waitFor(() => expect(nextButton).toBeEnabled())
await userEvent.click(nextButton)
})
await step('Staking Policy & Reward', async () => {
const createButton = getButtonByText(modal, 'Create Opening')
expect(createButton).toBeDisabled()
await userEvent.type(modal.getByLabelText('Staking amount *'), '100')
await userEvent.clear(modal.getByLabelText('Role cooldown period'))
await userEvent.type(modal.getByLabelText('Role cooldown period'), '1000')
await userEvent.type(modal.getByLabelText('Reward amount per Block'), '0.1')
await waitFor(() => expect(createButton).toBeEnabled())
await userEvent.click(createButton)
})

await step('Sign transaction and Create', async () => {
expect(await modal.findByText('You intend to create an Opening.'))
await userEvent.click(modal.getByText('Sign transaction and Create'))
})

step('Transaction parameters', () => {
const [description, openingType, stakePolicy, rewardPerBlock] = args.onCreateOpening.mock.calls.at(-1)

expect(stakePolicy.toJSON()).toEqual({
stakeAmount: 100_0000000000,
leavingUnstakingPeriod: 1000,
})
expect(new BN(rewardPerBlock).toNumber()).toEqual(1000000000)

expect(openingType).toEqual('Regular')
expect(metadataFromBytes(OpeningMetadata, description)).toEqual(WG_OPENING_METADATA)
})
},
}
export const CreateOpeningImport: Story = {
play: async ({ args, canvasElement, step }) => {
const screen = within(canvasElement)
const modal = withinModal(canvasElement)

await userEvent.click(screen.getByText('Add opening'))
expect(modal.getByText('Create Opening'))
const nextButton = getButtonByText(modal, 'Next step')

await step('Import File', async () => {
const importButton = getButtonByText(modal, 'Import')

await userEvent.click(importButton)

const uploadField = await modal.findByText(/Browse for file/)
expect(getButtonByText(modal, 'Preview Import')).toBeDisabled()

await userEvent.upload(
uploadField,
new File([JSON.stringify(WG_JSON_OPENING)], 'file.json', { type: 'application/json' })
)
})
await step('Check imported data', async () => {
expect(await modal.findByText(/File imported successfully, preview your input/))

const previewImportButton = getButtonByText(modal, 'Preview Import')
await waitFor(() => expect(previewImportButton).toBeEnabled())
await userEvent.click(previewImportButton)

expect(await modal.findByLabelText('Opening title'))
expect(nextButton).toBeEnabled()
await userEvent.click(nextButton)

expect(await modal.findByText('Opening Duration'))
await userEvent.click(modal.getByText('Limited'))
expect(nextButton).toBeEnabled()
await userEvent.click(nextButton)

expect(await modal.findByText('Application form'))
expect(nextButton).toBeEnabled()
await userEvent.click(nextButton)

expect(await modal.getByLabelText('Staking amount *'))
const createButton = getButtonByText(modal, 'Create Opening')
await userEvent.click(createButton)
})
await step('Sign transaction and Create', async () => {
expect(await modal.findByText('You intend to create an Opening.'))
await userEvent.click(modal.getByText('Sign transaction and Create'))
})
step('Transaction parameters', () => {
const [description, openingType, stakePolicy, rewardPerBlock] = args.onCreateOpening.mock.calls.at(-1)

expect(stakePolicy.toJSON()).toEqual({
stakeAmount: 200_0000000000,
leavingUnstakingPeriod: 200,
})
expect(new BN(rewardPerBlock).toNumber()).toEqual(200000000000)

expect(openingType).toEqual('Regular')
expect(metadataFromBytes(OpeningMetadata, description)).toEqual(WG_OPENING_METADATA)
})
},
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'

import { PageLayout, PageHeaderWrapper } from '@/app/components/PageLayout'
import { PageLayout, PageHeaderWrapper, PageHeaderRow } from '@/app/components/PageLayout'
import { ButtonsGroup } from '@/common/components/buttons'
import { Loading } from '@/common/components/Loading'
import { PageTitle } from '@/common/components/page/PageTitle'
import { PreviousPage } from '@/common/components/page/PreviousPage'
import { Tabs } from '@/common/components/Tabs'
import { nameMapping } from '@/common/helpers'
import { CreateOpeningButton } from '@/working-groups/components/CreateOpeningButton'
import { useMyWorkers } from '@/working-groups/hooks/useMyWorkers'
import { useWorkingGroup } from '@/working-groups/hooks/useWorkingGroup'
import { urlParamToWorkingGroupId } from '@/working-groups/model/workingGroupName'

Expand All @@ -22,6 +25,11 @@ export function WorkingGroup() {
const [currentTab, setCurrentTab] = useState<Tab>('OPENINGS')
const { name } = useParams<{ name: string }>()
const { isLoading, group } = useWorkingGroup({ name: urlParamToWorkingGroupId(name) })
const { workers } = useMyWorkers()
const isLead = useMemo(
() => group?.isActive && workers.find((w) => w.membership.id === group?.leadId),
[workers, group?.isActive, group?.leadId]
)

const tabs = [
{ title: 'Openings', active: currentTab === 'OPENINGS', onClick: () => setCurrentTab('OPENINGS') },
Expand Down Expand Up @@ -58,14 +66,22 @@ export function WorkingGroup() {
<PageLayout
header={
<PageHeaderWrapper>
<PreviousPage>
<PageTitle>{nameMapping(group?.name ?? name)}</PageTitle>
{group?.status && (
<StatusGroup>
<StatusBadge>{group?.status}</StatusBadge>
</StatusGroup>
<PageHeaderRow>
<PreviousPage>
<PageTitle>{nameMapping(group?.name ?? name)}</PageTitle>
{group?.status && (
<StatusGroup>
<StatusBadge>{group?.status}</StatusBadge>
</StatusGroup>
)}
</PreviousPage>
{group && isLead && currentTab === 'OPENINGS' && (
<ButtonsGroup>
<CreateOpeningButton group={group.id} />
</ButtonsGroup>
)}
</PreviousPage>
</PageHeaderRow>

<Tabs tabs={tabs} />
</PageHeaderWrapper>
}
Expand Down
10 changes: 6 additions & 4 deletions packages/ui/src/common/components/buttons/DownloadButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import {

export interface DownloadLinkProps {
name: string
parts: BlobPart[]
parts?: BlobPart[]
content?: string
type?: string
className?: string
disabled?: boolean
}

export const DownloadLink: FC<DownloadLinkProps> = ({ name, parts, type, className, children }) => {
export const DownloadLink: FC<DownloadLinkProps> = ({ name, parts, content, type, className, children }) => {
const [href, setHref] = useState('')

useEffect(() => {
const url = URL.createObjectURL(new Blob(parts, { type: type ?? getType(name) ?? undefined }))
const blobParts = content ? [content] : parts
const url = URL.createObjectURL(new Blob(blobParts, { type: type ?? getType(name) ?? undefined }))
setHref(url)
return () => URL.revokeObjectURL(url)
}, [])
}, [content])

return (
<a className={className} href={href} download={name} target="__blank">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export const StepperProposalWrapper = styled(StepperModalWrapper)`
grid-template-columns: 220px 336px 1fr;
`

const StyledStepperBody = styled(StepperBody)`
export const StyledStepperBody = styled(StepperBody)`
flex-direction: column;
row-gap: 20px;
`
Loading

0 comments on commit 1bb1095

Please sign in to comment.