mirror of https://github.com/usememos/memos.git
refactor(web): refactor MemoFilters component and add comprehensive filter support
- Refactored MemoFilters.tsx for better maintainability: * Centralized filter configuration with FILTER_CONFIGS object * Added TypeScript interfaces for type safety * Removed separate FactorIcon component * Extracted handleRemoveFilter function * Improved imports organization - Polished MemoFilters UI styles: * Changed to modern pill/badge design with rounded-full * Enhanced spacing and color schemes * Added smooth transitions and hover effects * Improved interactive remove button with destructive color hints * Better text readability with font-medium - Added comprehensive filter support to all pages: * Explore page: Added full filter support (was missing) * Archived page: Enhanced from basic to full filter support * UserProfile page: Enhanced from basic to full filter support * All pages now support: content search, tag search, pinned, hasLink, hasTaskList, hasCode, and displayTime filters - Consistency improvements: * All pages using PagedMemoList now have identical filter logic * Respects workspace settings for display time (created/updated) * Unified filter behavior across Home, Explore, Archived, and UserProfile pages
This commit is contained in:
parent
1e954070b9
commit
e915e3a46b
|
|
@ -2,7 +2,6 @@ import { observer } from "mobx-react-lite";
|
|||
import SearchBar from "@/components/SearchBar";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { cn } from "@/lib/utils";
|
||||
import MemoFilters from "../MemoFilters";
|
||||
import StatisticsView from "../StatisticsView";
|
||||
import ShortcutsSection from "./ShortcutsSection";
|
||||
import TagsSection from "./TagsSection";
|
||||
|
|
@ -22,7 +21,6 @@ const HomeSidebar = observer((props: Props) => {
|
|||
)}
|
||||
>
|
||||
<SearchBar />
|
||||
<MemoFilters />
|
||||
<div className="mt-1 px-1 w-full">
|
||||
<StatisticsView />
|
||||
{currentUser && <ShortcutsSection />}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import { isEqual } from "lodash-es";
|
||||
import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, HashIcon, LinkIcon, BookmarkIcon, SearchIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CalendarIcon,
|
||||
CheckCircleIcon,
|
||||
CodeIcon,
|
||||
EyeIcon,
|
||||
HashIcon,
|
||||
LinkIcon,
|
||||
LucideIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
|
@ -7,6 +18,46 @@ import { memoFilterStore } from "@/store";
|
|||
import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters } from "@/store/memoFilter";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
interface FilterConfig {
|
||||
icon: LucideIcon;
|
||||
getLabel: (value: string, t: ReturnType<typeof useTranslate>) => string;
|
||||
}
|
||||
|
||||
const FILTER_CONFIGS: Record<FilterFactor, FilterConfig> = {
|
||||
tagSearch: {
|
||||
icon: HashIcon,
|
||||
getLabel: (value) => value,
|
||||
},
|
||||
visibility: {
|
||||
icon: EyeIcon,
|
||||
getLabel: (value) => value,
|
||||
},
|
||||
contentSearch: {
|
||||
icon: SearchIcon,
|
||||
getLabel: (value) => value,
|
||||
},
|
||||
displayTime: {
|
||||
icon: CalendarIcon,
|
||||
getLabel: (value) => value,
|
||||
},
|
||||
pinned: {
|
||||
icon: BookmarkIcon,
|
||||
getLabel: (value) => value,
|
||||
},
|
||||
"property.hasLink": {
|
||||
icon: LinkIcon,
|
||||
getLabel: (_, t) => t("filters.has-link"),
|
||||
},
|
||||
"property.hasTaskList": {
|
||||
icon: CheckCircleIcon,
|
||||
getLabel: (_, t) => t("filters.has-task-list"),
|
||||
},
|
||||
"property.hasCode": {
|
||||
icon: CodeIcon,
|
||||
getLabel: (_, t) => t("filters.has-code"),
|
||||
},
|
||||
};
|
||||
|
||||
const MemoFilters = observer(() => {
|
||||
const t = useTranslate();
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
|
@ -18,63 +69,51 @@ const MemoFilters = observer(() => {
|
|||
searchParams.set("filter", stringifyFilters(filters));
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
}, [filters]);
|
||||
}, [filters, setSearchParams]);
|
||||
|
||||
const getFilterDisplayText = (filter: MemoFilter) => {
|
||||
if (filter.value) {
|
||||
return filter.value;
|
||||
const handleRemoveFilter = (filter: MemoFilter) => {
|
||||
memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter));
|
||||
};
|
||||
|
||||
const getFilterDisplayText = (filter: MemoFilter): string => {
|
||||
const config = FILTER_CONFIGS[filter.factor];
|
||||
if (!config) {
|
||||
return filter.value || filter.factor;
|
||||
}
|
||||
if (filter.factor.startsWith("property.")) {
|
||||
const factorLabel = filter.factor.replace("property.", "");
|
||||
switch (factorLabel) {
|
||||
case "hasLink":
|
||||
return t("filters.has-link");
|
||||
case "hasCode":
|
||||
return t("filters.has-code");
|
||||
case "hasTaskList":
|
||||
return t("filters.has-task-list");
|
||||
default:
|
||||
return factorLabel;
|
||||
}
|
||||
}
|
||||
return filter.factor;
|
||||
return config.getLabel(filter.value, t);
|
||||
};
|
||||
|
||||
if (filters.length === 0) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mt-2 flex flex-row justify-start items-center flex-wrap gap-x-2 gap-y-1">
|
||||
{filters.map((filter: MemoFilter) => (
|
||||
<div
|
||||
key={getMemoFilterKey(filter)}
|
||||
className="w-auto leading-7 h-7 shrink-0 flex flex-row items-center gap-1 bg-background border pl-1.5 pr-1 rounded-md hover:line-through cursor-pointer"
|
||||
onClick={() => memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter))}
|
||||
>
|
||||
<FactorIcon className="w-4 h-auto text-muted-foreground opacity-60" factor={filter.factor} />
|
||||
<span className="text-muted-foreground text-sm max-w-32 truncate">{getFilterDisplayText(filter)}</span>
|
||||
<button className="text-muted-foreground opacity-60 hover:opacity-100">
|
||||
<XIcon className="w-4 h-auto" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full mb-2 flex flex-row justify-start items-center flex-wrap gap-2">
|
||||
{filters.map((filter) => {
|
||||
const config = FILTER_CONFIGS[filter.factor];
|
||||
const Icon = config?.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={getMemoFilterKey(filter)}
|
||||
className="group inline-flex items-center gap-1.5 h-7 px-2.5 bg-accent/50 hover:bg-accent border border-border/50 rounded-full text-sm transition-all duration-200 hover:shadow-sm"
|
||||
>
|
||||
{Icon && <Icon className="w-3.5 h-3.5 text-muted-foreground shrink-0" />}
|
||||
<span className="text-foreground/80 font-medium max-w-32 truncate">{getFilterDisplayText(filter)}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveFilter(filter)}
|
||||
className="ml-0.5 -mr-1 p-0.5 text-muted-foreground/60 hover:text-destructive hover:bg-destructive/10 rounded-full transition-colors"
|
||||
aria-label="Remove filter"
|
||||
>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const FactorIcon = ({ factor, className }: { factor: FilterFactor; className?: string }) => {
|
||||
const iconMap = {
|
||||
tagSearch: <HashIcon className={className} />,
|
||||
visibility: <EyeIcon className={className} />,
|
||||
contentSearch: <SearchIcon className={className} />,
|
||||
displayTime: <CalendarIcon className={className} />,
|
||||
pinned: <BookmarkIcon className={className} />,
|
||||
"property.hasLink": <LinkIcon className={className} />,
|
||||
"property.hasTaskList": <CheckCircleIcon className={className} />,
|
||||
"property.hasCode": <CodeIcon className={className} />,
|
||||
};
|
||||
return iconMap[factor as keyof typeof iconMap] || <></>;
|
||||
};
|
||||
MemoFilters.displayName = "MemoFilters";
|
||||
|
||||
export default MemoFilters;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useTranslate } from "@/utils/i18n";
|
|||
import Empty from "../Empty";
|
||||
import MasonryView, { MemoRenderContext } from "../MasonryView";
|
||||
import MemoEditor from "../MemoEditor";
|
||||
import MemoFilters from "../MemoFilters";
|
||||
import MemoSkeleton from "../MemoSkeleton";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -153,7 +154,12 @@ const PagedMemoList = observer((props: Props) => {
|
|||
<MasonryView
|
||||
memoList={sortedMemoList}
|
||||
renderer={props.renderer}
|
||||
prefixElement={showMemoEditor ? <MemoEditor className="mb-2" cacheKey="home-memo-editor" /> : undefined}
|
||||
prefixElement={
|
||||
<>
|
||||
{showMemoEditor ? <MemoEditor className="mb-2" cacheKey="home-memo-editor" /> : undefined}
|
||||
<MemoFilters />
|
||||
</>
|
||||
}
|
||||
listMode={viewStore.state.layout === "LIST"}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ import { MemoRenderContext } from "@/components/MasonryView";
|
|||
import MemoView from "@/components/MemoView";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { viewStore } from "@/store";
|
||||
import { viewStore, workspaceStore } from "@/store";
|
||||
import { extractUserIdFromName } from "@/store/common";
|
||||
import memoFilterStore from "@/store/memoFilter";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
|
||||
|
||||
const Archived = observer(() => {
|
||||
const user = useCurrentUser();
|
||||
|
||||
// Build filter from active filters - no useMemo needed since component is MobX observer
|
||||
// Build filter from active filters
|
||||
const buildMemoFilter = () => {
|
||||
const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`];
|
||||
for (const filter of memoFilterStore.filters) {
|
||||
|
|
@ -21,6 +22,20 @@ const Archived = observer(() => {
|
|||
conditions.push(`content.contains("${filter.value}")`);
|
||||
} else if (filter.factor === "tagSearch") {
|
||||
conditions.push(`tag in ["${filter.value}"]`);
|
||||
} else if (filter.factor === "property.hasLink") {
|
||||
conditions.push(`has_link`);
|
||||
} else if (filter.factor === "property.hasTaskList") {
|
||||
conditions.push(`has_task_list`);
|
||||
} else if (filter.factor === "property.hasCode") {
|
||||
conditions.push(`has_code`);
|
||||
} else if (filter.factor === "displayTime") {
|
||||
const displayWithUpdateTime = workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.MEMO_RELATED).memoRelatedSetting
|
||||
?.displayWithUpdateTime;
|
||||
const factor = displayWithUpdateTime ? "updated_ts" : "created_ts";
|
||||
const filterDate = new Date(filter.value);
|
||||
const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;
|
||||
const timestampAfter = filterUtcTimestamp / 1000;
|
||||
conditions.push(`${factor} >= ${timestampAfter} && ${factor} < ${timestampAfter + 60 * 60 * 24}`);
|
||||
}
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(" && ") : undefined;
|
||||
|
|
|
|||
|
|
@ -5,13 +5,44 @@ import MemoView from "@/components/MemoView";
|
|||
import MobileHeader from "@/components/MobileHeader";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
import { viewStore } from "@/store";
|
||||
import { viewStore, workspaceStore } from "@/store";
|
||||
import memoFilterStore from "@/store/memoFilter";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
|
||||
|
||||
const Explore = observer(() => {
|
||||
const { md } = useResponsiveWidth();
|
||||
|
||||
// Build filter from active filters
|
||||
const buildMemoFilter = () => {
|
||||
const conditions: string[] = [];
|
||||
for (const filter of memoFilterStore.filters) {
|
||||
if (filter.factor === "contentSearch") {
|
||||
conditions.push(`content.contains("${filter.value}")`);
|
||||
} else if (filter.factor === "tagSearch") {
|
||||
conditions.push(`tag in ["${filter.value}"]`);
|
||||
} else if (filter.factor === "property.hasLink") {
|
||||
conditions.push(`has_link`);
|
||||
} else if (filter.factor === "property.hasTaskList") {
|
||||
conditions.push(`has_task_list`);
|
||||
} else if (filter.factor === "property.hasCode") {
|
||||
conditions.push(`has_code`);
|
||||
} else if (filter.factor === "displayTime") {
|
||||
const displayWithUpdateTime = workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.MEMO_RELATED).memoRelatedSetting
|
||||
?.displayWithUpdateTime;
|
||||
const factor = displayWithUpdateTime ? "updated_ts" : "created_ts";
|
||||
const filterDate = new Date(filter.value);
|
||||
const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;
|
||||
const timestampAfter = filterUtcTimestamp / 1000;
|
||||
conditions.push(`${factor} >= ${timestampAfter} && ${factor} < ${timestampAfter + 60 * 60 * 24}`);
|
||||
}
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(" && ") : undefined;
|
||||
};
|
||||
|
||||
const memoFilter = buildMemoFilter();
|
||||
|
||||
return (
|
||||
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
||||
{!md && <MobileHeader />}
|
||||
|
|
@ -30,6 +61,7 @@ const Explore = observer(() => {
|
|||
)
|
||||
}
|
||||
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
||||
filter={memoFilter}
|
||||
showCreator
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ import PagedMemoList from "@/components/PagedMemoList";
|
|||
import UserAvatar from "@/components/UserAvatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { viewStore, userStore } from "@/store";
|
||||
import { viewStore, userStore, workspaceStore } from "@/store";
|
||||
import { extractUserIdFromName } from "@/store/common";
|
||||
import memoFilterStore from "@/store/memoFilter";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { User } from "@/types/proto/api/v1/user_service";
|
||||
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
const UserProfile = observer(() => {
|
||||
|
|
@ -43,7 +44,7 @@ const UserProfile = observer(() => {
|
|||
});
|
||||
}, [params.username]);
|
||||
|
||||
// Build filter from active filters - no useMemo needed since component is MobX observer
|
||||
// Build filter from active filters
|
||||
const buildMemoFilter = () => {
|
||||
if (!user) {
|
||||
return undefined;
|
||||
|
|
@ -55,6 +56,22 @@ const UserProfile = observer(() => {
|
|||
conditions.push(`content.contains("${filter.value}")`);
|
||||
} else if (filter.factor === "tagSearch") {
|
||||
conditions.push(`tag in ["${filter.value}"]`);
|
||||
} else if (filter.factor === "pinned") {
|
||||
conditions.push(`pinned`);
|
||||
} else if (filter.factor === "property.hasLink") {
|
||||
conditions.push(`has_link`);
|
||||
} else if (filter.factor === "property.hasTaskList") {
|
||||
conditions.push(`has_task_list`);
|
||||
} else if (filter.factor === "property.hasCode") {
|
||||
conditions.push(`has_code`);
|
||||
} else if (filter.factor === "displayTime") {
|
||||
const displayWithUpdateTime = workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.MEMO_RELATED).memoRelatedSetting
|
||||
?.displayWithUpdateTime;
|
||||
const factor = displayWithUpdateTime ? "updated_ts" : "created_ts";
|
||||
const filterDate = new Date(filter.value);
|
||||
const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;
|
||||
const timestampAfter = filterUtcTimestamp / 1000;
|
||||
conditions.push(`${factor} >= ${timestampAfter} && ${factor} < ${timestampAfter + 60 * 60 * 24}`);
|
||||
}
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(" && ") : undefined;
|
||||
|
|
|
|||
Loading…
Reference in New Issue