mirror of https://gitee.com/answerdev/answer.git
commit
58f1c199fa
|
@ -1406,5 +1406,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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||
|
||||
import './i18n/init';
|
||||
import { routes, createBrowserRouter } from '@/router';
|
||||
import routes from '@/router';
|
||||
|
||||
function App() {
|
||||
const router = createBrowserRouter(routes);
|
||||
|
|
|
@ -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;
|
|
@ -92,6 +92,7 @@ const General: FC = () => {
|
|||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
);
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
@ -104,12 +105,21 @@ const General: FC = () => {
|
|||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
updateSiteInfo(reqParams);
|
||||
if (res.name) {
|
||||
formData.name.value = res.name;
|
||||
formData.description.value = res.description;
|
||||
formData.short_description.value = res.short_description;
|
||||
formData.site_url.value = res.site_url;
|
||||
formData.contact_email.value = res.contact_email;
|
||||
}
|
||||
|
||||
setFormData({ ...formData });
|
||||
updateSiteInfo(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -61,7 +62,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);
|
||||
|
@ -94,6 +97,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();
|
||||
|
@ -116,6 +153,7 @@ const Ask = () => {
|
|||
original_text: '',
|
||||
};
|
||||
});
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
});
|
||||
}, [qid]);
|
||||
|
@ -154,6 +192,7 @@ const Ask = () => {
|
|||
});
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
setContentChanged(false);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -219,6 +258,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,14 +1,14 @@
|
|||
import { Suspense, lazy } from 'react';
|
||||
import { RouteObject, createBrowserRouter } from 'react-router-dom';
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
|
||||
import Layout from '@/pages/Layout';
|
||||
import ErrorBoundary from '@/pages/50X';
|
||||
import baseRoutes, { RouteNode } from '@/router/routes';
|
||||
import RouteGuard from '@/router/RouteGuard';
|
||||
|
||||
const routes: RouteObject[] = [];
|
||||
const routes: RouteNode[] = [];
|
||||
|
||||
const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
||||
const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
||||
routeNodes.forEach((rn) => {
|
||||
if (rn.page === 'pages/Layout') {
|
||||
rn.element = rn.guard ? (
|
||||
|
@ -49,4 +49,4 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
|||
|
||||
routeWrapper(baseRoutes, routes);
|
||||
|
||||
export { routes, createBrowserRouter };
|
||||
export default routes as RouteObject[];
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { RouteObject } from 'react-router-dom';
|
||||
import type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom';
|
||||
|
||||
import { guard } from '@/utils';
|
||||
import type { TGuardFunc } from '@/utils/guard';
|
||||
|
||||
export interface RouteNode extends RouteObject {
|
||||
type IndexRouteNode = Omit<IndexRouteObject, 'children'>;
|
||||
type NonIndexRouteNode = Omit<NonIndexRouteObject, 'children'>;
|
||||
type UnionRouteNode = IndexRouteNode | NonIndexRouteNode;
|
||||
|
||||
export type RouteNode = UnionRouteNode & {
|
||||
page: string;
|
||||
children?: RouteNode[];
|
||||
/**
|
||||
|
@ -14,7 +18,7 @@ export interface RouteNode extends RouteObject {
|
|||
* then auto redirect route to the `redirect` target.
|
||||
*/
|
||||
guard?: TGuardFunc;
|
||||
}
|
||||
};
|
||||
|
||||
const routes: RouteNode[] = [
|
||||
{
|
||||
|
@ -230,6 +234,7 @@ const routes: RouteNode[] = [
|
|||
page: 'pages/Admin',
|
||||
loader: async () => {
|
||||
await guard.pullLoggedUser(true);
|
||||
return null;
|
||||
},
|
||||
guard: () => {
|
||||
return guard.admin();
|
||||
|
|
|
@ -85,6 +85,7 @@ export const pullLoggedUser = async (forceRePull = false) => {
|
|||
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
dedupeTimestamp = Date.now();
|
||||
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
||||
dedupeTimestamp = 0;
|
||||
|
|
Loading…
Reference in New Issue