Skip to content

Commit

Permalink
Merge pull request #631 from systemli/feat/signal-group
Browse files Browse the repository at this point in the history
✨ Add support to configure Signal group bridge
  • Loading branch information
doobry-systemli authored Jun 25, 2024
2 parents 9e003ae + 2f1f8e9 commit 1796777
Show file tree
Hide file tree
Showing 13 changed files with 719 additions and 5 deletions.
41 changes: 41 additions & 0 deletions src/api/Ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TickerFormData {
mastodon: TickerMastodon
telegram: TickerTelegram
bluesky: TickerBluesky
signalGroup: TickerSignalGroup
location: TickerLocation
}

Expand All @@ -37,6 +38,7 @@ export interface Ticker {
mastodon: TickerMastodon
telegram: TickerTelegram
bluesky: TickerBluesky
signalGroup: TickerSignalGroup
location: TickerLocation
}

Expand Down Expand Up @@ -94,6 +96,25 @@ export interface TickerBlueskyFormData {
appKey?: string
}

export interface TickerSignalGroup {
active: boolean
connected: boolean
groupID: string
groupName: string
groupDescription: string
groupInviteLink: string
}

export interface TickerSignalGroupFormData {
active: boolean
groupName?: string
groupDescription?: string
}

export interface TickerSignalGroupAdminFormData {
number: string
}

export interface TickerLocation {
lat: number
lon: number
Expand Down Expand Up @@ -190,3 +211,23 @@ export async function putTickerBlueskyApi(token: string, data: TickerBlueskyForm
export async function deleteTickerBlueskyApi(token: string, ticker: Ticker): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/bluesky`, { headers: apiHeaders(token), method: 'delete' })
}

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

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

export async function putTickerSignalGroupAdminApi(token: string, data: TickerSignalGroupAdminFormData, ticker: Ticker): Promise<ApiResponse<void>> {
return apiClient<void>(`${ApiUrl}/admin/tickers/${ticker.id}/signal_group/admin`, {
headers: apiHeaders(token),
method: 'put',
body: JSON.stringify(data),
})
}
27 changes: 22 additions & 5 deletions src/components/common/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, ReactNode } from 'react'
import { Breakpoint, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, SxProps } from '@mui/material'
import { Close } from '@mui/icons-material'
import { Box, Breakpoint, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, SxProps } from '@mui/material'
import { FC, ReactNode } from 'react'

interface Props {
children: ReactNode
Expand All @@ -14,6 +14,7 @@ interface Props {
open: boolean
submitForm?: string
title?: string
submitting?: boolean
}

const Modal: FC<Props> = ({
Expand All @@ -27,6 +28,7 @@ const Modal: FC<Props> = ({
onSubmitAction,
open,
submitForm,
submitting = false,
title,
}) => {
return (
Expand All @@ -42,9 +44,24 @@ const Modal: FC<Props> = ({
<DialogContent sx={dialogContentSx}>{children}</DialogContent>
<DialogActions>
{submitForm && (
<Button color="primary" form={submitForm} onClick={onSubmitAction} type="submit" variant="contained">
Save
</Button>
<Box sx={{ display: 'inline', position: 'relative' }}>
<Button color="primary" form={submitForm} onClick={onSubmitAction} type="submit" variant="contained" disabled={submitting}>
Save
</Button>
{submitting && (
<CircularProgress
size={24}
sx={{
color: 'primary',
position: 'absolute',
top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
}}
/>
)}
</Box>
)}
{onDangerAction && dangerActionButtonText && (
<Button color="error" onClick={onDangerAction} variant="contained">
Expand Down
99 changes: 99 additions & 0 deletions src/components/ticker/SignalGroupAdminForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import sign from 'jwt-encode'
import { MemoryRouter } from 'react-router'
import { Ticker } from '../../api/Ticker'
import { AuthProvider } from '../../contexts/AuthContext'
import { NotificationProvider } from '../../contexts/NotificationContext'
import SignalGroupAdminForm from './SignalGroupAdminForm'

const token = sign({ id: 1, email: 'user@example.org', roles: ['user'], exp: new Date().getTime() / 1000 + 600 }, 'secret')

describe('SignalGroupForm', () => {
beforeAll(() => {
localStorage.setItem('token', token)
})

const ticker = ({ active, connected }: { active: boolean; connected: boolean }) => {
return {
id: 1,
signalGroup: {
active: active,
connected: connected,
},
} as Ticker
}

const callback = vi.fn()
const setSubmitting = vi.fn()

beforeEach(() => {
fetchMock.resetMocks()
})

function setup(ticker: Ticker) {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return render(
<QueryClientProvider client={client}>
<MemoryRouter>
<AuthProvider>
<NotificationProvider>
<div>
<SignalGroupAdminForm callback={callback} ticker={ticker} setSubmitting={setSubmitting} />
<input name="Submit" type="submit" value="Submit" form="configureSignalGroupAdmin" />
</div>
</NotificationProvider>
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>
)
}

it('should render the component', async () => {
setup(ticker({ active: false, connected: false }))

expect(screen.getByText('Only do this if extra members with write access are needed.')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Phone number' })).toBeInTheDocument()

await userEvent.type(screen.getByRole('textbox', { name: 'Phone number' }), '+49123456789')

fetchMock.mockResponseOnce(JSON.stringify({ status: 'success' }))

await userEvent.click(screen.getByRole('button', { name: 'Submit' }))

expect(setSubmitting).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith('http://localhost:8080/v1/admin/tickers/1/signal_group/admin', {
body: '{"number":"+49123456789"}',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
method: 'put',
})
})

it('should render the error message', async () => {
setup(ticker({ active: false, connected: false }))

expect(screen.getByText('Only do this if extra members with write access are needed.')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Phone number' })).toBeInTheDocument()

await userEvent.type(screen.getByRole('textbox', { name: 'Phone number' }), '+49123456789')

fetchMock.mockResponseOnce(JSON.stringify({ status: 'error' }), { status: 400 })

await userEvent.click(screen.getByRole('button', { name: 'Submit' }))

expect(screen.getByText('Failed to add number to Signal group')).toBeInTheDocument()
})
})
73 changes: 73 additions & 0 deletions src/components/ticker/SignalGroupAdminForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { faPhone } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Alert, FormGroup, Grid, InputAdornment, TextField } from '@mui/material'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import { Ticker, TickerSignalGroupAdminFormData, putTickerSignalGroupAdminApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'

interface Props {
callback: () => void
ticker: Ticker
setSubmitting: (submitting: boolean) => void
}

const SignalGroupAdminForm: FC<Props> = ({ callback, ticker, setSubmitting }) => {
const { token } = useAuth()
const {
formState: { errors },
handleSubmit,
register,
setError,
} = useForm<TickerSignalGroupAdminFormData>()
const { createNotification } = useNotification()

const onSubmit = handleSubmit(data => {
setSubmitting(true)
putTickerSignalGroupAdminApi(token, data, ticker)
.then(response => {
if (response.status == 'error') {
createNotification({ content: 'Failed to add number to Signal group', severity: 'error' })
setError('number', { message: 'Failed to add number to Signal group' })
} else {
createNotification({ content: 'Number successfully added to Signal group', severity: 'success' })
callback()
}
})
.finally(() => {
setSubmitting(false)
})
})

return (
<form id="configureSignalGroupAdmin" onSubmit={onSubmit}>
<Grid columnSpacing={{ xs: 1, sm: 2, md: 3 }} container rowSpacing={1}>
<Grid item xs={12}>
<Alert severity="warning">Only do this if extra members with write access are needed.</Alert>
</Grid>
<Grid item xs={12}>
<FormGroup>
<TextField
{...register('number')}
label="Phone number"
placeholder="+49123456789"
required
helperText={errors.number ? errors.number?.message : null}
error={errors.number ? true : false}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FontAwesomeIcon icon={faPhone} />
</InputAdornment>
),
}}
/>
</FormGroup>
</Grid>
</Grid>
</form>
)
}

export default SignalGroupAdminForm
22 changes: 22 additions & 0 deletions src/components/ticker/SignalGroupAdminModalForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC, useState } from 'react'
import { Ticker } from '../../api/Ticker'
import Modal from '../common/Modal'
import SignalGroupAdminForm from './SignalGroupAdminForm'

interface Props {
onClose: () => void
open: boolean
ticker: Ticker
}

const SignalGroupAdminModalForm: FC<Props> = ({ onClose, open, ticker }) => {
const [submitting, setSubmitting] = useState<boolean>(false)

return (
<Modal maxWidth="sm" onClose={onClose} open={open} submitForm="configureSignalGroupAdmin" title="Add Admin to group" submitting={submitting}>
<SignalGroupAdminForm callback={onClose} ticker={ticker} setSubmitting={setSubmitting} />
</Modal>
)
}

export default SignalGroupAdminModalForm
Loading

0 comments on commit 1796777

Please sign in to comment.