feat(mobile): filter notes by tag
This commit is contained in:
1 parent
b55e81713b
commit
16934f668d
9 files changed
+144
-38
No files matched your search
@@ -15,6 +15,10 @@
|
||||
- **核心链路从严**:安装器、自动升级、数据存储、认证及迁移的机制或配置变化一律按高风险变更处理,必须验证真实的旧版本到新版本链路,不能只验证新版本自身。
|
||||
- **禁止无依据保证**:未完成对应平台及跨版本验证时,必须明确标注风险,严禁声称“不影响现有用户”或“不影响自动升级”。
|
||||
|
||||
## Android 调测约束
|
||||
|
||||
- **USB 真机优先**:Android 功能调测必须使用通过 USB 连接的真实设备和 `adb`,不得启动或使用 QEMU、AVD 等 Android 模拟器;未连接可用真机时,明确标注真机验证未完成,不得以模拟器结果替代。
|
||||
|
||||
## GitHub Actions 与 Release 约束及流程
|
||||
|
||||
1. **Fork 工作流边界**:配置 GitHub Actions 时必须考虑大量用户会 Fork 仓库进行自部署;仅官方仓库需要的 Job 必须使用 `github.repository == 'tianma-if/edgeever'` 门禁,严禁在下游 Fork 中分配 Runner 或执行。
|
||||
|
||||
@@ -29,6 +29,7 @@ export type LocalMemoListParams = {
|
||||
notebookId?: string | null;
|
||||
notebookIds?: string[];
|
||||
q?: string;
|
||||
tag?: string;
|
||||
trash?: boolean;
|
||||
sort?: MemoSortMode;
|
||||
filter?: MemoFilterMode;
|
||||
@@ -116,6 +117,12 @@ export const listLocalMemos = async (scope: string, params: LocalMemoListParams)
|
||||
const q = `%${params.q.trim()}%`;
|
||||
binds.push(q, q, q);
|
||||
}
|
||||
if (params.tag?.trim()) {
|
||||
conditions.push(
|
||||
"EXISTS (SELECT 1 FROM json_each(mobile_memos.data_json, '$.tags') AS memo_tag WHERE LOWER(CAST(memo_tag.value AS TEXT)) = LOWER(?))"
|
||||
);
|
||||
binds.push(params.tag.trim());
|
||||
}
|
||||
if (params.filter === "tagged") {
|
||||
conditions.push("tags_text <> ''");
|
||||
} else if (params.filter === "untagged") {
|
||||
|
||||
@@ -108,6 +108,10 @@ const mobileOnlyTranslations = new Map<string, string>([
|
||||
["笔记标签", "Note tags"],
|
||||
["选择笔记本", "Choose notebook"],
|
||||
["点选已有标签,或输入名称创建新标签", "Select existing tags or enter a name to create a new one"],
|
||||
["按标签筛选", "Filter by tag"],
|
||||
["选择一个标签,只查看带有该标签的笔记", "Choose a tag to view only notes with that tag"],
|
||||
["搜索标签", "Search tags"],
|
||||
["没有匹配的标签", "No matching tags"],
|
||||
["搜索或输入新标签", "Search or enter a new tag"],
|
||||
["没有匹配的现有标签,可直接新建", "No matching tags. You can create a new one."],
|
||||
["新建", "Create"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { MemoSortMode } from "@edgeever/client";
|
||||
import { Modal, ScrollView, View } from "react-native";
|
||||
import { Check, CheckSquare, FileText, Folder, List, MoreVertical, Sparkles, Trash2, X } from "../components/icons";
|
||||
import { Check, CheckSquare, FileText, Folder, List, MoreVertical, Sparkles, Tag, Trash2, X } from "../components/icons";
|
||||
import { Pressable, Text } from "../components/LocalizedText";
|
||||
import type { MobileMemoListDensity } from "../lib/preferences";
|
||||
import { styles } from "./workspace-styles";
|
||||
@@ -79,6 +79,7 @@ export const NotesActionsModal = ({
|
||||
onClose,
|
||||
onEnterSelection,
|
||||
onMemoListDensityChange,
|
||||
onOpenTagFilter,
|
||||
onSortModeChange,
|
||||
selectionMode,
|
||||
visible,
|
||||
@@ -92,6 +93,7 @@ export const NotesActionsModal = ({
|
||||
onClose: () => void;
|
||||
onEnterSelection: () => void;
|
||||
onMemoListDensityChange: (density: MobileMemoListDensity) => void;
|
||||
onOpenTagFilter: () => void;
|
||||
onSortModeChange: (sortMode: MemoSortMode) => void;
|
||||
selectionMode: boolean;
|
||||
visible: boolean;
|
||||
@@ -119,6 +121,12 @@ export const NotesActionsModal = ({
|
||||
label="选择笔记"
|
||||
onPress={onEnterSelection}
|
||||
/>
|
||||
<ActionSheetItem
|
||||
compact
|
||||
icon={<Tag color="#0f172a" size={18} />}
|
||||
label="按标签筛选"
|
||||
onPress={onOpenTagFilter}
|
||||
/>
|
||||
<View style={styles.listActionDivider} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -31,6 +31,7 @@ export const NotesView = ({
|
||||
notebooks,
|
||||
onCreate,
|
||||
onCreateFromTemplate,
|
||||
onClearTag,
|
||||
onClearSelection,
|
||||
onFilterModeChange,
|
||||
onOpenActions,
|
||||
@@ -43,6 +44,7 @@ export const NotesView = ({
|
||||
onSearchTextChange,
|
||||
onSetMemoView,
|
||||
searchText,
|
||||
selectedTag,
|
||||
totalMemoCount,
|
||||
selectedMemoIds,
|
||||
selectionMode,
|
||||
@@ -62,6 +64,7 @@ export const NotesView = ({
|
||||
notebooks: Notebook[];
|
||||
onCreate: () => void;
|
||||
onCreateFromTemplate?: () => void;
|
||||
onClearTag: () => void;
|
||||
onClearSelection: () => void;
|
||||
onFilterModeChange: (filterMode: MemoFilterMode) => void;
|
||||
onOpenActions: () => void;
|
||||
@@ -74,6 +77,7 @@ export const NotesView = ({
|
||||
onSearchTextChange: (value: string) => void;
|
||||
onSetMemoView: (memoView: MemoView) => void;
|
||||
searchText: string;
|
||||
selectedTag: string | null;
|
||||
totalMemoCount: number;
|
||||
selectionMode: boolean;
|
||||
selectedMemoIds: Set<string>;
|
||||
@@ -81,15 +85,17 @@ export const NotesView = ({
|
||||
const { resolvedTheme } = useMobileTheme();
|
||||
const { preference: localePreference, translate } = useMobileLocale();
|
||||
const searchActive = searchText.trim().length > 0;
|
||||
const filterActive = memoFilterMode !== "all";
|
||||
const filterActive = memoFilterMode !== "all" || Boolean(selectedTag);
|
||||
const searchStatusLabel = translate("正在搜索");
|
||||
const searchResultLabel = translate(`${totalMemoCount} 条结果`);
|
||||
const exitSearchLabel = translate("退出搜索");
|
||||
const activeFilterLabel = memoFilterMode === "pinned"
|
||||
? translate("置顶")
|
||||
: memoFilterMode === "tagged"
|
||||
? translate("有标签")
|
||||
: translate("无标签");
|
||||
const activeFilterLabel = selectedTag
|
||||
? `#${selectedTag}`
|
||||
: memoFilterMode === "pinned"
|
||||
? translate("置顶")
|
||||
: memoFilterMode === "tagged"
|
||||
? translate("有标签")
|
||||
: translate("无标签");
|
||||
const filterResultLabel = translate(`筛选:${activeFilterLabel} · ${totalMemoCount} 条`);
|
||||
const resetFilterLabel = translate("重置");
|
||||
|
||||
@@ -107,16 +113,16 @@ export const NotesView = ({
|
||||
) : null}
|
||||
<View style={styles.mobileListTitleRow}>
|
||||
<Pressable
|
||||
accessibilityLabel={memoView === "trash" ? "返回笔记列表" : "切换笔记本"}
|
||||
accessibilityLabel={memoView === "trash" || selectedTag ? "返回笔记列表" : "切换笔记本"}
|
||||
accessibilityRole="button"
|
||||
onPress={memoView === "trash" ? () => onSetMemoView("notebook") : onOpenNotebookPicker}
|
||||
onPress={memoView === "trash" ? () => onSetMemoView("notebook") : selectedTag ? onClearTag : onOpenNotebookPicker}
|
||||
style={styles.mobileNotebookTitleButton}
|
||||
>
|
||||
{memoView === "trash" ? <ChevronLeft color="#475569" size={18} /> : null}
|
||||
{memoView === "trash" || selectedTag ? <ChevronLeft color="#475569" size={18} /> : null}
|
||||
<Text numberOfLines={1} style={styles.mobileNotebookTitle}>
|
||||
{memoView === "trash" ? "回收站" : activeNotebook?.name ?? "全部笔记"}
|
||||
{memoView === "trash" ? "回收站" : selectedTag ? `#${selectedTag}` : activeNotebook?.name ?? "全部笔记"}
|
||||
</Text>
|
||||
{memoView === "notebook" ? <ChevronDown color="#64748b" size={16} /> : null}
|
||||
{memoView === "notebook" && !selectedTag ? <ChevronDown color="#64748b" size={16} /> : null}
|
||||
</Pressable>
|
||||
<Pressable accessibilityLabel={selectionMode ? "批量操作" : "列表选项"} accessibilityRole="button" onPress={onOpenActions} style={styles.mobileMoreButton}>
|
||||
<MoreHorizontal color="#475569" size={20} />
|
||||
@@ -177,7 +183,7 @@ export const NotesView = ({
|
||||
<Pressable
|
||||
accessibilityLabel={searchActive ? exitSearchLabel : resetFilterLabel}
|
||||
accessibilityRole="button"
|
||||
onPress={searchActive ? () => onSearchTextChange("") : () => onFilterModeChange("all")}
|
||||
onPress={searchActive ? () => onSearchTextChange("") : selectedTag ? onClearTag : () => onFilterModeChange("all")}
|
||||
>
|
||||
<Text style={[styles.mobileListConstraintAction, !searchActive && styles.mobileListConstraintActionFilter]}>
|
||||
{searchActive ? exitSearchLabel : resetFilterLabel}
|
||||
@@ -189,7 +195,7 @@ export const NotesView = ({
|
||||
</View>
|
||||
|
||||
<MemoList
|
||||
emptyActions={memoView === "notebook" && notebooks.length > 0 && !searchActive && memoFilterMode === "all"
|
||||
emptyActions={memoView === "notebook" && notebooks.length > 0 && !searchActive && !filterActive
|
||||
? [
|
||||
{ label: "新建笔记", onPress: onCreate, variant: "primary" as const },
|
||||
...(onCreateFromTemplate
|
||||
@@ -197,8 +203,8 @@ export const NotesView = ({
|
||||
: []),
|
||||
]
|
||||
: undefined}
|
||||
emptyDescription={searchActive ? "换个关键词再试" : memoFilterMode !== "all" ? "试试切换筛选条件,或调整搜索关键词。" : memoView === "trash" ? "删除的笔记会显示在这里。" : "先创建一条笔记,之后可以在这里快速预览、搜索和批量整理。"}
|
||||
emptyTitle={searchActive ? "没有找到匹配笔记" : memoFilterMode !== "all" ? "没有符合筛选的笔记" : memoView === "trash" ? "回收站为空" : "暂无笔记"}
|
||||
emptyDescription={searchActive ? "换个关键词再试" : filterActive ? "试试切换筛选条件,或调整搜索关键词。" : memoView === "trash" ? "删除的笔记会显示在这里。" : "先创建一条笔记,之后可以在这里快速预览、搜索和批量整理。"}
|
||||
emptyTitle={searchActive ? "没有找到匹配笔记" : filterActive ? "没有符合筛选的笔记" : memoView === "trash" ? "回收站为空" : "暂无笔记"}
|
||||
error={error}
|
||||
initialSyncProgress={initialSyncProgress}
|
||||
isError={isError}
|
||||
|
||||
@@ -351,16 +351,24 @@ export const SmartTagButton = ({
|
||||
};
|
||||
|
||||
export const TagPickerModal = ({
|
||||
allowCreate = true,
|
||||
dataScope,
|
||||
description = "点选已有标签,或输入名称创建新标签",
|
||||
maxSelections = 24,
|
||||
onChange,
|
||||
onClose,
|
||||
selectedTags,
|
||||
title = "选择标签",
|
||||
visible,
|
||||
}: {
|
||||
allowCreate?: boolean;
|
||||
dataScope: string;
|
||||
description?: string;
|
||||
maxSelections?: number;
|
||||
onChange: (tags: string[]) => void;
|
||||
onClose: () => void;
|
||||
selectedTags: string[];
|
||||
title?: string;
|
||||
visible: boolean;
|
||||
}) => {
|
||||
const { translate } = useMobileLocale();
|
||||
@@ -382,10 +390,14 @@ export const TagPickerModal = ({
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const commit = (nextTags: string[]) => onChange(Array.from(new Set(nextTags)).slice(0, 24));
|
||||
const toggleTag = (name: string) => commit(
|
||||
selectedTags.includes(name) ? selectedTags.filter((tag) => tag !== name) : [...selectedTags, name]
|
||||
);
|
||||
const commit = (nextTags: string[]) => onChange(Array.from(new Set(nextTags)).slice(0, maxSelections));
|
||||
const toggleTag = (name: string) => {
|
||||
if (selectedTags.includes(name)) {
|
||||
commit(selectedTags.filter((tag) => tag !== name));
|
||||
return;
|
||||
}
|
||||
commit(maxSelections === 1 ? [name] : [...selectedTags, name]);
|
||||
};
|
||||
const createTag = () => {
|
||||
const additions = parseTags(normalizedSearch);
|
||||
if (additions.length === 0) return;
|
||||
@@ -400,8 +412,8 @@ export const TagPickerModal = ({
|
||||
<View style={styles.actionSheetHandle} />
|
||||
<View style={styles.notebookPickerHeader}>
|
||||
<View style={styles.notebookPickerHeaderText}>
|
||||
<Text style={styles.actionSheetTitle}>{translate("选择标签")}</Text>
|
||||
<Text style={styles.panelLabel}>{translate("点选已有标签,或输入名称创建新标签")}</Text>
|
||||
<Text style={styles.actionSheetTitle}>{translate(title)}</Text>
|
||||
<Text style={styles.panelLabel}>{translate(description)}</Text>
|
||||
</View>
|
||||
<Pressable accessibilityLabel="关闭" accessibilityRole="button" onPress={onClose} style={styles.notebookPickerCloseButton}>
|
||||
<X color="#0f172a" size={20} />
|
||||
@@ -423,18 +435,18 @@ export const TagPickerModal = ({
|
||||
<View style={styles.notebookPickerSearchBox}>
|
||||
<Search color="#64748b" size={18} />
|
||||
<TextInput
|
||||
accessibilityLabel="搜索或输入新标签"
|
||||
accessibilityLabel={allowCreate ? "搜索或输入新标签" : "搜索标签"}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
onChangeText={setSearchText}
|
||||
onSubmitEditing={createTag}
|
||||
placeholder="搜索或输入新标签"
|
||||
onSubmitEditing={allowCreate ? createTag : undefined}
|
||||
placeholder={allowCreate ? "搜索或输入新标签" : "搜索标签"}
|
||||
placeholderTextColor="#94a3b8"
|
||||
returnKeyType="done"
|
||||
returnKeyType={allowCreate ? "done" : "search"}
|
||||
style={styles.notebookPickerSearchInput}
|
||||
value={searchText}
|
||||
/>
|
||||
{normalizedSearch && !exactMatch && selectedTags.length < 24 ? (
|
||||
{allowCreate && normalizedSearch && !exactMatch && selectedTags.length < maxSelections ? (
|
||||
<Pressable accessibilityLabel={`新建标签 ${normalizedSearch}`} accessibilityRole="button" onPress={createTag}>
|
||||
<Text style={styles.tagPickerCreateText}>{translate("新建")}</Text>
|
||||
</Pressable>
|
||||
@@ -446,7 +458,7 @@ export const TagPickerModal = ({
|
||||
) : visibleTags.length === 0 ? (
|
||||
<View style={styles.emptyInlinePanel}>
|
||||
<Tag color="#94a3b8" size={28} />
|
||||
<Text style={styles.mutedText}>{translate("没有匹配的现有标签,可直接新建")}</Text>
|
||||
<Text style={styles.mutedText}>{translate(allowCreate ? "没有匹配的现有标签,可直接新建" : "没有匹配的标签")}</Text>
|
||||
</View>
|
||||
) : visibleTags.map((tag) => {
|
||||
const selected = selectedTags.includes(tag.name);
|
||||
|
||||
@@ -102,6 +102,7 @@ import { MemoDetailModal } from "./WorkspaceMemoDetail";
|
||||
import {
|
||||
MoveSelectionModal,
|
||||
NotebookPickerModal,
|
||||
TagPickerModal,
|
||||
} from "./WorkspacePickers";
|
||||
import { RevisionHistoryModal } from "./WorkspaceRevisionHistory";
|
||||
import { CreateMemoModal, RichEditorModal } from "./WorkspaceEditors";
|
||||
@@ -152,6 +153,7 @@ export const WorkspaceScreen = ({
|
||||
const autoSelectedDemoNotebookRef = useRef(false);
|
||||
const [memoView, setMemoView] = useState<MemoView>("notebook");
|
||||
const [memoFilterMode, setMemoFilterMode] = useState<MemoFilterMode>("all");
|
||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||
const [memoSortMode, setMemoSortMode] = useState<MemoSortMode>("updated-desc");
|
||||
const [memoListDensity, setMemoListDensity] = useState<MobileMemoListDensity>("preview");
|
||||
const [imageCompressionEnabled, setImageCompressionEnabled] = useState(true);
|
||||
@@ -167,6 +169,7 @@ export const WorkspaceScreen = ({
|
||||
const [isImportingShare, setIsImportingShare] = useState(false);
|
||||
const [notesActionsOpen, setNotesActionsOpen] = useState(false);
|
||||
const [notebookPickerOpen, setNotebookPickerOpen] = useState(false);
|
||||
const [tagFilterPickerOpen, setTagFilterPickerOpen] = useState(false);
|
||||
const [richEditingSession, setRichEditingSession] = useState<RichEditingSession | null>(null);
|
||||
const [revisionMemo, setRevisionMemo] = useState<MemoDetail | null>(null);
|
||||
const {
|
||||
@@ -234,7 +237,7 @@ export const WorkspaceScreen = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!autoSelectedDemoNotebookRef.current && activeNotebookId === ALL_NOTES_ID) {
|
||||
if (!autoSelectedDemoNotebookRef.current && activeNotebookId === ALL_NOTES_ID && !selectedTag) {
|
||||
autoSelectedDemoNotebookRef.current = true;
|
||||
setActiveNotebookId(preferredNotebookId);
|
||||
return;
|
||||
@@ -243,16 +246,16 @@ export const WorkspaceScreen = ({
|
||||
if (activeNotebookId === alternateNotebookId) {
|
||||
setActiveNotebookId(preferredNotebookId);
|
||||
}
|
||||
}, [activeNotebookId, localePreference, notebooks]);
|
||||
}, [activeNotebookId, localePreference, notebooks, selectedTag]);
|
||||
|
||||
const activeNotebook = notebooks.find((notebook) => notebook.id === activeNotebookId) ?? null;
|
||||
const activeNotebookDescendantIds = useMemo(
|
||||
() => (activeNotebookId === ALL_NOTES_ID ? [] : getNotebookDescendantIds(notebooks, activeNotebookId)),
|
||||
[activeNotebookId, notebooks]
|
||||
() => (activeNotebookId === ALL_NOTES_ID || selectedTag ? [] : getNotebookDescendantIds(notebooks, activeNotebookId)),
|
||||
[activeNotebookId, notebooks, selectedTag]
|
||||
);
|
||||
|
||||
const memosQuery = useInfiniteQuery({
|
||||
queryKey: ["mobile", "memos", memoView, activeNotebookId, memoFilterMode, memoSortMode, activeNotebookDescendantIds, "paged-v2"],
|
||||
queryKey: ["mobile", "memos", memoView, activeNotebookId, memoFilterMode, memoSortMode, activeNotebookDescendantIds, selectedTag, "paged-v3"],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!client) {
|
||||
@@ -262,6 +265,7 @@ export const WorkspaceScreen = ({
|
||||
return listLocalMemos(dataScope, {
|
||||
notebookIds: activeNotebookDescendantIds,
|
||||
filter: memoFilterMode,
|
||||
tag: memoView === "notebook" ? selectedTag ?? undefined : undefined,
|
||||
limit: 50,
|
||||
offset: pageParam,
|
||||
sort: memoSortMode,
|
||||
@@ -274,7 +278,7 @@ export const WorkspaceScreen = ({
|
||||
});
|
||||
|
||||
const searchQuery = useInfiniteQuery({
|
||||
queryKey: ["mobile", "search", memoView, debouncedSearchText, activeNotebookId, memoFilterMode, memoSortMode, activeNotebookDescendantIds, "paged-v4"],
|
||||
queryKey: ["mobile", "search", memoView, debouncedSearchText, activeNotebookId, memoFilterMode, memoSortMode, activeNotebookDescendantIds, selectedTag, "paged-v5"],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!client) {
|
||||
@@ -285,6 +289,7 @@ export const WorkspaceScreen = ({
|
||||
q: debouncedSearchText,
|
||||
notebookIds: activeNotebookDescendantIds,
|
||||
filter: memoFilterMode,
|
||||
tag: memoView === "notebook" ? selectedTag ?? undefined : undefined,
|
||||
limit: 50,
|
||||
offset: pageParam,
|
||||
sort: memoSortMode,
|
||||
@@ -342,6 +347,11 @@ export const WorkspaceScreen = ({
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (selectedTag) {
|
||||
setSelectedTag(null);
|
||||
setMemoFilterMode("all");
|
||||
return true;
|
||||
}
|
||||
if (memoView === "trash") {
|
||||
setMemoView("notebook");
|
||||
return true;
|
||||
@@ -349,7 +359,7 @@ export const WorkspaceScreen = ({
|
||||
return false;
|
||||
});
|
||||
return () => subscription.remove();
|
||||
}, [activeView, clearSelection, memoView, searchText, selectedMemoId, selectionMode]);
|
||||
}, [activeView, clearSelection, memoView, searchText, selectedMemoId, selectedTag, selectionMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notebooksQuery.data && memosQuery.data) {
|
||||
@@ -386,13 +396,28 @@ export const WorkspaceScreen = ({
|
||||
setActiveView("notes");
|
||||
setMemoView("notebook");
|
||||
setActiveNotebookId(ALL_NOTES_ID);
|
||||
setSelectedTag(null);
|
||||
setSearchText("");
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const clearTagFilter = () => {
|
||||
setSelectedTag(null);
|
||||
setMemoFilterMode("all");
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const handleMemoFilterModeChange = (nextFilterMode: MemoFilterMode) => {
|
||||
if (nextFilterMode !== "all") {
|
||||
setSelectedTag(null);
|
||||
}
|
||||
setMemoFilterMode(nextFilterMode);
|
||||
};
|
||||
|
||||
const showTrash = () => {
|
||||
setMemoView("trash");
|
||||
setActiveNotebookId(ALL_NOTES_ID);
|
||||
setSelectedTag(null);
|
||||
setSearchText("");
|
||||
clearSelection();
|
||||
};
|
||||
@@ -671,7 +696,7 @@ export const WorkspaceScreen = ({
|
||||
|
||||
useEffect(() => {
|
||||
clearSelection();
|
||||
}, [activeNotebookId, clearSelection, memoFilterMode, memoSortMode, memoView]);
|
||||
}, [activeNotebookId, clearSelection, memoFilterMode, memoSortMode, memoView, selectedTag]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -1211,8 +1236,9 @@ export const WorkspaceScreen = ({
|
||||
notebooks={notebooks}
|
||||
onCreate={() => openCreateMemo()}
|
||||
onCreateFromTemplate={canCreateMemo ? openCreateFromTemplate : undefined}
|
||||
onClearTag={clearTagFilter}
|
||||
onClearSelection={clearSelection}
|
||||
onFilterModeChange={setMemoFilterMode}
|
||||
onFilterModeChange={handleMemoFilterModeChange}
|
||||
onOpenActions={() => setNotesActionsOpen(true)}
|
||||
onOpenNotebookPicker={() => setNotebookPickerOpen(true)}
|
||||
onMemoPress={handleMemoPress}
|
||||
@@ -1233,6 +1259,7 @@ export const WorkspaceScreen = ({
|
||||
}}
|
||||
onSetMemoView={(nextMemoView) => nextMemoView === "trash" ? showTrash() : showAllNotes()}
|
||||
searchText={searchText}
|
||||
selectedTag={selectedTag}
|
||||
totalMemoCount={searchActive
|
||||
? searchQuery.data?.pages[0]?.totalCount ?? searchResults.length
|
||||
: memosQuery.data?.pages[0]?.totalCount ?? memos.length}
|
||||
@@ -1290,12 +1317,33 @@ export const WorkspaceScreen = ({
|
||||
notebooks={notebooks}
|
||||
onClose={() => setNotebookPickerOpen(false)}
|
||||
onSelect={(notebookId) => {
|
||||
setSelectedTag(null);
|
||||
setActiveNotebookId(notebookId);
|
||||
setNotebookPickerOpen(false);
|
||||
}}
|
||||
visible
|
||||
/> : null}
|
||||
|
||||
{tagFilterPickerOpen ? <TagPickerModal
|
||||
allowCreate={false}
|
||||
dataScope={dataScope}
|
||||
description="选择一个标签,只查看带有该标签的笔记"
|
||||
maxSelections={1}
|
||||
onChange={(tags) => {
|
||||
setSelectedTag(tags[0] ?? null);
|
||||
setMemoFilterMode("all");
|
||||
setMemoView("notebook");
|
||||
setActiveNotebookId(ALL_NOTES_ID);
|
||||
setSearchText("");
|
||||
clearSelection();
|
||||
setTagFilterPickerOpen(false);
|
||||
}}
|
||||
onClose={() => setTagFilterPickerOpen(false)}
|
||||
selectedTags={selectedTag ? [selectedTag] : []}
|
||||
title="按标签筛选"
|
||||
visible
|
||||
/> : null}
|
||||
|
||||
{revisionMemo ? <RevisionHistoryModal
|
||||
memo={revisionMemo}
|
||||
onClose={() => setRevisionMemo(null)}
|
||||
@@ -1366,13 +1414,17 @@ export const WorkspaceScreen = ({
|
||||
memoListDensity={memoListDensity}
|
||||
memoSortMode={memoSortMode}
|
||||
listDescription={`${searchActive ? searchQuery.data?.pages[0]?.totalCount ?? searchResults.length : memosQuery.data?.pages[0]?.totalCount ?? memos.length} 条笔记`}
|
||||
listTitle={memoView === "trash" ? "回收站" : activeNotebook?.name ?? "全部笔记"}
|
||||
listTitle={memoView === "trash" ? "回收站" : selectedTag ? `#${selectedTag}` : activeNotebook?.name ?? "全部笔记"}
|
||||
onClose={() => setNotesActionsOpen(false)}
|
||||
onEnterSelection={() => {
|
||||
setNotesActionsOpen(false);
|
||||
enterSelectionMode();
|
||||
}}
|
||||
onMemoListDensityChange={handleMemoListDensityChange}
|
||||
onOpenTagFilter={() => {
|
||||
setNotesActionsOpen(false);
|
||||
setTagFilterPickerOpen(true);
|
||||
}}
|
||||
onSortModeChange={setMemoSortMode}
|
||||
selectionMode={selectionMode}
|
||||
visible
|
||||
|
||||
@@ -49,4 +49,13 @@ describe("mobile workspace memo cache", () => {
|
||||
expect(memoMatchesListQuery(memo, ["mobile", "memos", "notebook", "notebook_2", "all", "updated-desc", ["notebook_2"]])).toBe(false);
|
||||
expect(memoMatchesListQuery(memo, ["mobile", "memos", "notebook", "all", "tagged", "updated-desc", []])).toBe(false);
|
||||
});
|
||||
|
||||
test("matches one exact tag without depending on case", () => {
|
||||
const taggedMemo = { ...memo, tags: ["Project Alpha", "Work"] };
|
||||
const query = (tag: string) => ["mobile", "memos", "notebook", "all", "all", "updated-desc", [], tag];
|
||||
|
||||
expect(memoMatchesListQuery(taggedMemo, query("project alpha"))).toBe(true);
|
||||
expect(memoMatchesListQuery(taggedMemo, query("project"))).toBe(false);
|
||||
expect(memoMatchesListQuery(taggedMemo, query("homework"))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ export const memoMatchesListQuery = (memo: MemoSummary, queryKey: readonly unkno
|
||||
const notebookId = queryKey[3];
|
||||
const filter = queryKey[4];
|
||||
const notebookIds = Array.isArray(queryKey[6]) ? queryKey[6] : [];
|
||||
const tag = typeof queryKey[7] === "string" ? queryKey[7].trim().toLocaleLowerCase() : "";
|
||||
|
||||
if ((view === "trash") !== memo.isDeleted) {
|
||||
return false;
|
||||
@@ -51,6 +52,9 @@ export const memoMatchesListQuery = (memo: MemoSummary, queryKey: readonly unkno
|
||||
if (notebookId !== ALL_NOTES_ID && !notebookIds.includes(memo.notebookId)) {
|
||||
return false;
|
||||
}
|
||||
if (tag && !memo.tags.some((memoTag) => memoTag.toLocaleLowerCase() === tag)) {
|
||||
return false;
|
||||
}
|
||||
if (filter === "tagged" && memo.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user