diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 678e9217e..709ddcd59 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,6 +32,7 @@ A clear and concise description of what you expected to happen. - [ ] I have tested on Chrome desktop - [ ] I have posted a screenshot or video in my PR - [ ] I have rebased and tested locally before submitting my PR +- [ ] I can submit a PR within 2 days of taking the bounty Here is an [example unit test](https://github.com/stakwork/sphinx-tribes/blob/master/frontend/app/src/helpers/__test__/helpers.spec.ts) Here is an [example component test](https://github.com/stakwork/sphinx-tribes/blob/9310f49b3b17a51992dada932f4298eb9eba15ff/frontend/app/src/people/widgetViews/__tests__/AboutView.spec.tsx) diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index e3ee0c788..33b10fd68 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -14,8 +14,9 @@ assignees: '' ### Acceptance Criteria - [ ] I've tested on Chrome - [ ] I've submitted a screenshot or recording in my pr -- [ ] I've created a test that +- [ ] I've created a test that... - [ ] I have rebased and tested locally before submitting my PR +- [ ] I can submit a pr within 2 days of taking the bounty Here is an [example unit test](https://github.com/stakwork/sphinx-tribes/blob/master/frontend/app/src/helpers/__test__/helpers.spec.ts) diff --git a/.github/ISSUE_TEMPLATE/test-tickets.md b/.github/ISSUE_TEMPLATE/test-tickets.md index 6c18af667..cb2fa960b 100644 --- a/.github/ISSUE_TEMPLATE/test-tickets.md +++ b/.github/ISSUE_TEMPLATE/test-tickets.md @@ -18,6 +18,7 @@ assignees: '' ### Acceptance Criteria - [ ] I have rebased and tested locally before submitting my PR +- [ ] I can submit a pr within 2 days of taking the bounty ### References - Watch this [Jest Youtube playlist](https://www.youtube.com/watch?v=T2sv8jXoP4s&list=PLC3y8-rFHvwirqe1KHFCHJ0RqNuN61SJd) if you are new to testing diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index fcce331e2..e20dc098e 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -144,7 +144,12 @@ Cypress.Commands.add('create_bounty', (bounty) => { if (bounty.estimate_session_length) { cy.get('button[data-testid="Estimate Session Length"]').click({ force: true }); + cy.get('.euiSuperSelect__listbox').should('be.visible'); cy.get('.euiSuperSelect__listbox').contains(bounty.estimate_session_length).click(); + cy.get('button[data-testid="Estimate Session Length"]').should( + 'contain', + bounty.estimate_session_length + ); } if (bounty.estimate_completion_date) { diff --git a/src/__test__/__mockData__/bounty.ts b/src/__test__/__mockData__/bounty.ts new file mode 100644 index 000000000..b6e6b11b5 --- /dev/null +++ b/src/__test__/__mockData__/bounty.ts @@ -0,0 +1,87 @@ +export const mockBountyRoles = [ + { name: 'ADD BOUNTY' }, + { name: 'UPDATE BOUNTY' }, + { name: 'DELETE BOUNTY' }, + { name: 'PAY BOUNTY' }, + { name: 'VIEW REPORT' } +]; +export const bountyResponse = [ + { + bounty: { + id: 1384, + owner_id: 'dummy_owner_id', + paid: false, + show: true, + type: 'freelance_job_request', + award: '', + assigned_hours: 0, + bounty_expires: '', + commitment_fee: 0, + price: 250000, + title: 'Lambda Function: Extract files from a zip folder in an s3 bucket', + tribe: 'None', + assignee: 'dummy_assignee_id', + ticket_url: 'https://github.com/stakwork/stak-bounties/issues/9', + org_uuid: 'dummy_org_uuid', + description: 'dummy_description', + wanted_type: 'Web development', + deliverables: '', + github_description: true, + one_sentence_summary: '', + estimated_session_length: 'Less than 3 hours', + estimated_completion_date: '', + created: 1706907671, + updated: '2024-02-02T23:50:20.567929Z', + coding_languages: [] + }, + assignee: { + id: 258, + uuid: 'dummy_assignee_uuid', + owner_pubkey: 'dummy_owner_pubkey', + owner_alias: 'dummy_owner_alias', + unique_name: 'dummy_unique_name', + description: 'dummy_description', + tags: [], + img: 'https://dummy-image-url.com', + created: '2023-12-15T16:35:01.893063Z', + updated: '2024-01-31T14:32:16.852539Z', + unlisted: false, + deleted: false, + last_login: 1706562455, + owner_route_hint: 'dummy_owner_route_hint', + owner_contact_key: 'dummy_owner_contact_key', + price_to_meet: 0, + new_ticket_time: 0, + twitter_confirmed: false, + extras: null, + github_issues: null + }, + owner: { + id: 190, + uuid: 'dummy_owner_uuid', + owner_pubkey: 'dummy_owner_pubkey', + owner_alias: 'dummy_owner_alias', + unique_name: 'dummy_unique_name', + description: 'dummy_description', + tags: [], + img: 'https://dummy-image-url.com', + created: '2023-09-21T22:56:59.255469Z', + updated: '2023-09-21T22:56:59.255469Z', + unlisted: false, + deleted: false, + last_login: 1706730967, + owner_route_hint: 'dummy_owner_route_hint', + owner_contact_key: 'dummy_owner_contact_key', + price_to_meet: 0, + new_ticket_time: 0, + twitter_confirmed: false, + extras: null, + github_issues: null + }, + organization: { + uuid: 'dummy_org_uuid', + name: 'Dummy Organization', + img: 'https://dummy-image-url.com' + } + } +]; diff --git a/src/__test__/__mockData__/organization.ts b/src/__test__/__mockData__/organization.ts new file mode 100644 index 000000000..df5a5dcfa --- /dev/null +++ b/src/__test__/__mockData__/organization.ts @@ -0,0 +1,28 @@ +import { Organization } from 'store/main'; + +export const mockOrganizations: Organization[] = [ + { + bounty_count: 0, + created: '2024-01-03T20:34:09.585609Z', + deleted: false, + id: '51', + img: '', + name: 'TEST_NEW', + owner_pubkey: '03cbb9c01cdcf91a3ac3b543a556fbec9c4c3c2a6ed753e19f2706012a26367ae3', + show: false, + updated: '2024-01-03T20:34:09.585609Z', + uuid: 'cmas9gatu2rvqiev4ur0' + }, + { + bounty_count: 0, + created: '2024-01-03T20:34:09.585609Z', + deleted: false, + id: '52', + img: '', + name: 'TEST_SECOND', + owner_pubkey: '03cbb9c01cdcf91a3ac3b543a556fbec9c4c3c2a6ed753e19f2706012a26367ae3', + show: false, + updated: '2024-01-03T20:34:09.585609Z', + uuid: 'cmas9gatu2rvqiev4ur0' + } +]; diff --git a/src/__test__/__mockData__/persons.ts b/src/__test__/__mockData__/persons.ts index 5c2a2dc00..c3f4852af 100644 --- a/src/__test__/__mockData__/persons.ts +++ b/src/__test__/__mockData__/persons.ts @@ -19,6 +19,7 @@ export const people: Person[] = [ }, owner_alias: 'Vladimir', owner_pubkey: 'test_pub_key', + uuid: '23334444', unique_name: 'vladimir', tags: [], img: '', @@ -42,6 +43,7 @@ export const people: Person[] = [ }, owner_alias: 'Raphael', owner_pubkey: 'test_pub_key_2', + uuid: '23334434', unique_name: 'raphael', tags: [], img: '', diff --git a/src/__test__/__mockStore__/MockStoreEnvironment.tsx b/src/__test__/__mockStore__/MockStoreEnvironment.tsx new file mode 100644 index 000000000..b7c7addf5 --- /dev/null +++ b/src/__test__/__mockStore__/MockStoreEnvironment.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { ThemeProvider, createTheme } from '@mui/system'; +import { Router } from 'react-router-dom'; +import history from '../../config/history'; +import { withProviders } from '../../providers'; +import { MOCK_ENVIRONMENT_HOOKS } from './constants'; +import { useMockBountyData } from './useMockBountyData'; +import { useMockBountyRoleData } from './useMockBountyRoleData'; +import { useMockDropdownOrganizationData } from './useMockDropdownOrganizationData'; +import { useMockOrganizationsData } from './useMockOrganizationsData'; +import { useMockUsdToSatExchangeRate } from './useMockUsdToSatExchangeRate'; +import { useMockSelfProfileStore } from './useMockSelfProfileStore'; + +export function MockStoreEnvironment({ + children, + hooks = [] +}: { + children: React.ReactNode; + hooks: MOCK_ENVIRONMENT_HOOKS[]; +}) { + const theme = createTheme({ + spacing: 8 + }); + useMockBountyData({ enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.BOUNTY_DATA) }); + useMockBountyRoleData({ enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.BOUNTY_ROES) }); + useMockDropdownOrganizationData({ + enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.DROPDOWN_ORGANIZATION_DATA) + }); + useMockOrganizationsData({ enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.ORGANIZATION_DATA) }); + useMockUsdToSatExchangeRate({ + enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.USD_TO_SAT_EXCHANGE_RATE) + }); + useMockSelfProfileStore({ enabled: hooks.includes(MOCK_ENVIRONMENT_HOOKS.SELF_PROFILE_STORE) }); + + return ( + + {children} + + ); +} + +export default withProviders(MockStoreEnvironment); diff --git a/src/__test__/__mockStore__/constants.ts b/src/__test__/__mockStore__/constants.ts new file mode 100644 index 000000000..addd45d08 --- /dev/null +++ b/src/__test__/__mockStore__/constants.ts @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ +export enum MOCK_ENVIRONMENT_HOOKS { + BOUNTY_DATA = 'BOUNTY_DATA', + BOUNTY_ROES = 'BOUNTY_ROES', + DROPDOWN_ORGANIZATION_DATA = 'DROPDOWN_ORGANIZATION_DATA', + ORGANIZATION_DATA = 'ORGANIZATION_DATA', + SELF_PROFILE_STORE = 'SELF_PROFILE_STORE', + USD_TO_SAT_EXCHANGE_RATE = 'USD_TO_SAT_EXCHANGE_RATE' +} diff --git a/src/__test__/__mockStore__/useMockBountyData.ts b/src/__test__/__mockStore__/useMockBountyData.ts new file mode 100644 index 000000000..86619542c --- /dev/null +++ b/src/__test__/__mockStore__/useMockBountyData.ts @@ -0,0 +1,14 @@ +import { bountyResponse } from '__test__/__mockData__/bounty'; +import { useEffect } from 'react'; +import { useStores } from 'store'; +import { transformBountyWithPeopleBounty } from 'store/__test__/util'; + +export const useMockBountyData = ({ enabled }: { enabled: boolean }) => { + const { main } = useStores(); + useEffect(() => { + if (enabled) { + // TODO: should write proper types once its when proper are written or people bounty + main.setPeopleBounties([transformBountyWithPeopleBounty(bountyResponse[0])] as any); + } + }, [main, enabled]); +}; diff --git a/src/__test__/__mockStore__/useMockBountyRoleData.ts b/src/__test__/__mockStore__/useMockBountyRoleData.ts new file mode 100644 index 000000000..124eb687b --- /dev/null +++ b/src/__test__/__mockStore__/useMockBountyRoleData.ts @@ -0,0 +1,12 @@ +import { mockBountyRoles } from '__test__/__mockData__/bounty'; +import { useEffect } from 'react'; +import { useStores } from 'store'; + +export const useMockBountyRoleData = ({ enabled }: { enabled: boolean }) => { + const { main } = useStores(); + useEffect(() => { + if (enabled) { + main.setBountyRoles(mockBountyRoles); + } + }, [main, enabled]); +}; diff --git a/src/__test__/__mockStore__/useMockDropdownOrganizationData.ts b/src/__test__/__mockStore__/useMockDropdownOrganizationData.ts new file mode 100644 index 000000000..47253d379 --- /dev/null +++ b/src/__test__/__mockStore__/useMockDropdownOrganizationData.ts @@ -0,0 +1,19 @@ +import { mockOrganizations } from '__test__/__mockData__/organization'; +import { useEffect } from 'react'; +import { useStores } from 'store'; + +// This hook will populate the store with mock data for the dropdownOrganizations slice +export const useMockDropdownOrganizationData = ({ + personId, + enabled +}: { + personId?: string; + enabled: boolean; +}) => { + const { main } = useStores(); + useEffect(() => { + if (enabled) { + main.setDropdownOrganizations(mockOrganizations); + } + }, [main, personId, enabled]); +}; diff --git a/src/__test__/__mockStore__/useMockOrganizationsData.ts b/src/__test__/__mockStore__/useMockOrganizationsData.ts new file mode 100644 index 000000000..db0beedd3 --- /dev/null +++ b/src/__test__/__mockStore__/useMockOrganizationsData.ts @@ -0,0 +1,13 @@ +import { mockOrganizations } from '__test__/__mockData__/organization'; +import { useEffect } from 'react'; +import { useStores } from 'store'; + +// This hook will populate the store with mock data for the organization slice +export const useMockOrganizationsData = ({ enabled }: { enabled: boolean }) => { + const { main } = useStores(); + useEffect(() => { + if (enabled) { + main.setOrganizations(mockOrganizations); + } + }, [main, enabled]); +}; diff --git a/src/__test__/__mockStore__/useMockSelfProfileStore.ts b/src/__test__/__mockStore__/useMockSelfProfileStore.ts new file mode 100644 index 000000000..e4d64fcff --- /dev/null +++ b/src/__test__/__mockStore__/useMockSelfProfileStore.ts @@ -0,0 +1,21 @@ +import { user } from '__test__/__mockData__/user'; +import { useEffect } from 'react'; +import { useStores } from 'store'; +import { Person } from 'store/main'; + +export const useMockSelfProfileStore = ({ enabled }: { enabled: boolean }) => { + const { ui, main } = useStores(); + + useEffect(() => { + if (enabled) { + ui.setMeInfo(user); + ui.setSelectedPerson(user.id); + const person = { + ...user, + unique_name: Math.random().toString(36).substring(7), + tags: [] + } as Person; + main.setActivePerson(person); + } + }, [main, ui, enabled]); +}; diff --git a/src/__test__/__mockStore__/useMockUsdToSatExchangeRate.tsx b/src/__test__/__mockStore__/useMockUsdToSatExchangeRate.tsx new file mode 100644 index 000000000..5600974ca --- /dev/null +++ b/src/__test__/__mockStore__/useMockUsdToSatExchangeRate.tsx @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { useStores } from 'store'; + +export const useMockUsdToSatExchangeRate = ({ + time = 100000, + enabled +}: { + time?: number; + enabled: boolean; +}) => { + const { ui } = useStores(); + + useEffect(() => { + if (enabled) { + const getUsdToSatsExchangeRate = () => { + // random rate for 1 usd + const rate = Math.random() || 1; + + // 1 bitcoin is 1 million satoshis + const satoshisInABitcoin = 0.00000001; + ui.setUsdToSatsExchangeRate(rate / satoshisInABitcoin); + }; + + getUsdToSatsExchangeRate(); + const timer = setInterval(() => { + getUsdToSatsExchangeRate(); + }, time); + + return () => { + clearInterval(timer); + }; + } + }, [time, ui, enabled]); +}; diff --git a/src/bounties/BountyProfileView.tsx b/src/bounties/BountyProfileView.tsx index 3ed921b83..9033d806c 100644 --- a/src/bounties/BountyProfileView.tsx +++ b/src/bounties/BountyProfileView.tsx @@ -137,7 +137,7 @@ const BountyProfileView = (props: BountiesProfileProps) => { width={'100%'} height={'100%'} style={{ objectFit: 'cover' }} - src={props.assignee.img || main.getUserAvatarPlaceholder(props.assignee.owner_pubkey)} + src={props.assignee?.img || main.getUserAvatarPlaceholder(props.assignee?.owner_pubkey)} alt={'assigned_person'} /> @@ -166,8 +166,8 @@ const BountyProfileView = (props: BountiesProfileProps) => { `/p/${ { ...props.assignee - }.owner_pubkey - }?widget=wanted`, + }.uuid + }?widget=bounties`, '_blank' ); } @@ -188,8 +188,8 @@ const BountyProfileView = (props: BountiesProfileProps) => { `/p/${ { ...props.assignee - }.owner_pubkey - }?widget=wanted`, + }.uuid + }?widget=bounties`, '_blank' ); } diff --git a/src/bounties/__mock__/mockBounties.data.ts b/src/bounties/__mock__/mockBounties.data.ts index 38cf69c79..1c3e42b7a 100644 --- a/src/bounties/__mock__/mockBounties.data.ts +++ b/src/bounties/__mock__/mockBounties.data.ts @@ -434,3 +434,60 @@ export const assignedBounty = { img: 'https://example.com/public/another_random_image.png' // Randomized } }; + +export const createdBounty = { + bounty: { + id: 9999, + owner_id: 'random_owner_id', + paid: false, + show: true, + type: 'coding_task', + award: '', + assigned_hours: 0, + bounty_expires: '', + commitment_fee: 0, + price: 50000, + title: 'Show "0" value if % is too low instead of no value in superadmin', + tribe: 'None', + assignee: 'random_assignee_id', + ticket_url: 'https://example.com/issue/123', + org_uuid: 'random_org_uuid', + description: + '**Describe the bug**\nOn staging there is a really small (close to 0) value for % conversion. This results in a "0" value. However, no 0 value is being displayed.\n**Expected behavior**\nDisplay a 0 value if the % is below 0.00%.\n**Desktop**\n - Browser: Chrome\n### Acceptance Criteria\n- I have tested on Chrome desktop\n- I have posted a screenshot in my PR\n- I have created a test that shows a 0 value gets displayed when value is below 0.00%\n- I have rebased and tested locally before submitting my PR', + wanted_type: 'Web development', + github_description: true, + estimated_completion_date: '2024-02-12T22:55:12.301Z', + created: 1707432920, + updated: '2024-02-09T00:09:02.297044Z', + coding_languages: [] + }, + assignee: { + id: 123, + uuid: 'random_uuid', + owner_pubkey: 'random_owner_pubkey', + owner_alias: 'RandomName', + unique_name: 'random_unique_name', + description: 'description', + img: '', + created: '2024-02-05T19:29:36.419064Z', + updated: '2024-02-05T19:29:36.720503Z', + last_login: 1707770720, + price_to_meet: 0 + }, + owner: { + id: 456, + uuid: 'another_random_uuid', + owner_pubkey: 'another_random_owner_pubkey', + owner_alias: 'AnotherRandomName', + unique_name: 'another_random_unique_name', + description: 'Bitcoin PM test', + img: 'https://example.com/public/random_image.png', + created: '2023-09-05T21:34:39.170759Z', + updated: '2024-01-30T17:25:48.8506Z' + }, + organization: { + uuid: 'random_org_uuid_again', + name: 'Bounties Platform', + img: 'https://example.com/public/another_random_image.png' + } +}; diff --git a/src/components/auth/SphinxAppLoginDeepLink.tsx b/src/components/auth/SphinxAppLoginDeepLink.tsx index bef6de690..e88fb9409 100644 --- a/src/components/auth/SphinxAppLoginDeepLink.tsx +++ b/src/components/auth/SphinxAppLoginDeepLink.tsx @@ -57,7 +57,9 @@ export default function SphinxAppLoginDeeplink(props: AuthProps) { if (i > 100) { if (interval) clearInterval(interval); } - } catch (e) {} + } catch (e) { + console.log('Auth interval error', e); + } }, 3000); } diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index c32dcd8ed..895ab1447 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -65,6 +65,7 @@ export default function Button(props: ButtonProps) { paddingRight: props.leadingIcon && 10, ...props.style }} + {...(props?.dataTestId ? { 'data-testid': props?.dataTestId } : {})} disabled={props.disabled} onClick={props.onClick} > diff --git a/src/components/common/Select.tsx b/src/components/common/Select.tsx index d5d8ec527..597c263dd 100644 --- a/src/components/common/Select.tsx +++ b/src/components/common/Select.tsx @@ -30,7 +30,7 @@ const S = styled(EuiSuperSelect as any)` `; export default function Select(props: SelProps) { const color = colors['light']; - const { options, onChange, value, style, selectStyle, handleActive, testId } = props; + const { options, onChange, value, style, selectStyle, handleActive, testId, isOpen } = props; const opts = options ? options.map((o: any) => ({ @@ -73,6 +73,7 @@ export default function Select(props: SelProps) { return (
{ + beforeEach(async () => { + act(async () => { + render(
); + const PostBountyButton = await screen.findByRole('button', { name: /Post a Bounty/i }); + expect(PostBountyButton).toBeInTheDocument(); + fireEvent.click(PostBountyButton); + const StartButton = await screen.findByRole('button', { name: /Start/i }); + expect(StartButton).toBeInTheDocument(); + const bountyTitleInput = await screen.findByRole('input', { name: /Bounty Title /i }); + expect(bountyTitleInput).toBeInTheDocument(); + fireEvent.change(bountyTitleInput, { target: { value: 'new text' } }); + const dropdown = screen.getByText(/Category /i); + fireEvent.click(dropdown); + const desiredOption = screen.getByText(/Web Development/i); + fireEvent.click(desiredOption); + const NextButton = await screen.findByRole('button', { name: /Next/i }); + expect(NextButton).toBeInTheDocument(); + fireEvent.click(NextButton); + const DescriptionInput = await screen.findByRole('input', { name: /Description /i }); + expect(DescriptionInput).toBeInTheDocument(); + fireEvent.change(DescriptionInput, { target: { value: 'new text' } }); + const NextButton2 = await screen.findByRole('button', { name: /Next/i }); + expect(NextButton2).toBeInTheDocument(); + fireEvent.click(NextButton2); + const SatInput = await screen.findByRole('input', { name: /Price(Sats)/i }); + expect(SatInput).toBeInTheDocument(); + }); + }); + + test('Placeholder text for 0 Sats is visible for amount input box', async () => { + act(async () => { + const SatInput = await screen.findByRole('input', { name: /Price(Sats)/i }); + expect(SatInput).toBeInTheDocument(); + const amountInput = await screen.findByText('0'); + expect(amountInput).toBeInTheDocument(); + }); + }); + + test('Next button is disabled if amount is set to zero', async () => { + act(async () => { + const amountInput = await screen.findByText('0'); + fireEvent.change(amountInput, { target: { value: '0' } }); + const nextButton = screen.findByRole('button', { name: /Next/i }); + expect(nextButton).toBeDisabled(); + }); + }); + + test('Next button is enabled for amount > 0 and Disabled again when reverting to 0', async () => { + act(async () => { + const amountInput = await screen.findByText('0'); + fireEvent.change(amountInput, { target: { value: '50' } }); + const nextButton = screen.findByRole('button', { name: /Next/i }); + expect(nextButton).toBeEnabled(); + fireEvent.change(amountInput, { target: { value: '0' } }); + expect(nextButton).toBeDisabled(); + }); + }); + + test('Proceeding to next page of form only if value for amount is greater than 0', async () => { + act(async () => { + const amountInput = await screen.findByText('0'); + fireEvent.change(amountInput, { target: { value: '13' } }); + const nextButton = screen.findByRole('button', { name: /Next/i }); + fireEvent.click(await nextButton); + const nextPage = await screen.findByText('Assign Developer'); + expect(nextPage).toBeInTheDocument(); + const DecideLaterButton = await screen.findByRole('button', { name: /Decide Later/i }); + expect(DecideLaterButton).toBeInTheDocument(); + }); + }); + + test('Form does not proceed when only spaces are entered in title', async () => { + act(async () => { + const bountyTitleInput = await screen.findByRole('input', { name: /Bounty Title/i }); + fireEvent.change(bountyTitleInput, { target: { value: ' ' } }); + + const dropdown = screen.getByText(/Category /i); + fireEvent.click(dropdown); + const desiredOption = screen.getByText(/Web Development/i); + fireEvent.click(desiredOption); + + const nextButton = await screen.findByRole('button', { name: /Next/i }); + fireEvent.click(nextButton); + expect(nextButton).toBeDisabled(); + }); + }); + + test('Form does not proceed when only spaces are entered in description', async () => { + act(async () => { + const bountyTitleInput = await screen.findByRole('input', { name: /Bounty Title/i }); + fireEvent.change(bountyTitleInput, { target: { value: 'Test The Bounty' } }); + + const dropdown = screen.getByText(/Category /i); + fireEvent.click(dropdown); + const desiredOption = screen.getByText(/Web Development/i); + fireEvent.click(desiredOption); + + const nextButton = await screen.findByRole('button', { name: /Next/i }); + fireEvent.click(nextButton); + + const descriptionInput = await screen.findByRole('input', { name: /Description/i }); + fireEvent.change(descriptionInput, { target: { value: ' ' } }); + + const nextButton2 = await screen.findByRole('button', { name: /Next/i }); + fireEvent.click(nextButton2); + expect(nextButton2).toBeDisabled(); + }); + }); + + test('Spaces are trimmed from bounty title and description', async () => { + act(async () => { + const bountyTitleInput = await screen.findByRole('input', { name: /Bounty Title/i }); + fireEvent.change(bountyTitleInput, { target: { value: ' Te st Ti tle ' } }); + + const descriptionInput = await screen.findByRole('input', { name: /Description/i }); + fireEvent.change(descriptionInput, { target: { value: ' Te st Descri ption ' } }); + + const mockOnSubmit = jest.fn(); + render(); + + const submitButton = await screen.findByRole('button', { name: /Next/i }); + fireEvent.click(submitButton); + + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Test Title', + description: 'Test Description' + }) + ); + }); + }); +}); diff --git a/src/components/form/bounty/index.tsx b/src/components/form/bounty/index.tsx index 856e7fcf0..ad42c37e3 100644 --- a/src/components/form/bounty/index.tsx +++ b/src/components/form/bounty/index.tsx @@ -564,7 +564,7 @@ function Form(props: FormProps) { )} {!isBtnDisabled && (
{ if (schemaData.step === 5 && valid) { if (dynamicSchemaName) { @@ -595,7 +595,9 @@ function Form(props: FormProps) { {schemaData.step === 5 ? 'Decide Later' : 'Next'} ) : ( - Finish + + Finish + )}
)} diff --git a/src/components/form/inputs/NumberSatsInput.tsx b/src/components/form/inputs/NumberSatsInput.tsx index 554129e3f..f5ca067db 100644 --- a/src/components/form/inputs/NumberSatsInput.tsx +++ b/src/components/form/inputs/NumberSatsInput.tsx @@ -83,7 +83,7 @@ export default function NumberInputNew({ id={name} type={'text'} value={textValue} - placeholder={'1'} + placeholder={'0'} onFocus={handleFocus} onBlur={() => { handleBlur(); diff --git a/src/components/form/inputs/SelectInput.tsx b/src/components/form/inputs/SelectInput.tsx index 2491c424a..7261fabd5 100644 --- a/src/components/form/inputs/SelectInput.tsx +++ b/src/components/form/inputs/SelectInput.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { EuiIcon } from '@elastic/eui'; import { Select } from '../../common'; @@ -79,15 +79,28 @@ export default function SelectInput({ const color = colors['light']; if (error) labeltext = `${labeltext} (${error})`; const [active, setActive] = useState(false); + const [isSelectOpen, setIsSelectOpen] = useState(false); + + useEffect(() => { + if (active === false) { + setIsSelectOpen(false); + } + }, [isSelectOpen, active]); + return ( { + setActive(!active); + setIsSelectOpen(!isSelectOpen); + }} > { /> - + { /> - + { {currentPaymentsHistory.map((pay: PaymentHistory, i: number) => ( - - {pay.payment_type || 'Payment'} + + + {pay.payment_type || 'Payment'} + {moment(pay.created).format('MM/DD/YY')} - + {formatSat(pay.amount)} sats - + { /> {pay.payment_type === 'payment' ? : null} - + {pay.payment_type === 'payment' ? ( { {pay.payment_type === 'payment' ? ( - viewBounty(pay.bounty_id)}> + viewBounty(pay.bounty_id)} + data-testid={`payment-history-transaction-link`} + > View bounty diff --git a/src/people/widgetViews/postBounty/__tests__/PostModal.spec.tsx b/src/people/widgetViews/postBounty/__tests__/PostModal.spec.tsx index 0f212bbda..11a07d057 100644 --- a/src/people/widgetViews/postBounty/__tests__/PostModal.spec.tsx +++ b/src/people/widgetViews/postBounty/__tests__/PostModal.spec.tsx @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import React from 'react'; @@ -15,71 +15,110 @@ beforeAll(() => { }); describe('Post bounty modal', () => { - nock(user.url).get('/person/id/1').reply(200, {}); + nock(user.url).get('/person/id/1').reply(200, { user }); + nock(user.url).get('/ask').reply(200, {}); - test('placeholder', () => {}); - - /*test('Show and close modal', () => { + test('clicking on post a bounty button render a form', () => { const closeHandler = jest.fn(); - render(); + render(); expect(screen.getByRole('alertdialog')).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('close-btn')); - expect(closeHandler).toBeCalledTimes(1); + expect(screen.getByText(/Choose Bounty type/i)).toBeInTheDocument(); }); - test('If modal closed it isnt in the DOM', () => { + + test('start button is visible and navigates to the first step', () => { const closeHandler = jest.fn(); - render(); - expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); + render(); + const startButton = screen.getByText('Start'); + expect(startButton).toBeInTheDocument(); + fireEvent.click(startButton); + expect(screen.getByText('Basic info')).toBeInTheDocument(); }); - const formData = { - organization: 'organization', - title: 'title', - category: 'Web development', - description: 'description', - price: 1 - }; - test('FillForm', async () => { + test('back and the next button take you forward and backward respectively', () => { const closeHandler = jest.fn(); - const successHandler = jest.fn(() => {}); - render( - - ); + render(); + const startButton = screen.getByText('Start'); + fireEvent.click(startButton); + fireEvent.click(screen.getByText('Next')); + expect(screen.getByText('Freelance Job Request')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Back')); + expect(screen.getByText('Choose Bounty type')).toBeInTheDocument(); + }); - // 1step - expect(screen.queryByText('Choose Bounty type')).toBeInTheDocument(); - const button = await screen.findByText('Start'); - fireEvent.click(button); + test('all form field are rendered', () => { + const closeHandler = jest.fn(); + render(); + const startButton = screen.getByText('Start'); + fireEvent.click(startButton); + expect(screen.getByText('Basic info')).toBeInTheDocument(); + const Form1 = screen.getByText('Organization (optional)'); + const Form2 = screen.getByText('Bounty Title *'); + const Form3 = screen.getByText('Github Issue URL'); + const Form4 = screen.getByText('Category *'); + const Form5 = screen.getByText('Coding Language'); + expect(Form1).toBeInTheDocument(); + expect(Form2).toBeInTheDocument(); + expect(Form3).toBeInTheDocument(); + expect(Form4).toBeInTheDocument(); + expect(Form5).toBeInTheDocument(); + }); + + test('clicking on assign hunter', () => { + const closeHandler = jest.fn(); + const formData = { + organization: 'organization', + title: 'title', + category: 'Web development', + description: 'description', + price: 1 + }; + render(); + fireEvent.click(screen.getByText('Start')); - // 2 step expect(screen.queryByText('Basic info')).toBeInTheDocument(); expect(screen.queryByText('Next')).toHaveClass('disableText'); - await waitFor(async () => { + waitFor(async () => { await userEvent.type(screen.getByLabelText('Bounty Title'), formData.title); - await userEvent.click(screen.getByTestId('Category')); - await userEvent.click(screen.getByText(formData.category)); - await userEvent.click(screen.getByText('Next')); - }); + userEvent.click(screen.getByTestId('Category')); + userEvent.click(screen.getByText(formData.category)); + userEvent.click(screen.getByText('Next')); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.queryAllByText('Description')[0]).toBeInTheDocument(); + await waitFor(async () => { + await userEvent.type(screen.getByLabelText('Description'), formData.description); + userEvent.click(screen.getByText('Next')); + }); + expect(screen.getByText('Price and Estimate')).toBeInTheDocument(); + await waitFor(async () => { + await userEvent.type(screen.getByLabelText('Price (Sats)*'), String(formData.price)); + userEvent.click(screen.getByText('Next')); + }); + expect(screen.queryByText('Assign Developer')).toBeInTheDocument(); + await waitFor(async () => { + userEvent.click(await screen.findByText('Decide Later')); + }); + expect(screen.queryByText('Finish')).toBeInTheDocument(); - // 3 step - expect(screen.queryAllByText('Description')[0]).toBeInTheDocument(); - await waitFor(async () => { - await userEvent.type(screen.getByLabelText('Description'), formData.description); - await userEvent.click(screen.getByText('Next')); - }); + await waitFor(() => { + expect(screen.getByText('Type to search')).toBeInTheDocument(); + expect(screen.getByText('Skills')).toBeInTheDocument(); + const assignButtons = screen.getAllByText('Assign'); + expect(assignButtons.length).toBe(5); + }); - //4 step - expect(screen.queryByText('Price and Estimate')).toBeInTheDocument(); - await waitFor(async () => { - await userEvent.type(screen.getByLabelText('Price (Sats)*'), String(formData.price)); - await userEvent.click(screen.getByText('Next')); - }); + // Test That on clicking on the "type to search" box I should be able to type in any character and the list below should be filtered. + const searchBox = screen.getByPlaceholderText('Type to search ...'); + await userEvent.type(searchBox, 'John Doe'); - //5 step - expect(screen.queryByText('Assign Developer')).toBeInTheDocument(); - await waitFor(async () => { - await userEvent.click(await screen.findByText('Decide Later')); + // Test that on clicking on the "skills" box I should be able to type in any character and the list below should be filtered. + userEvent.click(screen.getByRole('button', { name: /Skills/i })); + const dropdown = screen.getByRole('listbox'); + userEvent.click(within(dropdown).getByText('JavaScript')); + userEvent.click(within(dropdown).getByText('Python')); + userEvent.click(within(dropdown).getByText('Golang')); + expect(screen.getByText('JavaScript')).toBeVisible(); + expect(screen.getByText('Python')).toBeVisible(); + expect(screen.getByText('Golang')).toBeVisible(); }); - expect(screen.queryByText('Finish')).toBeInTheDocument(); - });*/ + }); }); diff --git a/src/people/widgetViews/statusFilterCount.spec.tsx b/src/people/widgetViews/statusFilterCount.spec.tsx index 1cd189dad..d76d131cd 100644 --- a/src/people/widgetViews/statusFilterCount.spec.tsx +++ b/src/people/widgetViews/statusFilterCount.spec.tsx @@ -28,7 +28,7 @@ describe('BountyHeader', () => { it('displays the filter button and shows status count on click', async () => { render( { render( $@ { @@ -893,11 +895,14 @@ function MobileView(props: CodingBountiesProps) { /> SAT
- {satToUsd(bountyPrice)} USD + + {satToUsd(bountyPrice)} USD +
({})); + +jest.mock('rehype-raw', () => ({})); + describe('MobileView component', () => { beforeEach(() => { const mockIntersectionObserver = jest.fn(); @@ -16,6 +31,10 @@ describe('MobileView component', () => { window.IntersectionObserver = mockIntersectionObserver; }); + afterAll(() => { + jest.clearAllMocks(); + }); + const defaultProps: CodingBountiesProps = { deliverables: 'Default Deliverables', description: 'Default Description', @@ -143,7 +162,7 @@ describe('MobileView component', () => { id: 180, owner: 'Test-Owner', owner_pubkey: 'abc100', - widget: 'wanted' + widget: 'bounties' }; render(} />); @@ -163,4 +182,268 @@ describe('MobileView component', () => { const completionDate = screen.getByText('Jan 26, 2024'); expect(completionDate).toBeInTheDocument(); }); + + it('Test that on clicking on "not assigned", a pop up should appear to invite a developer including "type to search" box, a "skills" box, and a recommendation of 5 hunters.', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + assigneeValue: true, + creatorStep: 0, + peopleList: [ + { id: 1, owner_alias: '111' }, + { id: 2, owner_alias: '222' }, + { id: 3, owner_alias: '333' }, + { id: 4, owner_alias: '444' }, + { id: 5, owner_alias: '555' } + ] as any + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + render(); + + fireEvent.click(screen.getByText('Not Assigned')); + + await waitFor(() => { + expect(screen.getByText('Assign Developer')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Type to search ...')).toBeInTheDocument(); + expect(screen.getByText('Skills')).toBeInTheDocument(); + expect(document.querySelectorAll('.PeopleList .People')).toHaveLength(5); + }); + }); + + it('Test filter peopleList by "type to search" or "skills"', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + assigneeValue: true, + creatorStep: 0, + peopleList: [ + { id: 1, owner_alias: '111' }, + { id: 2, owner_alias: '222' }, + { id: 3, owner_alias: '333' }, + { id: 4, owner_alias: '444' }, + { id: 5, owner_alias: '555' } + ] as any + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + const mockPeopleList: any = [ + { + id: 1, + owner_alias: 'TEST_NAME_1', + extras: { + coding_languages: [ + { value: 'R', label: 'R' }, + { value: 'C++', label: 'C++' } + ] + } + }, + { + id: 2, + owner_alias: 'TEST_NAME_2', + extras: { coding_languages: [{ value: 'C', label: 'C' }] } + }, + { id: 3, owner_alias: 'TEST_NAME_3', extras: { coding_languages: [] } } + ]; + const mockSearch = jest + .spyOn(mainStore, 'getPeopleByNameAliasPubkey') + .mockResolvedValue(mockPeopleList); + + render(); + + fireEvent.click(screen.getByText('Not Assigned')); + + // filter by "type to search" + await waitFor(async () => { + expect(screen.getByText('Assign Developer')).toBeInTheDocument(); + fireEvent.click(screen.getByPlaceholderText('Type to search ...')); + await userEvent.type(screen.getByPlaceholderText('Type to search ...'), 'TEST_NAME'); + expect(document.querySelectorAll('.PeopleList .People')).toHaveLength(mockPeopleList.length); + + mockPeopleList.forEach((person: any) => { + expect(screen.getByText(person.owner_alias)).toBeInTheDocument(); + }); + }); + + // filter by "skills" + await waitFor(() => { + fireEvent.click(screen.getByText('Skills')); + fireEvent.click(screen.getByText('R')); + + fireEvent.keyDown(document, { key: 'Escape', keyCode: 27 }); + }); + + expect(document.querySelectorAll('.PeopleList .People')).toHaveLength(1); + expect(screen.getByText('TEST_NAME_1')).toBeInTheDocument(); + expect(screen.queryByText('TEST_NAME_2')).not.toBeInTheDocument(); + expect(screen.queryByText('TEST_NAME_3')).not.toBeInTheDocument(); + + mockSearch.mockRestore(); + }); + + it('Test that on clicking on "Assign" on a hunter, the pop up should clear and the hunter should be assigned to the bounty', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + creatorStep: 0, + peopleList: [ + { id: 1, owner_alias: 'NAME_1' }, + { id: 2, owner_alias: 'NAME_2' }, + { id: 3, owner_alias: 'NAME_3' }, + { id: 4, owner_alias: 'NAME_4' }, + { id: 5, owner_alias: 'NAME_5' } + ] as any + }; + const mockHandleAssigneeDetails = jest.fn(); + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + const App = () => { + const [assigneeValue, setAssigneeValue] = useState(false); + + return ( + setAssigneeValue((v: boolean) => !v)} + handleAssigneeDetails={() => { + setAssigneeValue((v: boolean) => !v); + mockHandleAssigneeDetails(); + }} + /> + ); + }; + + render(); + + fireEvent.click(screen.getByText('Not Assigned')); + + await waitFor(() => { + expect(screen.getByText('Assign Developer')).toBeInTheDocument(); + fireEvent.click(screen.getAllByText('Assign')[0]); + }); + + await waitFor(() => { + expect(screen.queryByText('Assign Developer')).toBe(null); + expect(mockHandleAssigneeDetails).toBeCalledTimes(1); + }); + }); + + it('Test that mark as paid button, renders: adjust amount , assignee name, amount set in Sats and next button', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + paid: false + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + render(); + + const inputSAT = screen.findByTestId('input_sats'); + const nextBTN = screen.findByTestId('next_btn'); + + expect(screen.queryByText('Adjust the amount')).toBeInTheDocument(); + expect(screen.queryByText('Guest Developer')).toBeInTheDocument(); + expect(screen.queryByTestId('USDText')).toBeInTheDocument(); + expect(screen.queryByText('SAT')).toBeInTheDocument(); + }); + + it('Test that increment and decrement button on input field for amount works correctly', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + paid: false + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + render(); + + const inputSAT = screen.getByTestId('input_sats'); + + expect(inputSAT).toBeInTheDocument(); + + fireEvent.change(inputSAT, { target: { value: 100 } }); + + await waitFor(() => expect(inputSAT).toHaveValue(100), { timeout: 3000 }); + }); + + it('Test that clicking on next button takes you to Award Badge section assert all the necessary ui elements', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + creatorStep: 2, + paid: false + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + render(); + expect(screen.getByText('Mark as Paid')).toBeInTheDocument(); + expect(screen.queryByText('Award Badge')).toBeInTheDocument(); + expect(screen.getAllByTestId('check_box').length).toBe(2); + }); + + it('Test that button state and text changes according to selection in Award Badge Section', () => { + const props: CodingBountiesProps = { + ...defaultProps, + creatorStep: 2, + paid: false + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + render(); + + const checkBox = screen.getAllByTestId('check_box'); + + fireEvent.click(checkBox[0]); + + (async () => { + await waitFor(() => { + expect(screen.getByText('Mark as Paid')); + }); + })(); + }); + + it('test that the mark as paid button updates the bounty paid state', async () => { + const props: CodingBountiesProps = { + ...defaultProps, + creatorStep: 0, + paid: false, + isAssigned: true + }; + + uiStore.setMeInfo({ + ...user, + owner_alias: props.person.owner_alias + }); + + const { container } = render(); + + const markAsPaid = screen.getByText('Mark as Paid'); + fireEvent.click(markAsPaid); + + (async () => { + await waitFor(() => expect(screen.getByText('complete'))); + })(); + }); }); diff --git a/src/people/widgetViews/wantedViews/DesktopView.tsx b/src/people/widgetViews/wantedViews/DesktopView.tsx index 0da1bc40f..e3365d5cf 100644 --- a/src/people/widgetViews/wantedViews/DesktopView.tsx +++ b/src/people/widgetViews/wantedViews/DesktopView.tsx @@ -70,7 +70,7 @@ function DesktopView(props: WantedViewsProps) { diff --git a/src/people/widgetViews/wantedViews/MobileView.tsx b/src/people/widgetViews/wantedViews/MobileView.tsx index 43ba922de..9b2fb975b 100644 --- a/src/people/widgetViews/wantedViews/MobileView.tsx +++ b/src/people/widgetViews/wantedViews/MobileView.tsx @@ -77,7 +77,7 @@ function MobileView(props: any) { { + const bounty = { ...bountyDetails.bounty }; + let assignee; + let organization; + const owner = { ...bountyDetails.owner }; + + if (bounty.assignee) { + assignee = { ...bountyDetails.assignee }; + } + if (bounty.org_uuid) { + organization = { ...bountyDetails.organization }; + } + return { + body: { ...bounty, assignee: assignee || '' }, + person: { ...owner, wanteds: [] } || { wanteds: [] }, + organization: { ...organization } + }; +}; diff --git a/src/store/main.ts b/src/store/main.ts index d20f0aa0f..9597fe44a 100644 --- a/src/store/main.ts +++ b/src/store/main.ts @@ -61,6 +61,7 @@ export interface Person { id: number; unique_name: string; owner_pubkey: string; + uuid: string; owner_alias: string; description: string; img: string; @@ -309,6 +310,12 @@ export const defaultOrgBountyStatus: OrgBountyStatus = { Completed: false }; +export const defaultSuperAdminBountyStatus: BountyStatus = { + Open: false, + Assigned: false, + Paid: false +}; + export class MainStore { [x: string]: any; tribes: Tribe[] = []; @@ -910,7 +917,7 @@ export class MainStore { ps3, (n: any) => uiStore.setPeopleBountiesPageNumber(n), queryParams, - 'wanted' + 'bounties' ); this.setPeopleBounties(wanteds); } @@ -927,17 +934,13 @@ export class MainStore { this.personAssignedBounties = bounties; } - async getPersonAssignedBounties(queryParams?: any, pubkey?: string): Promise { + async getPersonAssignedBounties(queryParams?: any, uuid?: string): Promise { queryParams = { ...queryParams, search: uiStore.searchText }; - const query = this.appendQueryParams( - `people/wanteds/assigned/${pubkey}`, - paginationQueryLimit, - { - sortBy: 'paid', - ...queryParams - } - ); + const query = this.appendQueryParams(`people/wanteds/assigned/${uuid}`, paginationQueryLimit, { + sortBy: 'paid', + ...queryParams + }); try { const ps2 = await api.get(query); @@ -978,10 +981,10 @@ export class MainStore { this.createdBounties = bounties; } - async getPersonCreatedBounties(queryParams?: any, pubkey?: string): Promise { + async getPersonCreatedBounties(queryParams?: any, uuid?: string): Promise { queryParams = { ...queryParams, search: uiStore.searchText }; - const query = this.appendQueryParams(`people/wanteds/created/${pubkey}`, paginationQueryLimit, { + const query = this.appendQueryParams(`people/wanteds/created/${uuid}`, paginationQueryLimit, { ...queryParams, sortBy: 'paid' }); @@ -1162,7 +1165,7 @@ export class MainStore { ps3, (n: any) => uiStore.setPeopleBountiesPageNumber(n), queryParams, - 'wanted' + 'bounties' ); this.setPeopleBounties(wanteds); @@ -1214,7 +1217,7 @@ export class MainStore { ps3, (n: any) => uiStore.setPeopleBountiesPageNumber(n), queryParams, - 'wanted' + 'bounties' ); this.setPeopleBounties(wanteds); @@ -1287,7 +1290,7 @@ export class MainStore { ps3, (n: any) => uiStore.setPeopleBountiesPageNumber(n), queryParams, - 'wanted' + 'bounties' ); this.setPeopleBounties(wanteds); @@ -1381,7 +1384,7 @@ export class MainStore { const l = [...currentList, ...newList]; const set = new Set(); - if (type === 'wanted') { + if (type === 'bounties') { const uniqueArray = l.filter((item: any) => { if (item.body && item.body.id && !set.has(item.body.id)) { set.add(item.body.id); @@ -1410,7 +1413,7 @@ export class MainStore { } setActivePerson(p: Person) { - this.activePerson = [p]; + this._activePerson = [p]; } @memo() @@ -1419,6 +1422,12 @@ export class MainStore { return p; } + @memo() + async getPersonByUuid(uuid: string): Promise { + const p = await api.get(`person/uuid/${uuid}`); + return p; + } + async getPersonById(id: number): Promise { const p = await api.get(`person/id/${id}`); this.setActivePerson(p);