Skip to content

Commit b7958f5

Browse files
authored
make segmentation page menus cleaner for future modification and easier reactifying (ESA-PhiLab#33)
2 parents 1521db8 + d383e8d commit b7958f5

38 files changed

Lines changed: 1234 additions & 365 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import SegmentationModals from './SegmentationModals';
4+
5+
describe('SegmentationModals', () => {
6+
const mockProps = {
7+
isPreferencesOpen: false,
8+
onClosePreferences: vi.fn(),
9+
isProfileOpen: false,
10+
onCloseProfile: vi.fn(),
11+
profileUserId: 'current',
12+
isLoginOpen: false,
13+
loginMode: 'login' as const,
14+
isHelpOpen: false,
15+
onCloseHelp: vi.fn(),
16+
isResetMaskOpen: false,
17+
onCloseResetMask: vi.fn(),
18+
onConfirmResetMask: vi.fn(),
19+
isClassSelectionOpen: false,
20+
onCloseClassSelection: vi.fn(),
21+
isImageInfoOpen: false,
22+
onCloseImageInfo: vi.fn(),
23+
isConfusionMatrixOpen: false,
24+
onCloseConfusionMatrix: vi.fn(),
25+
};
26+
27+
it('renders without crashing', () => {
28+
render(<SegmentationModals {...mockProps} />);
29+
expect(document.body).toBeInTheDocument();
30+
});
31+
32+
it('shows preferences modal when open', () => {
33+
render(<SegmentationModals {...mockProps} isPreferencesOpen={true} />);
34+
expect(screen.getByText('Preferences')).toBeInTheDocument();
35+
});
36+
37+
it('shows help modal when open', () => {
38+
render(<SegmentationModals {...mockProps} isHelpOpen={true} />);
39+
expect(screen.getByText('Help')).toBeInTheDocument();
40+
});
41+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from 'react';
2+
import PreferencesModal from '../PreferencesModal';
3+
import { UserProfileModal } from '../UserProfileModal';
4+
import { LoginForm } from '../LoginForm';
5+
import HelpModal from '../HelpModal';
6+
import ConfirmDialog from '../ConfirmDialog';
7+
import ClassSelectionModal from '../ClassSelectionModal';
8+
import ImageInfoModal from '../ImageInfoModal';
9+
import ConfusionMatrixModal from '../ConfusionMatrixModal';
10+
11+
interface SegmentationModalsProps {
12+
isPreferencesOpen: boolean;
13+
onClosePreferences: () => void;
14+
isProfileOpen: boolean;
15+
onCloseProfile: () => void;
16+
profileUserId: string;
17+
isLoginOpen: boolean;
18+
loginMode: 'login' | 'register';
19+
isHelpOpen: boolean;
20+
onCloseHelp: () => void;
21+
isResetMaskOpen: boolean;
22+
onCloseResetMask: () => void;
23+
onConfirmResetMask: () => void;
24+
isClassSelectionOpen: boolean;
25+
onCloseClassSelection: () => void;
26+
isImageInfoOpen: boolean;
27+
onCloseImageInfo: () => void;
28+
isConfusionMatrixOpen: boolean;
29+
onCloseConfusionMatrix: () => void;
30+
}
31+
32+
const SegmentationModals: React.FC<SegmentationModalsProps> = ({
33+
isPreferencesOpen,
34+
onClosePreferences,
35+
isProfileOpen,
36+
onCloseProfile,
37+
profileUserId,
38+
isLoginOpen,
39+
loginMode,
40+
isHelpOpen,
41+
onCloseHelp,
42+
isResetMaskOpen,
43+
onCloseResetMask,
44+
onConfirmResetMask,
45+
isClassSelectionOpen,
46+
onCloseClassSelection,
47+
isImageInfoOpen,
48+
onCloseImageInfo,
49+
isConfusionMatrixOpen,
50+
onCloseConfusionMatrix
51+
}) => {
52+
return (
53+
<>
54+
<PreferencesModal isOpen={isPreferencesOpen} onClose={onClosePreferences} />
55+
56+
<UserProfileModal
57+
isOpen={isProfileOpen}
58+
onClose={onCloseProfile}
59+
userId={profileUserId}
60+
/>
61+
62+
{isLoginOpen && <LoginForm initialMode={loginMode} />}
63+
64+
<HelpModal isOpen={isHelpOpen} onClose={onCloseHelp} />
65+
66+
<ConfirmDialog
67+
isOpen={isResetMaskOpen}
68+
onClose={onCloseResetMask}
69+
onConfirm={onConfirmResetMask}
70+
message="Are you sure you want to reset all your drawn pixels?"
71+
confirmText="Reset"
72+
cancelText="Cancel"
73+
type="warning"
74+
/>
75+
76+
<ClassSelectionModal
77+
isOpen={isClassSelectionOpen}
78+
onClose={onCloseClassSelection}
79+
/>
80+
81+
<ImageInfoModal isOpen={isImageInfoOpen} onClose={onCloseImageInfo} />
82+
83+
<ConfusionMatrixModal
84+
isOpen={isConfusionMatrixOpen}
85+
onClose={onCloseConfusionMatrix}
86+
/>
87+
</>
88+
);
89+
};
90+
91+
export default SegmentationModals;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render } from '@testing-library/react';
3+
import SegmentationStatusBar from './SegmentationStatusBar';
4+
5+
describe('SegmentationStatusBar', () => {
6+
const mockProps = {
7+
onOpenProfile: vi.fn(),
8+
onOpenImageInfo: vi.fn(),
9+
onOpenConfusionMatrix: vi.fn(),
10+
};
11+
12+
it('renders status bar with all sections', () => {
13+
const { container } = render(<SegmentationStatusBar {...mockProps} />);
14+
15+
const statusbar = container.querySelector('#statusbar');
16+
expect(statusbar).toBeInTheDocument();
17+
expect(statusbar).toHaveClass('statusbar');
18+
});
19+
20+
it('renders all status bar components', () => {
21+
const { container } = render(<SegmentationStatusBar {...mockProps} />);
22+
23+
expect(container.querySelector('#user-info')).toBeInTheDocument();
24+
expect(container.querySelector('#admin-button')).toBeInTheDocument();
25+
expect(container.querySelector('#image-info')).toBeInTheDocument();
26+
expect(container.querySelector('#different-classes')).toBeInTheDocument();
27+
expect(container.querySelector('#drawn-pixels')).toBeInTheDocument();
28+
expect(container.querySelector('#ai-score')).toBeInTheDocument();
29+
expect(container.querySelector('#ai-recommendation')).toBeInTheDocument();
30+
});
31+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import UserInfo from './statusbar/UserInfo';
3+
import AdminButton from './statusbar/AdminButton';
4+
import ImageInfo from './statusbar/ImageInfo';
5+
import StatsDisplay from './statusbar/StatsDisplay';
6+
import AIScore from './statusbar/AIScore';
7+
import AIRecommendation from './statusbar/AIRecommendation';
8+
9+
interface SegmentationStatusBarProps {
10+
onOpenProfile: () => void;
11+
onOpenImageInfo: () => void;
12+
onOpenConfusionMatrix: () => void;
13+
}
14+
15+
const SegmentationStatusBar: React.FC<SegmentationStatusBarProps> = ({
16+
onOpenProfile,
17+
onOpenImageInfo,
18+
onOpenConfusionMatrix
19+
}) => {
20+
return (
21+
<div
22+
id="statusbar"
23+
className="statusbar"
24+
style={{ visibility: 'hidden', position: 'fixed', bottom: '10px', zIndex: 10 }}
25+
>
26+
<UserInfo onOpenProfile={onOpenProfile} />
27+
<AdminButton />
28+
<ImageInfo onOpenImageInfo={onOpenImageInfo} />
29+
<StatsDisplay />
30+
<AIScore onOpenConfusionMatrix={onOpenConfusionMatrix} />
31+
<AIRecommendation />
32+
</div>
33+
);
34+
};
35+
36+
export default SegmentationStatusBar;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render } from '@testing-library/react';
3+
import SegmentationToolbar from './SegmentationToolbar';
4+
5+
describe('SegmentationToolbar', () => {
6+
const mockProps = {
7+
onExportGeoTIFF: vi.fn(),
8+
onSelectClass: vi.fn(),
9+
onResetMask: vi.fn(),
10+
onOpenHelp: vi.fn(),
11+
onOpenPreferences: vi.fn(),
12+
};
13+
14+
it('renders toolbar with all sections', () => {
15+
const { container } = render(<SegmentationToolbar {...mockProps} />);
16+
17+
const toolbar = container.querySelector('#toolbar');
18+
expect(toolbar).toBeInTheDocument();
19+
expect(toolbar).toHaveClass('toolbar');
20+
});
21+
22+
it('renders toolbar separators', () => {
23+
const { container } = render(<SegmentationToolbar {...mockProps} />);
24+
25+
const separators = container.querySelectorAll('.toolbar_separator');
26+
expect(separators.length).toBeGreaterThan(0);
27+
});
28+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import NavigationTools from './toolbar/NavigationTools';
3+
import EditingTools from './toolbar/EditingTools';
4+
import ClassSelector from './toolbar/ClassSelector';
5+
import DrawingTools from './toolbar/DrawingTools';
6+
import MaskTools from './toolbar/MaskTools';
7+
import FilterTools from './toolbar/FilterTools';
8+
import SettingsTools from './toolbar/SettingsTools';
9+
import ToolbarSeparator from './toolbar/ToolbarSeparator';
10+
11+
interface SegmentationToolbarProps {
12+
onExportGeoTIFF: () => void;
13+
onSelectClass: () => void;
14+
onResetMask: () => void;
15+
onOpenHelp: () => void;
16+
onOpenPreferences: () => void;
17+
}
18+
19+
const SegmentationToolbar: React.FC<SegmentationToolbarProps> = ({
20+
onExportGeoTIFF,
21+
onSelectClass,
22+
onResetMask,
23+
onOpenHelp,
24+
onOpenPreferences
25+
}) => {
26+
return (
27+
<ul className="toolbar" id="toolbar" style={{ visibility: 'hidden' }}>
28+
<NavigationTools onExportGeoTIFF={onExportGeoTIFF} />
29+
<EditingTools />
30+
<ToolbarSeparator />
31+
<ClassSelector onSelectClass={onSelectClass} />
32+
<ToolbarSeparator />
33+
<DrawingTools onResetMask={onResetMask} />
34+
<ToolbarSeparator />
35+
<MaskTools />
36+
<ToolbarSeparator />
37+
<FilterTools />
38+
<ToolbarSeparator />
39+
<SettingsTools onOpenHelp={onOpenHelp} onOpenPreferences={onOpenPreferences} />
40+
</ul>
41+
);
42+
};
43+
44+
export default SegmentationToolbar;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useEffect } from 'react';
2+
3+
interface UseSegmentationSetupProps {
4+
authChecked: boolean;
5+
onOpenPreferences: () => void;
6+
onOpenLogin: () => void;
7+
onOpenRegister: () => void;
8+
onOpenProfile: (userId?: string) => void;
9+
onOpenHelp: () => void;
10+
}
11+
12+
export const useSegmentationSetup = ({
13+
authChecked,
14+
onOpenPreferences,
15+
onOpenLogin,
16+
onOpenRegister,
17+
onOpenProfile,
18+
onOpenHelp
19+
}: UseSegmentationSetupProps) => {
20+
useEffect(() => {
21+
if (!authChecked) return;
22+
23+
const isDebugMode = window.location.search.includes('debug=1') || window.location.hostname === 'localhost';
24+
25+
if (isDebugMode) {
26+
console.log('🚀 IRIS Segmentation: React SPA initialized');
27+
}
28+
29+
// Check if we should auto-open preferences (from admin navigation)
30+
const urlParams = new URLSearchParams(window.location.search);
31+
if (urlParams.get('openPreferences') === 'true') {
32+
onOpenPreferences();
33+
// Clean up URL parameter
34+
urlParams.delete('openPreferences');
35+
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
36+
window.history.replaceState({}, '', newUrl);
37+
}
38+
39+
// Wait for legacy JavaScript to load, then check auth and initialize
40+
const waitForLegacyScripts = setInterval(() => {
41+
if (window.init_segmentation) {
42+
clearInterval(waitForLegacyScripts);
43+
44+
if (isDebugMode) console.log('✅ Legacy scripts loaded');
45+
46+
// Check authentication BEFORE calling init_segmentation
47+
fetch('/user/get/current')
48+
.then(response => {
49+
if (response.status === 403) {
50+
if (isDebugMode) console.log('❌ Not authenticated, showing React login modal');
51+
onOpenLogin();
52+
} else {
53+
if (isDebugMode) console.log('✅ Authenticated, calling init_segmentation()');
54+
window.init_segmentation();
55+
}
56+
})
57+
.catch(error => {
58+
console.error('Auth check failed:', error);
59+
window.init_segmentation();
60+
});
61+
}
62+
}, 50);
63+
64+
// Timeout waiting for scripts after 5 seconds
65+
setTimeout(() => {
66+
clearInterval(waitForLegacyScripts);
67+
if (!window.init_segmentation) {
68+
console.error('❌ Legacy scripts failed to load - init_segmentation not found');
69+
}
70+
}, 5000);
71+
72+
// Override global dialogue functions to use React modals
73+
(window as any).dialogue_config = onOpenPreferences;
74+
window.openUserProfile = onOpenProfile;
75+
window.openLogin = onOpenLogin;
76+
window.openRegister = onOpenRegister;
77+
78+
// Expose logout function for React
79+
(window as any).reactLogout = async (callback?: () => void) => {
80+
await fetch('/user/logout');
81+
if (callback) {
82+
callback();
83+
} else {
84+
window.location.reload();
85+
}
86+
};
87+
88+
// Expose React functions for legacy JavaScript integration
89+
window.irisReactApp = {
90+
openHelpModal: onOpenHelp,
91+
openUserProfile: onOpenProfile,
92+
openPreferences: onOpenPreferences,
93+
};
94+
}, [authChecked, onOpenPreferences, onOpenLogin, onOpenRegister, onOpenProfile, onOpenHelp]);
95+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import AIRecommendation from './AIRecommendation';
4+
5+
describe('AIRecommendation', () => {
6+
it('renders AI recommendation with default text', () => {
7+
const { container } = render(<AIRecommendation />);
8+
9+
const recommendation = container.querySelector('#ai-recommendation');
10+
expect(recommendation).toBeInTheDocument();
11+
expect(screen.getByText('AI is loading')).toBeInTheDocument();
12+
});
13+
});

0 commit comments

Comments
 (0)