mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement search multi tags
This commit is contained in:
@ -711,7 +711,7 @@ func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMe
|
|||||||
|
|
||||||
memoFind := &store.FindMemo{
|
memoFind := &store.FindMemo{
|
||||||
CreatorID: &user.ID,
|
CreatorID: &user.ID,
|
||||||
PayloadFind: &store.FindMemoPayload{Tag: &request.OldTag},
|
PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.OldTag}},
|
||||||
ExcludeComments: true,
|
ExcludeComments: true,
|
||||||
}
|
}
|
||||||
if (request.Parent) != "memos/-" {
|
if (request.Parent) != "memos/-" {
|
||||||
@ -765,7 +765,7 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe
|
|||||||
|
|
||||||
memoFind := &store.FindMemo{
|
memoFind := &store.FindMemo{
|
||||||
CreatorID: &user.ID,
|
CreatorID: &user.ID,
|
||||||
PayloadFind: &store.FindMemoPayload{Tag: &request.Tag},
|
PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.Tag}},
|
||||||
ExcludeContent: true,
|
ExcludeContent: true,
|
||||||
ExcludeComments: true,
|
ExcludeComments: true,
|
||||||
}
|
}
|
||||||
@ -928,11 +928,11 @@ func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.
|
|||||||
if len(filter.Visibilities) > 0 {
|
if len(filter.Visibilities) > 0 {
|
||||||
find.VisibilityList = filter.Visibilities
|
find.VisibilityList = filter.Visibilities
|
||||||
}
|
}
|
||||||
if filter.Tag != nil {
|
if filter.TagSearch != nil {
|
||||||
if find.PayloadFind == nil {
|
if find.PayloadFind == nil {
|
||||||
find.PayloadFind = &store.FindMemoPayload{}
|
find.PayloadFind = &store.FindMemoPayload{}
|
||||||
}
|
}
|
||||||
find.PayloadFind.Tag = filter.Tag
|
find.PayloadFind.TagSearch = filter.TagSearch
|
||||||
}
|
}
|
||||||
if filter.OrderByPinned {
|
if filter.OrderByPinned {
|
||||||
find.OrderByPinned = filter.OrderByPinned
|
find.OrderByPinned = filter.OrderByPinned
|
||||||
@ -1039,7 +1039,7 @@ func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
|
|||||||
var MemoFilterCELAttributes = []cel.EnvOption{
|
var MemoFilterCELAttributes = []cel.EnvOption{
|
||||||
cel.Variable("content_search", cel.ListType(cel.StringType)),
|
cel.Variable("content_search", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
cel.Variable("visibilities", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("tag", cel.StringType),
|
cel.Variable("tag_search", cel.ListType(cel.StringType)),
|
||||||
cel.Variable("order_by_pinned", cel.BoolType),
|
cel.Variable("order_by_pinned", cel.BoolType),
|
||||||
cel.Variable("display_time_before", cel.IntType),
|
cel.Variable("display_time_before", cel.IntType),
|
||||||
cel.Variable("display_time_after", cel.IntType),
|
cel.Variable("display_time_after", cel.IntType),
|
||||||
@ -1058,7 +1058,7 @@ var MemoFilterCELAttributes = []cel.EnvOption{
|
|||||||
type MemoFilter struct {
|
type MemoFilter struct {
|
||||||
ContentSearch []string
|
ContentSearch []string
|
||||||
Visibilities []store.Visibility
|
Visibilities []store.Visibility
|
||||||
Tag *string
|
TagSearch []string
|
||||||
OrderByPinned bool
|
OrderByPinned bool
|
||||||
DisplayTimeBefore *int64
|
DisplayTimeBefore *int64
|
||||||
DisplayTimeAfter *int64
|
DisplayTimeAfter *int64
|
||||||
@ -1110,9 +1110,13 @@ func findMemoField(callExpr *expr.Expr_Call, filter *MemoFilter) {
|
|||||||
visibilities = append(visibilities, store.Visibility(value))
|
visibilities = append(visibilities, store.Visibility(value))
|
||||||
}
|
}
|
||||||
filter.Visibilities = visibilities
|
filter.Visibilities = visibilities
|
||||||
} else if idExpr.Name == "tag" {
|
} else if idExpr.Name == "tag_search" {
|
||||||
tag := callExpr.Args[1].GetConstExpr().GetStringValue()
|
tagSearch := []string{}
|
||||||
filter.Tag = &tag
|
for _, expr := range callExpr.Args[1].GetListExpr().GetElements() {
|
||||||
|
value := expr.GetConstExpr().GetStringValue()
|
||||||
|
tagSearch = append(tagSearch, value)
|
||||||
|
}
|
||||||
|
filter.TagSearch = tagSearch
|
||||||
} else if idExpr.Name == "order_by_pinned" {
|
} else if idExpr.Name == "order_by_pinned" {
|
||||||
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
value := callExpr.Args[1].GetConstExpr().GetBoolValue()
|
||||||
filter.OrderByPinned = value
|
filter.OrderByPinned = value
|
||||||
|
@ -90,8 +90,10 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||||||
if v.Raw != nil {
|
if v.Raw != nil {
|
||||||
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
|
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
|
||||||
}
|
}
|
||||||
if v.Tag != nil {
|
if len(v.TagSearch) != 0 {
|
||||||
where, args = append(where, "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.property.tags'), ?)"), append(args, fmt.Sprintf(`["%s"]`, *v.Tag))
|
for _, tag := range v.TagSearch {
|
||||||
|
where, args = append(where, "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.property.tags'), ?)"), append(args, fmt.Sprintf(`%%"%s"%%`, tag))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v.HasLink {
|
if v.HasLink {
|
||||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
|
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
|
||||||
|
@ -81,8 +81,10 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||||||
if v.Raw != nil {
|
if v.Raw != nil {
|
||||||
where, args = append(where, "memo.payload = "+placeholder(len(args)+1)), append(args, *v.Raw)
|
where, args = append(where, "memo.payload = "+placeholder(len(args)+1)), append(args, *v.Raw)
|
||||||
}
|
}
|
||||||
if v.Tag != nil {
|
if len(v.TagSearch) != 0 {
|
||||||
where, args = append(where, "memo.payload->'property'->'tags' @> "+placeholder(len(args)+1)), append(args, fmt.Sprintf(`["%s"]`, *v.Tag))
|
for _, tag := range v.TagSearch {
|
||||||
|
where, args = append(where, "memo.payload->'property'->'tags' @> "+placeholder(len(args)+1)), append(args, fmt.Sprintf(`["%s"]`, tag))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v.HasLink {
|
if v.HasLink {
|
||||||
where = append(where, "(memo.payload->'property'->>'hasLink')::BOOLEAN IS TRUE")
|
where = append(where, "(memo.payload->'property'->>'hasLink')::BOOLEAN IS TRUE")
|
||||||
|
@ -82,8 +82,10 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||||||
if v.Raw != nil {
|
if v.Raw != nil {
|
||||||
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
|
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
|
||||||
}
|
}
|
||||||
if v.Tag != nil {
|
if len(v.TagSearch) != 0 {
|
||||||
where, args = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.tags') LIKE ?"), append(args, fmt.Sprintf(`%%"%s"%%`, *v.Tag))
|
for _, tag := range v.TagSearch {
|
||||||
|
where, args = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.tags') LIKE ?"), append(args, fmt.Sprintf(`%%"%s"%%`, tag))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v.HasLink {
|
if v.HasLink {
|
||||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
|
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
|
||||||
|
@ -84,7 +84,7 @@ type FindMemo struct {
|
|||||||
|
|
||||||
type FindMemoPayload struct {
|
type FindMemoPayload struct {
|
||||||
Raw *string
|
Raw *string
|
||||||
Tag *string
|
TagSearch []string
|
||||||
HasLink bool
|
HasLink bool
|
||||||
HasTaskList bool
|
HasTaskList bool
|
||||||
HasCode bool
|
HasCode bool
|
||||||
|
@ -36,12 +36,12 @@ const TagsSection = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTagClick = (tag: string) => {
|
const handleTagClick = (tag: string) => {
|
||||||
const isActive = memoFilterStore.getFiltersByFactor("tag").some((filter) => filter.value === tag);
|
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === tag);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
memoFilterStore.removeFilter((f) => f.factor === "tag" && f.value === tag);
|
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === tag);
|
||||||
} else {
|
} else {
|
||||||
memoFilterStore.addFilter({
|
memoFilterStore.addFilter({
|
||||||
factor: "tag",
|
factor: "tagSearch",
|
||||||
value: tag,
|
value: tag,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,12 @@ const Tag: React.FC<Props> = ({ content }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActive = memoFilterStore.getFiltersByFactor("tag").some((filter) => filter.value === content);
|
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === content);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
memoFilterStore.removeFilter((f) => f.factor === "tag" && f.value === content);
|
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === content);
|
||||||
} else {
|
} else {
|
||||||
memoFilterStore.addFilter({
|
memoFilterStore.addFilter({
|
||||||
factor: "tag",
|
factor: "tagSearch",
|
||||||
value: content,
|
value: content,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ const MemoFilters = () => {
|
|||||||
|
|
||||||
const FactorIcon = ({ factor, className }: { factor: FilterFactor; className?: string }) => {
|
const FactorIcon = ({ factor, className }: { factor: FilterFactor; className?: string }) => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
tag: <Icon.Tag className={className} />,
|
tagSearch: <Icon.Tag className={className} />,
|
||||||
visibility: <Icon.Eye className={className} />,
|
visibility: <Icon.Eye className={className} />,
|
||||||
contentSearch: <Icon.Search className={className} />,
|
contentSearch: <Icon.Search className={className} />,
|
||||||
displayTime: <Icon.Calendar className={className} />,
|
displayTime: <Icon.Calendar className={className} />,
|
||||||
|
@ -78,17 +78,17 @@ interface TagItemContainerProps {
|
|||||||
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {
|
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {
|
||||||
const { tag } = props;
|
const { tag } = props;
|
||||||
const memoFilterStore = useMemoFilterStore();
|
const memoFilterStore = useMemoFilterStore();
|
||||||
const tagFilters = memoFilterStore.getFiltersByFactor("tag");
|
const tagFilters = memoFilterStore.getFiltersByFactor("tagSearch");
|
||||||
const isActive = tagFilters.some((f) => f.value === tag.text);
|
const isActive = tagFilters.some((f) => f.value === tag.text);
|
||||||
const hasSubTags = tag.subTags.length > 0;
|
const hasSubTags = tag.subTags.length > 0;
|
||||||
const [showSubTags, toggleSubTags] = useToggle(false);
|
const [showSubTags, toggleSubTags] = useToggle(false);
|
||||||
|
|
||||||
const handleTagClick = () => {
|
const handleTagClick = () => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
memoFilterStore.removeFilter((f) => f.factor === "tag" && f.value === tag.text);
|
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === tag.text);
|
||||||
} else {
|
} else {
|
||||||
memoFilterStore.addFilter({
|
memoFilterStore.addFilter({
|
||||||
factor: "tag",
|
factor: "tagSearch",
|
||||||
value: tag.text,
|
value: tag.text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -37,16 +37,20 @@ const Archived = () => {
|
|||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const filters = [`creator == "${user.name}"`, `row_status == "ARCHIVED"`];
|
const filters = [`creator == "${user.name}"`, `row_status == "ARCHIVED"`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
|
const tagSearch: string[] = [];
|
||||||
for (const filter of memoFilterStore.filters) {
|
for (const filter of memoFilterStore.filters) {
|
||||||
if (filter.factor === "contentSearch") {
|
if (filter.factor === "contentSearch") {
|
||||||
contentSearch.push(`"${filter.value}"`);
|
contentSearch.push(`"${filter.value}"`);
|
||||||
} else if (filter.factor === "tag") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
filters.push(`tag == "${filter.value}"`);
|
tagSearch.push(`"${filter.value}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
}
|
}
|
||||||
|
if (tagSearch.length > 0) {
|
||||||
|
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
const response = await memoStore.fetchMemos({
|
const response = await memoStore.fetchMemos({
|
||||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
|
@ -34,16 +34,20 @@ const Explore = () => {
|
|||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
|
const tagSearch: string[] = [];
|
||||||
for (const filter of memoFilterStore.filters) {
|
for (const filter of memoFilterStore.filters) {
|
||||||
if (filter.factor === "contentSearch") {
|
if (filter.factor === "contentSearch") {
|
||||||
contentSearch.push(`"${filter.value}"`);
|
contentSearch.push(`"${filter.value}"`);
|
||||||
} else if (filter.factor === "tag") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
filters.push(`tag == "${filter.value}"`);
|
tagSearch.push(`"${filter.value}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
}
|
}
|
||||||
|
if (tagSearch.length > 0) {
|
||||||
|
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
const response = await memoStore.fetchMemos({
|
const response = await memoStore.fetchMemos({
|
||||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
|
@ -39,11 +39,12 @@ const Home = () => {
|
|||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
|
const tagSearch: string[] = [];
|
||||||
for (const filter of memoFilterStore.filters) {
|
for (const filter of memoFilterStore.filters) {
|
||||||
if (filter.factor === "contentSearch") {
|
if (filter.factor === "contentSearch") {
|
||||||
contentSearch.push(`"${filter.value}"`);
|
contentSearch.push(`"${filter.value}"`);
|
||||||
} else if (filter.factor === "tag") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
filters.push(`tag == "${filter.value}"`);
|
tagSearch.push(`"${filter.value}"`);
|
||||||
} else if (filter.factor === "property.hasLink") {
|
} else if (filter.factor === "property.hasLink") {
|
||||||
filters.push(`has_link == true`);
|
filters.push(`has_link == true`);
|
||||||
} else if (filter.factor === "property.hasTaskList") {
|
} else if (filter.factor === "property.hasTaskList") {
|
||||||
@ -55,6 +56,9 @@ const Home = () => {
|
|||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
}
|
}
|
||||||
|
if (tagSearch.length > 0) {
|
||||||
|
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
const response = await memoStore.fetchMemos({
|
const response = await memoStore.fetchMemos({
|
||||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
|
@ -70,16 +70,20 @@ const UserProfile = () => {
|
|||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
|
||||||
const contentSearch: string[] = [];
|
const contentSearch: string[] = [];
|
||||||
|
const tagSearch: string[] = [];
|
||||||
for (const filter of memoFilterStore.filters) {
|
for (const filter of memoFilterStore.filters) {
|
||||||
if (filter.factor === "contentSearch") {
|
if (filter.factor === "contentSearch") {
|
||||||
contentSearch.push(`"${filter.value}"`);
|
contentSearch.push(`"${filter.value}"`);
|
||||||
} else if (filter.factor === "tag") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
filters.push(`tag == "${filter.value}"`);
|
tagSearch.push(`"${filter.value}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentSearch.length > 0) {
|
if (contentSearch.length > 0) {
|
||||||
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
filters.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||||
}
|
}
|
||||||
|
if (tagSearch.length > 0) {
|
||||||
|
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
|
||||||
|
}
|
||||||
const response = await memoStore.fetchMemos({
|
const response = await memoStore.fetchMemos({
|
||||||
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
|
||||||
filter: filters.join(" && "),
|
filter: filters.join(" && "),
|
||||||
|
@ -3,7 +3,7 @@ import { create } from "zustand";
|
|||||||
import { combine } from "zustand/middleware";
|
import { combine } from "zustand/middleware";
|
||||||
|
|
||||||
export type FilterFactor =
|
export type FilterFactor =
|
||||||
| "tag"
|
| "tagSearch"
|
||||||
| "visibility"
|
| "visibility"
|
||||||
| "contentSearch"
|
| "contentSearch"
|
||||||
| "displayTime"
|
| "displayTime"
|
||||||
|
Reference in New Issue
Block a user