Merge remote-tracking branch 'origin/pr/562' into codex/pr-561-562-559

This commit is contained in:
Codex Integration
2026-09-12 15:53:44 -06:00
4 changed files with 235 additions and 2 deletions
@@ -0,0 +1,127 @@
import React from 'react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import FilterPanel from '@/components/FilterPanel';
import { getDefaultActiveLayers } from '@/lib/layerPreferences';
vi.mock('@/hooks/useDataStore', () => ({
useDataKeys: () => ({
commercial_flights: [],
private_flights: [],
private_jets: [],
military_flights: [],
tracked_flights: [],
ships: [],
}),
}));
vi.mock('@/i18n', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock('@/lib/motion', () => ({
motion: {
div: ({ children, ...props }: React.ComponentProps<'div'> & Record<string, unknown>) => {
const rest = { ...props };
for (const key of ['initial', 'animate', 'exit', 'transition']) delete rest[key];
return <div {...(rest as React.ComponentProps<'div'>)}>{children}</div>;
},
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
function openSection(title: string) {
fireEvent.click(screen.getByText('FILTERS.TITLE'));
fireEvent.click(screen.getByText(title));
}
describe('FilterPanel layer-off handling', () => {
afterEach(() => cleanup());
it('flags a section, warns, and enables the layer from the dialog', () => {
const layers = { ...getDefaultActiveLayers(), tracked: false };
const setActiveFilters = vi.fn();
const onEnableLayers = vi.fn();
render(
<FilterPanel
activeFilters={{}}
setActiveFilters={setActiveFilters}
activeLayers={layers}
onEnableLayers={onEnableLayers}
/>,
);
fireEvent.click(screen.getByText('FILTERS.TITLE'));
expect(screen.getAllByText('LAYER OFF')).toHaveLength(1);
fireEvent.click(screen.getByText('TRACKED AIRCRAFT'));
expect(screen.getByRole('status').textContent).toContain(
'TRACKED AIRCRAFT LAYER IS OFF — THIS FILTER WILL NOT CHANGE THE MAP',
);
fireEvent.click(screen.getByRole('button', { name: 'ENABLE TRACKED AIRCRAFT' }));
expect(onEnableLayers).toHaveBeenCalledWith(['tracked']);
// Apply stays a plain apply; it never toggles layers itself.
fireEvent.click(screen.getByText('Celebrity'));
fireEvent.click(screen.getByRole('button', { name: 'APPLY (1)' }));
expect(onEnableLayers).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({ tracked_category: ['Celebrity'] });
});
it('explains an empty list by layer state instead of "no matching results"', () => {
const layers = { ...getDefaultActiveLayers(), flights: false };
const { unmount } = render(
<FilterPanel activeFilters={{}} setActiveFilters={vi.fn()} activeLayers={layers} />,
);
openSection('COMMERCIAL FLIGHTS');
expect(screen.getByText('LAYER IS OFF — ENABLE IT ABOVE TO LOAD OPTIONS')).toBeTruthy();
// No enable control when the caller cannot toggle layers.
expect(screen.queryByRole('button', { name: /^ENABLE / })).toBeNull();
unmount();
render(
<FilterPanel
activeFilters={{}}
setActiveFilters={vi.fn()}
activeLayers={getDefaultActiveLayers()}
/>,
);
openSection('COMMERCIAL FLIGHTS');
expect(screen.queryByRole('status')).toBeNull();
expect(screen.getByText('WAITING FOR DATA…')).toBeTruthy();
});
it('offers one enable chip per layer for the private / jets section', () => {
const layers = { ...getDefaultActiveLayers(), private: false };
const onEnableLayers = vi.fn();
render(
<FilterPanel
activeFilters={{}}
setActiveFilters={vi.fn()}
activeLayers={layers}
onEnableLayers={onEnableLayers}
/>,
);
fireEvent.click(screen.getByText('FILTERS.TITLE'));
// Jets is still on, so the section itself is not flagged.
expect(screen.queryByText('LAYER OFF')).toBeNull();
fireEvent.click(screen.getByText('PRIVATE / JETS'));
expect(screen.getByRole('status').textContent).toContain(
'PRIVATE AIRCRAFT LAYER IS OFF — THIS FILTER ONLY AFFECTS THE ENABLED LAYERS',
);
expect(screen.queryByRole('button', { name: 'ENABLE PRIVATE JETS' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'ENABLE PRIVATE AIRCRAFT' }));
expect(onEnableLayers).toHaveBeenCalledWith(['private']);
});
it('does not flag anything when layer state is not supplied', () => {
render(<FilterPanel activeFilters={{}} setActiveFilters={vi.fn()} />);
openSection('TRACKED AIRCRAFT');
expect(screen.queryByText('LAYER OFF')).toBeNull();
expect(screen.queryByRole('status')).toBeNull();
});
});
+9
View File
@@ -225,6 +225,13 @@ export default function Dashboard() {
const resetActiveLayers = useCallback(() => {
setActiveLayers(getDefaultActiveLayers());
}, []);
const enableLayers = useCallback((keys: (keyof ActiveLayers)[]) => {
setActiveLayers((prev) => {
const next = { ...prev };
for (const key of keys) next[key] = true;
return next;
});
}, []);
const regionLat =
selectedEntity?.type === 'region_dossier' ? selectedEntity.extra?.lat : undefined;
const regionLng =
@@ -799,6 +806,8 @@ export default function Dashboard() {
<FilterPanel
activeFilters={activeFilters}
setActiveFilters={setActiveFilters}
activeLayers={activeLayers}
onEnableLayers={enableLayers}
/>
</ErrorBoundary>
</div>
@@ -23,6 +23,9 @@ interface AdvancedFilterModalProps {
accentColorName: string; // tailwind name e.g. 'cyan'
fields: FilterField[];
activeFilters: Record<string, string[]>;
/** Layers this filter acts on. Omit to skip the layer-state UI entirely. */
layers?: { id: string; label: string; enabled: boolean }[];
onEnableLayer?: (id: string) => void;
onApply: (filters: Record<string, string[]>) => void;
onClose: () => void;
}
@@ -34,6 +37,8 @@ export default function AdvancedFilterModal({
accentColorName,
fields,
activeFilters,
layers,
onEnableLayer,
onApply,
onClose,
}: AdvancedFilterModalProps) {
@@ -147,6 +152,12 @@ export default function AdvancedFilterModal({
const totalSelected = Object.values(draft).reduce((acc, s) => acc + s.size, 0);
// Option lists are built from live data, so a disabled layer means an empty
// list and nothing to search. Enabling from here refetches within a few
// seconds and the list fills in place.
const disabledLayers = layers?.filter((l) => !l.enabled) ?? [];
const anyLayerEnabled = !layers || layers.some((l) => l.enabled);
const activeField = fields.find((f) => f.key === activeTab);
const filteredOptions = useMemo(() => {
if (!activeField) return [];
@@ -262,6 +273,34 @@ export default function AdvancedFilterModal({
</button>
</div>
{disabledLayers.length > 0 && (
<div
role="status"
className="px-4 py-2 flex flex-col gap-2 text-[9px] tracking-widest text-amber-400 bg-amber-500/10 border-b border-amber-500/30 flex-shrink-0"
>
<span>
{disabledLayers.map((l) => l.label.toUpperCase()).join(' + ')} LAYER
{disabledLayers.length > 1 ? 'S ARE' : ' IS'} OFF {' '}
{anyLayerEnabled
? 'THIS FILTER ONLY AFFECTS THE ENABLED LAYERS'
: 'THIS FILTER WILL NOT CHANGE THE MAP UNTIL A LAYER IS ENABLED'}
</span>
{onEnableLayer && (
<span className="flex flex-wrap gap-1.5">
{disabledLayers.map((l) => (
<button
key={l.id}
onClick={() => onEnableLayer(l.id)}
className="border border-amber-500/50 bg-amber-500/15 hover:bg-amber-500/30 px-2 py-1 text-amber-300 transition-colors"
>
ENABLE {l.label.toUpperCase()}
</button>
))}
</span>
)}
</div>
)}
{/* ── Tab Bar (for multi-field categories) ── */}
{fields.length > 1 && (
<div className="flex border-b border-[var(--border-primary)]/40 px-3 pt-2 gap-1 flex-shrink-0">
@@ -362,7 +401,11 @@ export default function AdvancedFilterModal({
>
{filteredOptions.length === 0 ? (
<div className="text-center py-8 text-[var(--text-muted)] text-[10px] tracking-widest">
NO MATCHING RESULTS
{!anyLayerEnabled
? 'LAYER IS OFF — ENABLE IT ABOVE TO LOAD OPTIONS'
: activeField && activeField.options.length === 0
? 'WAITING FOR DATA…'
: 'NO MATCHING RESULTS'}
</div>
) : (
<div className="flex flex-col gap-px">
+55 -1
View File
@@ -17,10 +17,16 @@ import { useDataKeys } from '@/hooks/useDataStore';
import { airlineNames } from '../lib/airlineCodes';
import { useTranslation } from '@/i18n';
import { trackedCategories, trackedOperators } from '../lib/trackedData';
import type { ActiveLayers } from '@/types/dashboard';
type LayerKey = keyof ActiveLayers;
interface FilterPanelProps {
activeFilters: Record<string, string[]>;
setActiveFilters: (filters: Record<string, string[]>) => void;
/** Optional: lets the modal warn when the filtered layer is switched off. */
activeLayers?: ActiveLayers;
onEnableLayers?: (keys: LayerKey[]) => void;
}
type ModalConfig = {
@@ -28,6 +34,8 @@ type ModalConfig = {
icon: React.ReactNode;
accentColor: string;
accentColorName: string;
/** Layer toggles this filter acts on; the filter is inert while all are off. */
layers: { keys: LayerKey[]; label: string }[];
fields: {
key: string;
label: string;
@@ -36,7 +44,12 @@ type ModalConfig = {
}[];
};
const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFilters }: FilterPanelProps) {
const FilterPanel = React.memo(function FilterPanel({
activeFilters,
setActiveFilters,
activeLayers,
onEnableLayers,
}: FilterPanelProps) {
const { t } = useTranslation();
const data = useDataKeys(['commercial_flights', 'private_flights', 'private_jets', 'military_flights', 'tracked_flights', 'ships'] as const);
const [isMinimized, setIsMinimized] = useState(true);
@@ -162,6 +175,7 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
icon: <Plane size={13} className="text-cyan-400" />,
accentColor: '#00bcd4',
accentColorName: 'cyan',
layers: [{ keys: ['flights'], label: 'Commercial Flights' }],
fields: [
{ key: 'commercial_departure', label: 'DEPARTURE', options: uniqueOrigins },
{ key: 'commercial_arrival', label: 'ARRIVAL', options: uniqueDestinations },
@@ -178,6 +192,10 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
icon: <Plane size={13} className="text-orange-400" />,
accentColor: '#FF8C00',
accentColorName: 'orange',
layers: [
{ keys: ['private'], label: 'Private Aircraft' },
{ keys: ['jets'], label: 'Private Jets' },
],
fields: [
{ key: 'private_callsign', label: 'CALLSIGN / REG', options: uniquePrivateCallsigns },
{
@@ -192,6 +210,7 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
icon: <Shield size={13} className="text-yellow-400" />,
accentColor: '#EAB308',
accentColorName: 'yellow',
layers: [{ keys: ['military'], label: 'Military Flights' }],
fields: [
{ key: 'military_country', label: 'COUNTRY / REG', options: uniqueMilCountries },
{ key: 'military_aircraft_type', label: 'AIRCRAFT TYPE', options: uniqueMilAircraftTypes },
@@ -202,6 +221,7 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
icon: <Star size={13} className="text-pink-400" />,
accentColor: '#EC4899',
accentColorName: 'pink',
layers: [{ keys: ['tracked'], label: 'Tracked Aircraft' }],
fields: [
{ key: 'tracked_category', label: 'CATEGORY', options: uniqueTrackedCategories },
{ key: 'tracked_owner', label: 'OPERATOR / ENTITY', options: uniqueTrackedOperators },
@@ -212,6 +232,12 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
icon: <Ship size={13} className="text-blue-400" />,
accentColor: '#3B82F6',
accentColorName: 'blue',
layers: [
{
keys: ['ships_military', 'ships_cargo', 'ships_civilian', 'ships_passenger', 'ships_tracked_yachts'],
label: 'Maritime',
},
],
fields: [
{ key: 'ship_name', label: 'VESSEL NAME', options: uniqueShipNames },
{ key: 'ship_type', label: 'VESSEL TYPE', options: uniqueVesselTypes },
@@ -221,6 +247,24 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
const clearAll = () => setActiveFilters({});
// Unknown layer state (prop omitted) counts as enabled so nothing is flagged.
const isLayerEnabled = (keys: LayerKey[]) =>
!activeLayers || keys.some((key) => activeLayers[key]);
const isSectionEnabled = (config: ModalConfig) =>
config.layers.some((layer) => isLayerEnabled(layer.keys));
const modalLayers = (config: ModalConfig) =>
activeLayers
? config.layers.map((layer) => ({
id: layer.keys.join('+'),
label: layer.label,
enabled: isLayerEnabled(layer.keys),
}))
: undefined;
const enableModalLayer = (config: ModalConfig, id: string) => {
const layer = config.layers.find((l) => l.keys.join('+') === id);
if (layer && onEnableLayers) onEnableLayers(layer.keys);
};
const activeCount = Object.values(activeFilters).reduce((acc, arr) => acc + arr.length, 0);
const getCountForCategory = (category: string) => {
@@ -346,6 +390,7 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
{sections.map((section) => {
const count = getCountForCategory(section.key);
const layerOff = !isSectionEnabled(modalConfigs[section.key]);
return (
<div
key={section.key}
@@ -365,6 +410,11 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
{count}
</span>
)}
{layerOff && (
<span className="text-[9px] text-amber-400/80 tracking-widest">
LAYER OFF
</span>
)}
</div>
<SlidersHorizontal
size={10}
@@ -390,6 +440,10 @@ const FilterPanel = React.memo(function FilterPanel({ activeFilters, setActiveFi
accentColorName={modalConfigs[openModal].accentColorName}
fields={modalConfigs[openModal].fields}
activeFilters={activeFilters}
layers={modalLayers(modalConfigs[openModal])}
onEnableLayer={
onEnableLayers ? (id) => enableModalLayer(modalConfigs[openModal], id) : undefined
}
onApply={(filters) => handleModalApply(openModal, filters)}
onClose={() => setOpenModal(null)}
/>