mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Make pathing clearable on click
This commit is contained in:
@@ -134,6 +134,10 @@ export const api = {
|
||||
fetchJson<TraceResponse>(`/contacts/${publicKey}/trace`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
resetContactPath: (publicKey: string) =>
|
||||
fetchJson<{ status: string; public_key: string }>(`/contacts/${publicKey}/reset-path`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// Channels
|
||||
getChannels: () => fetchJson<Channel[]>('/channels'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react';
|
||||
import { toast } from './ui/sonner';
|
||||
import { api } from '../api';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from '../utils/pathUtils';
|
||||
import { getMapFocusHash } from '../utils/urlHash';
|
||||
@@ -87,9 +88,43 @@ export function ChatHeader({
|
||||
if (contact.last_path_len === -1) {
|
||||
parts.push('flood');
|
||||
} else if (contact.last_path_len === 0) {
|
||||
parts.push('direct');
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
direct
|
||||
</span>
|
||||
);
|
||||
} else if (contact.last_path_len > 0) {
|
||||
parts.push(`${contact.last_path_len} hop${contact.last_path_len > 1 ? 's' : ''}`);
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
{contact.last_path_len} hop{contact.last_path_len > 1 ? 's' : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidLocation(contact.lat, contact.lon)) {
|
||||
const distFromUs =
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Input } from './ui/input';
|
||||
import { Separator } from './ui/separator';
|
||||
import { RepeaterLogin } from './RepeaterLogin';
|
||||
import { useRepeaterDashboard } from '../hooks/useRepeaterDashboard';
|
||||
import { api } from '../api';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { isFavorite } from '../utils/favorites';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -849,9 +850,43 @@ export function RepeaterDashboard({
|
||||
if (contact.last_path_len === -1) {
|
||||
parts.push('flood');
|
||||
} else if (contact.last_path_len === 0) {
|
||||
parts.push('direct');
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
direct
|
||||
</span>
|
||||
);
|
||||
} else if (contact.last_path_len > 0) {
|
||||
parts.push(`${contact.last_path_len} hop${contact.last_path_len > 1 ? 's' : ''}`);
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
{contact.last_path_len} hop{contact.last_path_len > 1 ? 's' : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidLocation(contact.lat, contact.lon)) {
|
||||
const distFromUs =
|
||||
|
||||
@@ -268,4 +268,105 @@ describe('RepeaterDashboard', () => {
|
||||
|
||||
expect(screen.getByText('Type a CLI command below...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('path type display and reset', () => {
|
||||
it('shows flood when last_path_len is -1', () => {
|
||||
render(<RepeaterDashboard {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('flood')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows direct when last_path_len is 0', () => {
|
||||
const directContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 0, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={directContacts} />);
|
||||
|
||||
expect(screen.getByText('direct')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows N hops when last_path_len > 0', () => {
|
||||
const hoppedContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 3, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={hoppedContacts} />);
|
||||
|
||||
expect(screen.getByText('3 hops')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows 1 hop (singular) for single hop', () => {
|
||||
const oneHopContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 1, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={oneHopContacts} />);
|
||||
|
||||
expect(screen.getByText('1 hop')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('direct path is clickable with reset title', () => {
|
||||
const directContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 0, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={directContacts} />);
|
||||
|
||||
const directEl = screen.getByTitle('Click to reset path to flood');
|
||||
expect(directEl).toBeInTheDocument();
|
||||
expect(directEl.textContent).toBe('direct');
|
||||
});
|
||||
|
||||
it('clicking direct path calls resetContactPath on confirm', async () => {
|
||||
const directContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 0, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
// Mock window.confirm to return true
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
// Mock the api module
|
||||
const { api } = await import('../api');
|
||||
const resetSpy = vi.spyOn(api, 'resetContactPath').mockResolvedValue({
|
||||
status: 'ok',
|
||||
public_key: REPEATER_KEY,
|
||||
});
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={directContacts} />);
|
||||
|
||||
fireEvent.click(screen.getByTitle('Click to reset path to flood'));
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalledWith('Reset path to flood?');
|
||||
expect(resetSpy).toHaveBeenCalledWith(REPEATER_KEY);
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
resetSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('clicking path does not call API when confirm is cancelled', async () => {
|
||||
const directContacts: Contact[] = [
|
||||
{ ...contacts[0], last_path_len: 0, last_seen: 1700000000 },
|
||||
];
|
||||
|
||||
// Mock window.confirm to return false
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
|
||||
const { api } = await import('../api');
|
||||
const resetSpy = vi.spyOn(api, 'resetContactPath').mockResolvedValue({
|
||||
status: 'ok',
|
||||
public_key: REPEATER_KEY,
|
||||
});
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={directContacts} />);
|
||||
|
||||
fireEvent.click(screen.getByTitle('Click to reset path to flood'));
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalledWith('Reset path to flood?');
|
||||
expect(resetSpy).not.toHaveBeenCalled();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
resetSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user