Merge remote-tracking branch 'origin/dev' into feat/1.3.0/review
This commit is contained in:
@@ -1081,6 +1081,7 @@ ui:
|
||||
title: Answers
|
||||
score: Score
|
||||
newest: Newest
|
||||
oldest: Oldest
|
||||
btn_accept: Accept
|
||||
btn_accepted: Accepted
|
||||
write_answer:
|
||||
@@ -1596,6 +1597,9 @@ ui:
|
||||
msg: Contact email cannot be empty.
|
||||
validate: Contact email is not valid.
|
||||
text: Email address of key contact responsible for this site.
|
||||
check_update:
|
||||
label: Software updates
|
||||
text: Automatically check for updates
|
||||
interface:
|
||||
page_title: Interface
|
||||
language:
|
||||
|
||||
@@ -59,3 +59,6 @@ language_options:
|
||||
- label: "Slovak"
|
||||
value: "sk_SK"
|
||||
progress: 62
|
||||
- label: "فارسی"
|
||||
value: "fa_IR"
|
||||
progress: 85
|
||||
|
||||
@@ -1050,6 +1050,7 @@ ui:
|
||||
title: 个回答
|
||||
score: 评分
|
||||
newest: 最新
|
||||
oldest: 最旧
|
||||
btn_accept: 采纳
|
||||
btn_accepted: 已被采纳
|
||||
write_answer:
|
||||
@@ -1549,6 +1550,9 @@ ui:
|
||||
msg: 联系人邮箱不能为空。
|
||||
validate: 联系人邮箱无效。
|
||||
text: 本网站的主要联系邮箱地址。
|
||||
check_update:
|
||||
label: 软件更新
|
||||
text: 自动检查软件更新
|
||||
interface:
|
||||
page_title: 界面
|
||||
language:
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
AnswerSearchOrderByDefault = "default"
|
||||
AnswerSearchOrderByTime = "updated"
|
||||
AnswerSearchOrderByVote = "vote"
|
||||
AnswerSearchOrderByTimeAsc = "created"
|
||||
|
||||
AnswerStatusAvailable = 1
|
||||
AnswerStatusDeleted = 10
|
||||
|
||||
@@ -336,6 +336,8 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
|
||||
switch search.Order {
|
||||
case entity.AnswerSearchOrderByTime:
|
||||
session = session.OrderBy("created_at desc")
|
||||
case entity.AnswerSearchOrderByTimeAsc:
|
||||
session = session.OrderBy("created_at asc")
|
||||
case entity.AnswerSearchOrderByVote:
|
||||
session = session.OrderBy("vote_count desc")
|
||||
default:
|
||||
|
||||
@@ -41,12 +41,34 @@ type SearchDTO struct {
|
||||
func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) {
|
||||
// Replace special characters.
|
||||
// Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format.
|
||||
s.Query = regexp.MustCompile(`[+#.<>\-_()*]`).ReplaceAllString(s.Query, " ")
|
||||
s.Query = regexp.MustCompile(`\s+`).ReplaceAllString(s.Query, " ")
|
||||
s.Query = strings.TrimSpace(s.Query)
|
||||
replacedContent, patterns := ReplaceSearchContent(s.Query)
|
||||
s.Query = strings.Join(append(patterns, replacedContent), " ")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ReplaceSearchContent(content string) (string, []string) {
|
||||
// Define the regular expressions for key:value pairs and [tag]
|
||||
keyValueRegex := regexp.MustCompile(`\w+:\S+`)
|
||||
tagRegex := regexp.MustCompile(`\[\w+\]`)
|
||||
// Define the pattern for characters to replace
|
||||
replaceCharsPattern := regexp.MustCompile(`[+#.<>\-_()*]`)
|
||||
|
||||
// Extract key:value pairs
|
||||
keyValues := keyValueRegex.FindAllString(content, -1)
|
||||
// Extract [tag]
|
||||
tags := tagRegex.FindAllString(content, -1)
|
||||
|
||||
// Replace key:value pairs and [tag] with empty string
|
||||
contentWithoutPatterns := keyValueRegex.ReplaceAllString(content, "")
|
||||
contentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, "")
|
||||
|
||||
// Replace characters with pattern [+#.<>_()*] with space
|
||||
replacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, " ")
|
||||
|
||||
return strings.TrimSpace(replacedContent), append(keyValues, tags...)
|
||||
}
|
||||
|
||||
type SearchCondition struct {
|
||||
// search target type: all/question/answer
|
||||
TargetType string
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReplaceSearchContent(t *testing.T) {
|
||||
content := "user:aaa [tag] ssssfdfdf-as#fsadf"
|
||||
replacedContent, patterns := ReplaceSearchContent(content)
|
||||
ret := strings.Join(append(patterns, replacedContent), " ")
|
||||
|
||||
assert.Equal(t, "user:aaa [tag] ssssfdfdf as fsadf", ret)
|
||||
|
||||
content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3"
|
||||
replacedContent, patterns = ReplaceSearchContent(content)
|
||||
ret = strings.Join(append(patterns, replacedContent), " ")
|
||||
|
||||
assert.Equal(t, "user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf", ret)
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type SiteGeneralReq struct {
|
||||
Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"`
|
||||
SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
|
||||
CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"`
|
||||
}
|
||||
|
||||
func (r *SiteGeneralReq) FormatSiteUrl() {
|
||||
|
||||
@@ -1259,7 +1259,17 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin
|
||||
search.Tag = tagNames[0]
|
||||
}
|
||||
search.LoginUserID = loginUserID
|
||||
return qs.GetQuestionPage(ctx, search)
|
||||
similarQuestions, _, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var result []*schema.QuestionPageResp
|
||||
for _, v := range similarQuestions {
|
||||
if uid.DeShortID(v.ID) != questionID {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result, int64(len(result)), nil
|
||||
}
|
||||
|
||||
// GetQuestionPage query questions page
|
||||
|
||||
@@ -645,6 +645,12 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if email status is to be verified, active user as well
|
||||
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
|
||||
if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,11 +23,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/apache/incubator-answer/pkg/converter"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/apache/incubator-answer/pkg/converter"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
"github.com/apache/incubator-answer/internal/base/constant"
|
||||
@@ -101,7 +102,14 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI
|
||||
dashboardInfo.ReportCount = ds.reportCount(ctx)
|
||||
dashboardInfo.VoteCount = ds.voteCount(ctx)
|
||||
dashboardInfo.OccupyingStorageSpace = ds.calculateStorage()
|
||||
dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx)
|
||||
general, err := ds.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get general site info failed: %s", err)
|
||||
return dashboardInfo, nil
|
||||
}
|
||||
if general.CheckUpdate {
|
||||
dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx)
|
||||
}
|
||||
dashboardInfo.DatabaseVersion = ds.getDatabaseInfo()
|
||||
dashboardInfo.DatabaseSize = ds.GetDatabaseSize()
|
||||
}
|
||||
|
||||
@@ -92,7 +92,8 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str
|
||||
Type: "confirm",
|
||||
})
|
||||
}
|
||||
if canDelete || userID == creatorUserID {
|
||||
|
||||
if (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
Action: "delete",
|
||||
Name: translator.Tr(lang, deleteActionName),
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService {
|
||||
|
||||
// GetSiteGeneral get site info general
|
||||
func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
|
||||
resp = &schema.SiteGeneralResp{}
|
||||
resp = &schema.SiteGeneralResp{CheckUpdate: true}
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -250,7 +250,7 @@ export interface QuestionDetailRes {
|
||||
}
|
||||
|
||||
export interface AnswersReq extends Paging {
|
||||
order?: 'default' | 'updated';
|
||||
order?: 'default' | 'updated' | 'created';
|
||||
question_id: string;
|
||||
}
|
||||
|
||||
@@ -347,6 +347,8 @@ export interface AdminSettingsGeneral {
|
||||
description: string;
|
||||
site_url: string;
|
||||
contact_email: string;
|
||||
check_update: boolean;
|
||||
permalink?: number;
|
||||
}
|
||||
|
||||
export interface HelmetBase {
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useSkeletonControl } from '@/hooks';
|
||||
|
||||
export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [
|
||||
'active',
|
||||
@@ -59,11 +60,13 @@ const QuestionList: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const { isSkeletonShow } = useSkeletonControl(isLoading);
|
||||
const curOrder =
|
||||
order || urlSearchParams.get('order') || QUESTION_ORDER_KEYS[0];
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const pageSize = 20;
|
||||
const count = data?.count || 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 d-flex flex-wrap justify-content-between">
|
||||
@@ -80,7 +83,7 @@ const QuestionList: FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
<ListGroup className="rounded-0">
|
||||
{isLoading ? (
|
||||
{isSkeletonShow ? (
|
||||
<QuestionListLoader />
|
||||
) : (
|
||||
data?.list?.map((li) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import useLoginRedirect from './useLoginRedirect';
|
||||
import usePromptWithUnload from './usePrompt';
|
||||
import useActivationEmailModal from './useActivationEmailModal';
|
||||
import useCaptchaModal from './useCaptchaModal';
|
||||
import useSkeletonControl from './useSkeletonControl';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
@@ -43,4 +44,5 @@ export {
|
||||
usePromptWithUnload,
|
||||
useActivationEmailModal,
|
||||
useCaptchaModal,
|
||||
useSkeletonControl,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { SKELETON_SHOW_TIME } from '@/common/constants';
|
||||
|
||||
/**
|
||||
* @param needShowFirst whether the skeleton should show at first
|
||||
*
|
||||
* Why need 'needShowFirst' param?
|
||||
* Sometimes we need skeleton screens to take up space in the dom from the start
|
||||
*
|
||||
* If you set the 'needShowFirst' param as false, If the interface time is too short,
|
||||
* the skeleton screen will not be displayed, which can reduce the time occupation
|
||||
*/
|
||||
const useSkeletonControl = (isLoading: boolean) => {
|
||||
const [isSkeletonShow, setIsSkeletonShow] = useState(false);
|
||||
const timer = useRef<NodeJS.Timeout | null>(null);
|
||||
const openSkeleton = () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
timer.current = setTimeout(() => {
|
||||
setIsSkeletonShow(true);
|
||||
}, SKELETON_SHOW_TIME);
|
||||
};
|
||||
|
||||
const closeSkeleton = () => {
|
||||
clearTimeout(timer.current as NodeJS.Timeout);
|
||||
setIsSkeletonShow(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
openSkeleton();
|
||||
} else {
|
||||
closeSkeleton();
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
return { isSkeletonShow };
|
||||
};
|
||||
|
||||
export default useSkeletonControl;
|
||||
@@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
const { gt, gte } = require('semver');
|
||||
|
||||
@@ -33,6 +34,7 @@ interface IProps {
|
||||
const HealthStatus: FC<IProps> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
const { version, remote_version } = data.version_info || {};
|
||||
const { siteInfo } = siteInfoStore();
|
||||
let isLatest = false;
|
||||
let hasNewerVersion = false;
|
||||
if (version && remote_version) {
|
||||
@@ -65,7 +67,7 @@ const HealthStatus: FC<IProps> = ({ data }) => {
|
||||
{t('update_to')} {remote_version}
|
||||
</a>
|
||||
)}
|
||||
{!isLatest && !remote_version && (
|
||||
{!isLatest && !remote_version && siteInfo.check_update && (
|
||||
<a
|
||||
className="ms-1 badge rounded-pill text-bg-danger"
|
||||
target="_blank"
|
||||
|
||||
@@ -69,6 +69,11 @@ const General: FC = () => {
|
||||
title: t('contact_email.label'),
|
||||
description: t('contact_email.text'),
|
||||
},
|
||||
check_update: {
|
||||
type: 'boolean',
|
||||
title: t('check_update.label'),
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
@@ -107,6 +112,12 @@ const General: FC = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
check_update: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('check_update.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
@@ -121,6 +132,7 @@ const General: FC = () => {
|
||||
short_description: formData.short_description.value,
|
||||
site_url: formData.site_url.value,
|
||||
contact_email: formData.contact_email.value,
|
||||
check_update: formData.check_update.value,
|
||||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
@@ -135,6 +147,7 @@ const General: FC = () => {
|
||||
formData.short_description.value = res.short_description;
|
||||
formData.site_url.value = res.site_url;
|
||||
formData.contact_email.value = res.contact_email;
|
||||
formData.check_update.value = res.check_update;
|
||||
}
|
||||
|
||||
setFormData({ ...formData });
|
||||
|
||||
@@ -36,6 +36,10 @@ const sortBtns = [
|
||||
name: 'newest',
|
||||
sort: 'updated',
|
||||
},
|
||||
{
|
||||
name: 'oldest',
|
||||
sort: 'created',
|
||||
},
|
||||
];
|
||||
|
||||
const Index: FC<Props> = ({ count = 0, order = 'default' }) => {
|
||||
@@ -52,7 +56,13 @@ const Index: FC<Props> = ({ count = 0, order = 'default' }) => {
|
||||
</h5>
|
||||
<QueryGroup
|
||||
data={sortBtns}
|
||||
currentSort={order === 'updated' ? 'newest' : 'score'}
|
||||
currentSort={
|
||||
order === 'updated'
|
||||
? 'newest'
|
||||
: order === 'created'
|
||||
? 'oldest'
|
||||
: 'score'
|
||||
}
|
||||
i18nKeyPrefix="question_detail.answers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Pagination, CustomSidebar } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollToElementTop, scrollToDocTop } from '@/utils';
|
||||
import { usePageTags, usePageUsers } from '@/hooks';
|
||||
import { usePageTags, usePageUsers, useSkeletonControl } from '@/hooks';
|
||||
import type {
|
||||
ListResult,
|
||||
QuestionDetailRes,
|
||||
@@ -68,6 +68,7 @@ const Index = () => {
|
||||
const order = urlSearch.get('order') || '';
|
||||
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const { isSkeletonShow } = useSkeletonControl(isLoading);
|
||||
const [answers, setAnswers] = useState<ListResult<AnswerItem>>({
|
||||
count: -1,
|
||||
list: [],
|
||||
@@ -92,7 +93,7 @@ const Index = () => {
|
||||
|
||||
const requestAnswers = async () => {
|
||||
const res = await getAnswers({
|
||||
order: order === 'updated' ? order : 'default',
|
||||
order: order === 'updated' || order === 'created' ? order : 'default',
|
||||
question_id: qid,
|
||||
page: 1,
|
||||
page_size: 999,
|
||||
@@ -239,7 +240,7 @@ const Index = () => {
|
||||
<Row className="questionDetailPage pt-4 mb-5">
|
||||
<Col className="page-main flex-auto">
|
||||
{question?.operation?.level && <Alert data={question.operation} />}
|
||||
{isLoading ? (
|
||||
{isSkeletonShow ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<Question
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { usePageTags, useCaptchaModal } from '@/hooks';
|
||||
import { usePageTags, useCaptchaModal, useSkeletonControl } from '@/hooks';
|
||||
import { Pagination } from '@/components';
|
||||
import { getSearchResult } from '@/services';
|
||||
import type { SearchParams, SearchRes } from '@/common/interface';
|
||||
@@ -43,6 +43,7 @@ const Index = () => {
|
||||
const q = searchParams.get('q') || '';
|
||||
const order = searchParams.get('order') || 'active';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isSkeletonShow } = useSkeletonControl(isLoading);
|
||||
const [data, setData] = useState<SearchRes>({
|
||||
count: 0,
|
||||
list: [],
|
||||
@@ -102,7 +103,7 @@ const Index = () => {
|
||||
<Head data={extra} />
|
||||
<SearchHead sort={order} count={count} />
|
||||
<ListGroup className="rounded-0 mb-5">
|
||||
{isLoading ? (
|
||||
{isSkeletonShow ? (
|
||||
<ListLoader />
|
||||
) : (
|
||||
list?.map((item) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Row, Col, Card, Button, Form, Stack } from 'react-bootstrap';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { usePageTags, useSkeletonControl } from '@/hooks';
|
||||
import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
||||
import { formatCount, escapeRemove } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
@@ -52,6 +52,8 @@ const Tags = () => {
|
||||
...(sort ? { query_cond: sort } : {}),
|
||||
});
|
||||
|
||||
const { isSkeletonShow } = useSkeletonControl(isLoading);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setSearchTag(e.target.value);
|
||||
};
|
||||
@@ -67,9 +69,11 @@ const Tags = () => {
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
|
||||
usePageTags({
|
||||
title: t('tags', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Row className="py-4 mb-4">
|
||||
<Col xxl={12}>
|
||||
@@ -106,7 +110,7 @@ const Tags = () => {
|
||||
|
||||
<Col className="mt-4" xxl={12}>
|
||||
<Row>
|
||||
{isLoading ? (
|
||||
{isSkeletonShow ? (
|
||||
<TagsLoader />
|
||||
) : (
|
||||
tags?.list?.map((tag) => (
|
||||
|
||||
@@ -50,6 +50,7 @@ const siteInfo = create<SiteInfoType>((set) => ({
|
||||
short_description: '',
|
||||
site_url: '',
|
||||
contact_email: '',
|
||||
check_update: true,
|
||||
permalink: 1,
|
||||
},
|
||||
users: defaultUsersConf,
|
||||
|
||||
Reference in New Issue
Block a user