This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch test
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git


The following commit(s) were added to refs/heads/test by this push:
     new 885406dd Feat/1.3.6/UI (#1015)
885406dd is described below

commit 885406ddb37b5a7e4e91a1888b244dedd431ea1a
Author: dashuai <[email protected]>
AuthorDate: Fri Jul 12 14:13:34 2024 +0800

    Feat/1.3.6/UI (#1015)
    
    Co-authored-by: Kevin Gil <[email protected]>
    Co-authored-by: sy-records <[email protected]>
    Co-authored-by: robin <[email protected]>
---
 docs/release/NOTICE                                |   2 +-
 i18n/en_US.yaml                                    |  16 +-
 i18n/zh_CN.yaml                                    |   2 +-
 pkg/converter/markdown.go                          |   6 +-
 ui/scripts/plugin.js                               |   9 +-
 ui/src/common/color.scss                           |  22 +-
 ui/src/common/constants.ts                         |   1 -
 ui/src/common/interface.ts                         |   8 +-
 ui/src/components/Editor/utils/index.ts            |   3 +
 ui/src/components/Mentions/index.tsx               |   2 +-
 ui/src/components/QuestionList/index.tsx           |   7 +-
 ui/src/components/TagSelector/index.tsx            |  14 +-
 ui/src/index.tsx                                   |  21 ++
 ui/src/pages/Admin/Write/index.tsx                 | 234 ++++++++++++++-------
 ui/src/pages/Questions/Ask/index.tsx               |  76 +++----
 ui/src/pages/Questions/index.tsx                   |  11 +-
 .../pages/Search/components/SearchHead/index.tsx   |   2 +-
 ui/src/pages/Search/components/Tips/index.tsx      |   2 +-
 ui/src/pages/Search/index.tsx                      |   2 +-
 ui/src/pages/Tags/Detail/index.tsx                 |  11 +-
 ui/src/services/client/question.ts                 |   2 +-
 ui/src/stores/writeSetting.ts                      |   2 +-
 ui/src/utils/common.ts                             |  35 +--
 23 files changed, 304 insertions(+), 186 deletions(-)

diff --git a/docs/release/NOTICE b/docs/release/NOTICE
index 190e5f69..4eaee775 100644
--- a/docs/release/NOTICE
+++ b/docs/release/NOTICE
@@ -1,5 +1,5 @@
 Apache Answer (incubating)
-Copyright 2023 The Apache Software Foundation
+Copyright 2024 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (https://www.apache.org/).
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 88fd6e7f..ef1b0bc9 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1290,7 +1290,7 @@ ui:
     answers: Answers
     newest: Newest
     active: Active
-    frequent: Frequent
+    hot: Hot
     score: Score
     unanswered: Unanswered
     modified: modified
@@ -1747,19 +1747,21 @@ ui:
     write:
       page_title: Write
       restrict_answer:
-        title: Restrict answer
+        title: Answer write
         label: Each user can only write one answer for the same question
-        text: "Turn off to allow users to write multiple answers to the same 
question, which may cause answers to be unfocused."
+        text: "Turn off to allow users to write multiple answers to the same 
question, which may cause answers to be unfocused."
       recommend_tags:
         label: Recommend tags
-        text: "Please input tag slug above, one tag per line."
+        text: "Recommend tags will show in the dropdown list by default."
+        msg:
+          contain_reserved: "recommended tags cannot contain reserved tags"
       required_tag:
-        title: Required tag
-        label: Set recommend tag as required
+        title: Set required tags
+        label: Set “Recommend tags” as required tags
         text: "Every new question must have at least one recommend tag."
       reserved_tags:
         label: Reserved tags
-        text: "Reserved tags can only be added to a post by moderator."
+        text: "Reserved tags can only be used by moderator."
     seo:
       page_title: SEO
       permalink:
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 2c438316..5c3dd862 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1249,7 +1249,7 @@ ui:
     answers: 回答
     newest: 最新
     active: 活跃
-    frequent: 浏览量
+    hot: 热度
     score: 评分
     unanswered: 未回答
     modified: 更新于
diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go
index e537f554..af17d9bd 100644
--- a/pkg/converter/markdown.go
+++ b/pkg/converter/markdown.go
@@ -98,7 +98,6 @@ func (r *DangerousHTMLRenderer) RegisterFuncs(reg 
renderer.NodeRendererFuncRegis
        reg.Register(ast.KindRawHTML, r.renderRawHTML)
        reg.Register(ast.KindLink, r.renderLink)
        reg.Register(ast.KindAutoLink, r.renderAutoLink)
-
 }
 
 func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, 
node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -136,7 +135,6 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w 
util.BufWriter, source []byte,
 }
 
 func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, 
node ast.Node, entering bool) (ast.WalkStatus, error) {
-
        n := node.(*ast.Link)
        if entering && r.renderLinkIsUrl(string(n.Destination)) {
                _, _ = w.WriteString("<a href=\"")
@@ -186,5 +184,7 @@ func (r *DangerousHTMLRenderer) renderAutoLink(w 
util.BufWriter, source []byte,
 }
 
 func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool {
-       return govalidator.IsURL(verifyUrl)
+       isURL := govalidator.IsURL(verifyUrl)
+       isPath, _ := regexp.MatchString(`^/`, verifyUrl)
+       return isURL || isPath
 }
diff --git a/ui/scripts/plugin.js b/ui/scripts/plugin.js
index cdd84d17..a0d31e0c 100644
--- a/ui/scripts/plugin.js
+++ b/ui/scripts/plugin.js
@@ -19,15 +19,11 @@
 
 const path = require('path');
 const fs = require('fs');
+const humps = require('humps');
 
 const pluginPath = path.join(__dirname, '../src/plugins');
 const pluginFolders = fs.readdirSync(pluginPath);
 
-function pascalize(str) {
-  return str.replace(/\b\w/g, (match) => match.toUpperCase())
-    .replace(/[-_\s]+/g, '');
-}
-
 function resetPackageJson() {
   const packageJsonPath = path.join(__dirname, '..', 'package.json');
   const packageJsonContent = require(packageJsonPath);
@@ -63,7 +59,7 @@ function addPluginToIndexTs(packageName) {
   const indexTsPath = path.join(pluginPath, 'index.ts');
   const indexTsContent = fs.readFileSync(indexTsPath, 'utf-8');
   const lines = indexTsContent.split('\n');
-  const ComponentName = pascalize(packageName);
+  const ComponentName = humps.pascalize(packageName);
   const importLine = `export { default as ${ComponentName} } from 
'${packageName}';`;
   if (!lines.includes(importLine)) {
     lines.push(importLine);
@@ -94,7 +90,6 @@ pluginFolders.forEach((folder) => {
     const packageJson = require(path.join(pluginFolder, 'package.json'));
     const packageName = packageJson.name;
 
-
     addPluginToPackageJson(packageName);
     addPluginToIndexTs(packageName);
   }
diff --git a/ui/src/common/color.scss b/ui/src/common/color.scss
index 66436873..1a96b31d 100644
--- a/ui/src/common/color.scss
+++ b/ui/src/common/color.scss
@@ -72,19 +72,27 @@
   }
   /** CodeMirror **/
 
-  .CodeMirror {
+  .cm-editor {
     background: var(--bs-body-bg);
     color: var(--bs-body-color);
   }
-  .CodeMirror-cursor {
-    border-left: 1px solid var(--bs-body-color);
+
+  .cm-cursor {
+    border-left-color: var(--bs-body-color);
+  }
+  .ͼ2.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
+    background-color: #3E4451;
   }
-  .cm-header, .cm-link, .cm-url {
+   /** link color **/
+  .ͼc {
+    color: rgb(60, 138, 233);
+  }
+  .ͼ5 {
     color: var(--bs-body-color);
   }
-  div.CodeMirror-selected { background: rgba(127, 190, 244, 0.4); }
-  .CodeMirror-line::selection, .CodeMirror-line > span::selection, 
.CodeMirror-line > span > span::selection { background: rgba(84,174,255,0.4); }
-  .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, 
.CodeMirror-line > span > span::-moz-selection { background: 
rgba(84,174,255,0.4); }
+
+  .ͼ2 .cm-selectionBackground {background: #3E4451;}
+ /** CodeMirror end **/
 
   .bg-light {
     background-color: rgba(0, 0, 0, 0.5) !important;
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index e76c60b0..c846d4dd 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -27,7 +27,6 @@ export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
 export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
 export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
 export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
-export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_';
 export const DEFAULT_THEME = 'system';
 export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99;
 export const SKELETON_SHOW_TIME = 1000;
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 019786f1..ff7c0902 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -290,7 +290,7 @@ export interface LangsType {
 export type QuestionOrderBy =
   | 'newest'
   | 'active'
-  | 'frequent'
+  | 'hot'
   | 'score'
   | 'unanswered';
 
@@ -425,9 +425,9 @@ export interface AdminSettingsLegal {
 
 export interface AdminSettingsWrite {
   restrict_answer?: boolean;
-  recommend_tags?: string[];
-  required_tag?: string;
-  reserved_tags?: string[];
+  recommend_tags?: Tag[];
+  required_tag?: boolean;
+  reserved_tags?: Tag[];
 }
 
 export interface AdminSettingsSeo {
diff --git a/ui/src/components/Editor/utils/index.ts 
b/ui/src/components/Editor/utils/index.ts
index 406acb8d..d49da78c 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -97,6 +97,9 @@ export const useEditor = ({
         wordWrap: 'break-word',
         wordBreak: 'break-all',
       },
+      '.ͼ7, .ͼ6': {
+        textDecoration: 'none',
+      },
     });
 
     const startState = EditorState.create({
diff --git a/ui/src/components/Mentions/index.tsx 
b/ui/src/components/Mentions/index.tsx
index ae5ea2ff..662ffc97 100644
--- a/ui/src/components/Mentions/index.tsx
+++ b/ui/src/components/Mentions/index.tsx
@@ -107,7 +107,7 @@ const Mentions: FC<IProps> = ({ children, pageUsers, 
onSelected }) => {
       return;
     }
 
-    const text = `@${item?.userName}`;
+    const text = `@${item?.userName} `;
     onSelected(
       `${value.substring(
         0,
diff --git a/ui/src/components/QuestionList/index.tsx 
b/ui/src/components/QuestionList/index.tsx
index 94a300d8..d9b46dd2 100644
--- a/ui/src/components/QuestionList/index.tsx
+++ b/ui/src/components/QuestionList/index.tsx
@@ -22,7 +22,6 @@ import { ListGroup } from 'react-bootstrap';
 import { NavLink, useSearchParams } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
-import type { QuestionOrderBy } from '@/common/interface';
 import { pathFactory } from '@/router/pathFactory';
 import {
   Tag,
@@ -39,15 +38,15 @@ import * as Type from '@/common/interface';
 import { useSkeletonControl } from '@/hooks';
 
 export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [
-  'active',
   'newest',
-  'frequent',
+  'active',
+  'hot',
   'score',
   'unanswered',
 ];
 interface Props {
   source: 'questions' | 'tag';
-  order?: QuestionOrderBy;
+  order?: Type.QuestionOrderBy;
   data;
   isLoading: boolean;
 }
diff --git a/ui/src/components/TagSelector/index.tsx 
b/ui/src/components/TagSelector/index.tsx
index 845ace24..1204a686 100644
--- a/ui/src/components/TagSelector/index.tsx
+++ b/ui/src/components/TagSelector/index.tsx
@@ -41,6 +41,8 @@ interface IProps {
   showRequiredTag?: boolean;
   autoFocus?: boolean;
   isInvalid?: boolean;
+  tagStyleMode?: 'default' | 'simple';
+  formText?: string;
   errMsg?: string;
 }
 
@@ -55,6 +57,8 @@ const TagSelector: FC<IProps> = ({
   showRequiredTag = false,
   autoFocus = false,
   isInvalid = false,
+  formText = '',
+  tagStyleMode = 'default',
   errMsg = '',
 }) => {
   const containerRef = useRef<HTMLDivElement>(null);
@@ -366,8 +370,12 @@ const TagSelector: FC<IProps> = ({
                   key={item.slug_name}
                   className={classNames(
                     'badge-tag rounded-1 m-1 flex-shrink-0',
-                    item.reserved && 'badge-tag-reserved',
-                    item.recommend && 'badge-tag-required',
+                    tagStyleMode === 'default' &&
+                      item.reserved &&
+                      'badge-tag-reserved',
+                    tagStyleMode === 'default' &&
+                      item.recommend &&
+                      'badge-tag-required',
                     index === repeatIndex && 'bg-fade-out',
                   )}>
                   {item.display_name}
@@ -435,7 +443,7 @@ const TagSelector: FC<IProps> = ({
           )}
         </Dropdown.Menu>
       </div>
-      {!hiddenDescription && <Form.Text>{t('hint')}</Form.Text>}
+      {!hiddenDescription && <Form.Text>{formText || t('hint')}</Form.Text>}
       <Form.Control.Feedback type="invalid">{errMsg}</Form.Control.Feedback>
     </div>
   );
diff --git a/ui/src/index.tsx b/ui/src/index.tsx
index 6074ca72..e2930c08 100644
--- a/ui/src/index.tsx
+++ b/ui/src/index.tsx
@@ -60,8 +60,29 @@ const handleImgLoad = (evt: Event | UIEvent) => {
   }
 };
 
+/**
+ *Automatically jump when the href of a Link component within a matching 
project is not a front-end route.
+ *
+ */
+const handleClickLink = (evt: Event) => {
+  const { target } = evt;
+
+  if (target === null || !(target instanceof Element)) {
+    return;
+  }
+  if (!/A/i.test(target.nodeName)) {
+    return;
+  }
+
+  if (target.getAttribute('href')?.includes('/answer/api/')) {
+    evt.preventDefault();
+    window.location.href = target.getAttribute('href') || '';
+  }
+};
+
 document.addEventListener('error', handleImgLoad, true);
 document.addEventListener('load', handleImgLoad, true);
+document.addEventListener('click', handleClickLink, true);
 
 root.render(
   <React.StrictMode>
diff --git a/ui/src/pages/Admin/Write/index.tsx 
b/ui/src/pages/Admin/Write/index.tsx
index 42806111..442c58f6 100644
--- a/ui/src/pages/Admin/Write/index.tsx
+++ b/ui/src/pages/Admin/Write/index.tsx
@@ -19,8 +19,9 @@
 
 import { FC, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import { Form, Button } from 'react-bootstrap';
 
-import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
+import { TagSelector } from '@/components';
 import type * as Type from '@/common/interface';
 import { useToast } from '@/hooks';
 import {
@@ -30,80 +31,84 @@ import {
 import { handleFormError, scrollToElementTop } from '@/utils';
 import { writeSettingStore } from '@/stores';
 
+const initFormData = {
+  reserved_tags: {
+    value: [] as Type.Tag[], // Replace `Type.Tag` with the correct type for 
`reserved_tags.value`
+    errorMsg: '',
+    isInvalid: false,
+  },
+  recommend_tags: {
+    value: [] as Type.Tag[],
+    errorMsg: '',
+    isInvalid: false,
+  },
+  required_tag: {
+    value: false,
+    errorMsg: '',
+    isInvalid: false,
+  },
+  restrict_answer: {
+    value: false,
+    errorMsg: '',
+    isInvalid: false,
+  },
+};
+
 const Index: FC = () => {
   const { t } = useTranslation('translation', {
     keyPrefix: 'admin.write',
   });
   const Toast = useToast();
 
-  const schema: JSONSchema = {
-    title: t('page_title'),
-    properties: {
-      restrict_answer: {
-        type: 'boolean',
-        title: t('restrict_answer.title'),
-        description: t('restrict_answer.text'),
-        default: true,
-      },
-      recommend_tags: {
-        type: 'string',
-        title: t('recommend_tags.label'),
-        description: t('recommend_tags.text'),
-      },
-      required_tag: {
-        type: 'boolean',
-        title: t('required_tag.title'),
-        description: t('required_tag.text'),
-      },
-      reserved_tags: {
-        type: 'string',
-        title: t('reserved_tags.label'),
-        description: t('reserved_tags.text'),
-      },
-    },
+  const [formData, setFormData] = useState(initFormData);
+
+  const handleValueChange = (value) => {
+    setFormData({
+      ...formData,
+      ...value,
+    });
   };
-  const uiSchema: UISchema = {
-    restrict_answer: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('restrict_answer.label'),
-      },
-    },
-    recommend_tags: {
-      'ui:widget': 'textarea',
-      'ui:options': {
-        rows: 10,
-      },
-    },
-    required_tag: {
-      'ui:widget': 'switch',
-      'ui:options': {
-        label: t('required_tag.label'),
-      },
-    },
-    reserved_tags: {
-      'ui:widget': 'textarea',
-      'ui:options': {
-        rows: 10,
-      },
-    },
+
+  const checkValidated = (): boolean => {
+    let bol = true;
+    const { recommend_tags, reserved_tags } = formData;
+    // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签
+    // 通过标签中的 slug_name 来去重
+    const repeatTag = recommend_tags.value.filter((tag) =>
+      reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),
+    );
+    if (repeatTag.length > 0) {
+      handleValueChange({
+        recommend_tags: {
+          ...recommend_tags,
+          errorMsg: t('recommend_tags.msg.contain_reserved'),
+          isInvalid: true,
+        },
+      });
+      bol = false;
+      const ele = document.getElementById('recommend_tags');
+      scrollToElementTop(ele);
+    } else {
+      handleValueChange({
+        recommend_tags: {
+          ...recommend_tags,
+          errorMsg: '',
+          isInvalid: false,
+        },
+      });
+    }
+    return bol;
   };
-  const [formData, setFormData] = useState(initFormData(schema));
 
   const onSubmit = (evt) => {
     evt.preventDefault();
     evt.stopPropagation();
-    let recommend_tags = [];
-    if (formData.recommend_tags.value?.trim()) {
-      recommend_tags = formData.recommend_tags.value.trim().split('\n');
-    }
-    let reserved_tags = [];
-    if (formData.reserved_tags.value?.trim()) {
-      reserved_tags = formData.reserved_tags.value.trim().split('\n');
+    if (!checkValidated()) {
+      return;
     }
     const reqParams: Type.AdminSettingsWrite = {
-      recommend_tags,
-      reserved_tags,
+      recommend_tags: formData.recommend_tags.value,
+      reserved_tags: formData.reserved_tags.value,
       required_tag: formData.required_tag.value,
       restrict_answer: formData.restrict_answer.value,
     };
@@ -130,12 +135,12 @@ const Index: FC = () => {
   const initData = () => {
     getRequireAndReservedTag().then((res) => {
       if (Array.isArray(res.recommend_tags)) {
-        formData.recommend_tags.value = res.recommend_tags.join('\n');
+        formData.recommend_tags.value = res.recommend_tags;
       }
       formData.required_tag.value = res.required_tag;
       formData.restrict_answer.value = res.restrict_answer;
       if (Array.isArray(res.reserved_tags)) {
-        formData.reserved_tags.value = res.reserved_tags.join('\n');
+        formData.reserved_tags.value = res.reserved_tags;
       }
       setFormData({ ...formData });
     });
@@ -145,20 +150,103 @@ const Index: FC = () => {
     initData();
   }, []);
 
-  const handleOnChange = (data) => {
-    setFormData(data);
-  };
+  // const handleOnChange = (data) => {
+  //   setFormData(data);
+  // };
 
   return (
     <>
       <h3 className="mb-4">{t('page_title')}</h3>
-      <SchemaForm
-        schema={schema}
-        formData={formData}
-        onSubmit={onSubmit}
-        uiSchema={uiSchema}
-        onChange={handleOnChange}
-      />
+      <Form noValidate onSubmit={onSubmit}>
+        <Form.Group className="mb-3" controlId="reserved_tags">
+          <Form.Label>{t('reserved_tags.label')}</Form.Label>
+          <TagSelector
+            value={formData.reserved_tags.value}
+            onChange={(val) => {
+              handleValueChange({
+                reserved_tags: {
+                  value: val,
+                  errorMsg: '',
+                  isInvalid: false,
+                },
+              });
+            }}
+            showRequiredTag={false}
+            maxTagLength={0}
+            tagStyleMode="simple"
+            formText={t('reserved_tags.text')}
+            isInvalid={formData.reserved_tags.isInvalid}
+            errMsg={formData.reserved_tags.errorMsg}
+          />
+        </Form.Group>
+
+        <Form.Group className="mb-3" controlId="recommend_tags">
+          <Form.Label>{t('recommend_tags.label')}</Form.Label>
+          <TagSelector
+            value={formData.recommend_tags.value}
+            onChange={(val) => {
+              handleValueChange({
+                recommend_tags: {
+                  value: val,
+                  errorMsg: '',
+                  isInvalid: false,
+                },
+              });
+            }}
+            showRequiredTag={false}
+            tagStyleMode="simple"
+            formText={t('recommend_tags.text')}
+            isInvalid={formData.recommend_tags.isInvalid}
+            errMsg={formData.recommend_tags.errorMsg}
+          />
+        </Form.Group>
+
+        <Form.Group className="mb-3" controlId="required_tag">
+          <Form.Label>{t('required_tag.title')}</Form.Label>
+          <Form.Switch
+            label={t('required_tag.label')}
+            checked={formData.required_tag.value}
+            onChange={(evt) => {
+              handleValueChange({
+                required_tag: {
+                  value: evt.target.checked,
+                  errorMsg: '',
+                  isInvalid: false,
+                },
+              });
+            }}
+          />
+          <Form.Text>{t('required_tag.text')}</Form.Text>
+          <Form.Control.Feedback type="invalid">
+            {formData.required_tag.errorMsg}
+          </Form.Control.Feedback>
+        </Form.Group>
+
+        <Form.Group className="mb-3" controlId="restrict_answer">
+          <Form.Label>{t('restrict_answer.title')}</Form.Label>
+          <Form.Switch
+            label={t('restrict_answer.label')}
+            checked={formData.restrict_answer.value}
+            onChange={(evt) => {
+              handleValueChange({
+                restrict_answer: {
+                  value: evt.target.checked,
+                  errorMsg: '',
+                  isInvalid: false,
+                },
+              });
+            }}
+          />
+          <Form.Text>{t('restrict_answer.text')}</Form.Text>
+          <Form.Control.Feedback type="invalid">
+            {formData.restrict_answer.errorMsg}
+          </Form.Control.Feedback>
+        </Form.Group>
+
+        <Form.Group className="mb-3">
+          <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
+        </Form.Group>
+      </Form>
     </>
   );
 };
diff --git a/ui/src/pages/Questions/Ask/index.tsx 
b/ui/src/pages/Questions/Ask/index.tsx
index 40ff73e2..2b370de0 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -487,52 +487,17 @@ const Ask = () => {
               />
             </Form.Group>
 
-            {isEdit && (
-              <Form.Group controlId="edit_summary" className="my-3">
-                <Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
-                <Form.Control
-                  type="text"
-                  defaultValue={formData.edit_summary.value}
-                  isInvalid={formData.edit_summary.isInvalid}
-                  placeholder={t('form.fields.edit_summary.placeholder')}
-                  onChange={handleSummaryChange}
-                  contentEditable
-                />
-                <Form.Control.Feedback type="invalid">
-                  {formData.edit_summary.errorMsg}
-                </Form.Control.Feedback>
-              </Form.Group>
-            )}
-            {!checked && (
-              <div className="mt-3">
-                <Button type="submit" className="me-2">
-                  {isEdit ? t('btn_save_edits') : t('btn_post_question')}
-                </Button>
-                {isEdit && (
-                  <Button variant="link" onClick={backPage}>
-                    {t('cancel', { keyPrefix: 'btns' })}
-                  </Button>
-                )}
-
-                {hasDraft && (
-                  <Button variant="link" onClick={deleteDraft}>
-                    {t('discard_draft', { keyPrefix: 'btns' })}
-                  </Button>
-                )}
-              </div>
-            )}
             {!isEdit && (
               <>
-                <Form.Check
-                  className="mt-5"
+                <Form.Switch
                   checked={checked}
-                  type="checkbox"
+                  type="switch"
                   label={t('answer_question')}
                   onChange={(e) => setCheckState(e.target.checked)}
                   id="radio-answer"
                 />
                 {checked && (
-                  <Form.Group controlId="answer" className="mt-4">
+                  <Form.Group controlId="answer" className="mt-3">
                     <Form.Label>{t('form.fields.answer.label')}</Form.Label>
                     <Editor
                       value={formData.answer_content.value}
@@ -562,6 +527,41 @@ const Ask = () => {
                 )}
               </>
             )}
+
+            {isEdit && (
+              <Form.Group controlId="edit_summary" className="my-3">
+                <Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
+                <Form.Control
+                  type="text"
+                  defaultValue={formData.edit_summary.value}
+                  isInvalid={formData.edit_summary.isInvalid}
+                  placeholder={t('form.fields.edit_summary.placeholder')}
+                  onChange={handleSummaryChange}
+                  contentEditable
+                />
+                <Form.Control.Feedback type="invalid">
+                  {formData.edit_summary.errorMsg}
+                </Form.Control.Feedback>
+              </Form.Group>
+            )}
+            {!checked && (
+              <div className="mt-3">
+                <Button type="submit" className="me-2">
+                  {isEdit ? t('btn_save_edits') : t('btn_post_question')}
+                </Button>
+                {isEdit && (
+                  <Button variant="link" onClick={backPage}>
+                    {t('cancel', { keyPrefix: 'btns' })}
+                  </Button>
+                )}
+
+                {hasDraft && (
+                  <Button variant="link" onClick={deleteDraft}>
+                    {t('discard_draft', { keyPrefix: 'btns' })}
+                  </Button>
+                )}
+              </div>
+            )}
             {checked && (
               <div className="mt-3">
                 <Button type="submit">{t('post_question&answer')}</Button>
diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx
index e0bf27ff..d81a9027 100644
--- a/ui/src/pages/Questions/index.tsx
+++ b/ui/src/pages/Questions/index.tsx
@@ -36,8 +36,7 @@ import {
 } from '@/stores';
 import { useQuestionList } from '@/services';
 import * as Type from '@/common/interface';
-import { userCenter, floppyNavigation, Storage } from '@/utils';
-import { QUESTIONS_ORDER_STORAGE_KEY } from '@/common/constants';
+import { userCenter, floppyNavigation } from '@/utils';
 import { QUESTION_ORDER_KEYS } from '@/components/QuestionList';
 
 const Questions: FC = () => {
@@ -46,12 +45,8 @@ const Questions: FC = () => {
   const { user: loggedUser } = loggedUserInfoStore((_) => _);
   const [urlSearchParams] = useSearchParams();
   const curPage = Number(urlSearchParams.get('page')) || 1;
-  const storageOrder = Storage.get(QUESTIONS_ORDER_STORAGE_KEY);
-  const curOrder =
-    urlSearchParams.get('order') || storageOrder || QUESTION_ORDER_KEYS[0];
-  if (curOrder !== storageOrder) {
-    Storage.set(QUESTIONS_ORDER_STORAGE_KEY, curOrder);
-  }
+  const curOrder = (urlSearchParams.get('order') ||
+    QUESTION_ORDER_KEYS[0]) as Type.QuestionOrderBy;
   const reqParams: Type.QueryQuestionsReq = {
     page_size: 20,
     page: curPage,
diff --git a/ui/src/pages/Search/components/SearchHead/index.tsx 
b/ui/src/pages/Search/components/SearchHead/index.tsx
index 667c16ec..a175d381 100644
--- a/ui/src/pages/Search/components/SearchHead/index.tsx
+++ b/ui/src/pages/Search/components/SearchHead/index.tsx
@@ -22,7 +22,7 @@ import { useTranslation } from 'react-i18next';
 
 import { QueryGroup } from '@/components';
 
-const sortBtns = ['active', 'newest', 'relevance', 'score'];
+const sortBtns = ['relevance', 'newest', 'active', 'score'];
 
 interface Props {
   count: number;
diff --git a/ui/src/pages/Search/components/Tips/index.tsx 
b/ui/src/pages/Search/components/Tips/index.tsx
index 2fd151f8..fe6a63d9 100644
--- a/ui/src/pages/Search/components/Tips/index.tsx
+++ b/ui/src/pages/Search/components/Tips/index.tsx
@@ -26,7 +26,7 @@ const Index: FC = () => {
   return (
     <Card>
       <Card.Header>{t('search.tips.title')}</Card.Header>
-      <Card.Body className="small ext-secondary">
+      <Card.Body>
         <div className="mb-1">
           <Trans i18nKey="search.tips.tag" components={{ 1: <code /> }} />
         </div>
diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx
index a016af70..3eeb0ec9 100644
--- a/ui/src/pages/Search/index.tsx
+++ b/ui/src/pages/Search/index.tsx
@@ -42,7 +42,7 @@ const Index = () => {
   const [searchParams] = useSearchParams();
   const page = searchParams.get('page') || 1;
   const q = searchParams.get('q') || '';
-  const order = searchParams.get('order') || 'active';
+  const order = searchParams.get('order') || 'relevance';
   const [isLoading, setIsLoading] = useState(false);
   const { isSkeletonShow } = useSkeletonControl(isLoading);
   const [data, setData] = useState<SearchRes>({
diff --git a/ui/src/pages/Tags/Detail/index.tsx 
b/ui/src/pages/Tags/Detail/index.tsx
index c2351ce3..a07236da 100644
--- a/ui/src/pages/Tags/Detail/index.tsx
+++ b/ui/src/pages/Tags/Detail/index.tsx
@@ -38,9 +38,8 @@ import {
 } from '@/services';
 import QuestionList, { QUESTION_ORDER_KEYS } from '@/components/QuestionList';
 import HotQuestions from '@/components/HotQuestions';
-import { escapeRemove, guard, Storage, scrollToDocTop } from '@/utils';
+import { escapeRemove, guard, scrollToDocTop } from '@/utils';
 import { pathFactory } from '@/router/pathFactory';
-import { QUESTIONS_ORDER_STORAGE_KEY } from '@/common/constants';
 
 const Index: FC = () => {
   const { t } = useTranslation('translation', { keyPrefix: 'tags' });
@@ -48,12 +47,8 @@ const Index: FC = () => {
   const routeParams = useParams();
   const curTagName = routeParams.tagName || '';
   const [urlSearchParams] = useSearchParams();
-  const storageOrder = Storage.get(QUESTIONS_ORDER_STORAGE_KEY);
-  const curOrder =
-    urlSearchParams.get('order') || storageOrder || QUESTION_ORDER_KEYS[0];
-  if (curOrder !== storageOrder) {
-    Storage.set(QUESTIONS_ORDER_STORAGE_KEY, curOrder);
-  }
+  const curOrder = (urlSearchParams.get('order') ||
+    QUESTION_ORDER_KEYS[0]) as Type.QuestionOrderBy;
   const curPage = Number(urlSearchParams.get('page')) || 1;
   const reqParams: Type.QueryQuestionsReq = {
     page_size: 20,
diff --git a/ui/src/services/client/question.ts 
b/ui/src/services/client/question.ts
index d027c83b..0e382c26 100644
--- a/ui/src/services/client/question.ts
+++ b/ui/src/services/client/question.ts
@@ -40,7 +40,7 @@ export const useHotQuestions = (
   params: Type.QueryQuestionsReq = {
     page: 1,
     page_size: 6,
-    order: 'frequent',
+    order: 'hot',
     in_days: 7,
   },
 ) => {
diff --git a/ui/src/stores/writeSetting.ts b/ui/src/stores/writeSetting.ts
index 0ea04962..8e7c1f52 100644
--- a/ui/src/stores/writeSetting.ts
+++ b/ui/src/stores/writeSetting.ts
@@ -30,7 +30,7 @@ const Index = create<IProps>((set) => ({
   write: {
     restrict_answer: true,
     recommend_tags: [],
-    required_tag: '',
+    required_tag: false,
     reserved_tags: [],
   },
   update: (params) =>
diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts
index 97880bf7..d9d3bee5 100644
--- a/ui/src/utils/common.ts
+++ b/ui/src/utils/common.ts
@@ -118,7 +118,7 @@ function matchedUsers(markdown) {
  */
 function parseUserInfo(markdown) {
   const globalReg = /\B@([\w\\_\\.\\-]+)/g;
-  return markdown.replace(globalReg, '[@$1](/u/$1)');
+  return markdown.replace(globalReg, '[@$1](/users/$1)');
 }
 
 function parseEditMentionUser(markdown) {
@@ -188,6 +188,23 @@ function escapeHtml(str: string) {
   return str.replace(/[&<>"'`]/g, (tag) => tagsToReplace[tag] || tag);
 }
 
+function formatDiffPart(part: any, className: string): string {
+  if (part.value.replace(/\n/g, '').length <= 0) {
+    if (part.value.match(/\n/g)?.length > 1) {
+      const value = part.value.replace(/\n/, '');
+      return `<span class="${className}"> </span><div><span 
class="${className}">${value.replace(
+        /\n/g,
+        ' ',
+      )}</span></div>`;
+    }
+    return `<div><span class="${className}">${part.value.replace(
+      /\n/g,
+      ' ',
+    )}</span></div>`;
+  }
+  return `<span class="${className}">${part.value}</span>`;
+}
+
 function diffText(newText: string, oldText?: string): string {
   if (!newText) {
     return '';
@@ -200,22 +217,10 @@ function diffText(newText: string, oldText?: string): 
string {
   const diff = Diff.diffChars(escapeHtml(oldText), escapeHtml(newText));
   result = diff.map((part) => {
     if (part.added) {
-      if (part.value.replace(/\n/g, '').length <= 0) {
-        return `<span class="review-text-add d-block">${part.value.replace(
-          /\n/g,
-          '↵\n',
-        )}</span>`;
-      }
-      return `<span class="review-text-add">${part.value}</span>`;
+      return formatDiffPart(part, 'review-text-add');
     }
     if (part.removed) {
-      if (part.value.replace(/\n/g, '').length <= 0) {
-        return `<span class="review-text-delete text-decoration-none 
d-block">${part.value.replace(
-          /\n/g,
-          '↵\n',
-        )}</span>`;
-      }
-      return `<span class="review-text-delete">${part.value}</span>`;
+      return formatDiffPart(part, 'review-text-remove text-decoration-none');
     }
 
     return part.value;

Reply via email to