Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Extract Websites from Ticker to Integrations #701

Merged
merged 1 commit into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />
<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
Loading