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 SearchBar from "@/components/SearchBar";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import MemoFilters from "../MemoFilters";
|
|
||||||
import StatisticsView from "../StatisticsView";
|
import StatisticsView from "../StatisticsView";
|
||||||
import ShortcutsSection from "./ShortcutsSection";
|
import ShortcutsSection from "./ShortcutsSection";
|
||||||
import TagsSection from "./TagsSection";
|
import TagsSection from "./TagsSection";
|
||||||
|
|
@ -22,7 +21,6 @@ const HomeSidebar = observer((props: Props) => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<MemoFilters />
|
|
||||||
<div className="mt-1 px-1 w-full">
|
<div className="mt-1 px-1 w-full">
|
||||||
<StatisticsView />
|
<StatisticsView />
|
||||||
{currentUser && <ShortcutsSection />}
|
{currentUser && <ShortcutsSection />}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
import { isEqual } from "lodash-es";
|
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 { observer } from "mobx-react-lite";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
@ -7,6 +18,46 @@ import { memoFilterStore } from "@/store";
|
||||||
import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters } from "@/store/memoFilter";
|
import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters } from "@/store/memoFilter";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
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 MemoFilters = observer(() => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const [, setSearchParams] = useSearchParams();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
|
@ -18,63 +69,51 @@ const MemoFilters = observer(() => {
|
||||||
searchParams.set("filter", stringifyFilters(filters));
|
searchParams.set("filter", stringifyFilters(filters));
|
||||||
}
|
}
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
}, [filters]);
|
}, [filters, setSearchParams]);
|
||||||
|
|
||||||
const getFilterDisplayText = (filter: MemoFilter) => {
|
const handleRemoveFilter = (filter: MemoFilter) => {
|
||||||
if (filter.value) {
|
memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter));
|
||||||
return filter.value;
|
};
|
||||||
|
|
||||||
|
const getFilterDisplayText = (filter: MemoFilter): string => {
|
||||||
|
const config = FILTER_CONFIGS[filter.factor];
|
||||||
|
if (!config) {
|
||||||
|
return filter.value || filter.factor;
|
||||||
}
|
}
|
||||||
if (filter.factor.startsWith("property.")) {
|
return config.getLabel(filter.value, t);
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filters.length === 0) {
|
if (filters.length === 0) {
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mt-2 flex flex-row justify-start items-center flex-wrap gap-x-2 gap-y-1">
|
<div className="w-full mb-2 flex flex-row justify-start items-center flex-wrap gap-2">
|
||||||
{filters.map((filter: MemoFilter) => (
|
{filters.map((filter) => {
|
||||||
|
const config = FILTER_CONFIGS[filter.factor];
|
||||||
|
const Icon = config?.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={getMemoFilterKey(filter)}
|
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"
|
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"
|
||||||
onClick={() => memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter))}
|
|
||||||
>
|
>
|
||||||
<FactorIcon className="w-4 h-auto text-muted-foreground opacity-60" factor={filter.factor} />
|
{Icon && <Icon className="w-3.5 h-3.5 text-muted-foreground shrink-0" />}
|
||||||
<span className="text-muted-foreground text-sm max-w-32 truncate">{getFilterDisplayText(filter)}</span>
|
<span className="text-foreground/80 font-medium max-w-32 truncate">{getFilterDisplayText(filter)}</span>
|
||||||
<button className="text-muted-foreground opacity-60 hover:opacity-100">
|
<button
|
||||||
<XIcon className="w-4 h-auto" />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const FactorIcon = ({ factor, className }: { factor: FilterFactor; className?: string }) => {
|
MemoFilters.displayName = "MemoFilters";
|
||||||
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] || <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoFilters;
|
export default MemoFilters;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useTranslate } from "@/utils/i18n";
|
||||||
import Empty from "../Empty";
|
import Empty from "../Empty";
|
||||||
import MasonryView, { MemoRenderContext } from "../MasonryView";
|
import MasonryView, { MemoRenderContext } from "../MasonryView";
|
||||||
import MemoEditor from "../MemoEditor";
|
import MemoEditor from "../MemoEditor";
|
||||||
|
import MemoFilters from "../MemoFilters";
|
||||||
import MemoSkeleton from "../MemoSkeleton";
|
import MemoSkeleton from "../MemoSkeleton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -153,7 +154,12 @@ const PagedMemoList = observer((props: Props) => {
|
||||||
<MasonryView
|
<MasonryView
|
||||||
memoList={sortedMemoList}
|
memoList={sortedMemoList}
|
||||||
renderer={props.renderer}
|
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"}
|
listMode={viewStore.state.layout === "LIST"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,17 @@ import { MemoRenderContext } from "@/components/MasonryView";
|
||||||
import MemoView from "@/components/MemoView";
|
import MemoView from "@/components/MemoView";
|
||||||
import PagedMemoList from "@/components/PagedMemoList";
|
import PagedMemoList from "@/components/PagedMemoList";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { viewStore } from "@/store";
|
import { viewStore, workspaceStore } from "@/store";
|
||||||
import { extractUserIdFromName } from "@/store/common";
|
import { extractUserIdFromName } from "@/store/common";
|
||||||
import memoFilterStore from "@/store/memoFilter";
|
import memoFilterStore from "@/store/memoFilter";
|
||||||
import { State } from "@/types/proto/api/v1/common";
|
import { State } from "@/types/proto/api/v1/common";
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
|
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
|
||||||
|
|
||||||
const Archived = observer(() => {
|
const Archived = observer(() => {
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
|
|
||||||
// Build filter from active filters - no useMemo needed since component is MobX observer
|
// Build filter from active filters
|
||||||
const buildMemoFilter = () => {
|
const buildMemoFilter = () => {
|
||||||
const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`];
|
const conditions = [`creator_id == ${extractUserIdFromName(user.name)}`];
|
||||||
for (const filter of memoFilterStore.filters) {
|
for (const filter of memoFilterStore.filters) {
|
||||||
|
|
@ -21,6 +22,20 @@ const Archived = observer(() => {
|
||||||
conditions.push(`content.contains("${filter.value}")`);
|
conditions.push(`content.contains("${filter.value}")`);
|
||||||
} else if (filter.factor === "tagSearch") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
conditions.push(`tag in ["${filter.value}"]`);
|
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;
|
return conditions.length > 0 ? conditions.join(" && ") : undefined;
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,44 @@ import MemoView from "@/components/MemoView";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import PagedMemoList from "@/components/PagedMemoList";
|
import PagedMemoList from "@/components/PagedMemoList";
|
||||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
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 { State } from "@/types/proto/api/v1/common";
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
|
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
|
||||||
|
|
||||||
const Explore = observer(() => {
|
const Explore = observer(() => {
|
||||||
const { md } = useResponsiveWidth();
|
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 (
|
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">
|
<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 />}
|
{!md && <MobileHeader />}
|
||||||
|
|
@ -30,6 +61,7 @@ const Explore = observer(() => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
||||||
|
filter={memoFilter}
|
||||||
showCreator
|
showCreator
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@ import PagedMemoList from "@/components/PagedMemoList";
|
||||||
import UserAvatar from "@/components/UserAvatar";
|
import UserAvatar from "@/components/UserAvatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import { viewStore, userStore } from "@/store";
|
import { viewStore, userStore, workspaceStore } from "@/store";
|
||||||
import { extractUserIdFromName } from "@/store/common";
|
import { extractUserIdFromName } from "@/store/common";
|
||||||
import memoFilterStore from "@/store/memoFilter";
|
import memoFilterStore from "@/store/memoFilter";
|
||||||
import { State } from "@/types/proto/api/v1/common";
|
import { State } from "@/types/proto/api/v1/common";
|
||||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||||
import { User } from "@/types/proto/api/v1/user_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";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
const UserProfile = observer(() => {
|
const UserProfile = observer(() => {
|
||||||
|
|
@ -43,7 +44,7 @@ const UserProfile = observer(() => {
|
||||||
});
|
});
|
||||||
}, [params.username]);
|
}, [params.username]);
|
||||||
|
|
||||||
// Build filter from active filters - no useMemo needed since component is MobX observer
|
// Build filter from active filters
|
||||||
const buildMemoFilter = () => {
|
const buildMemoFilter = () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -55,6 +56,22 @@ const UserProfile = observer(() => {
|
||||||
conditions.push(`content.contains("${filter.value}")`);
|
conditions.push(`content.contains("${filter.value}")`);
|
||||||
} else if (filter.factor === "tagSearch") {
|
} else if (filter.factor === "tagSearch") {
|
||||||
conditions.push(`tag in ["${filter.value}"]`);
|
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;
|
return conditions.length > 0 ? conditions.join(" && ") : undefined;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue