Skip to content

Commit 02b6808

Browse files
authored
fix: prevent unsaved changes loss (#1786)
1 parent db03807 commit 02b6808

File tree

4 files changed

+64
-36
lines changed

4 files changed

+64
-36
lines changed

e2e/cypress/common/translations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ export const editCell = (oldValue: string, newValue?: string, save = true) => {
110110
}
111111
};
112112

113-
export function confirmSaveChanges() {
114-
cy.gcy('global-confirmation-confirm').contains('Save').click();
113+
export function confirmDiscard() {
114+
cy.gcy('global-confirmation-confirm').contains('Discard').click();
115115
}
116116

117117
export const toggleLang = (lang) => {

e2e/cypress/e2e/translations/with5translations/withViews.cy.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getAnyContainingText } from '../../../common/xPath';
22
import { ProjectDTO } from '../../../../../webapp/src/service/response.types';
33
import {
4-
confirmSaveChanges,
4+
confirmDiscard,
55
create4Translations,
66
editCell,
77
forEachView,
@@ -91,9 +91,8 @@ describe('Views with 5 Translations', () => {
9191
it('will ask for confirmation on changed edit', () => {
9292
editCell('Cool translated text 1', 'Cool translation edited', false);
9393
cy.contains('Cool translated text 4').click();
94-
cy.contains(`Unsaved changes`).should('be.visible');
95-
confirmSaveChanges();
96-
cy.contains('Cool translation edited');
94+
cy.contains(`Discard changes?`).should('be.visible');
95+
confirmDiscard();
9796
cy.gcy('global-editor')
9897
.contains('Cool translation edited')
9998
.should('not.exist');

webapp/src/views/projects/translations/context/TranslationsContext.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from 'react';
1+
import { useEffect, useMemo, useState } from 'react';
22
import ReactList from 'react-list';
33
import { useApiQuery } from 'tg.service/http/useQueryApi';
44

@@ -115,6 +115,18 @@ export const [
115115
selectionService.clear();
116116
};
117117

118+
useEffect(() => {
119+
// prevent leaving the page when there are unsaved changes
120+
if (editService.position?.changed) {
121+
const handler = (e) => {
122+
e.preventDefault();
123+
e.returnValue = '';
124+
};
125+
window.addEventListener('beforeunload', handler);
126+
return () => window.removeEventListener('beforeunload', handler);
127+
}
128+
}, [editService.position?.changed]);
129+
118130
// actions
119131

120132
const actions = {
@@ -126,18 +138,24 @@ export const [
126138
translationService.setUrlSearch(search);
127139
return handleTranslationsReset();
128140
},
129-
setFilters(filters: Filters) {
130-
translationService.setFilters(filters);
131-
return handleTranslationsReset();
141+
async setFilters(filters: Filters) {
142+
if (await editService.confirmUnsavedChanges()) {
143+
translationService.setFilters(filters);
144+
return handleTranslationsReset();
145+
}
132146
},
133-
setEdit(edit: Edit | undefined) {
134-
return editService.setEdit(edit);
147+
async setEdit(edit: Edit | undefined) {
148+
if (await editService.confirmUnsavedChanges(edit)) {
149+
return editService.setPositionAndFocus(edit);
150+
}
135151
},
136152
setEditForce(edit: Edit | undefined) {
137153
return editService.setPositionAndFocus(edit);
138154
},
139-
updateEdit(edit: Partial<Edit>) {
140-
return editService.updatePosition(edit);
155+
async updateEdit(edit: Partial<Edit>) {
156+
if (await editService.confirmUnsavedChanges(edit)) {
157+
return editService.updatePosition(edit);
158+
}
141159
},
142160
toggleSelect(index: number) {
143161
return selectionService.toggle(index);

webapp/src/views/projects/translations/context/services/useEditService.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { container } from 'tsyringe';
3-
import { T, useTranslate } from '@tolgee/react';
3+
import { T } from '@tolgee/react';
44

55
import { components } from 'tg.service/apiSchema.generated';
66
import {
@@ -37,7 +37,6 @@ export const useEditService = ({ translations, viewRefs }: Props) => {
3737
const putTranslation = usePutTranslation();
3838
const putTag = usePutTag();
3939
const deleteTag = useDeleteTag();
40-
const { t } = useTranslate();
4140

4241
useEffect(() => {
4342
// field is also focused, which breaks the scrolling
@@ -154,25 +153,37 @@ export const useEditService = ({ translations, viewRefs }: Props) => {
154153
});
155154
};
156155

157-
const setEdit = (newPosition: Edit | undefined) => {
158-
if (position?.changed) {
159-
setPositionAndFocus({ ...position, mode: 'editor' });
160-
confirmation({
161-
title: t('translations_unsaved_changes_confirmation_title'),
162-
message: t('translations_unsaved_changes_confirmation'),
163-
cancelButtonText: t('back_to_editing'),
164-
confirmButtonText: t('translations_cell_save'),
165-
onConfirm: () => {
166-
changeField({
167-
onSuccess() {
168-
setPositionAndFocus(newPosition);
169-
},
170-
});
171-
},
172-
});
173-
} else {
174-
setPositionAndFocus(newPosition);
175-
}
156+
const confirmUnsavedChanges = (newPosition?: Partial<Edit>) => {
157+
return new Promise<boolean>((resolve) => {
158+
const fieldIsDifferent =
159+
newPosition?.keyId !== undefined &&
160+
(newPosition?.keyId !== position?.keyId ||
161+
newPosition?.language !== position?.language);
162+
163+
if (
164+
position?.changed &&
165+
position.keyId !== undefined &&
166+
(!newPosition || fieldIsDifferent)
167+
) {
168+
setPositionAndFocus({ ...position, mode: 'editor' });
169+
confirmation({
170+
title: <T keyName="translations_discard_unsaved_title" />,
171+
message: <T keyName="translations_discard_unsaved_message" />,
172+
cancelButtonText: <T keyName="back_to_editing" />,
173+
confirmButtonText: (
174+
<T keyName="translations_discard_button_confirm" />
175+
),
176+
onConfirm() {
177+
resolve(true);
178+
},
179+
onCancel() {
180+
resolve(false);
181+
},
182+
});
183+
} else {
184+
resolve(true);
185+
}
186+
});
176187
};
177188

178189
const changeField = async (data: ChangeValue) => {
@@ -243,8 +254,8 @@ export const useEditService = ({ translations, viewRefs }: Props) => {
243254
setPosition,
244255
updatePosition,
245256
setPositionAndFocus,
246-
setEdit,
247257
changeField,
258+
confirmUnsavedChanges,
248259
isLoading:
249260
putKey.isLoading ||
250261
putTranslation.isLoading ||

0 commit comments

Comments
 (0)