From f0e5a722716d9354bd2e0ef2525440804390f069 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 27 May 2024 23:25:25 +0800 Subject: [PATCH] feat: update search memo filter --- server/router/api/v1/memo_service.go | 27 ++++++ store/db/mysql/memo.go | 9 ++ store/db/postgres/memo.go | 9 ++ store/db/sqlite/memo.go | 9 ++ store/memo.go | 7 +- web/src/components/MemoFilter.tsx | 112 ++++++++++++++-------- web/src/components/UserStatisticsView.tsx | 62 ++++++------ web/src/hooks/useFilterWithUrlParams.ts | 3 +- web/src/pages/Home.tsx | 23 +++-- web/src/store/module/filter.ts | 10 +- web/src/store/reducer/filter.ts | 22 ++++- 11 files changed, 216 insertions(+), 77 deletions(-) diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index db2628d2..09e71d2c 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -883,6 +883,9 @@ func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store. if find == nil { find = &store.FindMemo{} } + if find.PayloadFind == nil { + find.PayloadFind = &store.FindMemoPayload{} + } if filter != "" { filter, err := parseSearchMemosFilter(filter) if err != nil { @@ -956,6 +959,15 @@ func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store. if filter.IncludeComments { find.ExcludeComments = false } + if filter.HasLink { + find.PayloadFind.HasLink = true + } + if filter.HasTaskList { + find.PayloadFind.HasTaskList = true + } + if filter.HasCode { + find.PayloadFind.HasCode = true + } } user, err := s.GetCurrentUser(ctx) @@ -1006,6 +1018,9 @@ var SearchMemosFilterCELAttributes = []cel.EnvOption{ cel.Variable("random", cel.BoolType), cel.Variable("limit", cel.IntType), cel.Variable("include_comments", cel.BoolType), + cel.Variable("has_link", cel.BoolType), + cel.Variable("has_task_list", cel.BoolType), + cel.Variable("has_code", cel.BoolType), } type SearchMemosFilter struct { @@ -1021,6 +1036,9 @@ type SearchMemosFilter struct { Random bool Limit *int IncludeComments bool + HasLink bool + HasTaskList bool + HasCode bool } func parseSearchMemosFilter(expression string) (*SearchMemosFilter, error) { @@ -1090,6 +1108,15 @@ func findSearchMemosField(callExpr *expr.Expr_Call, filter *SearchMemosFilter) { } else if idExpr.Name == "include_comments" { value := callExpr.Args[1].GetConstExpr().GetBoolValue() filter.IncludeComments = value + } else if idExpr.Name == "has_link" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasLink = value + } else if idExpr.Name == "has_task_list" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasTaskList = value + } else if idExpr.Name == "has_code" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasCode = value } return } diff --git a/store/db/mysql/memo.go b/store/db/mysql/memo.go index 15b19dc6..e673010a 100644 --- a/store/db/mysql/memo.go +++ b/store/db/mysql/memo.go @@ -93,6 +93,15 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo if v.Tag != nil { where, args = append(where, "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.property.tags'), ?)"), append(args, fmt.Sprintf(`["%s"]`, *v.Tag)) } + if v.HasLink { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE") + } + if v.HasTaskList { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE") + } + if v.HasCode { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE") + } } if find.ExcludeComments { having = append(having, "`parent_id` IS NULL") diff --git a/store/db/postgres/memo.go b/store/db/postgres/memo.go index 2ec9956d..a89be252 100644 --- a/store/db/postgres/memo.go +++ b/store/db/postgres/memo.go @@ -84,6 +84,15 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo if v.Tag != nil { where, args = append(where, "memo.payload->'property'->'tags' @> "+placeholder(len(args)+1)), append(args, fmt.Sprintf(`["%s"]`, *v.Tag)) } + if v.HasLink { + where = append(where, "(memo.payload->'property'->>'hasLink')::BOOLEAN IS TRUE") + } + if v.HasTaskList { + where = append(where, "(memo.payload->'property'->>'hasTaskList')::BOOLEAN IS TRUE") + } + if v.HasCode { + where = append(where, "(memo.payload->'property'->>'hasCode')::BOOLEAN IS TRUE") + } } if find.ExcludeComments { where = append(where, "memo_relation.related_memo_id IS NULL") diff --git a/store/db/sqlite/memo.go b/store/db/sqlite/memo.go index a90418c1..11ca0c04 100644 --- a/store/db/sqlite/memo.go +++ b/store/db/sqlite/memo.go @@ -85,6 +85,15 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo if v.Tag != nil { where, args = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.tags') LIKE ?"), append(args, fmt.Sprintf(`%%"%s"%%`, *v.Tag)) } + if v.HasLink { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE") + } + if v.HasTaskList { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE") + } + if v.HasCode { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE") + } } if find.ExcludeComments { where = append(where, "`parent_id` IS NULL") diff --git a/store/memo.go b/store/memo.go index cabd96b5..b2068b19 100644 --- a/store/memo.go +++ b/store/memo.go @@ -83,8 +83,11 @@ type FindMemo struct { } type FindMemoPayload struct { - Raw *string - Tag *string + Raw *string + Tag *string + HasLink bool + HasTaskList bool + HasCode bool } type UpdateMemo struct { diff --git a/web/src/components/MemoFilter.tsx b/web/src/components/MemoFilter.tsx index 1ae2d2a5..534f34fa 100644 --- a/web/src/components/MemoFilter.tsx +++ b/web/src/components/MemoFilter.tsx @@ -14,8 +14,14 @@ const MemoFilter = (props: Props) => { const location = useLocation(); const filterStore = useFilterStore(); const filter = filterStore.state; - const { tag: tagQuery, text: textQuery, visibility } = filter; - const showFilter = Boolean(tagQuery || textQuery || visibility); + const showFilter = Boolean( + filter.tag || + filter.text || + filter.visibility || + filter.memoPropertyFilter?.hasLink || + filter.memoPropertyFilter?.hasTaskList || + filter.memoPropertyFilter?.hasCode, + ); useEffect(() => { filterStore.clearFilter(); @@ -36,42 +42,72 @@ const MemoFilter = (props: Props) => { {t("common.filter")}: -
{ - filterStore.setTagFilter(undefined); - }} - > - {tagQuery} - -
-
{ - filterStore.setMemoVisibilityFilter(undefined); - }} - > - {visibility} - -
-
{ - filterStore.setTextFilter(undefined); - }} - > - {textQuery} - -
+ {filter.tag && ( +
{ + filterStore.setTagFilter(undefined); + }} + > + {filter.tag} + +
+ )} + {filter.visibility && ( +
{ + filterStore.setMemoVisibilityFilter(undefined); + }} + > + {filter.visibility} + +
+ )} + {filter.text && ( +
{ + filterStore.setTextFilter(undefined); + }} + > + {filter.text} + +
+ )} + {filter.memoPropertyFilter?.hasLink && ( +
{ + filterStore.setMemoPropertyFilter({ hasLink: false }); + }} + > + Has Link + +
+ )} + {filter.memoPropertyFilter?.hasTaskList && ( +
{ + filterStore.setMemoPropertyFilter({ hasTaskList: false }); + }} + > + Has Task + +
+ )} + {filter.memoPropertyFilter?.hasCode && ( +
{ + filterStore.setMemoPropertyFilter({ hasCode: false }); + }} + > + Has Code + +
+ )} ); }; diff --git a/web/src/components/UserStatisticsView.tsx b/web/src/components/UserStatisticsView.tsx index e7b72985..b17aa3b5 100644 --- a/web/src/components/UserStatisticsView.tsx +++ b/web/src/components/UserStatisticsView.tsx @@ -1,6 +1,8 @@ +import { Divider } from "@mui/joy"; import { useEffect, useState } from "react"; import { memoServiceClient } from "@/grpcweb"; -import { useMemoStore, useTagStore } from "@/store/v1"; +import { useFilterStore } from "@/store/module"; +import { useMemoStore } from "@/store/v1"; import { User } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; @@ -19,13 +21,12 @@ const UserStatisticsView = (props: Props) => { const { user } = props; const t = useTranslate(); const memoStore = useMemoStore(); - const tagStore = useTagStore(); + const filterStore = useFilterStore(); const [memoAmount, setMemoAmount] = useState(0); const [isRequesting, setIsRequesting] = useState(false); const [memoStats, setMemoStats] = useState({ links: 0, todos: 0, code: 0 }); const days = Math.ceil((Date.now() - user.createTime!.getTime()) / 86400000); const memos = Object.values(memoStore.getState().memoMapByName); - const tags = tagStore.sortedTags().length; useEffect(() => { if (memos.length === 0) { @@ -60,7 +61,7 @@ const UserStatisticsView = (props: Props) => {

{t("common.statistics")}

-
+
@@ -75,33 +76,38 @@ const UserStatisticsView = (props: Props) => {
{isRequesting ? : {memoAmount}}
-
-
- - {t("common.tags")} + +
+
filterStore.setMemoPropertyFilter({ hasLink: true })} + > +
+ + Links +
+ {memoStats.links}
- {tags} -
-
-
- - Links +
filterStore.setMemoPropertyFilter({ hasTaskList: true })} + > +
+ + Todos +
+ {memoStats.todos}
- {memoStats.links} -
-
-
- - Todos +
filterStore.setMemoPropertyFilter({ hasCode: true })} + > +
+ + Code +
+ {memoStats.code}
- {memoStats.todos} -
-
-
- - Code -
- {memoStats.code}
diff --git a/web/src/hooks/useFilterWithUrlParams.ts b/web/src/hooks/useFilterWithUrlParams.ts index 57be7ccf..6b907c4c 100644 --- a/web/src/hooks/useFilterWithUrlParams.ts +++ b/web/src/hooks/useFilterWithUrlParams.ts @@ -5,7 +5,7 @@ import { useFilterStore } from "@/store/module"; const useFilterWithUrlParams = () => { const location = useLocation(); const filterStore = useFilterStore(); - const { tag, text } = filterStore.state; + const { tag, text, memoPropertyFilter } = filterStore.state; useEffect(() => { const urlParams = new URLSearchParams(location.search); @@ -38,6 +38,7 @@ const useFilterWithUrlParams = () => { return { tag, text, + memoPropertyFilter, }; }; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 98e07026..60b1e227 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -25,7 +25,7 @@ const Home = () => { const memoList = useMemoList(); const [isRequesting, setIsRequesting] = useState(true); const [nextPageToken, setNextPageToken] = useState(""); - const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); + const filter = useFilterWithUrlParams(); const sortedMemos = memoList.value .filter((memo) => memo.rowStatus === RowStatus.ACTIVE) .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)) @@ -34,20 +34,31 @@ const Home = () => { useEffect(() => { memoList.reset(); fetchMemos(""); - }, [tagQuery, textQuery]); + }, [filter.tag, filter.text, filter.memoPropertyFilter]); const fetchMemos = async (nextPageToken: string) => { setIsRequesting(true); const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`]; const contentSearch: string[] = []; - if (textQuery) { - contentSearch.push(JSON.stringify(textQuery)); + if (filter.tag) { + contentSearch.push(JSON.stringify(filter.tag)); } if (contentSearch.length > 0) { filters.push(`content_search == [${contentSearch.join(", ")}]`); } - if (tagQuery) { - filters.push(`tag == "${tagQuery}"`); + if (filter.text) { + filters.push(`tag == "${filter.text}"`); + } + if (filter.memoPropertyFilter) { + if (filter.memoPropertyFilter.hasLink) { + filters.push(`has_link == true`); + } + if (filter.memoPropertyFilter.hasTaskList) { + filters.push(`has_task_list == true`); + } + if (filter.memoPropertyFilter.hasCode) { + filters.push(`has_code == true`); + } } const response = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, diff --git a/web/src/store/module/filter.ts b/web/src/store/module/filter.ts index bb491777..913065d9 100644 --- a/web/src/store/module/filter.ts +++ b/web/src/store/module/filter.ts @@ -1,6 +1,6 @@ import { Visibility } from "@/types/proto/api/v1/memo_service"; import store, { useAppSelector } from ".."; -import { setFilter } from "../reducer/filter"; +import { MemoPropertyFilter, setFilter, setMemoPropertyFilter } from "../reducer/filter"; export const useFilterStore = () => { const state = useAppSelector((state) => state.filter); @@ -16,6 +16,7 @@ export const useFilterStore = () => { tag: undefined, text: undefined, visibility: undefined, + memoPropertyFilter: undefined, }), ); }, @@ -40,5 +41,12 @@ export const useFilterStore = () => { }), ); }, + setMemoPropertyFilter: (memoPropertyFilter: Partial) => { + store.dispatch( + setMemoPropertyFilter({ + ...memoPropertyFilter, + }), + ); + }, }; }; diff --git a/web/src/store/reducer/filter.ts b/web/src/store/reducer/filter.ts index 59ea8cf1..b42feab1 100644 --- a/web/src/store/reducer/filter.ts +++ b/web/src/store/reducer/filter.ts @@ -5,6 +5,13 @@ interface State { tag?: string; text?: string; visibility?: Visibility; + memoPropertyFilter?: MemoPropertyFilter; +} + +export interface MemoPropertyFilter { + hasLink?: boolean; + hasTaskList?: boolean; + hasCode?: boolean; } export type Filter = State; @@ -37,9 +44,22 @@ const filterSlice = createSlice({ ...action.payload, }; }, + setMemoPropertyFilter: (state, action: PayloadAction>) => { + if (JSON.stringify(action.payload) === state.memoPropertyFilter) { + return state; + } + + return { + ...state, + memoPropertyFilter: { + ...state.memoPropertyFilter, + ...action.payload, + }, + }; + }, }, }); -export const { setFilter } = filterSlice.actions; +export const { setFilter, setMemoPropertyFilter } = filterSlice.actions; export default filterSlice.reducer;