Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 99 additions & 8 deletions src/components/features/report/report-client-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
ArrowLeftIcon,
ArrowRightIcon,
CheckIcon,
CopyIcon,
PersonIcon,
} from '@radix-ui/react-icons';
import {
Expand All @@ -30,7 +32,7 @@ import { useOffline } from '@/contexts/offline-context';
import useGetPullRequestsReport from '@/hooks/use-get-pullrequests-report';
import useGetRepositories from '@/hooks/use-get-repositories';

import { TPullRequest } from '@/utils/schemas';
import { TPullRequest, TPullRequestReport } from '@/utils/schemas';

import TEAMS from '@/constants/teams';

Expand All @@ -40,13 +42,88 @@ import RepoPRStatusList from './repo-pr-status-list';

type RepoStatusMap = {
[repo: string]: {
[status: string]: TPullRequest[];
[status in 'in_progress' | 'waiting_for_review' | 'reviewed' | 'merged' | 'blocked']?: TPullRequest[];
};
};

export type Status = 'blocked' | 'in_progress' | 'merged' | 'reviewed' | 'waiting_for_review';
type TeamRepoStatusMap = {
map: Record<string, RepoStatusMap>;
foundAny: boolean;
}

type RepoStatusArray = [
repo: string,
statusMap: {
[status in Status]?: TPullRequest[];
}
];
export type Status = keyof TPullRequestReport;

export const STATUS_ORDER = ['waiting_for_review', 'in_progress', 'reviewed', 'merged', 'blocked'] as const satisfies readonly Status[];

const statusToEmoji = {
waiting_for_review: '🕒',
in_progress: '🚧',
reviewed: '✅',
merged: '🔀',
blocked: '⛔',
};

const reviewDecisionToEmoji = {
APPROVED: '🟢',
CHANGES_REQUESTED: '🟠',
REVIEW_REQUIRED: '🔵',
'': '',
};

const generateMarkdownReport = (startDate: Date, endDate: Date, selectedTeams: string[], teamRepoStatusMap: TeamRepoStatusMap) => {
let md = '# Weekly PR Report\n';
md += `**Week:** ${format(startDate, 'MMMM d, yyyy')} - ${format(endDate, 'MMMM d, yyyy')}\n\n`;
md += '**Legend:**\n';
md += '- 🟢 Approved\n';
md += '- 🟠 Changes Requested\n';
md += '- 🔵 Review Required\n\n';

selectedTeams.forEach((teamName) => {
const repoStatusMap = teamRepoStatusMap.map[teamName];
if (!repoStatusMap || Object.keys(repoStatusMap).length === 0) {
md += `No pull requests found for team **${teamName}**.\n\n`;
return md;
}

md += `## 👥 ${teamName}\n`;

export const STATUS_ORDER: Status[] = ['waiting_for_review', 'in_progress', 'reviewed', 'merged', 'blocked'];
const gnolangRepos: RepoStatusArray[] = [];
const otherRepos: RepoStatusArray[] = [];

Object.entries(repoStatusMap).forEach(([repo, statusMap]) => {
if (repo.startsWith('gnolang/')) gnolangRepos.push([repo, statusMap]);
else otherRepos.push([repo, statusMap]);
});
otherRepos.sort(([a], [b]) => a.localeCompare(b));

const sortedRepos = [...gnolangRepos, ...otherRepos];

sortedRepos.forEach(([repo, statusMap]) => {
md += `\n\n### ${repo}\n\n`;
STATUS_ORDER.forEach((status) => {
const prs = statusMap[status] || [];
if (prs.length === 0) return;
md += `\n - #### ${statusToEmoji[status] || ''} ${status.replace(/_/g, ' ').toUpperCase()}\n`;
prs.forEach((pr) => {
md += ` - **${pr.title}** `;
md += `([#${pr.number}](${pr.url})) by @${pr.authorLogin}`;
if (pr.reviewDecision) {
md += ` ${reviewDecisionToEmoji[(pr.reviewDecision as keyof typeof reviewDecisionToEmoji)] || ''}`;
}
md += '\n \n';
});
});
});
});

return md;
};

function groupPRsByRepoAndStatus(
pullRequests: Record<Status, TPullRequest[]>,
Expand Down Expand Up @@ -84,6 +161,7 @@ const ReportClientPage = () => {
const [endDate, setEndDate] = useState<Date>(endOfWeek(initialRefDate, { weekStartsOn: 0 }));
const [selectedTeams, setSelectedTeams] = useState<string[]>(['Core Team']);
const [selectedRepositories, setSelectedRepositories] = useState<string[]>(['gnolang/gno']);
const [copied, setCopied] = useState(false);

const { data: repositories = [] } = useGetRepositories();
const { data: pullRequests, isPending } = useGetPullRequestsReport({ startDate, endDate });
Expand Down Expand Up @@ -133,7 +211,7 @@ const ReportClientPage = () => {
}
};

const teamRepoStatusMap = useMemo(() => {
const teamRepoStatusMap: TeamRepoStatusMap = useMemo(() => {
if (!pullRequests) return { map: {}, foundAny: false };

const map: Record<string, RepoStatusMap> = {};
Expand All @@ -156,6 +234,17 @@ const ReportClientPage = () => {
return { map, foundAny };
}, [pullRequests, selectedRepositories, selectedTeams]);

const handleCopyMarkdown = async () => {
const md = generateMarkdownReport(startDate, endDate, selectedTeams, teamRepoStatusMap);
try {
await navigator.clipboard.writeText(md);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
setCopied(false);
}
};
Comment on lines +237 to +246
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clipboard implementation needs browser compatibility improvements.

The current implementation only uses the modern navigator.clipboard.writeText() API, which requires HTTPS/secure contexts and may not work in all browsers or deployment scenarios.

Consider implementing a fallback mechanism for better browser compatibility:

 const handleCopyMarkdown = async () => {
   const md = generateMarkdownReport(startDate, endDate, selectedTeams, teamRepoStatusMap);
   try {
-    await navigator.clipboard.writeText(md);
-    setCopied(true);
-    setTimeout(() => setCopied(false), 2000);
+    // Try modern clipboard API first
+    if (navigator.clipboard && window.isSecureContext) {
+      await navigator.clipboard.writeText(md);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } else {
+      // Fallback for insecure contexts or unsupported browsers
+      const textArea = document.createElement('textarea');
+      textArea.value = md;
+      textArea.style.position = 'absolute';
+      textArea.style.left = '-999999px';
+      document.body.appendChild(textArea);
+      textArea.select();
+      document.execCommand('copy');
+      document.body.removeChild(textArea);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    }
   } catch (e) {
+    console.warn('Clipboard operation failed:', e);
     setCopied(false);
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleCopyMarkdown = async () => {
const md = generateMarkdownReport(startDate, endDate, selectedTeams, teamRepoStatusMap);
try {
await navigator.clipboard.writeText(md);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
setCopied(false);
}
};
const handleCopyMarkdown = async () => {
const md = generateMarkdownReport(startDate, endDate, selectedTeams, teamRepoStatusMap);
try {
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(md);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else {
// Fallback for insecure contexts or unsupported browsers
const textArea = document.createElement('textarea');
textArea.value = md;
textArea.style.position = 'absolute';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
} catch (e) {
console.warn('Clipboard operation failed:', e);
setCopied(false);
}
};
🤖 Prompt for AI Agents
In src/components/features/report/report-client-page.tsx around lines 238 to
247, the clipboard write currently only uses navigator.clipboard.writeText which
fails in non-secure or older browsers; add feature detection and a fallback: try
navigator.clipboard.writeText(md) inside a try/catch, and if it throws or
navigator.clipboard is unavailable fall back to creating a hidden textarea, set
its value to md, append it to document.body, select its contents (using
range/selectAll if needed), call document.execCommand('copy'), then remove the
textarea; ensure setCopied(true)/setCopied(false) are set based on the success
of either method and keep the existing timeout for clearing the copied state,
and catch/log errors for debugging.


return (
<LayoutContainer mt={{ initial: '2', sm: '5' }}>
<Flex direction="column" gap="4" flexGrow="1">
Expand All @@ -175,19 +264,21 @@ const ReportClientPage = () => {
<ArrowLeftIcon />
<Text className="hidden sm:block">Previous Week</Text>
</Button>
<Flex gap="2">
<Flex direction={{ initial: 'column', sm: 'row' }} gap={{ initial: '1', sm: '2' }}>
<TeamSelector
teams={TEAMS}
selectedTeams={selectedTeams}
onSelectedTeamsChange={setSelectedTeams}
mb="3"
/>
<RepositoriesSelector
repositories={repositories}
selectedRepositories={selectedRepositories}
onSelectedRepositoriesChange={setSelectedRepositories}
mb="3"
/>
<Button onClick={handleCopyMarkdown} variant="soft">
{copied ? <CheckIcon /> : <CopyIcon />}
Copy as markdown
</Button>
</Flex>
<Button
variant="ghost"
Expand Down