Skip to content

Commit

Permalink
✨ Extract Websites from Ticker to Integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
0x46616c6b committed Jan 26, 2025
1 parent 7d89e41 commit 599bfbf
Show file tree
Hide file tree
Showing 18 changed files with 445 additions and 111 deletions.
88 changes: 88 additions & 0 deletions src/api/Ticker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
TickerFormData,
TickerMastodonFormData,
TickerTelegramFormData,
TickerWebsite,
deleteTickerApi,
deleteTickerBlueskyApi,
deleteTickerMastodonApi,
deleteTickerTelegramApi,
deleteTickerUserApi,
deleteTickerWebsitesApi,
fetchTickerApi,
fetchTickerUsersApi,
fetchTickersApi,
Expand All @@ -20,6 +22,7 @@ import {
putTickerResetApi,
putTickerTelegramApi,
putTickerUsersApi,
putTickerWebsitesApi,
} from './Ticker'
import { User } from './User'

Expand Down Expand Up @@ -347,6 +350,91 @@ describe('putTickerResetApi', () => {
})
})

describe('putTickerWebsitesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { ticker: { id: 1 } } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' }] }),
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' } as TickerWebsite] }),
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' } as TickerWebsite] }),
})
})
})

describe('deleteTickerWebsitesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { ticker: { id: 1 } } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})
})

