hello world
", "...", 0, 5) + assert.Equal(t, expected, actual) + + // test string with offset + expected = "...llo你好..." + actual = FetchRangedExcerpt("hello你好world
", "...", 2, 5) + assert.Equal(t, expected, actual) + + // test mixed string with emoticon with offset + expected = "...你好😂..." + actual = FetchRangedExcerpt("hello你好😂world
", "...", 5, 3) + assert.Equal(t, expected, actual) + + // test mixed string with offset and exceeding limit + expected = "...你好😂world" + actual = FetchRangedExcerpt("hello你好😂world
", "...", 5, 100) + assert.Equal(t, expected, actual) +} + +func TestFetchMatchedExcerpt(t *testing.T) { + var ( + expected, + actual string + ) + + html := "Hello, I have 中文 and 😂 and I am supposed to work fine
" + + // test find nothing + // it should return from the begin with double trimLength text + expected = "Hello, I h..." + actual = FetchMatchedExcerpt(html, []string{"youcantfindme"}, "...", 5) + assert.Equal(t, expected, actual) + + // test find the word at the end + // it should return the word beginning with double trimLenth plus len(word) + expected = "... work fine" + actual = FetchMatchedExcerpt(html, []string{"youcant", "fine"}, "...", 3) + assert.Equal(t, expected, actual) + + // test find multiple words + // it should return the first matched word with trimmedText + expected = "... have 中文 and 😂..." + actual = FetchMatchedExcerpt(html, []string{"中文", "😂"}, "...", 6) + assert.Equal(t, expected, actual) +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 79e43f687..e492fdc42 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -21,6 +21,7 @@ package plugin import ( "encoding/json" + "github.com/segmentfault/pacman/i18n" "github.com/apache/incubator-answer/internal/base/handler" @@ -88,6 +89,10 @@ func Register(p Base) { if _, ok := p.(Notification); ok { registerNotification(p.(Notification)) } + + if _, ok := p.(Reviewer); ok { + registerReviewer(p.(Reviewer)) + } } type Stack[T Base] struct { diff --git a/plugin/reviewer.go b/plugin/reviewer.go new file mode 100644 index 000000000..95569d390 --- /dev/null +++ b/plugin/reviewer.go @@ -0,0 +1,48 @@ +package plugin + +type Reviewer interface { + Base + Review(content *ReviewContent) (result *ReviewResult) +} + +// ReviewContent is a struct that contains the content of a review +type ReviewContent struct { + // The type of the content, e.g. question, answer + ObjectType string + // The title of the content, only available for the question + Title string + // The content of the review, always available + Content string + // The tags of the content, only available for the question + Tags []string + // The author of the content + Author ReviewContentAuthor + // Review Language, the site language. e.g. en_US + // The plugin may reply the review result according to the language + Language string +} + +type ReviewContentAuthor struct { + // The user's reputation + Rank int + // The amount of questions that has approved + ApprovedQuestionAmount int64 + // The amount of answers that has approved + ApprovedAnswerAmount int64 + // 1:User 2:Admin 3:Moderator + Role int +} + +// ReviewResult is a struct that contains the result of a review +type ReviewResult struct { + // If the review is approved + Approved bool + // The reason for the result + Reason string +} + +var ( + // CallReviewer is a function that calls all registered parsers + CallReviewer, + registerReviewer = MakePlugin[Reviewer](false) +) diff --git a/script/plugin_list b/script/plugin_list index 5e6a4182a..49324ca0c 100644 --- a/script/plugin_list +++ b/script/plugin_list @@ -1 +1,2 @@ -github.com/apache/incubator-answer-plugins/connector-basic@latest \ No newline at end of file +github.com/apache/incubator-answer-plugins/connector-basic@latest +github.com/apache/incubator-answer-plugins/reviewer-basic@latest \ No newline at end of file diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 293d6bc17..b3ff6e8ae 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: process.env.NODE_ENV === 'production' ? 1 : 2, + 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', // only package dependencies that are referenced asynchronously + reuseExistingChunk: true, // reuse an existing block + }, + 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 ac7e4411f..63a1f85d8 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 77f3c9b1f..83bdc9e24 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(ts-node@10.9.1) 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/test-result@27.5.1: @@ -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 + /btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + dev: true + /buffer-from@1.1.2: 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 /enhanced-resolve@5.15.0: @@ -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(ts-node@10.9.1) 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 /jest-snapshot@27.5.1: resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} @@ -8256,7 +8248,7 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.19.1) 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 /jest-util@28.1.3: @@ -8483,7 +8475,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 /jsonp@0.2.1: resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==} @@ -9153,6 +9145,14 @@ packages: mimic-fn: 4.0.0 dev: true + /open@7.4.2: + 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 + /open@8.4.0: resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} engines: {node: '>=12'} @@ -10792,6 +10792,13 @@ packages: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} dev: true + /rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -11151,6 +11158,25 @@ packages: /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + /source-map-explorer@2.5.3: + 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 + /source-map-js@1.0.2: 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'} + /temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + dev: true + /tempy@0.6.0: 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 /wbuf@1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} diff --git a/ui/src/behaviour/useLegalClick.tsx b/ui/src/behaviour/useLegalClick.tsx new file mode 100644 index 000000000..a098b3e8e --- /dev/null +++ b/ui/src/behaviour/useLegalClick.tsx @@ -0,0 +1,32 @@ +import { MouseEvent, useCallback } from 'react'; + +import { useLegalPrivacy, useLegalTos } from '@/services/client/legal'; + +export const useLegalClick = () => { + const { data: tos } = useLegalTos(); + const { data: privacy } = useLegalPrivacy(); + + const legalClick = useCallback( + (evt: MouseEvent, type: 'tos' | 'privacy') => { + evt.stopPropagation(); + const contentText = + type === 'tos' + ? tos?.terms_of_service_original_text + : privacy?.privacy_policy_original_text; + let matchUrl: URL | undefined; + try { + if (contentText) { + matchUrl = new URL(contentText); + } + // eslint-disable-next-line no-empty + } catch (ex) {} + if (matchUrl) { + evt.preventDefault(); + window.open(matchUrl.toString()); + } + }, + [tos, privacy], + ); + + return legalClick; +}; diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 373e41d21..e76c60b03 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -30,6 +30,7 @@ export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_'; export const DEFAULT_THEME = 'system'; export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99; +export const SKELETON_SHOW_TIME = 1000; export const USER_AGENT_NAMES = { SegmentFault: 'SegmentFault', @@ -54,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', @@ -66,6 +72,14 @@ export const ADMIN_LIST_STATUS = { variant: 'text-bg-danger', name: 'deleted', }, + pending: { + variant: 'text-bg-warning', + name: 'pending', + }, + unlisted: { + variant: 'text-bg-secondary', + name: 'unlisted', + }, }; export const ADMIN_NAV_MENUS = [ @@ -80,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 201a5ae0e..53f8eef59 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 { @@ -250,7 +251,7 @@ export interface QuestionDetailRes { } export interface AnswersReq extends Paging { - order?: 'default' | 'updated'; + order?: 'default' | 'updated' | 'created'; question_id: string; } @@ -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; @@ -347,6 +352,8 @@ export interface AdminSettingsGeneral { description: string; site_url: string; contact_email: string; + check_update: boolean; + permalink?: number; } export interface HelmetBase { @@ -561,7 +568,7 @@ export interface TimelineRes { timeline: TimelineItem[]; } -export interface ReviewItem { +export interface SuggestReviewItem { type: 'question' | 'answer' | 'tag'; info: { url_title?: string; @@ -583,9 +590,60 @@ export interface ReviewItem { content: Tag | QuestionDetailRes | AnswerItem; }; } -export interface ReviewResp { +export interface SuggestReviewResp { count: number; - list: ReviewItem[]; + 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; + url_title: string; + parsed_text: string; +} + +export interface FlagReviewItem extends BaseReviewItem { + reason: ReasonItem; + reason_content: string; + submitter_user: UserInfoBase; +} + +export interface FlagReviewResp { + count: number; + 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 { @@ -637,3 +695,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/common/pattern.ts b/ui/src/common/pattern.ts index 847c6d769..133228b18 100644 --- a/ui/src/common/pattern.ts +++ b/ui/src/common/pattern.ts @@ -20,6 +20,8 @@ const pattern = { email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, + search: + /(\[.*\])|(is:answer)|(is:question)|(score:\d*)|(user:\S*)|(answers:\d*)/g, uaWeChat: /micromessenger/i, uaWeCom: /wxwork/i, uaDingTalk: /dingtalk/i, diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx index 8809778c5..05ae5b2d5 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 = ({\n![${t('image.text')}]($3)\n
`, + `![${t('image.text')}]($3)\n\n
`, ), 'text/html', ) - .querySelector('body')?.innerText as string; + .querySelectorAll('body p'); + + allPTag.forEach((p, index) => { + const text = p.textContent || ''; + if (text !== '') { + if (index === allPTag.length - 1) { + innerText += `${p.textContent}`; + } else { + innerText += `${p.textContent}${text.endsWith('\n') ? '' : '\n\n'}`; + } + } + }); - editor.replaceSelection(newHtml); + editor.replaceSelection(innerText); }; const handleClick = () => { if (!link.value) { diff --git a/ui/src/components/HighlightText/index.scss b/ui/src/components/HighlightText/index.scss new file mode 100644 index 000000000..da0468e67 --- /dev/null +++ b/ui/src/components/HighlightText/index.scss @@ -0,0 +1,22 @@ +/* + * 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. + */ + +.highlight-text > mark { + padding: 0; +} diff --git a/ui/src/components/HighlightText/index.tsx b/ui/src/components/HighlightText/index.tsx new file mode 100644 index 000000000..0a56865b8 --- /dev/null +++ b/ui/src/components/HighlightText/index.tsx @@ -0,0 +1,48 @@ +/* + * 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 { memo, FC } from 'react'; + +import './index.scss'; + +interface IProps { + text: string; + keywords: string[]; +} + +const Index: FC{t('flagged')} | -{t('created')} | - {curFilter !== 'completed' ? ( -{t('action')} | - ) : null} -
---|---|---|
- |
-
- - {li.content} - - )} - |
- {curFilter !== 'completed' ? (
- - - | - ) : null} -
{data.description}{' '}
diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx
index 1fc90955c..474c83d64 100644
--- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx
@@ -99,6 +99,12 @@ const Index: FC
+ {object_type !== 'user'
+ ? t('flag_post_type', { type: reason?.name })
+ : t('flag_user_type', { type: reason?.name })}
+
+ {flagItemData?.reason_content &&
+ reason?.content_type &&
+ (reason?.reason_type !== 60 ? (
+ {flagItemData?.reason_content}
+ ) : flagItemData.reason_content?.startsWith('http') ? (
+
+
+ {' '}
+ {t('show_exist', { keyPrefix: 'question_detail' })}
+
+
+ ) : (
+ {flagItemData?.reason_content}
+ ))}
+ {t('approve_flag_tip')} {reason}
+ {object_type !== 'user'
+ ? t('approve_post_tip')
+ : t('approve_user_tip')}
+ {editSummary} {t('approve_revision_tip')}
- {t('edit_summary')}: {editSummary}
- {flagItemData?.title}
+ {flagItemData?.title}
+ {t('review')}
{t('keywords')}
- {q?.replace(reg, '')}
+ {q?.replace(Pattern.search, '')}
- {escapeRemove(data.object.excerpt)}
+
{options?.length && (
<>
diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx
index d9136b802..2b0f002b0 100644
--- a/ui/src/pages/Search/components/SearchItem/index.tsx
+++ b/ui/src/pages/Search/components/SearchItem/index.tsx
@@ -18,12 +18,19 @@
*/
import { memo, FC } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useSearchParams } from 'react-router-dom';
import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { pathFactory } from '@/router/pathFactory';
-import { Tag, FormatTime, BaseUserCard, Counts } from '@/components';
+import {
+ Tag,
+ FormatTime,
+ BaseUserCard,
+ Counts,
+ HighlightText,
+} from '@/components';
+import Pattern from '@/common/pattern';
import type { SearchResItem } from '@/common/interface';
import { escapeRemove } from '@/utils';
@@ -47,6 +54,14 @@ const Index: FC