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 7606940a Feat/1.3.0/UI (#840)
7606940a is described below

commit 7606940a9068fe2b3bca87e10ab3e3371ae54905
Author: dashuai <[email protected]>
AuthorDate: Thu Mar 14 10:06:41 2024 +0800

    Feat/1.3.0/UI (#840)
---
 i18n/en_US.yaml                                    |  36 +-
 i18n/zh_CN.yaml                                    |  13 +-
 internal/controller/template_controller.go         |  19 +-
 ui/config-overrides.js                             |  96 ++++++
 ui/package.json                                    |   4 +-
 ui/pnpm-lock.yaml                                  | 100 ++++--
 ui/src/common/constants.ts                         |  17 +-
 ui/src/common/interface.ts                         |  87 ++++-
 .../components/Comment/components/Form/index.tsx   |   7 +-
 ui/src/components/Comment/index.tsx                |   2 +-
 ui/src/components/SchemaForm/index.tsx             |   2 +-
 ui/src/hooks/usePageUsers/index.tsx                |   2 +-
 ui/src/hooks/useReportModal/index.tsx              |  28 +-
 ui/src/i18n/init.ts                                |   8 +-
 .../Admin/Answers/components/Action/index.tsx      |  12 +
 ui/src/pages/Admin/Answers/index.tsx               |  24 +-
 .../Dashboard/components/AnswerLinks/index.tsx     |  16 +-
 .../Dashboard/components/HealthStatus/index.tsx    |   4 +-
 .../Dashboard/components/Statistics/index.tsx      |   2 +-
 ui/src/pages/Admin/Flags/index.tsx                 | 186 ----------
 .../Admin/Questions/components/Action/index.tsx    |  12 +
 ui/src/pages/Admin/Questions/index.tsx             |  13 +-
 ui/src/pages/Admin/index.tsx                       |   2 +-
 .../Ask/components/SearchQuestion/index.tsx        |   6 +-
 ui/src/pages/Questions/Ask/index.tsx               |   3 +-
 .../Review/components/ApproveDropdown/index.tsx    | 202 +++++++++++
 .../Review/components/EditPostModal/index.scss     |   4 +
 .../Review/components/EditPostModal/index.tsx      | 375 +++++++++++++++++++++
 .../pages/Review/components/FlagContent/index.tsx  | 210 ++++++++++++
 .../Review/components/QueuedContent/index.tsx      | 203 +++++++++++
 .../pages/Review/components/ReviewType/index.tsx   |  40 +++
 .../Review/components/SuggestContent/index.tsx     | 238 +++++++++++++
 ui/src/pages/Review/components/index.ts            |  15 +
 ui/src/pages/Review/index.tsx                      | 274 ++++-----------
 ui/src/pages/Review/utils/generateData.ts          |  58 ++++
 .../components/Achievements/index.tsx              |   2 +-
 .../Users/Notifications/components/Inbox/index.tsx |   2 +-
 ui/src/pages/Users/Settings/Interface/index.tsx    |   3 +-
 ui/src/router/pathFactory.ts                       |   2 +-
 ui/src/router/routes.ts                            |   5 -
 ui/src/services/client/index.ts                    |   1 +
 ui/src/services/client/review.ts                   |  52 +++
 ui/src/services/client/revision.ts                 |   6 -
 ui/src/utils/common.ts                             |  49 +--
 ui/src/utils/saveDraft.ts                          |   2 +-
 ui/template/header.html                            |   4 +-
 46 files changed, 1921 insertions(+), 527 deletions(-)

diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 18312e5e..61af80e1 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1169,6 +1169,15 @@ ui:
     system_setting: System setting
     default: Default
     reset: Reset
+    tag: Tag
+    post_lowercase: post
+    filter: Filter
+    ignore: Ignore
+    submit: Submit
+    normal: Normal
+    closed: Closed
+    deleted: Deleted
+    pending: Pending
   search:
     title: Search Results
     keywords: Keywords
@@ -1444,6 +1453,7 @@ ui:
       github: GitHub
       blog: Blog
       contact: Contact
+      forum: Forum
       documents: Documents
       feedback: Feedback
       support: Support
@@ -1546,9 +1556,6 @@ ui:
         content: A suspended user can't log in.
     questions:
       page_title: Questions
-      normal: Normal
-      closed: Closed
-      deleted: Deleted
       unlisted: Unlisted
       post: Post
       votes: Votes
@@ -1557,12 +1564,11 @@ ui:
       status: Status
       action: Action
       change: Change
+      pending: Pending
       filter:
         placeholder: "Filter by title, question:id"
     answers:
       page_title: Answers
-      normal: Normal
-      deleted: Deleted
       post: Post
       votes: Votes
       created: Created
@@ -1676,8 +1682,8 @@ ui:
       page_title: Write
       restrict_answer:
         title: Restrict answer
-        label: Each user can only write one answer for each question
-        text: "They can use the edit link to refine and improve their existing 
answer, instead."
+        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."
       recommend_tags:
         label: Recommend tags
         text: "Please input tag slug above, one tag per line."
@@ -1811,6 +1817,22 @@ ui:
     edit_answer: Edit answer
     edit_tag: Edit tag
     empty: No review tasks left.
+    approve_revision_tip: Do you approve this revision?
+    approve_flag_tip: Do you approve this flag?
+    approve_post_tip: Do you approve this post?
+    approve_user_tip: Do you approve this user?
+    suggest_edits: Suggested edits
+    flag_post: Flag post
+    flag_user: Flag user
+    queued_post: Queued post
+    queued_user: Queued user
+    filter_label: Type
+    reputation: reputation
+    flag_post_type: Flagged this post as {{ type }}
+    flag_user_type: Flagged this user as {{ type }}
+    edit_post: Edit post
+    list_post: List post
+    unlist_post: Unlist post
   timeline:
     undeleted: undeleted
     deleted: deleted
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 01bc07b9..cdabc86f 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1133,6 +1133,15 @@ ui:
     system_setting: System setting
     default: Default
     reset: Reset
+    tag: Tag
+    post_lowercase: 帖子
+    filter: Filter
+    ignore: 忽略
+    submit: 提交
+    normal: 正常
+    closed: 已关闭
+    deleted: 已删除
+    pending: Pending
   search:
     title: 搜索结果
     keywords: 关键词
@@ -1631,8 +1640,8 @@ ui:
       page_title: 编辑
       restrict_answer:
         title: 限制一个回答
-        label: 每个用户对于每个问题只能有一个回答
-        text: "用户可以使用编辑按钮优化已有的回答"
+        label: 每个用户只能为同一问题写一个回答
+        text: "关闭以允许用户对同一问题编写多个回答,这可能会导致回答不集中。"
       recommend_tags:
         label: 推荐标签
         text: "请在上方输入标签固定链接,每行一个标签。"
diff --git a/internal/controller/template_controller.go 
b/internal/controller/template_controller.go
index 6b4c4bbe..b128b91a 100644
--- a/internal/controller/template_controller.go
+++ b/internal/controller/template_controller.go
@@ -22,7 +22,6 @@ package controller
 import (
        "encoding/json"
        "fmt"
-       "github.com/apache/incubator-answer/internal/entity"
        "html/template"
        "net/http"
        "regexp"
@@ -32,6 +31,7 @@ import (
        "github.com/apache/incubator-answer/internal/base/constant"
        "github.com/apache/incubator-answer/internal/base/handler"
        templaterender 
"github.com/apache/incubator-answer/internal/controller/template_render"
+       "github.com/apache/incubator-answer/internal/entity"
        "github.com/apache/incubator-answer/internal/schema"
        "github.com/apache/incubator-answer/internal/service/siteinfo_common"
        "github.com/apache/incubator-answer/pkg/checker"
@@ -47,7 +47,7 @@ import (
 var SiteUrl = ""
 
 type TemplateController struct {
-       scriptPath               string
+       scriptPath               []string
        cssPath                  string
        templateRenderController *templaterender.TemplateRenderController
        siteInfoService          siteinfo_common.SiteInfoCommonService
@@ -66,18 +66,21 @@ func NewTemplateController(
                siteInfoService:          siteInfoService,
        }
 }
-func GetStyle() (script, css string) {
+func GetStyle() (script []string, css string) {
        file, err := ui.Build.ReadFile("build/index.html")
        if err != nil {
                return
        }
-       scriptRegexp := regexp.MustCompile(`<script defer="defer" 
src="(.*)"></script>`)
-       scriptData := scriptRegexp.FindStringSubmatch(string(file))
+       scriptRegexp := regexp.MustCompile(`<script defer="defer" 
src="([^"]*)"></script>`)
+       scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1)
+       for _, s := range scriptData {
+               if len(s) == 2 {
+                       script = append(script, s[1])
+               }
+       }
+
        cssRegexp := regexp.MustCompile(`<link href="(.*)" rel="stylesheet">`)
        cssListData := cssRegexp.FindStringSubmatch(string(file))
-       if len(scriptData) == 2 {
-               script = scriptData[1]
-       }
        if len(cssListData) == 2 {
                css = cssListData[1]
        }
diff --git a/ui/config-overrides.js b/ui/config-overrides.js
index 293d6bc1..0df95a9b 100644
--- a/ui/config-overrides.js
+++ b/ui/config-overrides.js
@@ -20,6 +20,7 @@
 const {
   addWebpackModuleRule,
   addWebpackAlias,
+  setWebpackOptimizationSplitChunks,
 } = require("customize-cra");
 
 const path = require("path");
@@ -37,6 +38,101 @@ module.exports = {
       use: "yaml-loader"
     })(config);
 
+    setWebpackOptimizationSplitChunks({
+      maxInitialRequests: 20,
+      minSize: 20 * 1024,
+      minChunks: 2,
+      cacheGroups: {
+        automaticNamePrefix: 'chunk',
+        components: {
+          test: /[\\/]components[\\/]/,
+          name: 'components',
+          priority: 14,
+          reuseExistingChunk: true,
+          minChunks: 1,
+          chunks: 'initial',
+        },
+        i18next: {
+          name: 'i18next',
+          test: /[\/]node_modules[\/](i18next)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 12,
+          reuseExistingChunk: true,
+          minChunks: 1,
+          chunks: 'initial',
+        },
+        reactBootstrap: {
+          name: 'react-bootstrap',
+          test: /[\/]node_modules[\/](react-bootstrap)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 11,
+          minChunks: 1,
+          chunks: 'initial',
+          reuseExistingChunk: true,
+        },
+        lodash: {
+          name: 'lodash',
+          test: /[\/]node_modules[\/](lodash)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 10,
+          reuseExistingChunk: true,
+          minChunks: 1,
+          chunks: 'initial',
+        },
+        codemirror: {
+          name: 'codemirror',
+          test: /[\/]node_modules[\/](codemirror)[\/]/,
+          priority: 9,
+          reuseExistingChunk: true,
+          enforce: true,
+        },
+        nextShare: {
+          name: 'next-share',
+          test: /[\/]node_modules[\/](next-share)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 8,
+          reuseExistingChunk: true,
+          minChunks: 1,
+          chunks: 'initial',
+        },
+        marked: {
+          name: 'marked',
+          test: /[\/]node_modules[\/](marked)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 7,
+          reuseExistingChunk: true,
+          minChunks: 1,
+          chunks: 'initial',
+        },
+        reactDom: {
+          name: 'react-dom',
+          test: /[\/]node_modules[\/](react-dom)[\/]/,
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          priority: 7,
+          reuseExistingChunk: true,
+          chunks: 'all',
+          enforce: true,
+        },
+        nodesAsync: {
+          name: 'chunk-nodesAsync',
+          test: /[\/]node_modules[\/]/,
+          priority: 2,
+          minChunks: 2,
+          chunks: 'async', // 仅打包异步引用的依赖
+          reuseExistingChunk: true, // 重复使用已经存在的块
+        },
+        nodesInitial: {
+          name: 'chunk-nodesInitial',
+          filename: 'static/js/[name].[contenthash:8].chunk.js',
+          test: /[\/]node_modules[\/]/,
+          priority: 1,
+          minChunks: 1,
+          chunks: 'initial',
+          reuseExistingChunk: true,
+        },
+      },
+    })(config);
+
     // add i18n dir to ModuleScopePlugin allowedPaths
     const moduleScopePlugin = config.resolve.plugins.find(_ => 
_.constructor.name === "ModuleScopePlugin");
     if (moduleScopePlugin) {
diff --git a/ui/package.json b/ui/package.json
index ac7e4411..63a1f85d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -12,7 +12,8 @@
     "prepare": "pnpm build:packages",
     "pre-commit": "lint-staged",
     "build:packages": "pnpm -r --filter=./src/plugins/* run build",
-    "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules"
+    "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules",
+    "analyze": "source-map-explorer 'build/static/js/*.js'"
   },
   "dependencies": {
     "axios": "^0.27.2",
@@ -82,6 +83,7 @@
     "react-app-rewired": "^2.2.1",
     "react-scripts": "5.0.1",
     "sass": "^1.54.4",
+    "source-map-explorer": "^2.5.3",
     "typescript": "^4.9.5",
     "yaml-loader": "^0.8.0"
   },
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 77f3c9b1..83bdc9e2 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -1,20 +1,3 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
 lockfileVersion: '6.0'
 
 settings:
@@ -221,6 +204,9 @@ importers:
       sass:
         specifier: ^1.54.4
         version: 1.54.9
+      source-map-explorer:
+        specifier: ^2.5.3
+        version: 2.5.3
       typescript:
         specifier: ^4.9.5
         version: 4.9.5
@@ -2223,7 +2209,7 @@ packages:
       chalk: 4.1.2
       emittery: 0.8.1
       exit: 0.1.2
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-changed-files: 27.5.1
       jest-config: 27.5.1([email protected])
       jest-haste-map: 27.5.1
@@ -2295,7 +2281,7 @@ packages:
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
       glob: 7.2.3
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       istanbul-lib-coverage: 3.2.0
       istanbul-lib-instrument: 5.2.0
       istanbul-lib-report: 3.0.0
@@ -2324,7 +2310,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       callsites: 3.1.0
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       source-map: 0.6.1
 
   /@jest/[email protected]:
@@ -2350,7 +2336,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@jest/test-result': 27.5.1
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-haste-map: 27.5.1
       jest-runtime: 27.5.1
     transitivePeerDependencies:
@@ -4550,6 +4536,12 @@ packages:
     dependencies:
       node-int64: 0.4.0
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
+    engines: {node: '>= 0.4.0'}
+    hasBin: true
+    dev: true
+
   /[email protected]:
     resolution: {integrity: 
sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
 
@@ -5914,7 +5906,7 @@ packages:
     resolution: {integrity: 
sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==}
     engines: {node: '>=10.13.0'}
     dependencies:
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       tapable: 2.2.1
 
   /[email protected]:
@@ -7899,7 +7891,7 @@ packages:
       '@jest/types': 27.5.1
       chalk: 4.1.2
       exit: 0.1.2
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       import-local: 3.1.0
       jest-config: 27.5.1([email protected])
       jest-util: 27.5.1
@@ -7930,7 +7922,7 @@ packages:
       ci-info: 3.4.0
       deepmerge: 4.2.2
       glob: 7.2.3
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-circus: 27.5.1
       jest-environment-jsdom: 27.5.1
       jest-environment-node: 27.5.1
@@ -8034,7 +8026,7 @@ packages:
       '@types/node': 16.11.59
       anymatch: 3.1.2
       fb-watchman: 2.0.1
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-regex-util: 27.5.1
       jest-serializer: 27.5.1
       jest-util: 27.5.1
@@ -8102,7 +8094,7 @@ packages:
       '@jest/types': 27.5.1
       '@types/stack-utils': 2.0.1
       chalk: 4.1.2
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       micromatch: 4.0.5
       pretty-format: 27.5.1
       slash: 3.0.0
@@ -8185,7 +8177,7 @@ packages:
       '@types/node': 16.11.59
       chalk: 4.1.2
       emittery: 0.8.1
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-docblock: 27.5.1
       jest-environment-jsdom: 27.5.1
       jest-environment-node: 27.5.1
@@ -8220,7 +8212,7 @@ packages:
       collect-v8-coverage: 1.0.1
       execa: 5.1.1
       glob: 7.2.3
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-haste-map: 27.5.1
       jest-message-util: 27.5.1
       jest-mock: 27.5.1
@@ -8238,7 +8230,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@types/node': 16.11.59
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
 
   /[email protected]:
     resolution: {integrity: 
sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==}
@@ -8256,7 +8248,7 @@ packages:
       babel-preset-current-node-syntax: 1.0.1(@babel/[email protected])
       chalk: 4.1.2
       expect: 27.5.1
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       jest-diff: 27.5.1
       jest-get-type: 27.5.1
       jest-haste-map: 27.5.1
@@ -8277,7 +8269,7 @@ packages:
       '@types/node': 16.11.59
       chalk: 4.1.2
       ci-info: 3.4.0
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
       picomatch: 2.3.1
 
   /[email protected]:
@@ -8483,7 +8475,7 @@ packages:
     dependencies:
       universalify: 2.0.0
     optionalDependencies:
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
 
   /[email protected]:
     resolution: {integrity: 
sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==}
@@ -9153,6 +9145,14 @@ packages:
       mimic-fn: 4.0.0
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
+    engines: {node: '>=8'}
+    dependencies:
+      is-docker: 2.2.1
+      is-wsl: 2.2.0
+    dev: true
+
   /[email protected]:
     resolution: {integrity: 
sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
     engines: {node: '>=12'}
@@ -10792,6 +10792,13 @@ packages:
     resolution: {integrity: 
sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
+    hasBin: true
+    dependencies:
+      glob: 7.2.3
+    dev: true
+
   /[email protected]:
     resolution: {integrity: 
sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
     hasBin: true
@@ -11151,6 +11158,25 @@ packages:
   /[email protected]:
     resolution: {integrity: 
sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==}
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==}
+    engines: {node: '>=12'}
+    hasBin: true
+    dependencies:
+      btoa: 1.2.1
+      chalk: 4.1.2
+      convert-source-map: 1.8.0
+      ejs: 3.1.8
+      escape-html: 1.0.3
+      glob: 7.2.3
+      gzip-size: 6.0.0
+      lodash: 4.17.21
+      open: 7.4.2
+      source-map: 0.7.4
+      temp: 0.9.4
+      yargs: 16.2.0
+    dev: true
+
   /[email protected]:
     resolution: {integrity: 
sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
     engines: {node: '>=0.10.0'}
@@ -11593,6 +11619,14 @@ packages:
     resolution: {integrity: 
sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
     engines: {node: '>=8'}
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      mkdirp: 0.5.6
+      rimraf: 2.6.3
+    dev: true
+
   /[email protected]:
     resolution: {integrity: 
sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==}
     engines: {node: '>=10'}
@@ -12184,7 +12218,7 @@ packages:
     engines: {node: '>=10.13.0'}
     dependencies:
       glob-to-regexp: 0.4.1
-      graceful-fs: 4.2.10
+      graceful-fs: 4.2.11
 
   /[email protected]:
     resolution: {integrity: 
sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==}
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index ec7f1ba6..92944f83 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -55,6 +55,11 @@ export const ADMIN_LIST_STATUS = {
     variant: 'text-bg-danger',
     name: 'deleted',
   },
+  // pending
+  11: {
+    variant: 'text-bg-warning',
+    name: 'pending',
+  },
   normal: {
     variant: 'text-bg-success',
     name: 'normal',
@@ -67,9 +72,13 @@ export const ADMIN_LIST_STATUS = {
     variant: 'text-bg-danger',
     name: 'deleted',
   },
-  unlisted: {
+  pending: {
+    variant: 'text-bg-warning',
+    name: 'pending',
+  },
+  unlist: {
     variant: 'text-bg-secondary',
-    name: 'unlisted',
+    name: 'unlist',
   },
 };
 
@@ -85,10 +94,6 @@ export const ADMIN_NAV_MENUS = [
   {
     name: 'users',
   },
-  {
-    name: 'flags',
-    // badgeContent: 5,
-  },
   {
     name: 'customize',
     children: [
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 65117ae1..86c87798 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -56,6 +56,7 @@ export interface TagBase {
 export interface Tag extends TagBase {
   main_tag_slug_name?: string;
   parsed_text?: string;
+  tag_id?: string;
 }
 
 export interface SynonymsTag extends Tag {
@@ -299,9 +300,13 @@ export interface QueryQuestionsReq extends Paging {
   in_days?: number;
 }
 
-export type AdminQuestionStatus = 'available' | 'closed' | 'deleted';
+export type AdminQuestionStatus =
+  | 'available'
+  | 'pending'
+  | 'closed'
+  | 'deleted';
 
-export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
+export type AdminContentsFilterBy = 'normal' | 'pending' | 'closed' | 
'deleted';
 
 export interface AdminContentsReq extends Paging {
   status: AdminContentsFilterBy;
@@ -563,7 +568,7 @@ export interface TimelineRes {
   timeline: TimelineItem[];
 }
 
-export interface ReviewItem {
+export interface SuggestReviewItem {
   type: 'question' | 'answer' | 'tag';
   info: {
     url_title?: string;
@@ -585,9 +590,57 @@ export interface ReviewItem {
     content: Tag | QuestionDetailRes | AnswerItem;
   };
 }
-export interface ReviewResp {
+export interface SuggestReviewResp {
+  count: number;
+  list: SuggestReviewItem[];
+}
+
+export interface ReasonItem {
+  content_type: string;
+  description: string;
+  name: string;
+  placeholder: string;
+  reason_type: number;
+}
+
+export interface BaseReviewItem {
+  object_type: 'question' | 'answer' | 'comment' | 'user';
+  object_id: string;
+  object_show_status: number;
+  object_status: number;
+  tags: Tag[];
+  title: string;
+  original_text: string;
+  author_user_info: UserInfoBase;
+  created_at: number;
+  submit_at: number;
+  comment_id: string;
+  question_id: string;
+  answer_id: string;
+  answer_count: number;
+  answer_accepted?: boolean;
+  flag_id: string;
+}
+
+export interface FlagReviewItem extends BaseReviewItem {
+  reason: ReasonItem;
+  submitter_user: UserInfoBase;
+}
+
+export interface FlagReviewResp {
   count: number;
-  list: ReviewItem[];
+  list: FlagReviewItem[];
+}
+
+export interface QueuedReviewItem extends BaseReviewItem {
+  review_id: number;
+  reason: string;
+  submitter_display_name: string;
+}
+
+export interface QueuedReviewResp {
+  count: number;
+  list: QueuedReviewItem[];
 }
 
 export interface UserRoleItem {
@@ -639,3 +692,27 @@ export interface UserPluginsConfigRes {
   name: string;
   slug_name: string;
 }
+
+export interface ReviewTypeItem {
+  label: string;
+  name: string;
+  todo_amount: number;
+}
+
+export interface PutFlagReviewParams {
+  operation_type:
+    | 'edit_post'
+    | 'close_post'
+    | 'delete_post'
+    | 'unlist_post'
+    | 'ignore_report';
+  flag_id: string;
+  close_msg?: string;
+  close_type?: number;
+  title?: string;
+  content?: string;
+  tags?: Tag[];
+  // mention_username_list?: any;
+  captcha_code?: any;
+  captcha_id?: any;
+}
diff --git a/ui/src/components/Comment/components/Form/index.tsx 
b/ui/src/components/Comment/components/Form/index.tsx
index 8809778c..05ae5b2d 100644
--- a/ui/src/components/Comment/components/Form/index.tsx
+++ b/ui/src/components/Comment/components/Form/index.tsx
@@ -25,6 +25,7 @@ import classNames from 'classnames';
 
 import { TextArea, Mentions } from '@/components';
 import { usePageUsers, usePromptWithUnload } from '@/hooks';
+import { parseEditMentionUser } from '@/utils';
 
 const Index = ({
   className = '',
@@ -78,7 +79,11 @@ const Index = ({
           <Mentions
             pageUsers={pageUsers.getUsers()}
             onSelected={handleSelected}>
-            <TextArea size="sm" value={value} onChange={handleChange} />
+            <TextArea
+              size="sm"
+              value={type === 'edit' ? parseEditMentionUser(value) : value}
+              onChange={handleChange}
+            />
           </Mentions>
           <div className="form-text">{t(`tip_${mode}`)}</div>
         </div>
diff --git a/ui/src/components/Comment/index.tsx 
b/ui/src/components/Comment/index.tsx
index be2d5553..81345e7e 100644
--- a/ui/src/components/Comment/index.tsx
+++ b/ui/src/components/Comment/index.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
 import { Link } from 'react-router-dom';
 
 import classNames from 'classnames';
-import { unionBy } from 'lodash';
+import unionBy from 'lodash/unionBy';
 
 import * as Types from '@/common/interface';
 import { Modal } from '@/components';
diff --git a/ui/src/components/SchemaForm/index.tsx 
b/ui/src/components/SchemaForm/index.tsx
index 2bb91e31..d0afb692 100644
--- a/ui/src/components/SchemaForm/index.tsx
+++ b/ui/src/components/SchemaForm/index.tsx
@@ -26,7 +26,7 @@ import React, {
 import { Form, Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
-import { isEmpty } from 'lodash';
+import isEmpty from 'lodash/isEmpty';
 import classnames from 'classnames';
 
 import type * as Type from '@/common/interface';
diff --git a/ui/src/hooks/usePageUsers/index.tsx 
b/ui/src/hooks/usePageUsers/index.tsx
index 9db172da..c0b3836e 100644
--- a/ui/src/hooks/usePageUsers/index.tsx
+++ b/ui/src/hooks/usePageUsers/index.tsx
@@ -19,7 +19,7 @@
 
 import { useState } from 'react';
 
-import { uniqBy } from 'lodash';
+import uniqBy from 'lodash/uniqBy';
 
 import * as Types from '@/common/interface';
 
diff --git a/ui/src/hooks/useReportModal/index.tsx 
b/ui/src/hooks/useReportModal/index.tsx
index 9ad66513..88cd2442 100644
--- a/ui/src/hooks/useReportModal/index.tsx
+++ b/ui/src/hooks/useReportModal/index.tsx
@@ -25,7 +25,13 @@ import ReactDOM from 'react-dom/client';
 
 import { useToast, useCaptchaModal } from '@/hooks';
 import type * as Type from '@/common/interface';
-import { reportList, postReport, closeQuestion, putReport } from '@/services';
+import {
+  reportList,
+  postReport,
+  closeQuestion,
+  putReport,
+  putFlagReviewAction,
+} from '@/services';
 
 interface Params {
   isBackend?: boolean;
@@ -33,6 +39,7 @@ interface Params {
   id: string;
   title?: string;
   action: Type.ReportAction;
+  source?: string;
 }
 
 const useReportModal = (callback?: () => void) => {
@@ -63,7 +70,12 @@ const useReportModal = (callback?: () => void) => {
     rootRef.current.root = ReactDOM.createRoot(div);
   }, []);
   const getList = ({ type, action, isBackend }: Params) => {
-    reportList({ type, action, isBackend }).then((res) => {
+    // @ts-ignore
+    reportList({
+      type,
+      action,
+      isBackend,
+    }).then((res) => {
       setList(res);
       setShow(true);
     });
@@ -113,6 +125,18 @@ const useReportModal = (callback?: () => void) => {
       return;
     }
     if (params.type === 'question' && params.action === 'close') {
+      if (params?.source === 'review') {
+        putFlagReviewAction({
+          flag_id: params.id,
+          operation_type: 'close_post',
+          close_type: reportType.type,
+          close_msg: content.value,
+        }).then(() => {
+          onClose();
+          asyncCallback();
+        });
+        return;
+      }
       closeQuestion({
         id: params.id,
         close_type: reportType.type,
diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts
index 03f775cb..66d9576e 100644
--- a/ui/src/i18n/init.ts
+++ b/ui/src/i18n/init.ts
@@ -21,7 +21,7 @@ import { initReactI18next } from 'react-i18next';
 
 import i18next from 'i18next';
 import en_US from '@i18n/en_US.yaml';
-import zh_CN from '@i18n/zh_CN.yaml';
+// import zh_CN from '@i18n/zh_CN.yaml';
 
 import { DEFAULT_LANG, LANG_RESOURCE_STORAGE_KEY } from '@/common/constants';
 import Storage from '@/utils/storage';
@@ -34,9 +34,9 @@ const initResources = {
   en_US: {
     translation: en_US.ui,
   },
-  zh_CN: {
-    translation: zh_CN.ui,
-  },
+  // zh_CN: {
+  //   translation: zh_CN.ui,
+  // },
 };
 
 const storageLang = Storage.get(LANG_RESOURCE_STORAGE_KEY);
diff --git a/ui/src/pages/Admin/Answers/components/Action/index.tsx 
b/ui/src/pages/Admin/Answers/components/Action/index.tsx
index 7f34f219..c4308cd2 100644
--- a/ui/src/pages/Admin/Answers/components/Action/index.tsx
+++ b/ui/src/pages/Admin/Answers/components/Action/index.tsx
@@ -19,6 +19,7 @@
 
 import { Dropdown } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
 
 import { Icon, Modal } from '@/components';
 import { changeAnswerStatus } from '@/services';
@@ -58,6 +59,17 @@ const AnswerActions = ({ itemData, curFilter, refreshList }) 
=> {
     }
   };
 
+  if (curFilter === 'pending') {
+    return (
+      <Link
+        to={`/review?type=queued_post&objectId=${itemData.id}`}
+        className="btn btn-link p-0"
+        title={t('review', { keyPrefix: 'header.nav' })}>
+        <Icon name="three-dots-vertical" />
+      </Link>
+    );
+  }
+
   return (
     <Dropdown>
       <Dropdown.Toggle variant="link" className="no-toggle p-0">
diff --git a/ui/src/pages/Admin/Answers/index.tsx 
b/ui/src/pages/Admin/Answers/index.tsx
index 869bec9a..a653f7f5 100644
--- a/ui/src/pages/Admin/Answers/index.tsx
+++ b/ui/src/pages/Admin/Answers/index.tsx
@@ -40,7 +40,11 @@ import { pathFactory } from '@/router/pathFactory';
 
 import AnswerAction from './components/Action';
 
-const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted'];
+const answerFilterItems: Type.AdminContentsFilterBy[] = [
+  'normal',
+  'pending',
+  'deleted',
+];
 
 const Answers: FC = () => {
   const [urlSearchParams, setUrlSearchParams] = useSearchParams();
@@ -77,7 +81,7 @@ const Answers: FC = () => {
           data={answerFilterItems}
           currentSort={curFilter}
           sortKey="status"
-          i18nKeyPrefix="admin.answers"
+          i18nKeyPrefix="btns"
         />
 
         <Form.Control
@@ -118,13 +122,13 @@ const Answers: FC = () => {
                         className="text-break text-wrap"
                         rel="noreferrer">
                         {li.question_info.title}
+                        {li.accepted === 2 && (
+                          <Icon
+                            name="check-circle-fill"
+                            className="ms-2 text-success"
+                          />
+                        )}
                       </a>
-                      {li.accepted === 2 && (
-                        <Icon
-                          name="check-circle-fill"
-                          className="ms-2 text-success"
-                        />
-                      )}
                     </Stack>
                     <div
                       className="text-truncate-2 small"
@@ -150,7 +154,9 @@ const Answers: FC = () => {
                       'badge',
                       ADMIN_LIST_STATUS[curFilter]?.variant,
                     )}>
-                    {t(ADMIN_LIST_STATUS[curFilter]?.name)}
+                    {t(ADMIN_LIST_STATUS[curFilter]?.name, {
+                      keyPrefix: 'btns',
+                    })}
                   </span>
                 </td>
                 <td className="text-end">
diff --git a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx 
b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx
index b2a04ffa..39a803b0 100644
--- a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx
+++ b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx
@@ -45,16 +45,16 @@ const AnswerLinks = () => {
             </a>
           </Col>
           <Col xs={6}>
-            <a href="https://meta.answer.dev"; target="_blank" rel="noreferrer">
+            <a
+              href="https://answer.apache.org/community";
+              target="_blank"
+              rel="noreferrer">
               {t('support')}
             </a>
           </Col>
           <Col xs={6}>
-            <a
-              href="https://github.com/apache/incubator-answer";
-              target="_blank"
-              rel="noreferrer">
-              {t('github')}
+            <a href="https://meta.answer.dev"; target="_blank" rel="noreferrer">
+              {t('forum')}
             </a>
           </Col>
           <Col xs={6}>
@@ -67,10 +67,10 @@ const AnswerLinks = () => {
           </Col>
           <Col xs={6}>
             <a
-              href="https://answer.apache.org/contact";
+              href="https://github.com/apache/incubator-answer";
               target="_blank"
               rel="noreferrer">
-              {t('contact')}
+              {t('github')}
             </a>
           </Col>
         </Row>
diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx 
b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
index 64122669..36690198 100644
--- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
+++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
@@ -93,7 +93,9 @@ const HealthStatus: FC<IProps> = ({ data }) => {
           </Col>
           <Col xs={6}>
             <span className="text-secondary me-1">{t('timezone')}</span>
-            <strong>{data.time_zone.split('/')?.[1]}</strong>
+            <strong>
+              {data.time_zone.split('/')?.[1].replaceAll('_', ' ')}
+            </strong>
           </Col>
           <Col xs={6}>
             <span className="text-secondary me-1">{t('smtp')}</span>
diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx 
b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx
index 85262724..d0f5f410 100644
--- a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx
+++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx
@@ -58,7 +58,7 @@ const Statistics: FC<IProps> = ({ data }) => {
           <Col xs={6}>
             <span className="text-secondary me-1">{t('flags')}</span>
             <strong>
-              <Link to="/admin/flags" className="ms-2">
+              <Link to="/review?type=flagged_post" className="ms-2">
                 {data.report_count}
               </Link>
             </strong>
diff --git a/ui/src/pages/Admin/Flags/index.tsx 
b/ui/src/pages/Admin/Flags/index.tsx
deleted file mode 100644
index 73f32113..00000000
--- a/ui/src/pages/Admin/Flags/index.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React, { FC } from 'react';
-import { Button, Form, Table, Stack } from 'react-bootstrap';
-import { useSearchParams } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-
-import {
-  FormatTime,
-  BaseUserCard,
-  Empty,
-  Pagination,
-  QueryGroup,
-} from '@/components';
-import { useReportModal } from '@/hooks';
-import * as Type from '@/common/interface';
-import { useFlagSearch } from '@/services';
-import { escapeRemove } from '@/utils';
-import { pathFactory } from '@/router/pathFactory';
-
-const flagFilterKeys: Type.FlagStatus[] = ['pending', 'completed'];
-const flagTypeKeys: Type.FlagType[] = ['all', 'question', 'answer', 'comment'];
-
-const Flags: FC = () => {
-  const { t } = useTranslation('translation', { keyPrefix: 'admin.flags' });
-  const [urlSearchParams, setUrlSearchParams] = useSearchParams();
-  const curFilter = urlSearchParams.get('status') || flagFilterKeys[0];
-  const curType = urlSearchParams.get('type') || flagTypeKeys[0];
-  const PAGE_SIZE = 20;
-  const curPage = Number(urlSearchParams.get('page')) || 1;
-  const {
-    data: listData,
-    isLoading,
-    mutate: refreshList,
-  } = useFlagSearch({
-    page_size: PAGE_SIZE,
-    page: curPage,
-    status: curFilter as Type.FlagStatus,
-    object_type: curType as Type.FlagType,
-  });
-  const reportModal = useReportModal(refreshList);
-
-  const count = listData?.count || 0;
-
-  const onTypeChange = (evt) => {
-    urlSearchParams.set('type', evt.target.value);
-    setUrlSearchParams(urlSearchParams);
-  };
-
-  const handleReview = ({ id, object_type }) => {
-    reportModal.onShow({
-      id,
-      type: object_type,
-      isBackend: true,
-      action: 'review',
-    });
-  };
-
-  return (
-    <>
-      <h3 className="mb-4">{t('title')}</h3>
-      <div className="d-flex justify-content-between align-items-center mb-3">
-        <QueryGroup
-          data={flagFilterKeys}
-          currentSort={curFilter}
-          sortKey="status"
-          i18nKeyPrefix="admin.flags"
-        />
-
-        <Form.Select
-          value={curType}
-          onChange={onTypeChange}
-          size="sm"
-          style={{ width: '12.25rem' }}>
-          {flagTypeKeys.map((li) => {
-            return (
-              <option value={li} key={li}>
-                {t(li, { keyPrefix: 'btns' })}
-              </option>
-            );
-          })}
-        </Form.Select>
-      </div>
-      <Table>
-        <thead>
-          <tr>
-            <th>{t('flagged')}</th>
-            <th style={{ width: '20%' }}>{t('created')}</th>
-            {curFilter !== 'completed' ? (
-              <th style={{ width: '20%' }}>{t('action')}</th>
-            ) : null}
-          </tr>
-        </thead>
-        <tbody className="align-middle">
-          {listData?.list?.map((li) => {
-            return (
-              <tr key={li.id}>
-                <td>
-                  <Stack>
-                    <small className="text-secondary">
-                      {t('flagged_type', {
-                        type: t(li.object_type, { keyPrefix: 'btns' }),
-                      })}
-                    </small>
-                    <BaseUserCard
-                      data={li.reported_user}
-                      className="mt-2 small"
-                    />
-                    <a
-                      href={pathFactory.questionLanding(
-                        li.question_id,
-                        li.url_title,
-                      )}
-                      target="_blank"
-                      className="text-wrap text-break mt-2"
-                      rel="noreferrer">
-                      {li.title}
-                    </a>
-                    <small className="text-break text-wrap word">
-                      {escapeRemove(li.excerpt)}
-                    </small>
-                  </Stack>
-                </td>
-                <td>
-                  <Stack>
-                    <FormatTime
-                      time={li.created_at}
-                      className="small text-secondary"
-                    />
-                    <BaseUserCard
-                      data={li.report_user}
-                      className="mt-2 mb-2 small"
-                    />
-                    {li.flagged_reason ? (
-                      <small>{li.flagged_content}</small>
-                    ) : (
-                      <small>
-                        {li.reason?.name}
-                        <br />
-                        <span className="text-secondary">{li.content}</span>
-                      </small>
-                    )}
-                  </Stack>
-                </td>
-                {curFilter !== 'completed' ? (
-                  <td>
-                    <Button variant="link" onClick={() => handleReview(li)}>
-                      {t('review')}
-                    </Button>
-                  </td>
-                ) : null}
-              </tr>
-            );
-          })}
-        </tbody>
-      </Table>
-      {Number(count) <= 0 && !isLoading && <Empty />}
-      <div className="mt-4 mb-2 d-flex justify-content-center">
-        <Pagination
-          currentPage={curPage}
-          totalSize={count}
-          pageSize={PAGE_SIZE}
-        />
-      </div>
-    </>
-  );
-};
-
-export default Flags;
diff --git a/ui/src/pages/Admin/Questions/components/Action/index.tsx 
b/ui/src/pages/Admin/Questions/components/Action/index.tsx
index a1f7e2a2..f5d2b41f 100644
--- a/ui/src/pages/Admin/Questions/components/Action/index.tsx
+++ b/ui/src/pages/Admin/Questions/components/Action/index.tsx
@@ -19,6 +19,7 @@
 
 import { Dropdown } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
 
 import { Icon, Modal } from '@/components';
 import {
@@ -119,6 +120,17 @@ const AnswerActions = ({ itemData, refreshList, curFilter, 
show, pin }) => {
     }
   };
 
+  if (curFilter === 'pending') {
+    return (
+      <Link
+        to={`/review?type=queued_post&objectId=${itemData.id}`}
+        className="btn btn-link p-0"
+        title={t('review', { keyPrefix: 'header.nav' })}>
+        <Icon name="three-dots-vertical" />
+      </Link>
+    );
+  }
+
   return (
     <Dropdown>
       <Dropdown.Toggle variant="link" className="no-toggle p-0">
diff --git a/ui/src/pages/Admin/Questions/index.tsx 
b/ui/src/pages/Admin/Questions/index.tsx
index 27565fa6..0488a08b 100644
--- a/ui/src/pages/Admin/Questions/index.tsx
+++ b/ui/src/pages/Admin/Questions/index.tsx
@@ -41,6 +41,7 @@ import Action from './components/Action';
 
 const questionFilterItems: Type.AdminContentsFilterBy[] = [
   'normal',
+  'pending',
   'closed',
   'deleted',
 ];
@@ -78,7 +79,7 @@ const Questions: FC = () => {
           data={questionFilterItems}
           currentSort={curFilter}
           sortKey="status"
-          i18nKeyPrefix="admin.questions"
+          i18nKeyPrefix="btns"
         />
 
         <Form.Control
@@ -147,15 +148,19 @@ const Questions: FC = () => {
                       'mb-1',
                       ADMIN_LIST_STATUS[curFilter]?.variant,
                     )}>
-                    {t(ADMIN_LIST_STATUS[curFilter]?.name)}
+                    {t(ADMIN_LIST_STATUS[curFilter]?.name, {
+                      keyPrefix: 'btns',
+                    })}
                   </span>
                   {li.show === 2 && (
                     <span
                       className={classNames(
                         'badge',
-                        ADMIN_LIST_STATUS.unlisted.variant,
+                        ADMIN_LIST_STATUS.unlist.variant,
                       )}>
-                      {t(ADMIN_LIST_STATUS.unlisted.name)}
+                      {t(ADMIN_LIST_STATUS.unlist.name, {
+                        keyPrefix: 'btns',
+                      })}
                     </span>
                   )}
                 </td>
diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx
index 2ce145f9..b527088f 100644
--- a/ui/src/pages/Admin/index.tsx
+++ b/ui/src/pages/Admin/index.tsx
@@ -22,7 +22,7 @@ import { Container, Row, Col } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 import { Outlet, useMatch } from 'react-router-dom';
 
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
 
 import { usePageTags } from '@/hooks';
 import { AccordionNav } from '@/components';
diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx 
b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
index bda9112a..2f79305b 100644
--- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
+++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx
@@ -46,7 +46,7 @@ const SearchQuestion = ({ similarQuestions }) => {
                 <ListGroup.Item
                   action
                   as="a"
-                  className="link-dark text-wrap text-break"
+                  className="link-dark text-wrap text-break grid gap-0 
row-gap-3"
                   key={item.id}
                   href={pathFactory.questionLanding(item.id, item.url_title)}
                   target="_blank">
@@ -56,7 +56,7 @@ const SearchQuestion = ({ similarQuestions }) => {
                     : null}
 
                   {item.accepted_answer ? (
-                    <span className="small ms-3 text-success">
+                    <span className="small ms-3 text-success d-inline-block">
                       <Icon type="bi" name="check-circle-fill" />
                       <span className="ms-1">
                         {t('x_answers', {
@@ -67,7 +67,7 @@ const SearchQuestion = ({ similarQuestions }) => {
                     </span>
                   ) : (
                     item.answer_count > 0 && (
-                      <span className="small ms-3 text-secondary">
+                      <span className="small ms-3 text-secondary 
d-inline-block">
                         <Icon type="bi" name="chat-square-text-fill" />
                         <span className="ms-1">
                           {t('x_answers', {
diff --git a/ui/src/pages/Questions/Ask/index.tsx 
b/ui/src/pages/Questions/Ask/index.tsx
index 165e061a..9a1ff14a 100644
--- a/ui/src/pages/Questions/Ask/index.tsx
+++ b/ui/src/pages/Questions/Ask/index.tsx
@@ -24,7 +24,8 @@ import { useTranslation } from 'react-i18next';
 
 import dayjs from 'dayjs';
 import classNames from 'classnames';
-import { isEqual, debounce } from 'lodash';
+import isEqual from 'lodash/isEqual';
+import debounce from 'lodash/debounce';
 
 import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
 import { Editor, EditorRef, TagSelector } from '@/components';
diff --git a/ui/src/pages/Review/components/ApproveDropdown/index.tsx 
b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
new file mode 100644
index 00000000..05f92296
--- /dev/null
+++ b/ui/src/pages/Review/components/ApproveDropdown/index.tsx
@@ -0,0 +1,202 @@
+import { FC, useState } from 'react';
+import { Dropdown, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import { Modal } from '@/components';
+import { putFlagReviewAction } from '@/services';
+import { useCaptchaModal, useReportModal, useToast } from '@/hooks';
+import type * as Type from '@/common/interface';
+import EditPostModal from '../EditPostModal';
+
+interface IProps {
+  itemData: Type.FlagReviewItem | null;
+  curFilter: string;
+  objectType: Type.FlagReviewItem['object_type'] | '';
+  approveCallback: () => void;
+}
+
+const Index: FC<IProps> = ({
+  itemData,
+  objectType,
+  curFilter,
+  approveCallback,
+}) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+
+  const [isLoading, setIsLoading] = useState(false);
+  const [showEditPostModal, setShowEditPostModal] = useState(false);
+  const closeModal = useReportModal(approveCallback);
+  const toast = useToast();
+  const dCaptcha = useCaptchaModal('delete');
+
+  const handleEditPostModalState = () => {
+    setShowEditPostModal(!showEditPostModal);
+  };
+
+  const handleDelete = () => {
+    let content = '';
+
+    setIsLoading(true);
+
+    if (objectType === 'question') {
+      content =
+        Number(itemData?.answer_count) > 0
+          ? t('question', { keyPrefix: 'delete' })
+          : t('other', { keyPrefix: 'delete' });
+    }
+    if (objectType === 'answer') {
+      content = itemData?.answer_accepted
+        ? t('answer_accepted', { keyPrefix: 'delete' })
+        : t('other', { keyPrefix: 'delete' });
+    }
+    if (objectType === 'comment') {
+      content = t('other', { keyPrefix: 'delete' });
+    }
+    Modal.confirm({
+      title: t('title', { keyPrefix: 'delete' }),
+      content,
+      cancelBtnVariant: 'link',
+      confirmBtnVariant: 'danger',
+      confirmText: t('delete', { keyPrefix: 'btns' }),
+      onConfirm: () => {
+        dCaptcha.check(() => {
+          const req: Type.PutFlagReviewParams = {
+            operation_type: 'delete_post',
+            flag_id: String(itemData?.flag_id),
+            captcha_code: undefined,
+            captcha_id: undefined,
+          };
+          dCaptcha.resolveCaptchaReq(req);
+
+          delete req.captcha_code;
+          delete req.captcha_id;
+
+          putFlagReviewAction(req)
+            .then(async () => {
+              await dCaptcha.close();
+              let msg = '';
+              if (objectType === 'question') {
+                msg = t('post_deleted', { keyPrefix: 'messages' });
+              }
+              if (objectType === 'answer') {
+                msg = t('tip_answer_deleted');
+              }
+              if (objectType === 'answer' || objectType === 'question') {
+                toast.onShow({
+                  msg,
+                  variant: 'success',
+                });
+              }
+              approveCallback();
+            })
+            .catch((ex) => {
+              if (ex.isError) {
+                dCaptcha.handleCaptchaError(ex.list);
+              }
+            })
+            .finally(() => {
+              setIsLoading(false);
+            });
+        });
+      },
+    });
+  };
+
+  const handleAction = (type) => {
+    if (type === 'delete') {
+      handleDelete();
+    }
+
+    if (type === 'close') {
+      closeModal.onShow({
+        type: 'question',
+        id: itemData?.flag_id || '',
+        action: 'close',
+        source: 'review',
+      });
+    }
+
+    if (type === 'unlist') {
+      const keyPrefix = 'question_detail.unlist';
+      Modal.confirm({
+        title: t('title', { keyPrefix }),
+        content: t('content', { keyPrefix }),
+        cancelBtnVariant: 'link',
+        confirmText: t('confirm_btn', { keyPrefix }),
+        onConfirm: () => {
+          putFlagReviewAction({
+            operation_type: 'unlist_post',
+            flag_id: itemData?.flag_id || '',
+          }).then(() => {
+            toast.onShow({
+              msg: t(`post_${type}`, { keyPrefix: 'messages' }),
+              variant: 'success',
+            });
+            approveCallback();
+          });
+        },
+      });
+    }
+  };
+
+  const handleActionEdit = () => {
+    handleEditPostModalState();
+  };
+
+  return (
+    <div>
+      <Dropdown>
+        <Dropdown.Toggle
+          as={Button}
+          disabled={isLoading}
+          variant="outline-primary"
+          id="dropdown-basic">
+          {t('approve', { keyPrefix: 'btns' })}
+        </Dropdown.Toggle>
+
+        <Dropdown.Menu>
+          <Dropdown.Item onClick={() => handleActionEdit()}>
+            {t('edit_post')}
+          </Dropdown.Item>
+          {curFilter === 'normal' && objectType === 'question' && (
+            <Dropdown.Item onClick={() => handleAction('close')}>
+              {t('close', { keyPrefix: 'btns' })}
+            </Dropdown.Item>
+          )}
+          {curFilter !== 'deleted' && (
+            <Dropdown.Item onClick={() => handleAction('delete')}>
+              {t('delete', { keyPrefix: 'btns' })}
+            </Dropdown.Item>
+          )}
+          {objectType === 'question' && (
+            <>
+              <Dropdown.Divider />
+              {itemData?.object_show_status !== 2 && (
+                <Dropdown.Item onClick={() => handleAction('unlist')}>
+                  {t('unlist_post')}
+                </Dropdown.Item>
+              )}
+            </>
+          )}
+        </Dropdown.Menu>
+      </Dropdown>
+      <EditPostModal
+        visible={showEditPostModal}
+        handleClose={handleEditPostModalState}
+        objectType={objectType}
+        originalData={{
+          flag_id: itemData?.flag_id || '',
+          id: itemData?.object_id || '',
+          title: itemData?.title || '',
+          content: itemData?.original_text || '',
+          tags: itemData?.tags || [],
+          question_id: itemData?.question_id || '',
+          answer_id: itemData?.answer_id || '',
+        }}
+        callback={approveCallback}
+      />
+    </div>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/EditPostModal/index.scss 
b/ui/src/pages/Review/components/EditPostModal/index.scss
new file mode 100644
index 00000000..550155b2
--- /dev/null
+++ b/ui/src/pages/Review/components/EditPostModal/index.scss
@@ -0,0 +1,4 @@
+.edit-post-modal {
+  max-width: 746px;
+  width: 58.3333% !important;
+}
diff --git a/ui/src/pages/Review/components/EditPostModal/index.tsx 
b/ui/src/pages/Review/components/EditPostModal/index.tsx
new file mode 100644
index 00000000..e35a82ca
--- /dev/null
+++ b/ui/src/pages/Review/components/EditPostModal/index.tsx
@@ -0,0 +1,375 @@
+import { FC, useState, useEffect } from 'react';
+import { Modal, Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import classNames from 'classnames';
+
+import { putFlagReviewAction } from '@/services';
+import { useCaptchaModal, usePageUsers } from '@/hooks';
+import { Editor, TagSelector, Mentions, TextArea } from '@/components';
+import {
+  // matchedUsers,
+  parseUserInfo,
+  handleFormError,
+  parseEditMentionUser,
+} from '@/utils';
+import type * as Type from '@/common/interface';
+
+import './index.scss';
+
+interface Props {
+  originalData: {
+    id: string;
+    flag_id: string;
+    question_id?: string;
+    answer_id?: string;
+    title: string;
+    content: string;
+    tags: Type.Tag[];
+  };
+  objectType: Type.FlagReviewItem['object_type'] | '';
+  visible: boolean;
+  handleClose: () => void;
+  callback?: () => void;
+}
+
+interface FormDataItem {
+  title: Type.FormValue<string>;
+  tags: Type.FormValue<Type.Tag[]>;
+  content: Type.FormValue<string>;
+}
+
+const initFormData = {
+  title: {
+    value: '',
+    isInvalid: false,
+    errorMsg: '',
+  },
+  tags: {
+    value: [],
+    isInvalid: false,
+    errorMsg: '',
+  },
+  content: {
+    value: '',
+    isInvalid: false,
+    errorMsg: '',
+  },
+};
+
+const Index: FC<Props> = ({
+  originalData,
+  visible = false,
+  objectType,
+  handleClose,
+  callback,
+}) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'ask' });
+  const [formData, setFormData] = useState<FormDataItem>(initFormData);
+  const [focusEditor, setFocusEditor] = useState(false);
+  const [loaded, setLoaded] = useState(false);
+  const pageUsers = usePageUsers();
+
+  const editCaptcha = useCaptchaModal('edit');
+
+  const onClose = (bol) => {
+    if (bol) {
+      callback?.();
+    }
+    handleClose();
+    setLoaded(false);
+  };
+
+  const handleInput = (data: Partial<FormDataItem>) => {
+    if (!loaded) {
+      return;
+    }
+    setFormData({
+      ...formData,
+      ...data,
+    });
+  };
+
+  const checkValidated = (): boolean => {
+    let bol = true;
+    const { title, tags, content } = formData;
+    if (objectType === 'question') {
+      if (!title.value) {
+        bol = false;
+        formData.title = {
+          value: title.value,
+          isInvalid: true,
+          errorMsg: t('form.fields.title.msg.empty', {
+            keyPrefix: 'ask',
+          }),
+        };
+      }
+
+      if (!tags.value.length) {
+        bol = false;
+        formData.tags = {
+          value: tags.value,
+          isInvalid: true,
+          errorMsg: t('form.fields.tags.msg.empty', {
+            keyPrefix: 'ask',
+          }),
+        };
+      }
+    }
+
+    if (!content.value || Array.from(content.value.trim()).length < 6) {
+      bol = false;
+      formData.content = {
+        value: content.value,
+        isInvalid: true,
+        errorMsg: t('form.fields.answer.feedback.characters', {
+          keyPrefix: 'edit_answer',
+        }),
+      };
+    } else {
+      formData.content = {
+        value: content.value,
+        isInvalid: false,
+        errorMsg: '',
+      };
+    }
+
+    setFormData({
+      ...formData,
+    });
+    return bol;
+  };
+
+  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+
+    if (!checkValidated()) {
+      return;
+    }
+
+    const params: Type.PutFlagReviewParams = {
+      title: formData.title.value,
+      content: formData.content.value,
+      tags: formData.tags.value,
+      operation_type: 'edit_post',
+      flag_id: originalData.flag_id,
+    };
+    if (objectType === 'answer') {
+      delete params.title;
+      delete params.tags;
+    }
+    if (objectType === 'comment') {
+      const { value } = formData.content;
+      // const users = matchedUsers(value);
+      // const userNames = unionBy(users.map((user) => user.userName));
+      const commentMarkDown = parseUserInfo(value);
+
+      // params.mention_username_list = userNames;
+      params.content = commentMarkDown;
+
+      delete params.title;
+      delete params.tags;
+    }
+
+    editCaptcha.check(() => {
+      if (objectType === 'question') {
+        const imgCode = editCaptcha.getCaptcha();
+        if (imgCode.verify) {
+          params.captcha_code = imgCode.captcha_code;
+          params.captcha_id = imgCode.captcha_id;
+        }
+      }
+      putFlagReviewAction(params)
+        .then(async () => {
+          await editCaptcha.close();
+          onClose(true);
+        })
+        .catch((err) => {
+          if (err.isError) {
+            editCaptcha.handleCaptchaError(err.list);
+            const data = handleFormError(err, formData);
+            setFormData({ ...data });
+          }
+        });
+    });
+  };
+
+  const handleSelected = (val) => {
+    if (!loaded) {
+      return;
+    }
+    setFormData({
+      ...formData,
+      content: {
+        value: val,
+        errorMsg: '',
+        isInvalid: false,
+      },
+    });
+  };
+
+  useEffect(() => {
+    if (!visible) {
+      return;
+    }
+
+    formData.title.value = originalData.title;
+    formData.content.value = originalData.content;
+    formData.tags.value = originalData.tags.map((item) => {
+      return {
+        ...item,
+        parsed_text: '',
+        original_text: '',
+      };
+    });
+    setFormData({ ...formData });
+    setLoaded(true);
+  }, [visible]);
+
+  return (
+    <Modal
+      show={visible}
+      onHide={() => onClose(false)}
+      className="w-100"
+      dialogClassName="edit-post-modal">
+      <Modal.Header closeButton>
+        <Modal.Title>
+          {t('edit_post', { keyPrefix: 'page_review' })}
+        </Modal.Title>
+      </Modal.Header>
+      <Form noValidate onSubmit={handleSubmit}>
+        <Modal.Body>
+          {objectType === 'question' && (
+            <Form.Group controlId="title" className="mb-3">
+              <Form.Label>{t('form.fields.title.label')}</Form.Label>
+              <Form.Control
+                type="text"
+                value={formData.title.value}
+                isInvalid={formData.title.isInvalid}
+                onChange={(e) => {
+                  handleInput({
+                    title: {
+                      value: e.target.value,
+                      isInvalid: false,
+                      errorMsg: '',
+                    },
+                  });
+                }}
+                placeholder={t('form.fields.title.placeholder')}
+                autoFocus
+                contentEditable
+              />
+
+              <Form.Control.Feedback type="invalid">
+                {formData.title.errorMsg}
+              </Form.Control.Feedback>
+            </Form.Group>
+          )}
+
+          {objectType !== 'comment' && (
+            <Form.Group controlId="body">
+              <Form.Label>
+                {objectType === 'question'
+                  ? t('form.fields.body.label')
+                  : t('form.fields.answer.label')}
+              </Form.Label>
+              <Form.Control
+                defaultValue={formData.content.value}
+                isInvalid={formData.content.isInvalid}
+                hidden
+              />
+              <Editor
+                value={formData.content.value}
+                onChange={(value) => {
+                  handleInput({
+                    content: { value, errorMsg: '', isInvalid: false },
+                  });
+                }}
+                className={classNames(
+                  'form-control p-0',
+                  focusEditor ? 'focus' : '',
+                )}
+                onFocus={() => {
+                  setFocusEditor(true);
+                }}
+                onBlur={() => {
+                  setFocusEditor(false);
+                }}
+              />
+              <Form.Control.Feedback type="invalid">
+                {formData.content.errorMsg}
+              </Form.Control.Feedback>
+            </Form.Group>
+          )}
+
+          {objectType === 'question' && (
+            <Form.Group controlId="tags" className="my-3">
+              <Form.Label>{t('form.fields.tags.label')}</Form.Label>
+              <Form.Control
+                defaultValue={JSON.stringify(formData.tags.value)}
+                isInvalid={formData.tags.isInvalid}
+                hidden
+              />
+              <TagSelector
+                value={formData.tags.value}
+                onChange={(value) => {
+                  handleInput({
+                    tags: { value, errorMsg: '', isInvalid: false },
+                  });
+                }}
+                showRequiredTag
+                maxTagLength={5}
+              />
+              <Form.Control.Feedback type="invalid">
+                {formData.tags.errorMsg}
+              </Form.Control.Feedback>
+            </Form.Group>
+          )}
+
+          {objectType === 'comment' && (
+            <div className="w-100">
+              <div
+                className={classNames('custom-form-control', {
+                  'is-invalid': formData.content.isInvalid,
+                })}>
+                <Form.Label>Comment</Form.Label>
+                <Mentions
+                  pageUsers={pageUsers.getUsers()}
+                  onSelected={handleSelected}>
+                  <TextArea
+                    size="sm"
+                    rows={4}
+                    value={parseEditMentionUser(formData.content.value)}
+                    onChange={(e) => {
+                      handleInput({
+                        content: {
+                          value: e.target.value,
+                          errorMsg: '',
+                          isInvalid: false,
+                        },
+                      });
+                    }}
+                  />
+                </Mentions>
+              </div>
+              <Form.Control.Feedback type="invalid">
+                {formData.content.errorMsg}
+              </Form.Control.Feedback>
+            </div>
+          )}
+        </Modal.Body>
+        <Modal.Footer>
+          <Button variant="secondary" onClick={() => onClose(false)}>
+            {t('close', { keyPrefix: 'btns' })}
+          </Button>
+          <Button variant="primary" type="submit">
+            {t('submit', { keyPrefix: 'btns' })}
+          </Button>
+        </Modal.Footer>
+      </Form>
+    </Modal>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/FlagContent/index.tsx 
b/ui/src/pages/Review/components/FlagContent/index.tsx
new file mode 100644
index 00000000..11056c89
--- /dev/null
+++ b/ui/src/pages/Review/components/FlagContent/index.tsx
@@ -0,0 +1,210 @@
+import { FC, useEffect, useState } from 'react';
+import { Card, Alert, Stack, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+
+import classNames from 'classnames';
+
+import { getFlagReviewPostList, putFlagReviewAction } from '@/services';
+import { BaseUserCard, Tag, FormatTime } from '@/components';
+import { scrollToDocTop } from '@/utils';
+import type * as Type from '@/common/interface';
+import { ADMIN_LIST_STATUS } from '@/common/constants';
+import ApproveDropdown from '../ApproveDropdown';
+import generateData from '../../utils/generateData';
+
+interface IProps {
+  refreshCount: () => void;
+}
+
+const Index: FC<IProps> = ({ refreshCount }) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+  const [noTasks, setNoTasks] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [page, setPage] = useState(1);
+  const [reviewResp, setReviewResp] = useState<Type.FlagReviewResp>();
+  const flagItemData = reviewResp?.list[0] as Type.FlagReviewItem;
+
+  // console.log('reviewResp', reviewResp);
+
+  const resolveNextOne = (resp, pageNumber) => {
+    const { count, list = [] } = resp;
+    // auto rollback
+    if (!list.length && count && page !== 1) {
+      pageNumber = 1;
+      setPage(pageNumber);
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      queryNextOne(pageNumber);
+      return;
+    }
+    if (pageNumber !== page) {
+      setPage(pageNumber);
+    }
+    setReviewResp(resp);
+    if (!list.length) {
+      setNoTasks(true);
+    }
+    setTimeout(() => {
+      scrollToDocTop();
+    }, 150);
+  };
+
+  const queryNextOne = (pageNumber) => {
+    getFlagReviewPostList(pageNumber)
+      .then((resp) => {
+        resolveNextOne(resp, pageNumber);
+      })
+      .catch((ex) => {
+        console.error('review next error: ', ex);
+      });
+  };
+
+  useEffect(() => {
+    queryNextOne(page);
+  }, []);
+
+  const handlingApprove = () => {
+    if (!flagItemData) {
+      return;
+    }
+    refreshCount();
+    queryNextOne(page);
+  };
+
+  const handleIgnore = () => {
+    setIsLoading(true);
+    putFlagReviewAction({
+      operation_type: 'ignore_report',
+      flag_id: String(flagItemData?.flag_id),
+    })
+      .then(() => {
+        refreshCount();
+        queryNextOne(page);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  const {
+    object_type,
+    submitter_user,
+    author_user_info,
+    object_status,
+    reason,
+  } = flagItemData || {
+    object_type: '',
+    submitter_user: null,
+    author_user_info: null,
+    reason: null,
+    object_status: 0,
+  };
+
+  const { itemLink, itemId, itemTimePrefix } = generateData(flagItemData);
+
+  if (noTasks) return null;
+  return (
+    <Card>
+      <Card.Header>
+        {object_type !== 'user' ? t('flag_post') : t('flag_user')}
+      </Card.Header>
+      <Card.Body className="p-0">
+        <Alert variant="info" className="border-0 rounded-0 mb-0">
+          <Stack
+            direction="horizontal"
+            gap={1}
+            className="align-items-center mb-2">
+            <BaseUserCard data={submitter_user} avatarSize="24" />
+            {flagItemData?.submit_at && (
+              <FormatTime
+                time={flagItemData.submit_at}
+                className="small text-secondary"
+                preFix={t('proposed')}
+              />
+            )}
+          </Stack>
+          <Stack className="align-items-start">
+            <p className="mb-0">
+              {object_type !== 'user'
+                ? t('flag_post_type', { type: reason?.name })
+                : t('flag_user_type', { type: reason?.name })}
+            </p>
+          </Stack>
+        </Alert>
+        <div className="p-3">
+          <small className="d-block text-secondary mb-4">
+            <span>{t(object_type, { keyPrefix: 'btns' })} </span>
+            <Link to={itemLink} target="_blank" className="link-secondary">
+              #{itemId}
+            </Link>
+          </small>
+          {object_type === 'question' && (
+            <>
+              <h5 className="mb-3">{flagItemData?.title}</h5>
+              <div className="mb-4">
+                {flagItemData?.tags?.map((item) => {
+                  return (
+                    <Tag key={item.slug_name} className="me-1" data={item} />
+                  );
+                })}
+              </div>
+            </>
+          )}
+          <div className="small font-monospace">
+            {flagItemData?.original_text}
+          </div>
+          <div className="d-flex flex-wrap align-items-center 
justify-content-between mt-4">
+            <div>
+              <span
+                className={classNames(
+                  'badge',
+                  ADMIN_LIST_STATUS[object_status]?.variant,
+                )}>
+                {t(ADMIN_LIST_STATUS[object_status]?.name, {
+                  keyPrefix: 'btns',
+                })}
+              </span>
+              {flagItemData?.object_show_status === 2 && (
+                <span
+                  className={classNames(
+                    'ms-1 badge',
+                    ADMIN_LIST_STATUS.unlist.variant,
+                  )}>
+                  {t(ADMIN_LIST_STATUS.unlist.name, { keyPrefix: 'btns' })}
+                </span>
+              )}
+            </div>
+            <div className="d-flex align-items-center small">
+              <BaseUserCard data={author_user_info} avatarSize="24" />
+              <FormatTime
+                time={Number(flagItemData?.created_at)}
+                className="text-secondary ms-1 flex-shrink-0"
+                preFix={t(itemTimePrefix, { keyPrefix: 'question_detail' })}
+              />
+            </div>
+          </div>
+        </div>
+      </Card.Body>
+
+      <Card.Footer className="p-3">
+        <p>{t('approve_flag_tip')}</p>
+        <Stack direction="horizontal" gap={2}>
+          <ApproveDropdown
+            objectType={object_type}
+            itemData={flagItemData}
+            curFilter={ADMIN_LIST_STATUS[object_status]?.name}
+            approveCallback={handlingApprove}
+          />
+          <Button
+            variant="outline-primary"
+            disabled={isLoading}
+            onClick={handleIgnore}>
+            {t('ignore', { keyPrefix: 'btns' })}
+          </Button>
+        </Stack>
+      </Card.Footer>
+    </Card>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/QueuedContent/index.tsx 
b/ui/src/pages/Review/components/QueuedContent/index.tsx
new file mode 100644
index 00000000..bd22d297
--- /dev/null
+++ b/ui/src/pages/Review/components/QueuedContent/index.tsx
@@ -0,0 +1,203 @@
+import { FC, useEffect, useState } from 'react';
+import { Card, Alert, Stack, Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import { Link, useSearchParams } from 'react-router-dom';
+
+import classNames from 'classnames';
+
+import { getPendingReviewPostList, putPendingReviewAction } from '@/services';
+import { BaseUserCard, Tag, FormatTime, Icon } from '@/components';
+import { scrollToDocTop } from '@/utils';
+import type * as Type from '@/common/interface';
+import { ADMIN_LIST_STATUS } from '@/common/constants';
+import generateData from '../../utils/generateData';
+
+interface IProps {
+  refreshCount: () => void;
+}
+
+const Index: FC<IProps> = ({ refreshCount }) => {
+  const [urlSearch, setUrlSearchParams] = useSearchParams();
+  const objectId = urlSearch.get('objectId') || '';
+  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+  const [noTasks, setNoTasks] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [page, setPage] = useState(1);
+  const [reviewResp, setReviewResp] = useState<Type.QuestionDetailRes>();
+  const flagItemData = reviewResp?.list[0] as Type.QueuedReviewItem;
+
+  // console.log('pendingResp', reviewResp);
+
+  const resolveNextOne = (resp, pageNumber) => {
+    const { count, list = [] } = resp;
+    // auto rollback
+    if (!list.length && count && page !== 1) {
+      pageNumber = 1;
+      setPage(pageNumber);
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      queryNextOne(pageNumber, '');
+      return;
+    }
+    if (pageNumber !== page) {
+      setPage(pageNumber);
+    }
+    setReviewResp(resp);
+    if (!list.length) {
+      setNoTasks(true);
+    }
+    setTimeout(() => {
+      scrollToDocTop();
+    }, 150);
+  };
+
+  const queryNextOne = (pageNumber, id) => {
+    getPendingReviewPostList(pageNumber, id).then((resp) => {
+      resolveNextOne(resp, pageNumber);
+    });
+  };
+
+  useEffect(() => {
+    queryNextOne(page, objectId);
+  }, []);
+
+  const handleAction = (type: 'approve' | 'reject') => {
+    if (!flagItemData) {
+      return;
+    }
+    setIsLoading(true);
+    putPendingReviewAction({
+      status: type,
+      review_id: flagItemData?.review_id,
+    })
+      .then(() => {
+        refreshCount();
+        queryNextOne(page, '');
+        if (objectId) {
+          urlSearch.delete('objectId');
+          setUrlSearchParams(urlSearch);
+        }
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  const { object_type, author_user_info, object_status, reason } =
+    flagItemData || {
+      object_type: '',
+      author_user_info: null,
+      reason: null,
+      object_status: 0,
+    };
+
+  const { itemLink, itemId, itemTimePrefix } = generateData(flagItemData);
+
+  if (noTasks) return null;
+  return (
+    <Card>
+      <Card.Header>
+        {object_type !== 'user' ? t('queued_post') : t('queued_post_user')}
+      </Card.Header>
+      <Card.Body className="p-0">
+        <Alert variant="info" className="border-0 rounded-0 mb-0">
+          <Stack
+            direction="horizontal"
+            gap={1}
+            className="align-items-center mb-2">
+            <div className="small d-flex align-items-center">
+              <Icon type="bi" name="plugin" size="24px" className="me-1" />
+              <span>{flagItemData?.submitter_display_name}</span>
+            </div>
+            {flagItemData?.submit_at && (
+              <FormatTime
+                time={flagItemData.submit_at}
+                className="small text-secondary"
+                preFix={t('proposed')}
+              />
+            )}
+          </Stack>
+          <Stack className="align-items-start">
+            <p className="mb-0">{reason}</p>
+          </Stack>
+        </Alert>
+        <div className="p-3">
+          <small className="d-block text-secondary mb-4">
+            <span>{t(object_type, { keyPrefix: 'btns' })} </span>
+            <Link to={itemLink} target="_blank" className="link-secondary">
+              #{itemId}
+            </Link>
+          </small>
+          {object_type === 'question' && (
+            <>
+              <h5 className="mb-3">{flagItemData?.title}</h5>
+              <div className="mb-4">
+                {flagItemData?.tags?.map((item) => {
+                  return (
+                    <Tag key={item.slug_name} className="me-1" data={item} />
+                  );
+                })}
+              </div>
+            </>
+          )}
+          <div className="small font-monospace">
+            {flagItemData?.original_text}
+          </div>
+          <div className="d-flex flex-wrap align-items-center 
justify-content-between mt-4">
+            <div>
+              <span
+                className={classNames(
+                  'badge',
+                  ADMIN_LIST_STATUS[object_status]?.variant,
+                )}>
+                {t(ADMIN_LIST_STATUS[object_status]?.name, {
+                  keyPrefix: 'btns',
+                })}
+              </span>
+              {flagItemData?.object_show_status === 2 && (
+                <span
+                  className={classNames(
+                    'ms-1 badge',
+                    ADMIN_LIST_STATUS.unlist.variant,
+                  )}>
+                  {t(ADMIN_LIST_STATUS.unlist.name, { keyPrefix: 'btns' })}
+                </span>
+              )}
+            </div>
+            <div className="d-flex align-items-center small">
+              <BaseUserCard data={author_user_info} avatarSize="24" />
+              <FormatTime
+                time={Number(flagItemData?.created_at)}
+                className="text-secondary ms-1 flex-shrink-0"
+                preFix={t(itemTimePrefix, { keyPrefix: 'question_detail' })}
+              />
+            </div>
+          </div>
+        </div>
+      </Card.Body>
+
+      <Card.Footer className="p-3">
+        <p>
+          {object_type !== 'user'
+            ? t('approve_post_tip')
+            : t('approve_user_tip')}
+        </p>
+        <Stack direction="horizontal" gap={2}>
+          <Button
+            variant="outline-primary"
+            disabled={isLoading}
+            onClick={() => handleAction('approve')}>
+            {t('approve', { keyPrefix: 'btns' })}
+          </Button>
+          <Button
+            variant="outline-primary"
+            disabled={isLoading}
+            onClick={() => handleAction('reject')}>
+            {t('reject', { keyPrefix: 'btns' })}
+          </Button>
+        </Stack>
+      </Card.Footer>
+    </Card>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/ReviewType/index.tsx 
b/ui/src/pages/Review/components/ReviewType/index.tsx
new file mode 100644
index 00000000..16a392e2
--- /dev/null
+++ b/ui/src/pages/Review/components/ReviewType/index.tsx
@@ -0,0 +1,40 @@
+import { FC } from 'react';
+import { Card, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import * as Type from '@/common/interface';
+
+interface IProps {
+  list: Type.ReviewTypeItem[] | undefined;
+  checked: string;
+  callback: (type: string) => void;
+}
+
+const Index: FC<IProps> = ({ list, checked, callback }) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+  return (
+    <Card>
+      <Card.Header>{t('filter', { keyPrefix: 'btns' })}</Card.Header>
+      <Card.Body>
+        <Form.Group>
+          <Form.Label>{t('filter_label')}</Form.Label>
+          {list?.map((item) => {
+            return (
+              <Form.Check
+                key={item.name}
+                type="radio"
+                id={item.name}
+                disabled={item.todo_amount <= 0}
+                label={`${item.label} (${item.todo_amount})`}
+                checked={checked === item.name}
+                onChange={() => callback(item.name)}
+              />
+            );
+          })}
+        </Form.Group>
+      </Card.Body>
+    </Card>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/SuggestContent/index.tsx 
b/ui/src/pages/Review/components/SuggestContent/index.tsx
new file mode 100644
index 00000000..8b618a91
--- /dev/null
+++ b/ui/src/pages/Review/components/SuggestContent/index.tsx
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FC, useEffect, useState } from 'react';
+import { Alert, Stack, Button, Card } from 'react-bootstrap';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+import { BaseUserCard, FormatTime, DiffContent } from '@/components';
+import { getSuggestReviewList, revisionAudit } from '@/services';
+import { pathFactory } from '@/router/pathFactory';
+import { scrollToDocTop } from '@/utils';
+import type * as Type from '@/common/interface';
+
+interface IProps {
+  refreshCount: () => void;
+}
+
+const Index: FC<IProps> = ({ refreshCount }) => {
+  const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+  const [isLoading, setIsLoading] = useState(false);
+  const [noTasks, setNoTasks] = useState(false);
+  const [page, setPage] = useState(1);
+  const [reviewResp, setReviewResp] = useState<Type.SuggestReviewResp>();
+  const ro = reviewResp?.list[0];
+  const { info, type, unreviewed_info } = ro || {
+    info: null,
+    type: '',
+    unreviewed_info: null,
+  };
+  const resolveNextOne = (resp, pageNumber) => {
+    const { count, list = [] } = resp;
+    // auto rollback
+    if (!list.length && count && page !== 1) {
+      pageNumber = 1;
+      setPage(pageNumber);
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      queryNextOne(pageNumber);
+      return;
+    }
+    if (pageNumber !== page) {
+      setPage(pageNumber);
+    }
+    setReviewResp(resp);
+    if (!list.length) {
+      setNoTasks(true);
+    }
+    setTimeout(() => {
+      scrollToDocTop();
+    }, 150);
+  };
+  const queryNextOne = (pageNumber) => {
+    getSuggestReviewList(pageNumber).then((resp) => {
+      resolveNextOne(resp, pageNumber);
+    });
+  };
+  const reviewInfo = unreviewed_info?.content;
+
+  const handlingApprove = () => {
+    if (!unreviewed_info) {
+      return;
+    }
+    setIsLoading(true);
+    revisionAudit(unreviewed_info.id, 'approve')
+      .then(() => {
+        refreshCount();
+        queryNextOne(page);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  const handlingReject = () => {
+    if (!unreviewed_info) {
+      return;
+    }
+    setIsLoading(true);
+    revisionAudit(unreviewed_info.id, 'reject')
+      .then(() => {
+        refreshCount();
+        queryNextOne(page);
+      })
+      .catch((ex) => {
+        console.error('revisionAudit reject error: ', ex);
+      })
+      .finally(() => {
+        setIsLoading(false);
+      });
+  };
+
+  let itemLink = '';
+  let itemId = '';
+  let editSummary = unreviewed_info?.reason;
+  const editor = unreviewed_info?.user_info;
+  const editTime = unreviewed_info?.create_at;
+  if (type === 'question') {
+    itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);
+    itemId = info?.object_id;
+    editSummary ||= t('edit_question');
+  } else if (type === 'answer') {
+    itemLink = pathFactory.answerLanding({
+      // @ts-ignore
+      questionId: unreviewed_info.content.question_id,
+      slugTitle: info?.url_title,
+      answerId: unreviewed_info.object_id,
+    });
+    itemId = unreviewed_info.object_id;
+    editSummary ||= t('edit_answer');
+  } else if (type === 'tag') {
+    const tagInfo = unreviewed_info.content as Type.Tag;
+    itemLink = pathFactory.tagLanding(tagInfo.slug_name);
+    itemId = tagInfo?.tag_id || tagInfo.slug_name;
+    editSummary ||= t('edit_tag');
+  }
+  useEffect(() => {
+    queryNextOne(page);
+  }, []);
+
+  if (noTasks) return null;
+
+  let newData: Record<string, any> = {};
+  let oldData: Record<string, any> = {};
+  let diffOpts: Partial<{
+    showTitle: boolean;
+    showTagUrlSlug: boolean;
+  }> = {
+    showTitle: true,
+    showTagUrlSlug: true,
+  };
+  if (type === 'question' && info && reviewInfo && 'content' in reviewInfo) {
+    newData = {
+      title: reviewInfo.title,
+      original_text: reviewInfo.content,
+      tags: reviewInfo.tags,
+    };
+    oldData = {
+      title: info.title,
+      original_text: info.content,
+      tags: info.tags,
+    };
+  }
+  if (type === 'answer' && info && reviewInfo && 'content' in reviewInfo) {
+    newData = {
+      original_text: reviewInfo.content,
+    };
+    oldData = {
+      original_text: info.content,
+    };
+  }
+
+  if (type === 'tag' && info && reviewInfo) {
+    newData = {
+      original_text: reviewInfo.original_text,
+    };
+    oldData = {
+      original_text: info.content,
+    };
+    diffOpts = { showTitle: false, showTagUrlSlug: false };
+  }
+
+  return (
+    <Card>
+      <Card.Header>{t('suggest_edits')}</Card.Header>
+      <Card.Body className="p-0">
+        <Alert variant="info" className="border-0 rounded-0 mb-0">
+          <Stack
+            direction="horizontal"
+            gap={1}
+            className="align-items-center mb-2">
+            <BaseUserCard data={editor} avatarSize="24" />
+            {editTime && (
+              <FormatTime
+                time={editTime}
+                className="small text-secondary"
+                preFix={t('proposed')}
+              />
+            )}
+          </Stack>
+          <Stack className="align-items-start">
+            <p className="mb-0">{editSummary}</p>
+          </Stack>
+        </Alert>
+        <div className="p-3">
+          <small className="d-block text-secondary mb-4">
+            <span>{t(type, { keyPrefix: 'btns' })} </span>
+            <Link to={itemLink} target="_blank" className="link-secondary">
+              #{itemId}
+            </Link>
+          </small>
+
+          <DiffContent
+            className="mt-2"
+            objectType={type}
+            newData={newData}
+            oldData={oldData}
+            opts={diffOpts}
+          />
+        </div>
+      </Card.Body>
+      <Card.Footer className="p-3">
+        <p>{t('approve_revision_tip')}</p>
+        <Stack direction="horizontal" gap={2}>
+          <Button
+            variant="outline-primary"
+            disabled={isLoading}
+            onClick={handlingApprove}>
+            {t('approve', { keyPrefix: 'btns' })}
+          </Button>
+          <Button
+            variant="outline-primary"
+            disabled={isLoading}
+            onClick={handlingReject}>
+            {t('reject', { keyPrefix: 'btns' })}
+          </Button>
+        </Stack>
+      </Card.Footer>
+    </Card>
+  );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/index.ts 
b/ui/src/pages/Review/components/index.ts
new file mode 100644
index 00000000..0e02a69b
--- /dev/null
+++ b/ui/src/pages/Review/components/index.ts
@@ -0,0 +1,15 @@
+import ReviewType from './ReviewType';
+import ApproveDropdown from './ApproveDropdown';
+import EditPostModal from './EditPostModal';
+import SuggestContent from './SuggestContent';
+import FlagContent from './FlagContent';
+import QueuedContent from './QueuedContent';
+
+export {
+  ReviewType,
+  ApproveDropdown,
+  EditPostModal,
+  FlagContent,
+  SuggestContent,
+  QueuedContent,
+};
diff --git a/ui/src/pages/Review/index.tsx b/ui/src/pages/Review/index.tsx
index 927e95a1..4febbb9e 100644
--- a/ui/src/pages/Review/index.tsx
+++ b/ui/src/pages/Review/index.tsx
@@ -18,235 +18,103 @@
  */
 
 import { FC, useEffect, useState } from 'react';
-import { Row, Col, Alert, Stack, Button } from 'react-bootstrap';
-import { Link } from 'react-router-dom';
+import { Row, Col } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
+import { useSearchParams } from 'react-router-dom';
 
 import { usePageTags } from '@/hooks';
-import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
-import { getReviewList, revisionAudit } from '@/services';
-import { pathFactory } from '@/router/pathFactory';
-import { scrollToDocTop } from '@/utils';
+import { Empty } from '@/components';
+import { getReviewType } from '@/services';
 import type * as Type from '@/common/interface';
 
+import {
+  ReviewType,
+  FlagContent,
+  SuggestContent,
+  QueuedContent,
+} from './components';
+
 const Index: FC = () => {
+  const [urlSearch, setUrlSearchParams] = useSearchParams();
+  const searchType = urlSearch.get('type');
   const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
-  const [isLoading, setIsLoading] = useState(false);
-  const [noTasks, setNoTasks] = useState(false);
-  const [page, setPage] = useState(1);
-  const [reviewResp, setReviewResp] = useState<Type.ReviewResp>();
-  const ro = reviewResp?.list[0];
-  const { info, type, unreviewed_info } = ro || {
-    info: null,
-    type: '',
-    unreviewed_info: null,
-  };
-  const resolveNextOne = (resp, pageNumber) => {
-    const { count, list = [] } = resp;
-    // auto rollback
-    if (!list.length && count && page !== 1) {
-      pageNumber = 1;
-      setPage(pageNumber);
-      // eslint-disable-next-line @typescript-eslint/no-use-before-define
-      queryNextOne(pageNumber);
-      return;
-    }
-    if (pageNumber !== page) {
-      setPage(pageNumber);
-    }
-    setReviewResp(resp);
-    if (!list.length) {
-      setNoTasks(true);
-    }
-    setTimeout(() => {
-      scrollToDocTop();
-    }, 150);
-  };
-  const queryNextOne = (pageNumber) => {
-    getReviewList(pageNumber)
+  const [reviewTypeList, setReviewTypeList] = 
useState<Type.ReviewTypeItem[]>();
+  const [currentReviewType, setCurrentReviewType] = useState('');
+  const [isEmpty, setIsEmpty] = useState(false);
+
+  const fetchReviewType = (changeReviewType: boolean) => {
+    getReviewType()
       .then((resp) => {
-        resolveNextOne(resp, pageNumber);
+        if (searchType) {
+          const filterData = resp.find((item) => item.name === searchType);
+          if (Number(filterData?.todo_amount) > 0) {
+            setCurrentReviewType(filterData?.name || '');
+          } else {
+            setIsEmpty(true);
+          }
+        } else {
+          const filterData = resp.filter((item) => item.todo_amount > 0);
+          if (filterData.length > 0) {
+            if (changeReviewType) {
+              setCurrentReviewType(filterData[0].name);
+            } else {
+              const currentTypeItem = resp.find(
+                (item) => item.name === currentReviewType,
+              );
+              if (currentTypeItem?.todo_amount === 0) {
+                setCurrentReviewType(filterData[0].name);
+              }
+            }
+          } else {
+            setIsEmpty(true);
+          }
+        }
+        setReviewTypeList(resp);
       })
       .catch((ex) => {
-        console.error('review next error: ', ex);
+        console.error('getReviewType error: ', ex);
       });
   };
-  const reviewInfo = unreviewed_info?.content;
-  const handlingSkip = () => {
-    queryNextOne(page + 1);
-  };
-  const handlingApprove = () => {
-    if (!unreviewed_info) {
-      return;
-    }
-    setIsLoading(true);
-    revisionAudit(unreviewed_info.id, 'approve')
-      .then(() => {
-        queryNextOne(page);
-      })
-      .catch((ex) => {
-        console.error('revisionAudit approve error: ', ex);
-      })
-      .finally(() => {
-        setIsLoading(false);
-      });
-  };
-  const handlingReject = () => {
-    if (!unreviewed_info) {
-      return;
-    }
-    setIsLoading(true);
-    revisionAudit(unreviewed_info.id, 'reject')
-      .then(() => {
-        queryNextOne(page);
-      })
-      .catch((ex) => {
-        console.error('revisionAudit reject error: ', ex);
-      })
-      .finally(() => {
-        setIsLoading(false);
-      });
+
+  const handleTypeChange = (name) => {
+    urlSearch.delete('type');
+    setUrlSearchParams(urlSearch);
+    setCurrentReviewType(name);
   };
 
-  let itemLink = '';
-  let itemTitle = '';
-  let editBadge = '';
-  let editSummary = unreviewed_info?.reason;
-  const editor = unreviewed_info?.user_info;
-  const editTime = unreviewed_info?.create_at;
-  if (type === 'question') {
-    itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);
-    itemTitle = info?.title;
-    editBadge = t('question_edit');
-    editSummary ||= t('edit_question');
-  } else if (type === 'answer') {
-    itemLink = pathFactory.answerLanding({
-      // @ts-ignore
-      questionId: unreviewed_info.content.question_id,
-      slugTitle: info?.url_title,
-      answerId: unreviewed_info.object_id,
-    });
-    itemTitle = info?.title;
-    editBadge = t('answer_edit');
-    editSummary ||= t('edit_answer');
-  } else if (type === 'tag') {
-    const tagInfo = unreviewed_info.content as Type.Tag;
-    itemLink = pathFactory.tagLanding(tagInfo.slug_name);
-    itemTitle = tagInfo.display_name;
-    editBadge = t('tag_edit');
-    editSummary ||= t('edit_tag');
-  }
   useEffect(() => {
-    queryNextOne(page);
+    fetchReviewType(true);
   }, []);
+
   usePageTags({
     title: t('review'),
   });
+
   return (
     <Row className="pt-4 mb-5">
       <h3 className="mb-4">{t('review')}</h3>
       <Col className="page-main flex-auto">
-        {!noTasks && ro && (
-          <>
-            <Alert variant="secondary">
-              <Stack className="align-items-start">
-                <span className="badge text-bg-secondary mb-2">
-                  {editBadge}
-                </span>
-                <Link to={itemLink} target="_blank">
-                  {itemTitle}
-                </Link>
-                <p className="mb-0">
-                  {t('edit_summary')}: {editSummary}
-                </p>
-              </Stack>
-              <Stack
-                direction="horizontal"
-                gap={1}
-                className="align-items-baseline mt-2">
-                <BaseUserCard data={editor} avatarSize="24" />
-                {editTime && (
-                  <FormatTime
-                    time={editTime}
-                    className="small text-secondary"
-                    preFix={t('proposed')}
-                  />
-                )}
-              </Stack>
-            </Alert>
-            {type === 'question' &&
-              info &&
-              reviewInfo &&
-              'content' in reviewInfo && (
-                <DiffContent
-                  className="mt-2"
-                  objectType={type}
-                  oldData={{
-                    title: info.title,
-                    original_text: info.content,
-                    tags: info.tags,
-                  }}
-                  newData={{
-                    title: reviewInfo.title,
-                    original_text: reviewInfo.content,
-                    tags: reviewInfo.tags,
-                  }}
-                />
-              )}
-            {type === 'answer' &&
-              info &&
-              reviewInfo &&
-              'content' in reviewInfo && (
-                <DiffContent
-                  className="mt-2"
-                  objectType={type}
-                  newData={{
-                    original_text: reviewInfo.content,
-                  }}
-                  oldData={{
-                    original_text: info.content,
-                  }}
-                />
-              )}
-            {type === 'tag' && info && reviewInfo && (
-              <DiffContent
-                className="mt-2"
-                objectType={type}
-                newData={{
-                  original_text: reviewInfo.original_text,
-                }}
-                oldData={{
-                  original_text: info.content,
-                }}
-                opts={{ showTitle: false, showTagUrlSlug: false }}
-              />
-            )}
-            <Stack direction="horizontal" gap={2} className="mt-4">
-              <Button
-                variant="outline-primary"
-                disabled={isLoading}
-                onClick={handlingApprove}>
-                {t('approve', { keyPrefix: 'btns' })}
-              </Button>
-              <Button
-                variant="outline-primary"
-                disabled={isLoading}
-                onClick={handlingReject}>
-                {t('reject', { keyPrefix: 'btns' })}
-              </Button>
-              <Button
-                variant="outline-primary"
-                disabled={isLoading}
-                onClick={handlingSkip}>
-                {t('skip', { keyPrefix: 'btns' })}
-              </Button>
-            </Stack>
-          </>
+        {currentReviewType === 'suggested_post_edit' && (
+          <SuggestContent refreshCount={() => fetchReviewType(false)} />
         )}
-        {noTasks && <Empty>{t('empty')}</Empty>}
+
+        {currentReviewType === 'flagged_post' && (
+          <FlagContent refreshCount={() => fetchReviewType(false)} />
+        )}
+
+        {currentReviewType === 'queued_post' && (
+          <QueuedContent refreshCount={() => fetchReviewType(false)} />
+        )}
+        {isEmpty && <Empty>{t('empty')}</Empty>}
       </Col>
 
-      <Col className="page-right-side mt-4 mt-xl-0" />
+      <Col className="page-right-side mt-4 mt-xl-0">
+        <ReviewType
+          list={reviewTypeList}
+          checked={currentReviewType}
+          callback={handleTypeChange}
+        />
+      </Col>
     </Row>
   );
 };
diff --git a/ui/src/pages/Review/utils/generateData.ts 
b/ui/src/pages/Review/utils/generateData.ts
new file mode 100644
index 00000000..42d80726
--- /dev/null
+++ b/ui/src/pages/Review/utils/generateData.ts
@@ -0,0 +1,58 @@
+import { pathFactory } from '@/router/pathFactory';
+
+export default (data: any) => {
+  if (!data?.object_id) {
+    return {
+      itemLink: '',
+      itemId: '',
+      itemTimePrefix: '',
+    };
+  }
+
+  const {
+    object_type = '',
+    object_id = '',
+    question_id = '',
+    answer_id = '',
+    comment_id = '',
+    title = '',
+  } = data;
+  let itemLink = '';
+  let itemId = '';
+  let itemTimePrefix = '';
+
+  if (object_type === 'question') {
+    itemLink = pathFactory.questionLanding(String(object_id), title);
+    itemId = String(question_id);
+    itemTimePrefix = 'asked';
+  } else if (object_type === 'answer') {
+    itemLink = pathFactory.answerLanding({
+      // @ts-ignore
+      questionId: question_id,
+      slugTitle: title,
+      answerId: String(object_id),
+    });
+    itemId = String(object_id);
+    itemTimePrefix = 'answered';
+  } else if (object_type === 'comment') {
+    if (question_id && answer_id) {
+      itemLink = `${pathFactory.answerLanding({
+        questionId: question_id,
+        answerId: answer_id,
+      })}?commentId=${comment_id}`;
+    } else {
+      itemLink = `${pathFactory.questionLanding(
+        String(question_id),
+        title,
+      )}?commentId=${comment_id}`;
+    }
+    itemId = String(comment_id);
+    itemTimePrefix = 'commented';
+  }
+
+  return {
+    itemLink,
+    itemId,
+    itemTimePrefix,
+  };
+};
diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx 
b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx
index d8a96ab3..5f2e8f58 100644
--- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx
+++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx
@@ -21,7 +21,7 @@ import { ListGroup } from 'react-bootstrap';
 import { Link } from 'react-router-dom';
 
 import classNames from 'classnames';
-import { isEmpty } from 'lodash';
+import isEmpty from 'lodash/isEmpty';
 
 import { Empty } from '@/components';
 
diff --git a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx 
b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx
index 6e6e5c2f..64a2c34b 100644
--- a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx
+++ b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx
@@ -22,7 +22,7 @@ import { Link } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
 
 import classNames from 'classnames';
-import { isEmpty } from 'lodash';
+import isEmpty from 'lodash/isEmpty';
 
 import { FormatTime, Empty } from '@/components';
 
diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx 
b/ui/src/pages/Users/Settings/Interface/index.tsx
index 575995ed..046bccd6 100644
--- a/ui/src/pages/Users/Settings/Interface/index.tsx
+++ b/ui/src/pages/Users/Settings/Interface/index.tsx
@@ -71,6 +71,7 @@ const Index = () => {
       },
     },
   };
+
   const uiSchema: UISchema = {
     language: {
       'ui:widget': 'select',
@@ -86,7 +87,7 @@ const Index = () => {
       ...formData,
       language: {
         ...formData.language,
-        value: res[0].value,
+        value: loggedUserInfo.language || res[0].value,
       },
     });
     setLangs(res);
diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts
index ec326acf..ea353604 100644
--- a/ui/src/router/pathFactory.ts
+++ b/ui/src/router/pathFactory.ts
@@ -41,7 +41,7 @@ const questionLanding = (questionId: string, slugTitle: 
string = '') => {
   }
   // @ts-ignore
   if (/[13]/.test(seo.permalink) && slugTitle) {
-    return `/questions/${questionId}/${slugTitle}`;
+    return `/questions/${questionId}/${encodeURIComponent(slugTitle)}`;
   }
 
   return `/questions/${questionId}`;
diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts
index bab8b34b..5c420e36 100644
--- a/ui/src/router/routes.ts
+++ b/ui/src/router/routes.ts
@@ -219,7 +219,6 @@ const routes: RouteNode[] = [
           },
         ],
       },
-
       {
         path: 'users/login',
         page: 'pages/Users/Login',
@@ -333,10 +332,6 @@ const routes: RouteNode[] = [
             path: 'answers',
             page: 'pages/Admin/Answers',
           },
-          {
-            path: 'flags',
-            page: 'pages/Admin/Flags',
-          },
           {
             path: 'themes',
             page: 'pages/Admin/Themes',
diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts
index 89d38a95..0d718423 100644
--- a/ui/src/services/client/index.ts
+++ b/ui/src/services/client/index.ts
@@ -29,3 +29,4 @@ export * from './timeline';
 export * from './revision';
 export * from './user';
 export * from './Oauth';
+export * from './review';
diff --git a/ui/src/services/client/review.ts b/ui/src/services/client/review.ts
new file mode 100644
index 00000000..de289453
--- /dev/null
+++ b/ui/src/services/client/review.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// import useSWR from 'swr';
+
+import request from '@/utils/request';
+import * as Type from '@/common/interface';
+
+export const getSuggestReviewList = (page: number) => {
+  const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;
+  return request.get<Type.SuggestReviewResp>(apiUrl);
+};
+
+export const getReviewType = () => {
+  return request.get<Type.ReviewTypeItem[]>('/answer/api/v1/reviewing/type');
+};
+
+export const getFlagReviewPostList = (page: number) => {
+  const apiUrl = `/answer/api/v1/report/unreviewed/post?page=${page}`;
+  return request.get<Type.FlagReviewResp>(apiUrl);
+};
+
+export const putFlagReviewAction = (params: Type.PutFlagReviewParams) => {
+  return request.put('/answer/api/v1/report/review', params);
+};
+
+export const getPendingReviewPostList = (page: number, objectId?: string) => {
+  const apiUrl = 
`/answer/api/v1/review/pending/post/page?page=${page}&object_id=${objectId}`;
+  return request.get<Type.QueuedReviewResp>(apiUrl);
+};
+
+export const putPendingReviewAction = (params: {
+  review_id: number;
+  status: 'approve' | 'reject';
+}) => {
+  return request.put('/answer/api/v1/review/pending/post', params);
+};
diff --git a/ui/src/services/client/revision.ts 
b/ui/src/services/client/revision.ts
index b71d7f24..489730e4 100644
--- a/ui/src/services/client/revision.ts
+++ b/ui/src/services/client/revision.ts
@@ -18,7 +18,6 @@
  */
 
 import request from '@/utils/request';
-import * as Type from '@/common/interface';
 
 export const editCheck = (id: string, passingError: boolean = false) => {
   const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`;
@@ -34,8 +33,3 @@ export const revisionAudit = (id: string, operation: 
'approve' | 'reject') => {
     operation,
   });
 };
-
-export const getReviewList = (page: number) => {
-  const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;
-  return request.get<Type.ReviewResp>(apiUrl);
-};
diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts
index 07dc3400..13644cd0 100644
--- a/ui/src/utils/common.ts
+++ b/ui/src/utils/common.ts
@@ -108,6 +108,11 @@ function parseUserInfo(markdown) {
   return markdown.replace(globalReg, '[@$1](/u/$1)');
 }
 
+function parseEditMentionUser(markdown) {
+  const globalReg = /\[@([^\]]+)\]\([^)]+\)/g;
+  return markdown.replace(globalReg, '@$1');
+}
+
 function formatUptime(value) {
   const t = i18next.t.bind(i18next);
   const second = parseInt(value, 10);
@@ -170,8 +175,6 @@ function escapeHtml(str: string) {
   return str.replace(/[&<>"'`]/g, (tag) => tagsToReplace[tag] || tag);
 }
 
-const Diff = require('diff');
-
 function diffText(newText: string, oldText?: string): string {
   if (!newText) {
     return '';
@@ -180,28 +183,31 @@ function diffText(newText: string, oldText?: string): 
string {
   if (typeof oldText !== 'string') {
     return escapeHtml(newText);
   }
-  const diff = Diff.diffChars(escapeHtml(oldText), escapeHtml(newText));
-  const 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>`;
+  let result = [];
+  import('diff').then((Diff) => {
+    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 `<span class="review-text-add">${part.value}</span>`;
-    }
-    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>`;
+      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 `<span class="review-text-delete">${part.value}</span>`;
-    }
 
-    return part.value;
+      return part.value;
+    });
   });
 
   return result.join('');
@@ -273,6 +279,7 @@ export {
   bgFadeOut,
   matchedUsers,
   parseUserInfo,
+  parseEditMentionUser,
   formatUptime,
   escapeRemove,
   handleFormError,
diff --git a/ui/src/utils/saveDraft.ts b/ui/src/utils/saveDraft.ts
index e2dd7c17..6d7f1814 100644
--- a/ui/src/utils/saveDraft.ts
+++ b/ui/src/utils/saveDraft.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { debounce } from 'lodash';
+import debounce from 'lodash/debounce';
 
 import {
   DRAFT_QUESTION_STORAGE_KEY,
diff --git a/ui/template/header.html b/ui/template/header.html
index 85425674..75c696f9 100644
--- a/ui/template/header.html
+++ b/ui/template/header.html
@@ -54,7 +54,9 @@
       href="{{.siteinfo.Branding.SquareIcon}}"
       data-rh="true"
     />
-    <script defer="defer" src="{{.scriptPath}}"></script>
+    {{range $path := .scriptPath}}
+    <script defer="defer" src="{{$path}}"></script>
+    {{end}}
     {{if $.siteinfo.JsonLD }}{{ .siteinfo.JsonLD | templateHTML}}{{end}}
   </head>
 

Reply via email to