describe('putTickerMastodonApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
Expand Down
24 changes: 22 additions & 2 deletions src/api/Ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export interface TickerFormData {
export interface Ticker {
id: number
createdAt: Date
domain: string
title: string
description: string
active: boolean
information: TickerInformation
websites: Array<TickerWebsite>
mastodon: TickerMastodon
telegram: TickerTelegram
bluesky: TickerBluesky
Expand All @@ -53,6 +53,11 @@ export interface TickerInformation {
bluesky: string
}

export interface TickerWebsite {
id: number
origin: string
}

export interface TickerTelegram {
active: boolean
connected: boolean
Expand Down Expand Up @@ -118,7 +123,7 @@ export interface TickerLocation {

export interface GetTickersQueryParams {
active?: boolean
domain?: string
origin?: string
title?: string
order_by?: string
sort?: SortDirection
Expand Down Expand Up @@ -172,6 +177,21 @@ export async function putTickerResetApi(token: string, ticker: Ticker): Promise<
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/reset`, { headers: apiHeaders(token), method: 'put' })
}

export async function putTickerWebsitesApi(token: string, ticker: Ticker, websites: Array<TickerWebsite>): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/websites`, {
headers: apiHeaders(token),
method: 'put',
body: JSON.stringify({ websites: websites }),
})
}

export async function deleteTickerWebsitesApi(token: string, ticker: Ticker): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/websites`, {
headers: apiHeaders(token),
method: 'delete',
})
}

export async function putTickerMastodonApi(token: string, data: TickerMastodonFormData, ticker: Ticker): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/mastodon`, {
headers: apiHeaders(token),
Expand Down
17 changes: 5 additions & 12 deletions src/components/ticker/TickerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { faCheck, faHeading, faXmark } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Box, Card, CardContent, Typography } from '@mui/material'
import { FC } from 'react'
import { Box, Card, CardContent, Link, Typography } from '@mui/material'
import { Ticker } from '../../api/Ticker'
import NamedListItem from '../common/NamedListItem'
import SocialConnectionChip from './SocialConnectionChip'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck, faHeading, faLink, faXmark } from '@fortawesome/free-solid-svg-icons'

interface Props {
ticker: Ticker
Expand All @@ -26,16 +26,9 @@ const TickerCard: FC<Props> = ({ ticker }) => {
{ticker.active ? 'Active' : 'Inactive'}
</Typography>
</NamedListItem>
<NamedListItem title="Domain">
<Typography>
<FontAwesomeIcon color="GrayText" icon={faLink} style={{ width: 16, paddingRight: 2 }} />
<Link href={'https://' + ticker.domain} rel="noreferrer" target="_blank">
{ticker.domain}
</Link>
</Typography>
</NamedListItem>
<NamedListItem title="Social Connections">
<NamedListItem title="Integrations">
<Box>
<SocialConnectionChip active={ticker.websites.length > 0} label="Website" />

Check failure on line 31 in src/components/ticker/TickerCard.tsx

View workflow job for this annotation

GitHub Actions / Test

Unhandled error

TypeError: Cannot read properties of undefined (reading 'length') ❯ TickerCard src/components/ticker/TickerCard.tsx:31:59 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25889:20 ❯ performSyncWorkOnRoot node_modules/react-dom/cjs/react-dom.development.js:26135:20 This error originated in "src/views/TickerView.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "should render the ticker". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
<SocialConnectionChip active={ticker.mastodon.active} label="Mastodon" />
<SocialConnectionChip active={ticker.telegram.active} label="Telegram" />
<SocialConnectionChip active={ticker.bluesky.active} label="Bluesky" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Grid } from '@mui/material'
import { FC } from 'react'
import { Ticker } from '../../api/Ticker'
import MastodonCard from './MastodonCard'
import TelegramCard from './TelegramCard'
import BlueskyCard from './BlueskyCard'
import MastodonCard from './MastodonCard'
import SignalGroupCard from './SignalGroupCard'
import TelegramCard from './TelegramCard'
import WebsiteCard from './WebsiteCard'

interface Props {
ticker: Ticker
}

const TickerSocialConnections: FC<Props> = ({ ticker }) => {
const TickerIntegrations: FC<Props> = ({ ticker }) => {
return (
<Grid container spacing={2}>
<Grid item md={6} xs={12}>
<WebsiteCard ticker={ticker} />
</Grid>
<Grid item md={6} xs={12}>
<MastodonCard ticker={ticker} />
</Grid>
Expand All @@ -29,4 +33,4 @@ const TickerSocialConnections: FC<Props> = ({ ticker }) => {
)
}

export default TickerSocialConnections
export default TickerIntegrations
10 changes: 3 additions & 7 deletions src/components/ticker/TickerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const TickerList: FC<Props> = ({ token }) => {
if (params.order_by) newSearchParams.set('order_by', params.order_by)
if (params.sort) newSearchParams.set('sort', params.sort)
if (params.title) newSearchParams.set('title', params.title)
if (params.domain) newSearchParams.set('domain', params.domain)
if (params.origin) newSearchParams.set('domain', params.origin)
if (params.active !== undefined) newSearchParams.set('active', String(params.active))
setSearchParams(newSearchParams)
}, [debouncedValue, params, setSearchParams])
Expand Down Expand Up @@ -64,7 +64,7 @@ const TickerList: FC<Props> = ({ token }) => {
<TableHead>
<TableRow>
<TableCell colSpan={5} sx={{ p: 1 }}>
<TickerListFilter params={params} onTitleChange={handleFilterChange} onDomainChange={handleFilterChange} onActiveChange={handleActiveChange} />
<TickerListFilter params={params} onTitleChange={handleFilterChange} onOriginChange={handleFilterChange} onActiveChange={handleActiveChange} />
</TableCell>
</TableRow>
<TableRow>
Expand All @@ -83,11 +83,7 @@ const TickerList: FC<Props> = ({ token }) => {
Title
</TableSortLabel>
</TableCell>
<TableCell sortDirection={setDirection('domain')}>
<TableSortLabel active={sortActive('domain')} direction={setDirection('domain')} onClick={() => handleSortChange('domain')}>
Domain
</TableSortLabel>
</TableCell>
<TableCell sortDirection={setDirection('origin')}>Web Origins</TableCell>
<TableCell />
</TableRow>
</TableHead>
Expand Down
44 changes: 22 additions & 22 deletions src/components/ticker/TickerListFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
import { render, screen } from '@testing-library/react'
import TickerListFilter from './TickerListFilter'
import { GetTickersQueryParams } from '../../api/Ticker'
import userEvent from '@testing-library/user-event'
import { GetTickersQueryParams } from '../../api/Ticker'
import TickerListFilter from './TickerListFilter'

describe('TickerListFilter', async function () {
it('should render', function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

expect(onTitleChange).not.toHaveBeenCalled()
expect(onDomainChange).not.toHaveBeenCalled()
expect(onOriginChange).not.toHaveBeenCalled()
expect(onActiveChange).not.toHaveBeenCalled()

expect(screen.getByLabelText('Title')).toBeInTheDocument()
expect(screen.getByLabelText('Domain')).toBeInTheDocument()
expect(screen.getByLabelText('Origin')).toBeInTheDocument()
expect(screen.getByText('All')).toBeInTheDocument()
expect(screen.getByText('Active')).toBeInTheDocument()
expect(screen.getByText('Inactive')).toBeInTheDocument()

expect(screen.getByLabelText('Title')).toHaveValue('')
expect(screen.getByLabelText('Domain')).toHaveValue('')
expect(screen.getByLabelText('Origin')).toHaveValue('')
expect(screen.getByText('All')).toHaveClass('Mui-selected')
})

it('should call onTitleChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.type(screen.getByLabelText('Title'), 'foo')
expect(onTitleChange).toHaveBeenCalledWith('title', 'f')
expect(onTitleChange).toHaveBeenCalledWith('title', 'o')
expect(onTitleChange).toHaveBeenCalledWith('title', 'o')
})

it('should call onDomainChange', async function () {
it('should call onOriginChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.type(screen.getByLabelText('Domain'), 'foo')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'f')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'o')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'o')
await userEvent.type(screen.getByLabelText('Origin'), 'foo')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'f')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'o')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'o')
})

it('should call onActiveChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.click(screen.getByText('Active'))
expect(onActiveChange).toHaveBeenCalledWith(expect.anything(), 'true')
Expand Down
6 changes: 3 additions & 3 deletions src/components/ticker/TickerListFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { GetTickersQueryParams } from '../../api/Ticker'
interface Props {
params: GetTickersQueryParams
onTitleChange: (field: string, value: string) => void
onDomainChange: (field: string, value: string) => void
onOriginChange: (field: string, value: string) => void
onActiveChange: (e: React.MouseEvent<HTMLElement, MouseEvent>, value: unknown) => void
}

const TickerListFilter: FC<Props> = ({ params, onTitleChange, onDomainChange, onActiveChange }) => {
const TickerListFilter: FC<Props> = ({ params, onTitleChange, onOriginChange, onActiveChange }) => {
return (
<Stack direction="row" alignItems="center">
<Box sx={{ px: 1 }}>
Expand All @@ -28,7 +28,7 @@ const TickerListFilter: FC<Props> = ({ params, onTitleChange, onDomainChange, on
/>
</Box>
<Box sx={{ px: 1 }}>
<TextField label="Domain" onChange={e => onDomainChange('domain', e.target.value)} placeholder="Filter by domain" size="small" value={params.domain} />
<TextField label="Origin" onChange={e => onOriginChange('origin', e.target.value)} placeholder="Filter by origin" size="small" value={params.origin} />
</Box>
<Box sx={{ px: 1 }}>
<ToggleButtonGroup size="small" value={params.active} exclusive onChange={onActiveChange}>
Expand Down
Loading

0 comments on commit 599bfbf

Please sign in to comment.