mirror of https://gitee.com/answerdev/answer.git
fix: content changes have not been saved to leave the page to increase the prompt
This commit is contained in:
parent
ce88efb174
commit
c57830f7b4
|
@ -1404,5 +1404,7 @@ ui:
|
|||
staffs: Our community staff
|
||||
reputation: reputation
|
||||
votes: votes
|
||||
|
||||
prompt:
|
||||
leave_page: "Are you sure you want to leave the page?"
|
||||
changes_not_save: "Your changes may not be saved."
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-i18next": "^11.18.3",
|
||||
"react-router-dom": "^6.4.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"semver": "^7.3.8",
|
||||
"swr": "^1.3.0",
|
||||
"urlcat": "^3.0.0",
|
||||
|
|
1476
ui/pnpm-lock.yaml
1476
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="generator" content="Answer %AnswerVersion% - https://github.com/answerdev/answer">
|
||||
<meta name="generator" content="Answer - https://github.com/answerdev/answer">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
import { usePageUsers, usePromptWithUnload } from '@/hooks';
|
||||
|
||||
const Index = ({
|
||||
className = '',
|
||||
|
@ -16,13 +16,19 @@ const Index = ({
|
|||
mode,
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [immData, setImmData] = useState('');
|
||||
const pageUsers = usePageUsers();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
|
||||
const [validationErrorMsg, setValidationErrorMsg] = useState('');
|
||||
|
||||
usePromptWithUnload({
|
||||
when: type === 'edit' ? immData !== value : Boolean(value),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!initialValue) {
|
||||
return;
|
||||
}
|
||||
setImmData(initialValue);
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
|
|
|
@ -5,13 +5,18 @@ import { useTranslation } from 'react-i18next';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
import { usePageUsers, usePromptWithUnload } from '@/hooks';
|
||||
|
||||
const Index = ({ userName, onSendReply, onCancel, mode }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const pageUsers = usePageUsers();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
|
||||
const [validationErrorMsg, setValidationErrorMsg] = useState('');
|
||||
|
||||
usePromptWithUnload({
|
||||
when: Boolean(value),
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import useChangeUserRoleModal from './useChangeUserRoleModal';
|
|||
import useUserModal from './useUserModal';
|
||||
import useChangePasswordModal from './useChangePasswordModal';
|
||||
import usePageTags from './usePageTags';
|
||||
import usePromptWithUnload from './usePrompt';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -20,4 +21,5 @@ export {
|
|||
useUserModal,
|
||||
useChangePasswordModal,
|
||||
usePageTags,
|
||||
usePromptWithUnload,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { useCallback } from 'react';
|
||||
import {
|
||||
useBeforeUnload,
|
||||
unstable_usePrompt as usePrompt,
|
||||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// https://gist.github.com/chaance/2f3c14ec2351a175024f62fd6ba64aa6
|
||||
// The link above is an example of implementing usePromt with useBlocer.
|
||||
interface PromptProps {
|
||||
when: boolean;
|
||||
beforeUnload?: boolean;
|
||||
}
|
||||
|
||||
const usePromptWithUnload = ({
|
||||
when = false,
|
||||
beforeUnload = true,
|
||||
}: PromptProps) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'prompt' });
|
||||
usePrompt({
|
||||
when,
|
||||
message: `${t('leave_page')} ${t('changes_not_save')}`,
|
||||
});
|
||||
|
||||
useBeforeUnload(
|
||||
useCallback(
|
||||
(event) => {
|
||||
if (beforeUnload && when) {
|
||||
const msg = t('changes_not_save');
|
||||
event.preventDefault();
|
||||
event.returnValue = msg;
|
||||
}
|
||||
},
|
||||
[when, beforeUnload],
|
||||
),
|
||||
{ capture: true },
|
||||
);
|
||||
};
|
||||
|
||||
export default usePromptWithUnload;
|
|
@ -5,8 +5,9 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { Editor, EditorRef, TagSelector } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
|
@ -60,7 +61,9 @@ const Ask = () => {
|
|||
};
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'ask' });
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData, setImmData] = useState<FormDataItem>(initFormData);
|
||||
const [checked, setCheckState] = useState(false);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
const [focusType, setForceType] = useState('');
|
||||
const resetForm = () => {
|
||||
setFormData(initFormData);
|
||||
|
@ -82,6 +85,40 @@ const Ask = () => {
|
|||
const { data: similarQuestions = { list: [] } } = useQueryQuestionByTitle(
|
||||
isEdit ? '' : formData.title.value,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { title, tags, content, answer } = formData;
|
||||
const { title: editTitle, tags: editTags, content: editContent } = immData;
|
||||
|
||||
// edited
|
||||
if (qid) {
|
||||
if (
|
||||
editTitle.value !== title.value ||
|
||||
editContent.value !== content.value ||
|
||||
!isEqual(
|
||||
editTags.value.map((v) => v.slug_name),
|
||||
tags.value.map((v) => v.slug_name),
|
||||
)
|
||||
) {
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
setContentChanged(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// write
|
||||
if (title.value || tags.value.length > 0 || content.value || answer.value) {
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
setContentChanged(false);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: contentChanged,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) {
|
||||
resetForm();
|
||||
|
@ -103,6 +140,7 @@ const Ask = () => {
|
|||
original_text: '',
|
||||
};
|
||||
});
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
});
|
||||
}, [qid]);
|
||||
|
@ -141,6 +179,7 @@ const Ask = () => {
|
|||
});
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
setContentChanged(false);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -206,6 +245,7 @@ const Ask = () => {
|
|||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
const bool = similarQuestions.length > 0 && !isEdit;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { marked } from 'marked';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { usePromptWithUnload } from '@/hooks';
|
||||
import { Editor, Modal, TextArea } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
|
@ -35,6 +36,10 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: Boolean(formData.content.value),
|
||||
});
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { content } = formData;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -7,7 +7,7 @@ import dayjs from 'dayjs';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { handleFormError } from '@/utils';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Editor, EditorRef, Icon } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
@ -44,6 +44,8 @@ const Index = () => {
|
|||
|
||||
const { data } = useQueryAnswerInfo(aid);
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData, setImmData] = useState(initFormData);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
|
||||
initFormData.content.value = data?.info.content || '';
|
||||
|
||||
|
@ -55,6 +57,19 @@ const Index = () => {
|
|||
|
||||
const questionContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
usePromptWithUnload({
|
||||
when: contentChanged,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { content, description } = formData;
|
||||
if (immData.content.value !== content.value || description.value) {
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
setContentChanged(false);
|
||||
}
|
||||
}, [formData.content.value, formData.description.value]);
|
||||
|
||||
const handleAnswerChange = (value: string) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
|
@ -93,7 +108,9 @@ const Index = () => {
|
|||
return bol;
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
setContentChanged(false);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
|
@ -131,6 +148,7 @@ const Index = () => {
|
|||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||
import { Editor, EditorRef } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
@ -40,6 +40,7 @@ const initFormData = {
|
|||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||
|
||||
|
@ -54,11 +55,45 @@ const Index = () => {
|
|||
initFormData.slugName.value = data?.slug_name || '';
|
||||
initFormData.description.value = data?.original_text || '';
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [immData, setImmData] = useState(initFormData);
|
||||
const [contentChanged, setContentChanged] = useState(false);
|
||||
|
||||
const editorRef = useRef<EditorRef>({
|
||||
getHtml: () => '',
|
||||
});
|
||||
|
||||
usePromptWithUnload({
|
||||
when: contentChanged,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { displayName, slugName, description, editSummary } = formData;
|
||||
const {
|
||||
displayName: display_name,
|
||||
slugName: slug_name,
|
||||
description: original_text,
|
||||
} = immData;
|
||||
if (!display_name || !slug_name || !original_text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
display_name.value !== displayName.value ||
|
||||
slug_name.value !== slugName.value ||
|
||||
original_text.value !== description.value ||
|
||||
editSummary.value
|
||||
) {
|
||||
setContentChanged(true);
|
||||
} else {
|
||||
setContentChanged(false);
|
||||
}
|
||||
}, [
|
||||
formData.displayName.value,
|
||||
formData.slugName.value,
|
||||
formData.description.value,
|
||||
formData.editSummary.value,
|
||||
]);
|
||||
|
||||
const handleDescriptionChange = (value: string) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
|
@ -91,6 +126,8 @@ const Index = () => {
|
|||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
setContentChanged(false);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
|
@ -117,6 +154,9 @@ const Index = () => {
|
|||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.description.value = revision.content.original_text;
|
||||
formData.displayName.value = revision.content.display_name;
|
||||
formData.slugName.value = revision.content.slug_name;
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { RouteObject } from 'react-router-dom';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
|
||||
import { guard } from '@/utils';
|
||||
import type { TGuardFunc } from '@/utils/guard';
|
||||
|
||||
export interface RouteNode extends RouteObject {
|
||||
export type RouteNode = RouteObject & {
|
||||
page: string;
|
||||
children?: RouteNode[];
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ export interface RouteNode extends RouteObject {
|
|||
* then auto redirect route to the `redirect` target.
|
||||
*/
|
||||
guard?: TGuardFunc;
|
||||
}
|
||||
};
|
||||
|
||||
const routes: RouteNode[] = [
|
||||
{
|
||||
|
@ -113,6 +113,7 @@ const routes: RouteNode[] = [
|
|||
children: [
|
||||
{
|
||||
index: true,
|
||||
// @ts-ignore
|
||||
page: 'pages/Users/Settings/Profile',
|
||||
},
|
||||
{
|
||||
|
@ -237,6 +238,7 @@ const routes: RouteNode[] = [
|
|||
children: [
|
||||
{
|
||||
index: true,
|
||||
// @ts-ignore
|
||||
page: 'pages/Admin/Dashboard',
|
||||
},
|
||||
{
|
||||
|
@ -329,6 +331,7 @@ const routes: RouteNode[] = [
|
|||
children: [
|
||||
{
|
||||
path: 'tos',
|
||||
// @ts-ignore
|
||||
page: 'pages/Legal/Tos',
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue