Skip to content

Commit 18182b0

Browse files
✨ feat(meeting-minutes-recording-protection): prevent navigation duri… (#1340)
1 parent 81f51cb commit 18182b0

File tree

11 files changed

+192
-3
lines changed

11 files changed

+192
-3
lines changed

packages/web/public/locales/translation/en.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,11 @@ diagram:
433433
desc: XY charts show relationships between two variables
434434
exampleTitle: Sleep vs Work Hours Example
435435
title: XY Chart
436+
dialog:
437+
unsavedChanges:
438+
leave: Leave
439+
message: If you leave this page, your input will be lost. Are you sure you want to leave?
440+
title: Unsaved Changes
436441
drawer:
437442
ai_services: AI Services
438443
builder_mode: Builder Mode

packages/web/public/locales/translation/ja.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ diagram:
371371
desc: XYチャートは2つの変数間の関係を表現します
372372
exampleTitle: 睡眠・労働時間例
373373
title: XYチャート
374+
dialog:
375+
unsavedChanges:
376+
leave: 移動する
377+
message: このページから移動すると、入力内容が失われます。本当に移動しますか?
378+
title: 入力内容が保存されていません
374379
drawer:
375380
ai_services: AI サービス
376381
builder_mode: ビルダーモード

packages/web/public/locales/translation/ko.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,11 @@ diagram:
293293
desc: XY 차트는 두 변수 간의 관계를 보여줍니다
294294
exampleTitle: 수면 vs 근무 시간 예시
295295
title: XY 차트
296+
dialog:
297+
unsavedChanges:
298+
leave: 이동
299+
message: 이 페이지를 떠나면 입력한 내용이 손실됩니다. 정말 이동하시겠습니까?
300+
title: 저장되지 않은 변경사항
296301
drawer:
297302
ai_services: AI 서비스
298303
builder_mode: 빌더 모드

packages/web/public/locales/translation/th.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ diagram:
306306
desc: XY chart แสดงความสัมพันธ์ระหว่างตัวแปรสองตัว
307307
exampleTitle: ตัวอย่างชั่วโมงนอนเทียบกับชั่วโมงทำงาน
308308
title: XY Chart
309+
dialog:
310+
unsavedChanges:
311+
leave: ออก
312+
message: หากคุณออกจากหน้านี้ ข้อมูลที่ป้อนจะสูญหาย คุณแน่ใจหรือไม่ว่าต้องการออก?
313+
title: การเปลี่ยนแปลงที่ยังไม่ได้บันทึก
309314
drawer:
310315
ai_services: บริการ AI
311316
builder_mode: Builder Mode

packages/web/public/locales/translation/vi.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ diagram:
304304
desc: XY chart thể hiện mối quan hệ giữa hai biến
305305
exampleTitle: Ví dụ thời gian ngủ・làm việc
306306
title: XY Chart
307+
dialog:
308+
unsavedChanges:
309+
leave: Rời khỏi
310+
message: Nếu bạn rời khỏi trang này, nội dung đã nhập sẽ bị mất. Bạn có chắc chắn muốn rời khỏi?
311+
title: Thay đổi chưa được lưu
307312
drawer:
308313
ai_services: Dịch vụ AI
309314
builder_mode: Chế độ builder

packages/web/public/locales/translation/zh.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ diagram:
230230
desc: XY图表表示两个变量之间的关系
231231
exampleTitle: 睡眠-工作时间示例
232232
title: XY图表
233+
dialog:
234+
unsavedChanges:
235+
leave: 离开
236+
message: 如果离开此页面,您输入的内容将丢失。确定要离开吗?
237+
title: 未保存的更改
233238
drawer:
234239
ai_services: AI 服务
235240
builder_mode: 构建者模式

packages/web/src/components/MeetingMinutes/MeetingMinutesRealtimeTranslation.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,16 @@ interface RealtimeSegment {
7878
interface MeetingMinutesRealtimeTranslationProps {
7979
/** Callback when transcript text changes */
8080
onTranscriptChange?: (text: string) => void;
81+
/** Callback when recording state changes */
82+
onRecordingStateChange?: (state: {
83+
micRecording: boolean;
84+
screenRecording: boolean;
85+
}) => void;
8186
}
8287

8388
const MeetingMinutesRealtimeTranslation: React.FC<
8489
MeetingMinutesRealtimeTranslationProps
85-
> = ({ onTranscriptChange }) => {
90+
> = ({ onTranscriptChange, onRecordingStateChange }) => {
8691
const { t } = useTranslation();
8792
const transcriptContainerRef = useRef<HTMLDivElement>(null);
8893
const isAtBottomRef = useRef<boolean>(true);
@@ -109,6 +114,14 @@ const MeetingMinutesRealtimeTranslation: React.FC<
109114
rawTranscripts: screenRawTranscripts,
110115
} = useScreenAudio();
111116

117+
// Notify parent component of recording state changes
118+
useEffect(() => {
119+
onRecordingStateChange?.({
120+
micRecording,
121+
screenRecording,
122+
});
123+
}, [micRecording, screenRecording, onRecordingStateChange]);
124+
112125
// Internal state management
113126
const [primaryLanguage, setPrimaryLanguage] = useState('en-US');
114127
const [speakerLabel, setSpeakerLabel] = useState(false);

packages/web/src/components/MeetingMinutes/MeetingMinutesTranscription.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ interface TranscriptionSegment {
3636
interface MeetingMinutesTranscriptionProps {
3737
/** Callback when transcript text changes */
3838
onTranscriptChange?: (text: string) => void;
39+
/** Callback when recording state changes */
40+
onRecordingStateChange?: (state: {
41+
micRecording: boolean;
42+
screenRecording: boolean;
43+
}) => void;
3944
}
4045

4146
const MeetingMinutesTranscription: React.FC<
4247
MeetingMinutesTranscriptionProps
43-
> = ({ onTranscriptChange }) => {
48+
> = ({ onTranscriptChange, onRecordingStateChange }) => {
4449
const { t, i18n } = useTranslation();
4550
const transcriptContainerRef = useRef<HTMLDivElement>(null);
4651
const isAtBottomRef = useRef<boolean>(true);
@@ -65,6 +70,14 @@ const MeetingMinutesTranscription: React.FC<
6570
rawTranscripts: screenRawTranscripts,
6671
} = useScreenAudio();
6772

73+
// Notify parent component of recording state changes
74+
useEffect(() => {
75+
onRecordingStateChange?.({
76+
micRecording,
77+
screenRecording,
78+
});
79+
}, [micRecording, screenRecording, onRecordingStateChange]);
80+
6881
// Internal state management
6982
const [languageCode, setLanguageCode] = useState('auto');
7083
const [speakerLabel, setSpeakerLabel] = useState(false);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import ModalDialog from './ModalDialog';
3+
import Button from './Button';
4+
import { useTranslation } from 'react-i18next';
5+
6+
type Props = {
7+
isOpen: boolean;
8+
onCancel: () => void;
9+
onConfirm: () => void;
10+
};
11+
12+
const NavigationBlockDialog: React.FC<Props> = ({
13+
isOpen,
14+
onCancel,
15+
onConfirm,
16+
}) => {
17+
const { t } = useTranslation();
18+
19+
return (
20+
<ModalDialog
21+
isOpen={isOpen}
22+
title={t('dialog.unsavedChanges.title')}
23+
onClose={onCancel}>
24+
<div className="flex flex-col gap-4">
25+
<div>{t('dialog.unsavedChanges.message')}</div>
26+
<div className="flex justify-end gap-2">
27+
<Button onClick={onCancel} className="p-2">
28+
{t('common.cancel')}
29+
</Button>
30+
<Button onClick={onConfirm} className="bg-red-500 p-2">
31+
{t('dialog.unsavedChanges.leave')}
32+
</Button>
33+
</div>
34+
</div>
35+
</ModalDialog>
36+
);
37+
};
38+
39+
export default NavigationBlockDialog;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useEffect } from 'react';
2+
import { useBlocker } from 'react-router-dom';
3+
4+
interface UsePreventNavigationOptions {
5+
/** Block app-internal navigation (sidebar links, back button). Default: true */
6+
blockInternalNavigation?: boolean;
7+
/** Block browser operations (reload, close tab). Default: true */
8+
blockBrowserNavigation?: boolean;
9+
}
10+
11+
/**
12+
* Prevents page navigation when there are unsaved changes
13+
* @param hasUnsavedChanges - Boolean indicating if there are unsaved changes
14+
* @param options - Customization options for blocking behavior
15+
* @returns Blocker object from useBlocker
16+
*/
17+
const usePreventNavigation = (
18+
hasUnsavedChanges: boolean,
19+
options: UsePreventNavigationOptions = {}
20+
) => {
21+
const { blockInternalNavigation = true, blockBrowserNavigation = true } =
22+
options;
23+
24+
// Block app-internal navigation (sidebar links, back button)
25+
const blocker = useBlocker(
26+
({ currentLocation, nextLocation }) =>
27+
blockInternalNavigation &&
28+
hasUnsavedChanges &&
29+
currentLocation.pathname !== nextLocation.pathname
30+
);
31+
32+
// Block browser operations (reload, close tab, navigate to external URL)
33+
useEffect(() => {
34+
if (!blockBrowserNavigation || !hasUnsavedChanges) return;
35+
36+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
37+
e.preventDefault();
38+
e.returnValue = '';
39+
};
40+
41+
window.addEventListener('beforeunload', handleBeforeUnload);
42+
43+
return () => {
44+
window.removeEventListener('beforeunload', handleBeforeUnload);
45+
};
46+
}, [hasUnsavedChanges, blockBrowserNavigation]);
47+
48+
return blocker;
49+
};
50+
51+
export default usePreventNavigation;

0 commit comments

Comments
 (0)