mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: fix memo search
This commit is contained in:
@@ -56,8 +56,8 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
|
||||||
}
|
}
|
||||||
if filter.Visibility != nil {
|
if len(filter.ContentSearch) > 0 {
|
||||||
memoFind.VisibilityList = []store.Visibility{*filter.Visibility}
|
memoFind.ContentSearch = filter.ContentSearch
|
||||||
}
|
}
|
||||||
if len(filter.Visibilities) > 0 {
|
if len(filter.Visibilities) > 0 {
|
||||||
memoFind.VisibilityList = filter.Visibilities
|
memoFind.VisibilityList = filter.Visibilities
|
||||||
@@ -84,12 +84,6 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
|
|||||||
}
|
}
|
||||||
memoFind.CreatorID = &user.ID
|
memoFind.CreatorID = &user.ID
|
||||||
}
|
}
|
||||||
if filter.Tag != nil {
|
|
||||||
memoFind.ContentSearch = append(memoFind.ContentSearch, fmt.Sprintf("#%s", *filter.Tag))
|
|
||||||
}
|
|
||||||
if filter.ContentSearch != nil {
|
|
||||||
memoFind.ContentSearch = append(memoFind.ContentSearch, *filter.ContentSearch)
|
|
||||||
}
|
|
||||||
if filter.RowStatus != nil {
|
if filter.RowStatus != nil {
|
||||||
memoFind.RowStatus = filter.RowStatus
|
memoFind.RowStatus = filter.RowStatus
|
||||||
}
|
}
|
||||||
@@ -198,6 +192,14 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
|
|||||||
} else if path == "created_ts" {
|
} else if path == "created_ts" {
|
||||||
createdTs := request.Memo.CreateTime.AsTime().Unix()
|
createdTs := request.Memo.CreateTime.AsTime().Unix()
|
||||||
update.CreatedTs = &createdTs
|
update.CreatedTs = &createdTs
|
||||||
|
} else if path == "pinned" {
|
||||||
|
if _, err := s.Store.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{
|
||||||
|
MemoID: request.Id,
|
||||||
|
UserID: user.ID,
|
||||||
|
Pinned: request.Memo.Pinned,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to upsert memo organizer")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,22 +514,20 @@ func convertVisibilityToStore(visibility apiv2pb.Visibility) store.Visibility {
|
|||||||
|
|
||||||
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
|
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
|
||||||
var ListMemosFilterCELAttributes = []cel.EnvOption{
|
var ListMemosFilterCELAttributes = []cel.EnvOption{
|
||||||
cel.Variable("visibility", cel.StringType),
|
|
||||||
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("created_ts_before", cel.IntType),
|
cel.Variable("created_ts_before", cel.IntType),
|
||||||
cel.Variable("created_ts_after", cel.IntType),
|
cel.Variable("created_ts_after", cel.IntType),
|
||||||
cel.Variable("creator", cel.StringType),
|
cel.Variable("creator", cel.StringType),
|
||||||
|
cel.Variable("content_search", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("row_status", cel.StringType),
|
cel.Variable("row_status", cel.StringType),
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListMemosFilter struct {
|
type ListMemosFilter struct {
|
||||||
Visibility *store.Visibility
|
ContentSearch []string
|
||||||
Visibilities []store.Visibility
|
Visibilities []store.Visibility
|
||||||
CreatedTsBefore *int64
|
CreatedTsBefore *int64
|
||||||
CreatedTsAfter *int64
|
CreatedTsAfter *int64
|
||||||
Creator *string
|
Creator *string
|
||||||
Tag *string
|
|
||||||
ContentSearch *string
|
|
||||||
RowStatus *store.RowStatus
|
RowStatus *store.RowStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,39 +554,30 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
|
|||||||
if len(callExpr.Args) == 2 {
|
if len(callExpr.Args) == 2 {
|
||||||
idExpr := callExpr.Args[0].GetIdentExpr()
|
idExpr := callExpr.Args[0].GetIdentExpr()
|
||||||
if idExpr != nil {
|
if idExpr != nil {
|
||||||
if idExpr.Name == "visibility" {
|
if idExpr.Name == "content_search" {
|
||||||
visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue())
|
contentSearch := []string{}
|
||||||
filter.Visibility = &visibility
|
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
||||||
}
|
value := expr.GetConstExpr().GetStringValue()
|
||||||
if idExpr.Name == "visibilities" {
|
contentSearch = append(contentSearch, value)
|
||||||
|
}
|
||||||
|
filter.ContentSearch = contentSearch
|
||||||
|
} else if idExpr.Name == "visibilities" {
|
||||||
visibilities := []store.Visibility{}
|
visibilities := []store.Visibility{}
|
||||||
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
||||||
value := expr.GetConstExpr().GetStringValue()
|
value := expr.GetConstExpr().GetStringValue()
|
||||||
visibilities = append(visibilities, store.Visibility(value))
|
visibilities = append(visibilities, store.Visibility(value))
|
||||||
}
|
}
|
||||||
filter.Visibilities = visibilities
|
filter.Visibilities = visibilities
|
||||||
}
|
} else if idExpr.Name == "created_ts_before" {
|
||||||
if idExpr.Name == "created_ts_before" {
|
|
||||||
createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
||||||
filter.CreatedTsBefore = &createdTsBefore
|
filter.CreatedTsBefore = &createdTsBefore
|
||||||
}
|
} else if idExpr.Name == "created_ts_after" {
|
||||||
if idExpr.Name == "created_ts_after" {
|
|
||||||
createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
|
||||||
filter.CreatedTsAfter = &createdTsAfter
|
filter.CreatedTsAfter = &createdTsAfter
|
||||||
}
|
} else if idExpr.Name == "creator" {
|
||||||
if idExpr.Name == "creator" {
|
|
||||||
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
|
||||||
filter.Creator = &creator
|
filter.Creator = &creator
|
||||||
}
|
} else if idExpr.Name == "row_status" {
|
||||||
if idExpr.Name == "tag" {
|
|
||||||
tag := callExpr.Args[1].GetConstExpr().GetStringValue()
|
|
||||||
filter.Tag = &tag
|
|
||||||
}
|
|
||||||
if idExpr.Name == "content_search" {
|
|
||||||
contentSearch := callExpr.Args[1].GetConstExpr().GetStringValue()
|
|
||||||
filter.ContentSearch = &contentSearch
|
|
||||||
}
|
|
||||||
if idExpr.Name == "row_status" {
|
|
||||||
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
|
||||||
filter.RowStatus = &rowStatus
|
filter.RowStatus = &rowStatus
|
||||||
}
|
}
|
||||||
|
@@ -135,7 +135,7 @@ message ListMemosRequest {
|
|||||||
int32 limit = 2;
|
int32 limit = 2;
|
||||||
|
|
||||||
// Filter is used to filter memos returned in the list.
|
// Filter is used to filter memos returned in the list.
|
||||||
// Format: "creator == users/{username} && visibility == PUBLIC"
|
// Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']"
|
||||||
string filter = 3;
|
string filter = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1762,7 +1762,7 @@
|
|||||||
| ----- | ---- | ----- | ----------- |
|
| ----- | ---- | ----- | ----------- |
|
||||||
| offset | [int32](#int32) | | offset is the offset of the first memo to return. |
|
| offset | [int32](#int32) | | offset is the offset of the first memo to return. |
|
||||||
| limit | [int32](#int32) | | limit is the maximum number of memos to return. |
|
| limit | [int32](#int32) | | limit is the maximum number of memos to return. |
|
||||||
| filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibility == PUBLIC" |
|
| filter | [string](#string) | | Filter is used to filter memos returned in the list. Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']" |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -316,7 +316,7 @@ type ListMemosRequest struct {
|
|||||||
// limit is the maximum number of memos to return.
|
// limit is the maximum number of memos to return.
|
||||||
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
|
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||||
// Filter is used to filter memos returned in the list.
|
// Filter is used to filter memos returned in the list.
|
||||||
// Format: "creator == users/{username} && visibility == PUBLIC"
|
// Format: "creator == users/{username} && visibilities == ['PUBLIC', 'PROTECTED']"
|
||||||
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,7 +47,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||||||
}
|
}
|
||||||
if v := find.ContentSearch; len(v) != 0 {
|
if v := find.ContentSearch; len(v) != 0 {
|
||||||
for _, s := range v {
|
for _, s := range v {
|
||||||
where, args = append(where, "memo.content LIKE ?"), append(args, "%"+s+"%")
|
where, args = append(where, "memo.content LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := find.VisibilityList; len(v) != 0 {
|
if v := find.VisibilityList; len(v) != 0 {
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Empty from "@/components/Empty";
|
import Empty from "@/components/Empty";
|
||||||
import MemoFilter from "@/components/MemoFilter";
|
import MemoFilter from "@/components/MemoFilter";
|
||||||
import MemoViewV1 from "@/components/MemoViewV1";
|
import MemoViewV1 from "@/components/MemoViewV1";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
|
import { getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { useFilterStore } from "@/store/module";
|
import { useFilterStore } from "@/store/module";
|
||||||
import { useMemoV1Store } from "@/store/v1";
|
import { useMemoV1Store } from "@/store/v1";
|
||||||
@@ -15,27 +16,37 @@ const Explore = () => {
|
|||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const memoStore = useMemoV1Store();
|
const memoStore = useMemoV1Store();
|
||||||
const [memos, setMemos] = useState<Memo[]>([]);
|
|
||||||
const [isComplete, setIsComplete] = useState(false);
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
const [isRequesting, setIsRequesting] = useState(false);
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
|
const memosRef = useRef<Memo[]>([]);
|
||||||
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
||||||
|
const sortedMemos = memosRef.current.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
memosRef.current = [];
|
||||||
fetchMemos();
|
fetchMemos();
|
||||||
}, [tagQuery, textQuery]);
|
}, [tagQuery, textQuery]);
|
||||||
|
|
||||||
const fetchMemos = async () => {
|
const fetchMemos = async () => {
|
||||||
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
||||||
if (tagQuery) filters.push(`tags == "${tagQuery}"`);
|
const contentSearch: string[] = [];
|
||||||
if (textQuery) filters.push(`content_search == "${textQuery}"`);
|
if (tagQuery) {
|
||||||
|
contentSearch.push(`"#${tagQuery}"`);
|
||||||
|
}
|
||||||
|
if (textQuery) {
|
||||||
|
contentSearch.push(`"${textQuery}"`);
|
||||||
|
}
|
||||||
|
if (contentSearch.length > 0) {
|
||||||
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const data = await memoStore.fetchMemos({
|
const data = await memoStore.fetchMemos({
|
||||||
limit: DEFAULT_MEMO_LIMIT,
|
limit: DEFAULT_MEMO_LIMIT,
|
||||||
offset: memos.length,
|
offset: memosRef.current.length,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
});
|
});
|
||||||
setIsRequesting(false);
|
setIsRequesting(false);
|
||||||
setMemos([...memos, ...data]);
|
memosRef.current = [...memosRef.current, ...data];
|
||||||
setIsComplete(data.length < DEFAULT_MEMO_LIMIT);
|
setIsComplete(data.length < DEFAULT_MEMO_LIMIT);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,7 +55,7 @@ const Explore = () => {
|
|||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
<div className="relative w-full h-auto flex flex-col justify-start items-start px-4 sm:px-6">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{memos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showCreator showParent />
|
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showCreator showParent />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -54,7 +65,7 @@ const Explore = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
memos.length === 0 && (
|
sortedMemos.length === 0 && (
|
||||||
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
||||||
<Empty />
|
<Empty />
|
||||||
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Empty from "@/components/Empty";
|
import Empty from "@/components/Empty";
|
||||||
import HomeSidebar from "@/components/HomeSidebar";
|
import HomeSidebar from "@/components/HomeSidebar";
|
||||||
import HomeSidebarDrawer from "@/components/HomeSidebarDrawer";
|
import HomeSidebarDrawer from "@/components/HomeSidebarDrawer";
|
||||||
@@ -7,6 +7,7 @@ import MemoFilter from "@/components/MemoFilter";
|
|||||||
import MemoViewV1 from "@/components/MemoViewV1";
|
import MemoViewV1 from "@/components/MemoViewV1";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
|
import { getTimeStampByDate } from "@/helpers/datetime";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||||
import { useFilterStore } from "@/store/module";
|
import { useFilterStore } from "@/store/module";
|
||||||
@@ -20,33 +21,45 @@ const Home = () => {
|
|||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const memoStore = useMemoV1Store();
|
const memoStore = useMemoV1Store();
|
||||||
const [memos, setMemos] = useState<Memo[]>([]);
|
|
||||||
const [isComplete, setIsComplete] = useState(false);
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
const [isRequesting, setIsRequesting] = useState(false);
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
|
const memosRef = useRef<Memo[]>([]);
|
||||||
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
||||||
|
const sortedMemos = memosRef.current
|
||||||
|
.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime))
|
||||||
|
.sort((a, b) => Number(b.pinned) - Number(a.pinned));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
memosRef.current = [];
|
||||||
fetchMemos();
|
fetchMemos();
|
||||||
}, [tagQuery, textQuery]);
|
}, [tagQuery, textQuery]);
|
||||||
|
|
||||||
const fetchMemos = async () => {
|
const fetchMemos = async () => {
|
||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`];
|
||||||
if (tagQuery) filters.push(`tags == "${tagQuery}"`);
|
const contentSearch: string[] = [];
|
||||||
if (textQuery) filters.push(`content_search == "${textQuery}"`);
|
if (tagQuery) {
|
||||||
|
contentSearch.push(`"#${tagQuery}"`);
|
||||||
|
}
|
||||||
|
if (textQuery) {
|
||||||
|
contentSearch.push(`"${textQuery}"`);
|
||||||
|
}
|
||||||
|
if (contentSearch.length > 0) {
|
||||||
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const data = await memoStore.fetchMemos({
|
const data = await memoStore.fetchMemos({
|
||||||
limit: DEFAULT_MEMO_LIMIT,
|
limit: DEFAULT_MEMO_LIMIT,
|
||||||
offset: memos.length,
|
offset: memosRef.current.length,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
});
|
});
|
||||||
setIsRequesting(false);
|
setIsRequesting(false);
|
||||||
setMemos([...memos, ...data]);
|
memosRef.current = [...memosRef.current, ...data];
|
||||||
setIsComplete(data.length < DEFAULT_MEMO_LIMIT);
|
setIsComplete(data.length < DEFAULT_MEMO_LIMIT);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemoCreated = async (memoId: number) => {
|
const handleMemoCreated = async (memoId: number) => {
|
||||||
const memo = await memoStore.getOrFetchMemoById(memoId);
|
const memo = await memoStore.getOrFetchMemoById(memoId);
|
||||||
setMemos([memo, ...memos]);
|
memosRef.current = [memo, ...memosRef.current];
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +70,7 @@ const Home = () => {
|
|||||||
<MemoEditorV1 className="mb-2" cacheKey="home-memo-editor" onConfirm={handleMemoCreated} />
|
<MemoEditorV1 className="mb-2" cacheKey="home-memo-editor" onConfirm={handleMemoCreated} />
|
||||||
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
|
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{memos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showVisibility showPinnedStyle showParent />
|
<MemoViewV1 key={memo.id} memo={memo} lazyRendering showVisibility showPinnedStyle showParent />
|
||||||
))}
|
))}
|
||||||
{isRequesting && (
|
{isRequesting && (
|
||||||
@@ -66,7 +79,7 @@ const Home = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
memos.length === 0 && (
|
sortedMemos.length === 0 && (
|
||||||
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
|
||||||
<Empty />
|
<Empty />
|
||||||
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
|
||||||
|
Reference in New Issue
Block a user