-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: implement @mention feature for calendar notes #500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6ecef45
57d9717
0b81ac5
8d9d1c4
9141d6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Users } from "lucide-react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { searchUsers } from "@/lib/actions/users"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type React from "react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect } from "react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ChevronLeft, ChevronRight, MapPin } from "lucide-react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,6 +24,10 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [dateOffset, setDateOffset] = useState(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [taggedLocation, setTaggedLocation] = useState<any | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [showSuggestions, setShowSuggestions] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [userSuggestions, setUserSuggestions] = useState<any[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [mentionQuery, setMentionQuery] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding proper TypeScript types for user suggestions.
♻️ Proposed typing+type UserSuggestion = { id: string; email: string | null };
+
export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
// ...
- const [userSuggestions, setUserSuggestions] = useState<any[]>([]);
+ const [userSuggestions, setUserSuggestions] = useState<UserSuggestion[]>([]);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchNotes = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchedNotes = await getNotes(selectedDate, chatId ?? null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -85,6 +90,42 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleNoteContentChange = async (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = e.target.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setNoteContent(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cursorPosition = e.target.selectionStart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const textBeforeCursor = value.substring(0, cursorPosition); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const words = textBeforeCursor.split(/\s/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastWord = words[words.length - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastWord.startsWith("@")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const query = lastWord.slice(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMentionQuery(query); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const results = await searchUsers(query); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUserSuggestions(results); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowSuggestions(results.length > 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowSuggestions(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+94
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention search triggers a server request on every keystroke (no debouncing / cancellation)
Because this runs in a client component, you should debounce input and ignore out-of-order responses (or use SuggestionDebounce the query and guard against stale responses. For example: const [mentionQuery, setMentionQuery] = useState("")
const latestQueryRef = useRef("")
useEffect(() => {
if (!mentionQuery) {
setShowSuggestions(false)
setUserSuggestions([])
return
}
latestQueryRef.current = mentionQuery
const handle = setTimeout(async () => {
const q = latestQueryRef.current
const results = await searchUsers(q)
// ignore stale responses
if (latestQueryRef.current !== q) return
setUserSuggestions(results)
setShowSuggestions(results.length > 0)
}, 150)
return () => clearTimeout(handle)
}, [mentionQuery])Then in Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
Comment on lines
+94
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential race condition with async autocomplete on every keystroke.
🔧 Proposed debounce approach+import { useRef } from "react";
+
export function CalendarNotepad({ chatId }: CalendarNotepadProps) {
// ... existing state ...
+ const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleNoteContentChange = async (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setNoteContent(value);
const cursorPosition = e.target.selectionStart;
const textBeforeCursor = value.substring(0, cursorPosition);
const words = textBeforeCursor.split(/\s/);
const lastWord = words[words.length - 1];
if (lastWord.startsWith("@")) {
const query = lastWord.slice(1);
setMentionQuery(query);
- const results = await searchUsers(query);
- setUserSuggestions(results);
- setShowSuggestions(results.length > 0);
+ if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
+ searchTimeoutRef.current = setTimeout(async () => {
+ const results = await searchUsers(query);
+ setUserSuggestions(results);
+ setShowSuggestions(results.length > 0);
+ }, 150);
} else {
setShowSuggestions(false);
}
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSelectUser = (email: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefix = email.split('@')[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setNoteContent(prev => prev.replace(/@\w*$/, `@${prefix} `)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowSuggestions(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Selecting a user replaces the last token in the entire textarea, not the token at the cursor
This makes the autocomplete feel broken in common editing scenarios (e.g., adding a mention mid-note). SuggestionTrack the selection start/end and replace relative to the cursor, not the end of the string. One approach: const [selectionStart, setSelectionStart] = useState<number | null>(null)
const handleNoteContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setNoteContent(e.target.value)
setSelectionStart(e.target.selectionStart)
// compute mentionQuery from textBeforeCursor and setMentionQuery(...)
}
const handleSelectUser = (email: string) => {
const prefix = email.split("@")[0]
setNoteContent(prev => {
const pos = selectionStart ?? prev.length
const before = prev.slice(0, pos)
const after = prev.slice(pos)
// replace the last @token immediately before cursor
const updatedBefore = before.replace(/(^|\s)@[^\s@]*$/, `$1@${prefix} `)
return updatedBefore + after
})
setShowSuggestions(false)
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const renderContent = (text: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!text) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return text.split(/(@\w+|#location)/g).map((part, i) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (part.startsWith('@')) return <span key={i} className="text-primary font-medium">{part}</span>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (part === '#location') return <span key={i} className="text-primary font-medium">{part}</span>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return part; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+127
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention tokenization is too restrictive (
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleFlyTo = (location: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (location && location.coordinates) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMapData(prev => ({ ...prev, targetPosition: location.coordinates })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -133,15 +174,30 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <textarea | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={noteContent} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setNoteContent(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleNoteContentChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyDown={handleAddNote} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Add note... (⌘+Enter to save, @mention, #location)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full p-2 bg-input rounded-md border focus:ring-ring focus:ring-2 focus:outline-none pr-10" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rows={3} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {showSuggestions && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-full mt-2 w-full bg-background border rounded-md shadow-lg z-50 p-1 max-h-40 overflow-y-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {userSuggestions.map(user => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={user.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => handleSelectUser(user.email)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full text-left px-3 py-2 hover:bg-accent rounded-sm text-sm truncate" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={user.email} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {user.email} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
183
to
196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add null check for The 🛡️ Proposed fix with null filtering {showSuggestions && (
<div className="absolute bottom-full mb-2 w-full bg-background border rounded-md shadow-lg z-50 p-1 max-h-40 overflow-y-auto">
- {userSuggestions.map(user => (
+ {userSuggestions.filter(user => user.email).map(user => (
<button
key={user.id}
type="button"
onClick={() => handleSelectUser(user.email)}
className="w-full text-left px-3 py-2 hover:bg-accent rounded-sm text-sm truncate"
title={user.email}
>
{user.email}
</button>
))}
</div>
)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleTagLocation} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute right-2 top-2 text-muted-foreground hover:text-foreground" title="Tag current map location" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <MapPin className="h-5 w-5" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -157,13 +213,18 @@ export function CalendarNotepad({ chatId }: CalendarNotepadProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-xs text-muted-foreground mb-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {new Date(note.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-sm whitespace-pre-wrap break-words">{note.content}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-sm whitespace-pre-wrap break-words">{renderContent(note.content)}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {note.locationTags && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button onClick={() => handleFlyTo(note.locationTags)} className="text-muted-foreground hover:text-foreground ml-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <MapPin className="h-5 w-5" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {note.userTags && note.userTags.length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-muted-foreground ml-2 flex items-center" title={`${note.userTags.length} user(s) tagged`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Users className="h-4 w-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,10 +143,10 @@ export function UserManagementForm({ form }: UserManagementFormProps) { | |
| <TableCell>{user.email}</TableCell> | ||
| <TableCell className="capitalize">{user.role}</TableCell> | ||
| <TableCell className="text-right"> | ||
| <Button variant="ghost" size="icon" onClick={() => console.log('Edit user:', user.id)} className="mr-2"> | ||
| <Button type="button" variant="ghost" size="icon" onClick={() => console.log('Edit user:', user.id)} className="mr-2"> | ||
| <Edit3 className="h-4 w-4" /> | ||
| </Button> | ||
|
Comment on lines
+146
to
148
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edit button is a no-op stub. Adding Would you like me to open an issue to track implementing the edit-user functionality? 🤖 Prompt for AI Agents |
||
| <Button variant="ghost" size="icon" onClick={() => remove(index)}> | ||
| <Button type="button" variant="ghost" size="icon" onClick={() => remove(index)}> | ||
| <Trash2 className="h-4 w-4 text-destructive" /> | ||
| </Button> | ||
|
Comment on lines
+149
to
151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for removeUser server action
rg -n 'removeUser|deleteUser' --type=ts --type=tsx -C 3
echo "---"
echo "Checking parent form submission and how removed users are handled:"
rg -n 'onSubmit|handleSubmit|updateSettingsAndUsers' --type=ts --type=tsx -C 5 -g '*settings*'Repository: QueueLab/QCX Length of output: 199 🏁 Script executed: #!/bin/bash
# Check the user-management-form to understand how the field array is used
rg -n 'useFieldArray' --type=ts --type=tsx -C 5 -g '*user-management*'Repository: QueueLab/QCX Length of output: 83 🏁 Script executed: #!/bin/bash
# Look at the addUser implementation mentioned in the review
rg -n 'addUser' --type=ts --type=tsx -C 5 -g '*user-management*'Repository: QueueLab/QCX Length of output: 83 🏁 Script executed: #!/bin/bash
# Find the user-management-form file
fd -name '*user-management*' -type f
echo "---"
# Find settings-related files
fd -name '*settings*' -type f | head -20Repository: QueueLab/QCX Length of output: 538 🏁 Script executed: #!/bin/bash
# Search for removeUser without type restrictions
rg 'removeUser|deleteUser' -C 3Repository: QueueLab/QCX Length of output: 481 🏁 Script executed: #!/bin/bash
# Search in the specific file mentioned in the review
cat -n components/settings/components/user-management-form.tsx | head -160 | tail -30Repository: QueueLab/QCX Length of output: 1600 🏁 Script executed: #!/bin/bash
# Look for the form submission handler and how fields are synced to server
cat components/settings/components/user-management-form.tsx | head -80Repository: QueueLab/QCX Length of output: 3723 🏁 Script executed: #!/bin/bash
# Check for addUser server action and its implementation
rg 'addUser|export async function addUser' -C 5 lib/actions/Repository: QueueLab/QCX Length of output: 747 🏁 Script executed: #!/bin/bash
# Search for where this form is used - the parent component
rg 'user-management-form|UserManagementForm' -C 3Repository: QueueLab/QCX Length of output: 2795 🏁 Script executed: #!/bin/bash
# Check the parent settings form for submission handler
cat components/settings/components/settings.tsx | grep -A 30 'onSubmit\|handleSubmit'Repository: QueueLab/QCX Length of output: 4072 🏁 Script executed: #!/bin/bash
# Look at more of the settings.tsx to understand form submission
cat components/settings/components/settings.tsx | tail -100Repository: QueueLab/QCX Length of output: 5814 🏁 Script executed: #!/bin/bash
# Check if removeUser is imported or used anywhere in user-management-form
rg 'removeUser' components/settings/components/user-management-form.tsxRepository: QueueLab/QCX Length of output: 38 Import and call The
🤖 Prompt for AI Agents |
||
| </TableCell> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE "users" ADD COLUMN "email" text; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Migration adds
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE "users" ADD COLUMN "role" text; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||||||||||||||
| ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint | ||||||||||||||||||
| ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'viewer';--> statement-breakpoint | ||||||||||||||||||
| ALTER TABLE "users" ALTER COLUMN "role" SET NOT NULL; | ||||||||||||||||||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Migration will fail if existing rows have NULL Migrations 0002 and 0003 added Proposed safe migration+UPDATE "users" SET "email" = '' WHERE "email" IS NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "role" SET DEFAULT 'viewer';--> statement-breakpoint
+UPDATE "users" SET "role" = 'viewer' WHERE "role" IS NULL;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "role" SET NOT NULL;Choose a meaningful sentinel for email (or skip 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple new
anyusages were introduced (taggedLocation: any,userSuggestions: any[],handleFlyTo(location: any)). Even iftscpasses, this makes the new feature brittle and harder to evolve safely.Since the suggestions are coming from the
userstable, you can type the shape minimally (e.g.,{ id: string; email: string | null }).Suggestion
Replace
anywith small, local types to keep the feature maintainable.For locations, consider a minimal GeoJSON type instead of
any.Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.