liam icon indicating copy to clipboard operation
liam copied to clipboard

✨ Display last repository commit time and organization icon in ProjectItem

Open devin-ai-integration[bot] opened this issue 7 months ago • 6 comments
trafficstars

https://github.com/user-attachments/assets/bc09f1ce-0fa6-413c-9941-038203a39a10

Display last repository commit time and organization icon in ProjectItem

Overview

  • Display the last commit time of the repository linked to a project instead of project creation time
  • Show the GitHub organization icon for the repository in the project icon placeholder

Changes

  • Added and functions to GitHub API
  • Modified project data retrieval to include repository information
  • Created server components to fetch and display repository data
  • Updated ProjectItem component to show repository information

Link to Devin run: https://app.devin.ai/sessions/8077ac441097437a90cb905785b91dbd User: [email protected]

⚠️ No Changeset found

Latest commit: 67c5cece34733f81b5d45490b047466be5984d56

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

changeset-bot[bot] avatar Apr 16 '25 12:04 changeset-bot[bot]

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
liam-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 18, 2025 7:25am
liam-erd-sample ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 18, 2025 7:25am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
liam-docs ⬜️ Ignored (Inspect) Visit Preview Apr 18, 2025 7:25am

vercel[bot] avatar Apr 16 '25 12:04 vercel[bot]

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • [x] Disable automatic comment and CI monitoring

(aside) I have made many changes of my own to accommodate console error. I would like someone to review them for me.🙏

To solve this problem to address the following error, we separate the client component from the server component.

OrganizationIcon.tsx:13 Cannot update a component (`Router`) while rendering a different component (`OrganizationIcon`). 
To locate the bad setState() call inside `OrganizationIcon`, follow the stack trace as described in https://react.dev/link/setstate-in-render

Like this:

LastCommitInfo.tsx (server component) ↓ Get data LastCommitInfoClient.tsx (client component) ↓ Display ProjectItem.tsx (use data acquisition function in client component)

FunamaYukina avatar Apr 17 '25 05:04 FunamaYukina

PR Reviewer Guide 🔍

(Review updated until commit https://github.com/liam-hq/liam/commit/f391335b7b78dfa1b5f2c33f9d779ff85adc494f)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Redundant Component

This component appears to be unused and duplicates functionality already implemented in LastCommitDataWrapper. It performs the same task of fetching and displaying commit information.

import { getLastCommit } from '@liam-hq/github'

interface LastCommitInfoProps {
  installationId: number
  owner: string
  repo: string
  defaultDate: string
}

export async function LastCommitInfo({
  installationId,
  owner,
  repo,
  defaultDate,
}: LastCommitInfoProps) {
  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    return date.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
    })
  }

  try {
    const commitInfo = await getLastCommit(installationId, owner, repo)

    if (commitInfo) {
      return (
        <>
          <span>{commitInfo.author}</span>
          <span>committed</span>
          <span>on {formatDate(commitInfo.date)}</span>
        </>
      )
    }
  } catch (error) {
    console.error('Failed to fetch last commit info:', error)
  }

  return (
    <>
      <span>User</span>
      <span>committed</span>
      <span>on {formatDate(defaultDate)}</span>
    </>
  )
}
Loading State

The loading state displays "Loading commit info..." text which may cause layout shifts when data loads. Consider using a skeleton loader or maintaining consistent layout during loading.

  return <span>Loading commit info...</span>
}
Null Safety

When accessing repository properties like installation_id, owner, and name, there's no null check for the repository object itself, which could lead to runtime errors if repository is undefined.

<LastCommitDataWrapper
  installationId={repository.installation_id}
  owner={repository.owner}
  repo={repository.name}
  defaultDate={project.created_at}

PR Code Suggestions ✨

Latest suggestions up to f391335

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Remove duplicate component file
Suggestion Impact:The entire LastCommitInfo.tsx file was removed as suggested, eliminating the duplicate functionality and reducing maintenance overhead

code diff:

-import { getLastCommit } from '@liam-hq/github'
 
-interface LastCommitInfoProps {
-  installationId: number
-  owner: string
-  repo: string
-  defaultDate: string
-}
-
-export async function LastCommitInfo({
-  installationId,
-  owner,
-  repo,
-  defaultDate,
-}: LastCommitInfoProps) {
-  const formatDate = (dateString: string) => {
-    const date = new Date(dateString)
-    return date.toLocaleDateString('en-US', {
-      month: 'short',
-      day: 'numeric',
-      year: 'numeric',
-    })
-  }
-
-  try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
-
-    if (commitInfo) {
-      return (
-        <>
-          <span>{commitInfo.author}</span>
-          <span>committed</span>
-          <span>on {formatDate(commitInfo.date)}</span>
-        </>
-      )
-    }
-  } catch (error) {
-    console.error('Failed to fetch last commit info:', error)
-  }
-
-  return (
-    <>
-      <span>User</span>
-      <span>committed</span>
-      <span>on {formatDate(defaultDate)}</span>
-    </>
-  )
-}

The LastCommitInfo.tsx file appears to be unused in the PR but duplicates
functionality already implemented in LastCommitData.tsx and
LastCommitDataWrapper.tsx. This creates maintenance overhead and potential
confusion. Consider removing this file to avoid duplication.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [1-8]

-import { getLastCommit } from '@liam-hq/github'
+// This file should be removed as it duplicates functionality in LastCommitData.tsx and LastCommitDataWrapper.tsx
 
-interface LastCommitInfoProps {
-  installationId: number
-  owner: string
-  repo: string
-  defaultDate: string
-}
-

[Suggestion has been applied]

Suggestion importance[1-10]: 8

__

Why: The LastCommitInfo.tsx file duplicates functionality already implemented in LastCommitData.tsx and LastCommitDataWrapper.tsx, creating unnecessary maintenance overhead and potential confusion in the codebase.

Medium
Improve loading state consistency

The loading state displays a text that might disrupt the UI layout. Consider
using a more subtle loading indicator or maintaining the same structure as the
loaded state to prevent layout shifts.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitDataWrapper.tsx [52-54]

 if (isLoading) {
-  return <span>Loading commit info...</span>
+  return (
+    <>
+      <span>Loading</span>
+      <span>commit</span>
+      <span>info...</span>
+    </>
+  )
 }
  • [ ] Apply this suggestion
Suggestion importance[1-10]: 5

__

Why: The suggestion improves UI consistency by maintaining the same structure during loading as in the loaded state, which prevents layout shifts and provides a better user experience.

Low
Possible issue
Add image error handling

The Image component is missing error handling. If the image fails to load, it
will silently fall back to the default Next.js error display. Add an onError
handler to gracefully fall back to the ProjectIcon component.

frontend/apps/app/features/projects/components/ProjectItem/OrganizationIcon.tsx [12-26]

 export function OrganizationIcon({ avatarUrl, owner }: OrganizationIconProps) {
-  if (avatarUrl) {
+  const [hasError, setHasError] = useState(false);
+  
+  if (avatarUrl && !hasError) {
     return (
       <Image
         src={avatarUrl}
         alt={`${owner} organization icon`}
         width={32}
         height={32}
         className={styles.projectIcon}
+        onError={() => setHasError(true)}
       />
     )
   }
 
   return <ProjectIcon className={styles.projectIcon} />
 }
  • [ ] Apply this suggestion
Suggestion importance[1-10]: 7

__

Why: Adding error handling for the Image component is important to gracefully handle cases where GitHub avatar images fail to load, preventing unexpected UI behavior and providing a consistent fallback.

Medium
Learned
best practice
Add stronger input validation with type checking and format validation for function parameters to prevent potential runtime errors

The current validation only checks if the parameters exist but doesn't validate
their types or formats. For installationId, we should verify it's a positive
number, and for owner and repo, we should check they are non-empty strings with
valid GitHub repository naming patterns.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitData.tsx [6-29]

 export async function fetchLastCommitData(
   installationId: number,
   owner: string,
   repo: string,
 ) {
   try {
-    if (!installationId || !owner || !repo) {
+    // Validate installation ID is a positive number
+    if (!installationId || typeof installationId !== 'number' || installationId <= 0) {
+      console.error('Invalid installation ID:', installationId)
+      return null
+    }
+    
+    // Validate owner and repo are non-empty strings with valid GitHub naming patterns
+    if (!owner || typeof owner !== 'string' || !/^[a-zA-Z0-9-]+$/.test(owner)) {
+      console.error('Invalid owner name:', owner)
+      return null
+    }
+    
+    if (!repo || typeof repo !== 'string' || !/^[a-zA-Z0-9._-]+$/.test(repo)) {
+      console.error('Invalid repository name:', repo)
       return null
     }
 
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return {
         author: commitInfo.author,
         date: commitInfo.date,
       }
     }
 
     return null
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
     return null
   }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6
Low
  • [ ] Update

Previous suggestions

✅ Suggestions up to commit 3ee5ddc
CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent state updates after unmount
Suggestion Impact:The commit completely refactored the component to move the data fetching logic out of the component, eliminating the need for the suggested pattern. Instead of implementing the suggested cleanup pattern, the commit solved the underlying problem by moving the async operations to child components.

code diff:

-import { useEffect, useState } from 'react'
 import type { FC } from 'react'
-import { getLastCommitData } from './LastCommitInfo'
-import { LastCommitInfoClient } from './LastCommitInfoClient'
-import { getOrganizationData } from './OrganizationIcon'
-import { OrganizationIconClient } from './OrganizationIconClient'
+import { LastCommitInfo } from './LastCommitInfo'
+import { OrganizationIcon } from './OrganizationIcon'
 import { ProjectIcon } from './ProjectIcon'
 import styles from './ProjectItem.module.css'
 
@@ -24,14 +19,6 @@
 }
 
 export const ProjectItem: FC<ProjectItemProps> = ({ project }) => {
-  // State for organization avatar and commit info
-  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
-  const [commitInfo, setCommitInfo] = useState<{
-    author: string
-    date: string
-  } | null>(null)
-  const [isLoading, setIsLoading] = useState(true)
-
   // Format date to "MMM DD, YYYY" format
   const formatDate = (dateString: string) => {
     const date = new Date(dateString)
@@ -45,39 +32,6 @@
   const repositoryName = project.name?.toLowerCase() || 'untitled-project'
   const repository = project.ProjectRepositoryMapping?.[0]?.repository
 
-  // Fetch organization and commit data
-  useEffect(() => {
-    if (repository) {
-      const fetchData = async () => {
-        setIsLoading(true)
-
-        try {
-          // Fetch organization data
-          const orgData = await getOrganizationData({
-            installationId: repository.installationId,
-            owner: repository.owner,
-            repo: repository.name,
-          })
-          setAvatarUrl(orgData)
-
-          // Fetch commit data
-          const commitData = await getLastCommitData({
-            installationId: repository.installationId,
-            owner: repository.owner,
-            repo: repository.name,
-          })
-          setCommitInfo(commitData)
-        } catch (error) {
-          console.error('Error fetching data:', error)
-        } finally {
-          setIsLoading(false)
-        }
-      }
-
-      fetchData()
-    }
-  }, [repository])

The fetchData function is defined inside a useEffect hook without being wrapped
in useCallback, which could lead to unnecessary re-renders. Additionally,
there's no cleanup function to handle component unmounting during async
operations, which could cause memory leaks or state updates on unmounted
components.

frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx [48-79]

 // Fetch organization and commit data
 useEffect(() => {
+  let isMounted = true;
+  
   if (repository) {
     const fetchData = async () => {
       setIsLoading(true)
 
       try {
         // Fetch organization data
         const orgData = await getOrganizationData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
-        setAvatarUrl(orgData)
+        if (isMounted) setAvatarUrl(orgData)
 
         // Fetch commit data
         const commitData = await getLastCommitData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
-        setCommitInfo(commitData)
+        if (isMounted) setCommitInfo(commitData)
       } catch (error) {
         console.error('Error fetching data:', error)
       } finally {
-        setIsLoading(false)
+        if (isMounted) setIsLoading(false)
       }
     }
 
     fetchData()
   }
+  
+  return () => {
+    isMounted = false;
+  }
 }, [repository])

[Suggestion has been applied]

Suggestion importance[1-10]: 8

__

Why: This suggestion addresses an important React pattern to prevent memory leaks by adding cleanup logic that prevents state updates after component unmounting. This is a significant improvement that helps avoid potential bugs when asynchronous operations complete after a component is no longer mounted.

Medium
General
Eliminate duplicate API call

The function duplicates logic already present in getLastCommitData(). Consider
reusing that function instead of directly calling getLastCommit() again, which
would reduce code duplication and ensure consistent behavior between the two
functions.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]

 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
   try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
+    const commitData = await getLastCommitData({ installationId, owner, repo })
 
-    if (commitInfo) {
+    if (commitData) {
       return (
         <LastCommitInfoClient
-          author={commitInfo.author}
-          date={commitInfo.date}
+          author={commitData.author}
+          date={commitData.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies code duplication where LastCommitInfo directly calls getLastCommit instead of reusing the existing getLastCommitData function. This refactoring would improve maintainability and ensure consistent behavior between the two functions.

Medium
Learned
best practice
Add input validation for required parameters before processing to prevent potential runtime errors

Add validation for required parameters at the beginning of the function to
prevent potential runtime errors. The function should validate that
installationId, owner, and repo are valid values before attempting to fetch
commit information.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]

 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
+  if (!installationId || !owner || !repo) {
+    console.error('Missing required parameters for LastCommitInfo');
+    return <LastCommitInfoClient date={defaultDate} />;
+  }
+  
   try {
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return (
         <LastCommitInfoClient
           author={commitInfo.author}
           date={commitInfo.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
Suggestion importance[1-10]: 6
Low
Suggestions up to commit 3ee5ddc
CategorySuggestion                                                                                                                                    Impact
General
Eliminate duplicate code

The function duplicates logic that already exists in getLastCommitData(). You
should reuse the existing function to avoid code duplication and ensure
consistent behavior.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]

 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
-  try {
-    const commitInfo = await getLastCommit(installationId, owner, repo)
-
-    if (commitInfo) {
-      return (
-        <LastCommitInfoClient
-          author={commitInfo.author}
-          date={commitInfo.date}
-        />
-      )
-    }
-  } catch (error) {
-    console.error('Failed to fetch last commit info:', error)
-  }
-
-  return <LastCommitInfoClient date={defaultDate} />
+  const commitInfo = await getLastCommitData({ installationId, owner, repo });
+  
+  return (
+    <LastCommitInfoClient
+      author={commitInfo?.author}
+      date={commitInfo?.date || defaultDate}
+    />
+  );
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies code duplication between LastCommitInfo and getLastCommitData functions. Refactoring to reuse the existing function improves maintainability and reduces the chance of inconsistent behavior between the two implementations.

Medium
Learned
best practice
Add input validation to check for required parameters before making API calls to prevent runtime errors

Add input validation at the beginning of the function to check if required
parameters are present and valid before making the API call. This prevents
unnecessary API calls and potential runtime errors when parameters are missing
or invalid.

frontend/apps/app/features/projects/components/ProjectItem/LastCommitInfo.tsx [34-56]

 export async function LastCommitInfo({
   installationId,
   owner,
   repo,
   defaultDate,
 }: LastCommitInfoProps) {
+  // Validate required parameters
+  if (!installationId || !owner || !repo) {
+    console.error('Missing required parameters for LastCommitInfo')
+    return <LastCommitInfoClient date={defaultDate} />
+  }
+  
   try {
     const commitInfo = await getLastCommit(installationId, owner, repo)
 
     if (commitInfo) {
       return (
         <LastCommitInfoClient
           author={commitInfo.author}
           date={commitInfo.date}
         />
       )
     }
   } catch (error) {
     console.error('Failed to fetch last commit info:', error)
   }
 
   return <LastCommitInfoClient date={defaultDate} />
 }
Suggestion importance[1-10]: 6
Low
Possible issue
Fix effect dependency

The effect doesn't handle the case when repository is undefined but later
becomes defined. Add a condition to also run the effect when isLoading is true
to ensure data is fetched when repository becomes available.

frontend/apps/app/features/projects/components/ProjectItem/ProjectItem.tsx [49-79]

 useEffect(() => {
   if (repository) {
     const fetchData = async () => {
       setIsLoading(true)
 
       try {
         // Fetch organization data
         const orgData = await getOrganizationData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
         setAvatarUrl(orgData)
 
         // Fetch commit data
         const commitData = await getLastCommitData({
           installationId: repository.installationId,
           owner: repository.owner,
           repo: repository.name,
         })
         setCommitInfo(commitData)
       } catch (error) {
         console.error('Error fetching data:', error)
       } finally {
         setIsLoading(false)
       }
     }
 
     fetchData()
   }
-}, [repository])
+}, [repository, isLoading])
Suggestion importance[1-10]: 3

__

Why: Adding isLoading to the dependency array could create an infinite loop since isLoading is set within the effect itself. This would cause the effect to run again whenever isLoading changes, which happens in every execution of the effect.

Low

This pull request refactors the database schema by renaming tables, columns, and indexes to snake_case with pluralized names and updates foreign keys accordingly. The key concern is ensuring robust rollback and data integrity during these atomic migration operations. Overall, the changes align with project rules and enhance maintainability and security while warranting thorough testing of performance and RLS impacts.

Migration URL: https://liam-app-git-staging-route-06-core.vercel.app/app/projects/9d777f64-400a-42f3-a60e-98a59fc97279/ref/devin%2F1744807573-project-last-commit-time/migrations/58440237-7d77-44e1-b8ee-5e023ac6117a

ER Diagram:

  • View ERD for frontend/packages/db/schema/schema.sql: https://liam-app-git-staging-route-06-core.vercel.app/app/projects/9d777f64-400a-42f3-a60e-98a59fc97279/ref/devin%2F1744807573-project-last-commit-time/schema/frontend/packages/db/schema/schema.sql