mirror of
https://github.com/usememos/memos.git
synced 2025-12-17 14:19:17 +08:00
fix: implement tag suggestions functionality
Backend changes: - Fix ListAllUserStats to calculate and return tag statistics - Previously only returned name and timestamps, missing TagCount - Now properly aggregates tags, pinned memos, and memo type stats Frontend changes: - Initialize user stats on app startup to populate tag data - Show all tags when typing just '#' (fix empty Fuse.js search) - Auto-refresh stats after creating/updating/deleting memos - Fix Checkbox component ref warning with forwardRef 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7d4d1e8517
commit
ef9eee19d6
5 changed files with 68 additions and 8 deletions
|
|
@ -49,14 +49,59 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser
|
|||
|
||||
userMemoStatMap := make(map[int32]*v1pb.UserStats)
|
||||
for _, memo := range memos {
|
||||
// Initialize user stats if not exists
|
||||
if _, exists := userMemoStatMap[memo.CreatorID]; !exists {
|
||||
userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{
|
||||
Name: fmt.Sprintf("users/%d/stats", memo.CreatorID),
|
||||
TagCount: make(map[string]int32),
|
||||
MemoDisplayTimestamps: []*timestamppb.Timestamp{},
|
||||
PinnedMemos: []string{},
|
||||
MemoTypeStats: &v1pb.UserStats_MemoTypeStats{
|
||||
LinkCount: 0,
|
||||
CodeCount: 0,
|
||||
TodoCount: 0,
|
||||
UndoCount: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
stats := userMemoStatMap[memo.CreatorID]
|
||||
|
||||
// Add display timestamp
|
||||
displayTs := memo.CreatedTs
|
||||
if instanceMemoRelatedSetting.DisplayWithUpdateTime {
|
||||
displayTs = memo.UpdatedTs
|
||||
}
|
||||
userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{
|
||||
Name: fmt.Sprintf("users/%d/stats", memo.CreatorID),
|
||||
stats.MemoDisplayTimestamps = append(stats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
|
||||
|
||||
// Count memo stats
|
||||
stats.TotalMemoCount++
|
||||
|
||||
// Count tags and other properties
|
||||
if memo.Payload != nil {
|
||||
for _, tag := range memo.Payload.Tags {
|
||||
stats.TagCount[tag]++
|
||||
}
|
||||
if memo.Payload.Property != nil {
|
||||
if memo.Payload.Property.HasLink {
|
||||
stats.MemoTypeStats.LinkCount++
|
||||
}
|
||||
if memo.Payload.Property.HasCode {
|
||||
stats.MemoTypeStats.CodeCount++
|
||||
}
|
||||
if memo.Payload.Property.HasTaskList {
|
||||
stats.MemoTypeStats.TodoCount++
|
||||
}
|
||||
if memo.Payload.Property.HasIncompleteTasks {
|
||||
stats.MemoTypeStats.UndoCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track pinned memos
|
||||
if memo.Pinned {
|
||||
stats.PinnedMemos = append(stats.PinnedMemos, fmt.Sprintf("users/%d/memos/%d", memo.CreatorID, memo.ID))
|
||||
}
|
||||
userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps = append(userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))
|
||||
}
|
||||
|
||||
userMemoStats := []*v1pb.UserStats{}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ const TagSuggestions = observer(({ editorRef, editorActions }: Props) => {
|
|||
const suggestionsRef = useRef<string[]>([]);
|
||||
suggestionsRef.current = (() => {
|
||||
const search = getCurrentWord()[0].slice(1).toLowerCase();
|
||||
if (search === "") {
|
||||
return tags; // Show all tags when no search term
|
||||
}
|
||||
const fuse = new Fuse(tags);
|
||||
return fuse.search(search).map((result) => result.item);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import { CheckIcon } from "lucide-react";
|
|||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-border data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow disabled:cursor-not-allowed disabled:opacity-50",
|
||||
|
|
@ -18,6 +22,7 @@ function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxP
|
|||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
});
|
||||
Checkbox.displayName = "Checkbox";
|
||||
|
||||
export { Checkbox };
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { makeAutoObservable } from "mobx";
|
|||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { CreateMemoRequest, ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { RequestDeduplicator, createRequestKey, StoreError } from "./store-utils";
|
||||
import userStore from "./user";
|
||||
|
||||
class LocalState {
|
||||
stateId: string = uniqueId();
|
||||
|
|
@ -112,6 +113,8 @@ const memoStore = (() => {
|
|||
stateId: uniqueId(),
|
||||
memoMapByName: memoMap,
|
||||
});
|
||||
// Refresh user stats to update tag counts
|
||||
userStore.fetchUserStats().catch(console.error);
|
||||
return memo;
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +145,8 @@ const memoStore = (() => {
|
|||
stateId: uniqueId(),
|
||||
memoMapByName: confirmedMemoMap,
|
||||
});
|
||||
// Refresh user stats to update tag counts
|
||||
userStore.fetchUserStats().catch(console.error);
|
||||
return memo;
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
|
|
@ -166,6 +171,8 @@ const memoStore = (() => {
|
|||
stateId: uniqueId(),
|
||||
memoMapByName: memoMap,
|
||||
});
|
||||
// Refresh user stats to update tag counts
|
||||
userStore.fetchUserStats().catch(console.error);
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -343,10 +343,10 @@ export const initialUserStore = async () => {
|
|||
},
|
||||
});
|
||||
|
||||
// Step 3: Fetch user settings
|
||||
// Step 3: Fetch user settings and stats
|
||||
// CRITICAL: This must happen after currentUser is set in step 2
|
||||
// The fetchUserSettings() method checks state.currentUser internally
|
||||
await userStore.fetchUserSettings();
|
||||
// The fetchUserSettings() and fetchUserStats() methods check state.currentUser internally
|
||||
await Promise.all([userStore.fetchUserSettings(), userStore.fetchUserStats()]);
|
||||
|
||||
// Step 4: Apply user preferences to instance
|
||||
// CRITICAL: This must happen after fetchUserSettings() completes
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue