diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 46cf3d6b..9da46700 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -6,7 +6,10 @@ on: jobs: claude-review: - if: github.event.pull_request.draft == false + if: | + github.event.pull_request.draft == false && + github.actor != 'graphite-app[bot]' && + github.actor != 'dependabot[bot]' runs-on: ubuntu-latest permissions: diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 9fde6c14..fb82bcb9 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -155,13 +155,66 @@ export default function NewPage() { const resetDraft = useQuickNoteDraftReset(selectedProject) const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "") - const { noteMutation } = useDocumentMutations({ + const { noteMutation, bulkDeleteMutation } = useDocumentMutations({ onClose: () => { resetDraft() setIsFullscreen(false) }, }) + const [selectedDocumentIds, setSelectedDocumentIds] = useState>( + new Set(), + ) + const [isSelectionMode, setIsSelectionMode] = useState(false) + + const handleToggleSelection = useCallback((documentId: string) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + if (next.has(documentId)) { + next.delete(documentId) + } else { + next.add(documentId) + } + return next + }) + }, []) + + const handleClearSelection = useCallback(() => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + }, []) + + const handleEnterSelectionMode = useCallback(() => { + setIsSelectionMode(true) + }, []) + + const handleSelectAllVisible = useCallback((visibleIds: string[]) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + for (const id of visibleIds) { + next.add(id) + } + return next + }) + }, []) + + const handleBulkDelete = useCallback(() => { + const ids = Array.from(selectedDocumentIds) + if (ids.length === 0) return + bulkDeleteMutation.mutate( + { documentIds: ids }, + { + onSuccess: () => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + if (selectedDocument && ids.includes(selectedDocument.id ?? "")) { + setDocId(null) + } + }, + }, + ) + }, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId]) + type SpaceHighlightsResponse = { highlights: HighlightItem[] questions: string[] @@ -387,6 +440,14 @@ export default function NewPage() { void + isSelectionMode?: boolean + selectedDocumentIds?: Set + onEnterSelectionMode?: () => void + onToggleSelection?: (documentId: string) => void + onClearSelection?: () => void + onSelectAllVisible?: (visibleIds: string[]) => void + onBulkDelete?: () => void + isBulkDeleting?: boolean quickNoteProps?: QuickNoteProps highlightsProps?: HighlightsProps emptyStateProps?: NovaEmptyStateProps @@ -102,10 +121,19 @@ interface MemoriesGridProps { export function MemoriesGrid({ isChatOpen, onOpenDocument, + isSelectionMode = false, + selectedDocumentIds = new Set(), + onEnterSelectionMode, + onToggleSelection, + onClearSelection, + onSelectAllVisible, + onBulkDelete, + isBulkDeleting = false, quickNoteProps, highlightsProps, emptyStateProps, }: MemoriesGridProps) { + const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false) const { user } = useAuth() const { effectiveContainerTags } = useProject() const isMobile = useIsMobile() @@ -253,11 +281,31 @@ export function MemoriesGrid({ const handleCardClick = useCallback( (document: DocumentWithMemories) => { - onOpenDocument(document) + if (isSelectionMode && onToggleSelection && document.id) { + onToggleSelection(document.id) + } else { + onOpenDocument(document) + } }, - [onOpenDocument], + [isSelectionMode, onToggleSelection, onOpenDocument], ) + const handleSelectAllVisible = useCallback(() => { + if (onSelectAllVisible) { + onSelectAllVisible(documents.map((d) => d.id).filter(Boolean) as string[]) + } + }, [documents, onSelectAllVisible]) + + const handleBulkDeleteClick = useCallback(() => { + if (selectedDocumentIds.size === 0) return + setShowBulkDeleteConfirm(true) + }, [selectedDocumentIds.size]) + + const handleBulkDeleteConfirm = useCallback(() => { + setShowBulkDeleteConfirm(false) + onBulkDelete?.() + }, [onBulkDelete]) + const renderMasonryItem = useCallback( ({ index, @@ -269,13 +317,21 @@ export function MemoriesGrid({ width: number }) => { if (data.type === "document") { + const doc = data.data return ( onToggleSelection(doc.id as string) + : undefined + } /> ) @@ -283,7 +339,7 @@ export function MemoriesGrid({ return null }, - [handleCardClick], + [handleCardClick, isSelectionMode, selectedDocumentIds, onToggleSelection], ) if (!user) { @@ -303,6 +359,11 @@ export function MemoriesGrid({
{!isEmpty && (
+
+
+ {selectedDocumentIds.size > 0 ? ( + <> + + + + ) : ( +

+ Select one or more documents +

+ )} + + )} + {!isSelectionMode && onEnterSelectionMode && ( + + )} +
+
+ + + + + + Delete selected memories? + + + This will permanently delete {selectedDocumentIds.size}{" "} + {selectedDocumentIds.size === 1 ? "memory" : "memories"}. This + action cannot be undone. + + + + setShowBulkDeleteConfirm(false)} + > + Cancel + + + {isBulkDeleting ? "Deleting…" : "Delete"} + + + + + {error ? (
@@ -435,18 +595,31 @@ function DocumentUrlDisplay({ url }: { url: string }) { ) } +function isTemporaryId(id: string | null | undefined): boolean { + if (!id) return false + return id.startsWith("temp-") || id.startsWith("temp-file-") +} + const DocumentCard = memo( ({ index: _index, data: document, width, onClick, + isSelectionMode = false, + isSelected = false, + onToggleSelection, }: { index: number data: DocumentWithMemories width: number onClick: (document: DocumentWithMemories) => void + isSelectionMode?: boolean + isSelected?: boolean + onToggleSelection?: () => void }) => { + const canSelect = + !isTemporaryId(document.id) && !isTemporaryId(document.customId) const [rotation, setRotation] = useState({ rotateX: 0, rotateY: 0 }) const cardRef = useRef(null) const [ogData, setOgData] = useState(null) @@ -489,8 +662,12 @@ const DocumentCard = memo( } }, [needsOgData, ogData, isLoadingOg, document.url]) + useEffect(() => { + if (isSelectionMode) setRotation({ rotateX: 0, rotateY: 0 }) + }, [isSelectionMode]) + const handleMouseMove = (e: React.MouseEvent) => { - if (!cardRef.current) return + if (isSelectionMode || !cardRef.current) return const rect = cardRef.current.getBoundingClientRect() const centerX = rect.left + rect.width / 2 @@ -511,12 +688,32 @@ const DocumentCard = memo( } return ( -
+
+ {isSelectionMode && canSelect && ( + + )}