From 72ab1c54be7829fe87aa3f1b967ed7b07f59f81f Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jan 2025 10:20:34 +0000 Subject: [PATCH 1/4] Add Milltime project connection feature - Add milltime_project_id column to repositories table - Add endpoint to update Milltime project ID for a repository - Add Milltime project selection to repository card - Modify Review/Develop buttons to show project/activity selection and start timer --- app/src/lib/api/mutations/repositories.ts | 24 ++ app/src/lib/api/queries/differs.ts | 1 + app/src/routes/_layout/prs/$prId/route.tsx | 229 ++++++++++++++++-- .../repositories/-components/repo-card.tsx | 40 +++ ...20240124000000_add_milltime_project_id.sql | 1 + toki-api/src/domain/repository.rs | 1 + toki-api/src/repositories/repository_repo.rs | 24 +- toki-api/src/routes/differs.rs | 4 + toki-api/src/routes/repositories.rs | 31 +++ 9 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 toki-api/migrations/20240124000000_add_milltime_project_id.sql diff --git a/app/src/lib/api/mutations/repositories.ts b/app/src/lib/api/mutations/repositories.ts index 89740eba..d827f159 100644 --- a/app/src/lib/api/mutations/repositories.ts +++ b/app/src/lib/api/mutations/repositories.ts @@ -9,6 +9,7 @@ export const repositoriesMutations = { useAddRepository, useFollowRepository, useDeleteRepository, + useUpdateMilltimeProject, }; function useAddRepository(options?: DefaultMutationOptions) { @@ -107,3 +108,26 @@ export type DeleteRepositoryBody = { project: string; repoName: string; }; + +type UpdateMilltimeProjectBody = RepoKey & { + milltimeProjectId: string; +}; + +function useUpdateMilltimeProject( + options?: DefaultMutationOptions, +) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["updateMilltimeProject"], + mutationFn: (body: UpdateMilltimeProjectBody) => + api.post("repositories/milltime-project", { + json: body, + }), + ...options, + onSuccess: (data, vars, ctx) => { + queryClient.invalidateQueries(queries.differs()); + options?.onSuccess?.(data, vars, ctx); + }, + }); +} diff --git a/app/src/lib/api/queries/differs.ts b/app/src/lib/api/queries/differs.ts index a7bb9866..0a630ccf 100644 --- a/app/src/lib/api/queries/differs.ts +++ b/app/src/lib/api/queries/differs.ts @@ -22,4 +22,5 @@ export type Differ = { nanos: number; } | null; isInvalid: boolean; + milltimeProjectId?: string; }; diff --git a/app/src/routes/_layout/prs/$prId/route.tsx b/app/src/routes/_layout/prs/$prId/route.tsx index a22a9897..bbe51c2e 100644 --- a/app/src/routes/_layout/prs/$prId/route.tsx +++ b/app/src/routes/_layout/prs/$prId/route.tsx @@ -10,6 +10,18 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { ListPullRequest, Thread as PullRequestThread, @@ -52,7 +64,26 @@ function PRDetailsDialog() { select: (data) => data.find((pr) => pr.id === +prId), }); - const copyTimeReportTextToClipboard = (mode: "review" | "develop") => { + const { data: projects } = useSuspenseQuery(queries.listProjects()); + const [selectedProject, setSelectedProject] = React.useState(null); + const [selectedActivity, setSelectedActivity] = React.useState(null); + const [timeReportMode, setTimeReportMode] = React.useState<"review" | "develop" | null>(null); + + const { data: activities } = useSuspenseQuery({ + ...queries.listActivities(selectedProject ?? ""), + enabled: !!selectedProject, + }); + + const { mutate: startTimer } = mutations.useStartTimer({ + onSuccess: () => { + toast.success("Timer started successfully"); + setSelectedProject(null); + setSelectedActivity(null); + setTimeReportMode(null); + }, + }); + + const getTimeReportText = (mode: "review" | "develop") => { let text = ""; const workItem = pr?.workItems.at(0); if (!workItem) { @@ -61,7 +92,10 @@ function PRDetailsDialog() { const parentWorkItem = workItem.parentId; text = `${parentWorkItem ? `#${parentWorkItem} ` : ""}#${workItem.id} - ${mode === "review" ? "[CR] " : ""}${workItem.title}`; } + return text; + }; + const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast.info(
@@ -73,6 +107,35 @@ function PRDetailsDialog() { ); }; + const startTimeReport = () => { + if (!timeReportMode || !selectedProject || !selectedActivity) return; + + const text = getTimeReportText(timeReportMode); + copyToClipboard(text); + + const project = projects.find(p => p.projectId === selectedProject); + if (!project) { + toast.error("Project not found"); + return; + } + + const activity = activities?.find(a => a.activity === selectedActivity); + if (!activity) { + toast.error("Activity not found"); + return; + } + + startTimer({ + projectId: project.projectId, + projectName: project.projectName, + activity: activity.activity, + activityName: activity.activityName, + userNote: text, + regDay: dayjs().format("YYYY-MM-DD"), + weekNumber: dayjs().week(), + }); + }; + if (!pr) { return null; } @@ -91,26 +154,150 @@ function PRDetailsDialog() { - - + { + if (open) { + setTimeReportMode("review"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }}> + + + + +
+
+

Select Project

+ +
+ {selectedProject && ( +
+

Select Activity

+ +
+ )} + +
+
+
+ { + if (open) { + setTimeReportMode("develop"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }}> + + + + +
+
+

Select Project

+ +
+ {selectedProject && ( +
+

Select Activity

+ +
+ )} + +
+
+
diff --git a/app/src/routes/_layout/repositories/-components/repo-card.tsx b/app/src/routes/_layout/repositories/-components/repo-card.tsx index ef875594..22b34aa8 100644 --- a/app/src/routes/_layout/repositories/-components/repo-card.tsx +++ b/app/src/routes/_layout/repositories/-components/repo-card.tsx @@ -24,8 +24,18 @@ import { PlayCircle, Trash, Unplug, + Timer, } from "lucide-react"; import { toast } from "sonner"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { queries } from "@/lib/api/queries/queries"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; export const RepoCard = (props: { differ: Differ; @@ -47,6 +57,13 @@ export const RepoCard = (props: { const { mutate: deleteRepository, isPending: isDeleting } = mutations.useDeleteRepository(); + const { data: projects } = useSuspenseQuery(queries.listProjects()); + const { mutate: updateMilltimeProject } = mutations.useUpdateMilltimeProject({ + onSuccess: () => { + toast.success("Milltime project updated successfully"); + }, + }); + return ( +
+ + +
)} diff --git a/toki-api/migrations/20240124000000_add_milltime_project_id.sql b/toki-api/migrations/20240124000000_add_milltime_project_id.sql new file mode 100644 index 00000000..0cfe34e6 --- /dev/null +++ b/toki-api/migrations/20240124000000_add_milltime_project_id.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ADD COLUMN milltime_project_id TEXT; \ No newline at end of file diff --git a/toki-api/src/domain/repository.rs b/toki-api/src/domain/repository.rs index bf5426d6..ab6aeef1 100644 --- a/toki-api/src/domain/repository.rs +++ b/toki-api/src/domain/repository.rs @@ -9,6 +9,7 @@ pub struct Repository { pub organization: String, pub project: String, pub repo_name: String, + pub milltime_project_id: Option, } impl From<&Repository> for RepoKey { diff --git a/toki-api/src/repositories/repository_repo.rs b/toki-api/src/repositories/repository_repo.rs index 6c594d48..442497f6 100644 --- a/toki-api/src/repositories/repository_repo.rs +++ b/toki-api/src/repositories/repository_repo.rs @@ -7,7 +7,8 @@ use super::repo_error::RepositoryError; pub trait RepoRepository { async fn get_repositories(&self) -> Result, RepositoryError>; async fn upsert_repository(&self, repository: &NewRepository) -> Result; - async fn delete_repository(&self, repo_key: &RepoKey) -> Result<(), RepositoryError>; // Added method + async fn delete_repository(&self, repo_key: &RepoKey) -> Result<(), RepositoryError>; + async fn update_milltime_project(&self, repo_key: &RepoKey, milltime_project_id: Option) -> Result<(), RepositoryError>; } pub struct RepoRepositoryImpl { @@ -25,7 +26,7 @@ impl RepoRepository for RepoRepositoryImpl { let repos = sqlx::query_as!( Repository, r#" - SELECT id, organization, project, repo_name + SELECT id, organization, project, repo_name, milltime_project_id FROM repositories "# ) @@ -72,6 +73,25 @@ impl RepoRepository for RepoRepositoryImpl { Ok(()) } + + async fn update_milltime_project(&self, repo_key: &RepoKey, milltime_project_id: Option) -> Result<(), RepositoryError> { + sqlx::query!( + r#" + UPDATE repositories + SET milltime_project_id = $4 + WHERE organization = $1 AND project = $2 AND repo_name = $3 + "#, + repo_key.organization, + repo_key.project, + repo_key.repo_name, + milltime_project_id, + ) + .execute(&self.pool) + .await + .map_err(RepositoryError::from)?; + + Ok(()) + } } pub struct NewRepository { diff --git a/toki-api/src/routes/differs.rs b/toki-api/src/routes/differs.rs index 8b078eef..85b43f15 100644 --- a/toki-api/src/routes/differs.rs +++ b/toki-api/src/routes/differs.rs @@ -40,6 +40,7 @@ struct Differ { refresh_interval: Option, followed: bool, is_invalid: bool, + milltime_project_id: Option, } #[instrument(name = "get_differs", skip(user, app_state))] @@ -82,6 +83,7 @@ async fn get_differs( let last_updated = *differ.last_updated.read().await; let refresh_interval = *differ.interval.read().await; + let repo = all_repos.iter().find(|r| RepoKey::from(*r) == key).unwrap(); differ_dtos.push(Differ { key: key.clone(), status, @@ -90,6 +92,7 @@ async fn get_differs( followed: followed_repos.contains(&key), is_invalid: false, repo_id: differ_to_repo_id[&key], + milltime_project_id: repo.milltime_project_id.clone(), }); } @@ -105,6 +108,7 @@ async fn get_differs( followed: followed_repos.contains(&key), is_invalid: true, repo_id: repo.id, + milltime_project_id: repo.milltime_project_id.clone(), }); } } diff --git a/toki-api/src/routes/repositories.rs b/toki-api/src/routes/repositories.rs index 0638e1fb..41398976 100644 --- a/toki-api/src/routes/repositories.rs +++ b/toki-api/src/routes/repositories.rs @@ -25,6 +25,7 @@ pub fn router() -> Router { .route("/", get(get_repositories)) .route("/", post(add_repository)) .route("/follow", post(follow_repository)) + .route("/milltime-project", post(update_milltime_project)) } #[instrument(name = "GET /repositories", skip(app_state))] @@ -181,3 +182,33 @@ async fn delete_repository( Ok(StatusCode::OK) } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct UpdateMilltimeProjectBody { + organization: String, + project: String, + repo_name: String, + milltime_project_id: Option, +} + +#[instrument(name = "POST /repositories/milltime-project", skip(app_state))] +async fn update_milltime_project( + State(app_state): State, + Json(body): Json, +) -> Result { + let repo_key = RepoKey::new(&body.organization, &body.project, &body.repo_name); + let repository_repo = app_state.repository_repo.clone(); + + repository_repo + .update_milltime_project(&repo_key, body.milltime_project_id) + .await + .map_err(|err| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to update Milltime project: {}", err), + ) + })?; + + Ok(StatusCode::OK) +} From d6c750e6fa51e3b6efe5071d21e7dd70dc07294c Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jan 2025 11:11:40 +0000 Subject: [PATCH 2/4] Support multiple Milltime projects per repository - Add repository_milltime_projects table for many-to-many relationship - Update repository model to include multiple project IDs - Update repository card to show checkboxes for project selection - Update PR details to show only connected projects in dropdown --- app/src/lib/api/mutations/repositories.ts | 14 ++-- .../repositories/-components/repo-card.tsx | 48 ++++++-------- ...00000_add_repository_milltime_projects.sql | 5 ++ toki-api/src/domain/repository.rs | 2 +- toki-api/src/repositories/repository_repo.rs | 65 +++++++++++++++---- toki-api/src/routes/repositories.rs | 16 ++--- 6 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 toki-api/migrations/20240124000000_add_repository_milltime_projects.sql diff --git a/app/src/lib/api/mutations/repositories.ts b/app/src/lib/api/mutations/repositories.ts index d827f159..dd4c4963 100644 --- a/app/src/lib/api/mutations/repositories.ts +++ b/app/src/lib/api/mutations/repositories.ts @@ -109,19 +109,19 @@ export type DeleteRepositoryBody = { repoName: string; }; -type UpdateMilltimeProjectBody = RepoKey & { - milltimeProjectId: string; +type UpdateMilltimeProjectsBody = RepoKey & { + milltimeProjectIds: string[]; }; -function useUpdateMilltimeProject( - options?: DefaultMutationOptions, +function useUpdateMilltimeProjects( + options?: DefaultMutationOptions, ) { const queryClient = useQueryClient(); return useMutation({ - mutationKey: ["updateMilltimeProject"], - mutationFn: (body: UpdateMilltimeProjectBody) => - api.post("repositories/milltime-project", { + mutationKey: ["updateMilltimeProjects"], + mutationFn: (body: UpdateMilltimeProjectsBody) => + api.post("repositories/milltime-projects", { json: body, }), ...options, diff --git a/app/src/routes/_layout/repositories/-components/repo-card.tsx b/app/src/routes/_layout/repositories/-components/repo-card.tsx index 22b34aa8..4035ca03 100644 --- a/app/src/routes/_layout/repositories/-components/repo-card.tsx +++ b/app/src/routes/_layout/repositories/-components/repo-card.tsx @@ -29,13 +29,7 @@ import { import { toast } from "sonner"; import { useSuspenseQuery } from "@tanstack/react-query"; import { queries } from "@/lib/api/queries/queries"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; export const RepoCard = (props: { differ: Differ; @@ -159,26 +153,26 @@ export const RepoCard = (props: {
- +
+

Connected Milltime Projects

+ {projects.map((project) => ( +
+ { + const newProjectIds = checked + ? [...props.differ.milltimeProjectIds, project.projectId] + : props.differ.milltimeProjectIds.filter(id => id !== project.projectId); + updateMilltimeProjects({ + ...props.differ, + milltimeProjectIds: newProjectIds, + }); + }} + /> + +
+ ))} +
)} diff --git a/toki-api/migrations/20240124000000_add_repository_milltime_projects.sql b/toki-api/migrations/20240124000000_add_repository_milltime_projects.sql new file mode 100644 index 00000000..21ad0495 --- /dev/null +++ b/toki-api/migrations/20240124000000_add_repository_milltime_projects.sql @@ -0,0 +1,5 @@ +CREATE TABLE repository_milltime_projects ( + repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE, + milltime_project_id TEXT NOT NULL, + PRIMARY KEY (repository_id, milltime_project_id) +); \ No newline at end of file diff --git a/toki-api/src/domain/repository.rs b/toki-api/src/domain/repository.rs index ab6aeef1..d7262471 100644 --- a/toki-api/src/domain/repository.rs +++ b/toki-api/src/domain/repository.rs @@ -9,7 +9,7 @@ pub struct Repository { pub organization: String, pub project: String, pub repo_name: String, - pub milltime_project_id: Option, + pub milltime_project_ids: Vec, } impl From<&Repository> for RepoKey { diff --git a/toki-api/src/repositories/repository_repo.rs b/toki-api/src/repositories/repository_repo.rs index 442497f6..a5b46c5f 100644 --- a/toki-api/src/repositories/repository_repo.rs +++ b/toki-api/src/repositories/repository_repo.rs @@ -8,7 +8,7 @@ pub trait RepoRepository { async fn get_repositories(&self) -> Result, RepositoryError>; async fn upsert_repository(&self, repository: &NewRepository) -> Result; async fn delete_repository(&self, repo_key: &RepoKey) -> Result<(), RepositoryError>; - async fn update_milltime_project(&self, repo_key: &RepoKey, milltime_project_id: Option) -> Result<(), RepositoryError>; + async fn update_milltime_projects(&self, repo_key: &RepoKey, milltime_project_ids: Vec) -> Result<(), RepositoryError>; } pub struct RepoRepositoryImpl { @@ -23,17 +23,28 @@ impl RepoRepositoryImpl { impl RepoRepository for RepoRepositoryImpl { async fn get_repositories(&self) -> Result, RepositoryError> { - let repos = sqlx::query_as!( - Repository, + let repos = sqlx::query!( r#" - SELECT id, organization, project, repo_name, milltime_project_id - FROM repositories + SELECT r.id, r.organization, r.project, r.repo_name, + ARRAY_AGG(rmp.milltime_project_id) FILTER (WHERE rmp.milltime_project_id IS NOT NULL) as milltime_project_ids + FROM repositories r + LEFT JOIN repository_milltime_projects rmp ON r.id = rmp.repository_id + GROUP BY r.id, r.organization, r.project, r.repo_name "# ) .fetch_all(&self.pool) .await?; - Ok(repos) + Ok(repos + .into_iter() + .map(|r| Repository { + id: r.id, + organization: r.organization, + project: r.project, + repo_name: r.repo_name, + milltime_project_ids: r.milltime_project_ids.unwrap_or_default(), + }) + .collect()) } async fn upsert_repository(&self, repository: &NewRepository) -> Result { @@ -74,22 +85,48 @@ impl RepoRepository for RepoRepositoryImpl { Ok(()) } - async fn update_milltime_project(&self, repo_key: &RepoKey, milltime_project_id: Option) -> Result<(), RepositoryError> { - sqlx::query!( + async fn update_milltime_projects(&self, repo_key: &RepoKey, milltime_project_ids: Vec) -> Result<(), RepositoryError> { + let mut tx = self.pool.begin().await?; + + // Get repository ID + let repo = sqlx::query!( r#" - UPDATE repositories - SET milltime_project_id = $4 + SELECT id FROM repositories WHERE organization = $1 AND project = $2 AND repo_name = $3 "#, repo_key.organization, repo_key.project, repo_key.repo_name, - milltime_project_id, ) - .execute(&self.pool) - .await - .map_err(RepositoryError::from)?; + .fetch_one(&mut *tx) + .await?; + + // Delete existing connections + sqlx::query!( + r#" + DELETE FROM repository_milltime_projects + WHERE repository_id = $1 + "#, + repo.id, + ) + .execute(&mut *tx) + .await?; + + // Insert new connections + for project_id in milltime_project_ids { + sqlx::query!( + r#" + INSERT INTO repository_milltime_projects (repository_id, milltime_project_id) + VALUES ($1, $2) + "#, + repo.id, + project_id, + ) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; Ok(()) } } diff --git a/toki-api/src/routes/repositories.rs b/toki-api/src/routes/repositories.rs index 41398976..68e87bfe 100644 --- a/toki-api/src/routes/repositories.rs +++ b/toki-api/src/routes/repositories.rs @@ -25,7 +25,7 @@ pub fn router() -> Router { .route("/", get(get_repositories)) .route("/", post(add_repository)) .route("/follow", post(follow_repository)) - .route("/milltime-project", post(update_milltime_project)) + .route("/milltime-projects", post(update_milltime_projects)) } #[instrument(name = "GET /repositories", skip(app_state))] @@ -185,28 +185,28 @@ async fn delete_repository( #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -struct UpdateMilltimeProjectBody { +struct UpdateMilltimeProjectsBody { organization: String, project: String, repo_name: String, - milltime_project_id: Option, + milltime_project_ids: Vec, } -#[instrument(name = "POST /repositories/milltime-project", skip(app_state))] -async fn update_milltime_project( +#[instrument(name = "POST /repositories/milltime-projects", skip(app_state))] +async fn update_milltime_projects( State(app_state): State, - Json(body): Json, + Json(body): Json, ) -> Result { let repo_key = RepoKey::new(&body.organization, &body.project, &body.repo_name); let repository_repo = app_state.repository_repo.clone(); repository_repo - .update_milltime_project(&repo_key, body.milltime_project_id) + .update_milltime_projects(&repo_key, body.milltime_project_ids) .await .map_err(|err| { ( StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to update Milltime project: {}", err), + format!("Failed to update Milltime projects: {}", err), ) })?; From 0fee51d0387b764c7927cab6e01fb097feb90a95 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jan 2025 12:00:48 +0000 Subject: [PATCH 3/4] Refactor PR details dialog - Extract PR details dialog to separate component - Extract timer selection form to separate component - Update PR details to show only connected Milltime projects --- app/src/routes/_layout/prs/$prId/route.tsx | 55 +---- .../prs/-components/pr-details-dialog.tsx | 210 ++++++++++++++++++ .../prs/-components/timer-selection-form.tsx | 91 ++++++++ 3 files changed, 308 insertions(+), 48 deletions(-) create mode 100644 app/src/routes/_layout/prs/-components/pr-details-dialog.tsx create mode 100644 app/src/routes/_layout/prs/-components/timer-selection-form.tsx diff --git a/app/src/routes/_layout/prs/$prId/route.tsx b/app/src/routes/_layout/prs/$prId/route.tsx index bbe51c2e..f2fb8b18 100644 --- a/app/src/routes/_layout/prs/$prId/route.tsx +++ b/app/src/routes/_layout/prs/$prId/route.tsx @@ -1,52 +1,7 @@ -import { AzureAvatar } from "@/components/azure-avatar"; -import BranchLink from "@/components/branch-link"; -import { PRLink } from "@/components/pr-link"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - ListPullRequest, - Thread as PullRequestThread, - User, -} from "@/lib/api/queries/pullRequests"; +import { createFileRoute } from "@tanstack/react-router"; import { queries } from "@/lib/api/queries/queries"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import dayjs from "dayjs"; -import { - ClipboardCopy, - CodeXmlIcon, - MessageCircleCodeIcon, -} from "lucide-react"; -import { toast } from "sonner"; -import Markdown from "react-markdown"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import React from "react"; -import { PRNotificationSettings } from "../-components/pr-notification-settings"; +import { PRDetailsDialog } from "../-components/pr-details-dialog"; export const Route = createFileRoute("/_layout/prs/$prId")({ loader: ({ context }) => @@ -64,11 +19,15 @@ function PRDetailsDialog() { select: (data) => data.find((pr) => pr.id === +prId), }); - const { data: projects } = useSuspenseQuery(queries.listProjects()); + const { data: differs } = useSuspenseQuery(queries.differs()); + const { data: allProjects } = useSuspenseQuery(queries.listProjects()); const [selectedProject, setSelectedProject] = React.useState(null); const [selectedActivity, setSelectedActivity] = React.useState(null); const [timeReportMode, setTimeReportMode] = React.useState<"review" | "develop" | null>(null); + const differ = differs.find(d => d.repoName === pr?.repoName); + const connectedProjects = allProjects.filter(p => differ?.milltimeProjectIds.includes(p.projectId)); + const { data: activities } = useSuspenseQuery({ ...queries.listActivities(selectedProject ?? ""), enabled: !!selectedProject, diff --git a/app/src/routes/_layout/prs/-components/pr-details-dialog.tsx b/app/src/routes/_layout/prs/-components/pr-details-dialog.tsx new file mode 100644 index 00000000..3667df79 --- /dev/null +++ b/app/src/routes/_layout/prs/-components/pr-details-dialog.tsx @@ -0,0 +1,210 @@ +import { AzureAvatar } from "@/components/azure-avatar"; +import BranchLink from "@/components/branch-link"; +import { PRLink } from "@/components/pr-link"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ListPullRequest } from "@/lib/api/queries/pullRequests"; +import { queries } from "@/lib/api/queries/queries"; +import { mutations } from "@/lib/api/mutations/mutations"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import dayjs from "dayjs"; +import { + ClipboardCopy, + CodeXmlIcon, + MessageCircleCodeIcon, +} from "lucide-react"; +import { toast } from "sonner"; +import React from "react"; +import { PRNotificationSettings } from "./pr-notification-settings"; +import { TimerSelectionForm } from "./timer-selection-form"; + +interface PRDetailsDialogProps { + pr: ListPullRequest; + prId: string; + parentSearch: Record; +} + +export function PRDetailsDialog({ pr, prId, parentSearch }: PRDetailsDialogProps) { + const navigate = useNavigate(); + + const { data: differs } = useSuspenseQuery(queries.differs()); + const { data: allProjects } = useSuspenseQuery(queries.listProjects()); + const [selectedProject, setSelectedProject] = React.useState(null); + const [selectedActivity, setSelectedActivity] = React.useState(null); + const [timeReportMode, setTimeReportMode] = React.useState<"review" | "develop" | null>(null); + + const differ = differs.find(d => d.repoName === pr?.repoName); + const connectedProjects = allProjects.filter(p => differ?.milltimeProjectIds.includes(p.projectId)); + + const { data: activities } = useSuspenseQuery({ + ...queries.listActivities(selectedProject ?? ""), + enabled: !!selectedProject, + }); + + const { mutate: startTimer } = mutations.useStartTimer({ + onSuccess: () => { + toast.success("Timer started successfully"); + setSelectedProject(null); + setSelectedActivity(null); + setTimeReportMode(null); + }, + }); + + const getTimeReportText = (mode: "review" | "develop") => { + let text = ""; + const workItem = pr?.workItems.at(0); + if (!workItem) { + text = `!${prId} - ${mode === "review" ? "[CR] " : ""}${pr?.title}`; + } else { + const parentWorkItem = workItem.parentId; + text = `${parentWorkItem ? `#${parentWorkItem} ` : ""}#${workItem.id} - ${mode === "review" ? "[CR] " : ""}${workItem.title}`; + } + return text; + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.info( +
+ +

+ Copied {text} to clipboard +

+
, + ); + }; + + const startTimeReport = () => { + if (!timeReportMode || !selectedProject || !selectedActivity) return; + + const text = getTimeReportText(timeReportMode); + copyToClipboard(text); + + const project = allProjects.find(p => p.projectId === selectedProject); + if (!project) { + toast.error("Project not found"); + return; + } + + const activity = activities?.find(a => a.activity === selectedActivity); + if (!activity) { + toast.error("Activity not found"); + return; + } + + startTimer({ + projectId: project.projectId, + projectName: project.projectName, + activity: activity.activity, + activityName: activity.activityName, + userNote: text, + regDay: dayjs().format("YYYY-MM-DD"), + weekNumber: dayjs().week(), + }); + }; + + return ( + { + if (!open) { + navigate({ to: "..", search: parentSearch }); + } + }} + > + + + + + +

{pr.title}

+
+
+ + + +
+ + + { + if (open) { + setTimeReportMode("review"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }}> + + + + + + + + { + if (open) { + setTimeReportMode("develop"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }}> + + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/app/src/routes/_layout/prs/-components/timer-selection-form.tsx b/app/src/routes/_layout/prs/-components/timer-selection-form.tsx new file mode 100644 index 00000000..e3bc91a3 --- /dev/null +++ b/app/src/routes/_layout/prs/-components/timer-selection-form.tsx @@ -0,0 +1,91 @@ +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Activity, ProjectSearchItem } from "@/lib/api/queries/milltime"; + +interface TimerSelectionFormProps { + projects: ProjectSearchItem[]; + selectedProject: string | null; + setSelectedProject: (value: string) => void; + selectedActivity: string | null; + setSelectedActivity: (value: string) => void; + activities: Activity[] | undefined; + onStartTimer: () => void; +} + +export function TimerSelectionForm({ + projects, + selectedProject, + setSelectedProject, + selectedActivity, + setSelectedActivity, + activities, + onStartTimer, +}: TimerSelectionFormProps) { + if (!projects.length) { + return ( +
+

+ No Milltime projects connected to this repository. Connect projects in the Repositories view. +

+
+ ); + } + + return ( +
+
+

Select Project

+ +
+ {selectedProject && ( +
+

Select Activity

+ +
+ )} + +
+ ) +} \ No newline at end of file From 8c605b5bcecc27150f61e4bc514715ca614a4fde Mon Sep 17 00:00:00 2001 From: ponbac Date: Tue, 7 Jan 2025 18:36:02 +0100 Subject: [PATCH 4/4] huh --- app/bun.lockb | Bin 283264 -> 283982 bytes app/package.json | 1 + app/src/components/ui/checkbox.tsx | 28 +++++ app/src/routes/_layout/prs/$prId/route.tsx | 122 +++++++++++++++------ 4 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 app/src/components/ui/checkbox.tsx diff --git a/app/bun.lockb b/app/bun.lockb index 9faef3bdd0908a6479a7d9b085d2023cfe9f31e1..d5b56ce8c6c0be901a78599eb1e1f1459d595945 100755 GIT binary patch delta 45024 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 z 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 UZh%`k(dK}|ds9<4x7w}#KX*fqjsO4v delta 44981 zcmeIbd3;S*_dkBmxw+(+V~7kQ<{}c3kVF$Ms^+oAC?z5YAtb4qlV}xfX*p$!*3{NK z6>U-T)EbJa8bT>Gl(vdKPm7{{@AujJNTT%7&*%Gm{`vL1toz<;?X{=1*B;Kk`^v$k zUKtC#-U_L9e)#5B3r_2p>X`ZKD=V&=J)$nvo8Dw(RKYfN`c3});*NsX+j}_pES}mW zP@gz&>hN+#A|wSIj*QMz=a)_|o!{Xo31&!QN?ck(s$z^<@}qRpv1w6L*i0XE1?94G8W>4coKY3U`=FN1UOsD(=+x! zPz-{NKxX_=A(>zTkhSayA+aNP{L8owzBKS6kQMY%n2lT!%&3Y|sQ;hxk?GSN8NE|cE$*F0OIX3%9{Se5R-{82x$%%a&j&|TtcE*UfK}jgz z;m{#qA2m^mWBp{pDDcd%3aZTvhR_l46bDbAsfc)5sFczxsM0SjBlGK>5*H7ba+D8{ z>H7hh-v%JdL%C+SvsAUkWo7(Lm(1^HAmaxmr1VP&b2x1IFmtH0bhXHGvii4xbcn{~ zWk2+V?bvU9l7_L8jtk~$XJGos3bM?Yiqc=>29FpNmloeYE~THP^))~bvgh=^vAoEkrd^%%euww?R&)nq|hh0^`D9J!)4{R<*6jo?$h%gv;u!0CE-?);BRJ0dXJJmen9PsLl@62wxb->m0wz?m{$0>aVLGYkaELGV+3 zS%D_tL&4Vr(x&e;a5ylcGFkv>luX6<0>XDQ!htN<59kYwf5yr_!Pq_&Xy3BV$p!G3@OyK!7o*%jA;?23q1vgMV4-KKaGLX%5G6l+%MI+lc^cya0&FB;!pW zTT%-C$I*D#spbZ(alw5Dr(OqN2=aj=EQ2`)B&WqCB6uEh`qHKjGJcIY!ZR>^338#~ zeLBhNpVwt+@ttKy<%68fE8RtQSOvth^qpO$oqEMePa1|2Sn3k+MS$&qoO_~yoI`%= zCj0BE!b*^%-1Lm~$cU~u4T$L?V`z8jaU;R|fbRojfz5%mQ!o&&nNbKx51RUd!%+eF z9V*C<{uPLfGxnpoKEPE#>UZp=R)RD31H7Y4G5H~U!W(lag~z=U4{S-=(` z3tp=Dw}C7m6G%PO%`$3N72in3hbX=bkm(nojqDV!LDELQB}p63fSj%qI+(tQQD!fr zZ4p5W*z3Bz&Rgp_c7fbE#U-UWUV=WC{hmN}@{knSX(dvn zFKfW!9A&>Ef;DXi&u2?*{u%HruqKd}sR*Q=U14kv;*#R}Bs?5%Z_!4?B_$=Mra4j* z1`XrV*B- zJiUAdyxbQ!5XgcnXUbW|7f5?~0NJejKsMJN_WhERd($qNNXPglFH1YO2G7YR%96uF z5?_%K_n^Q6%Oiq5JZ!u)=;P*(hlg=5=(B=;6XiUi1DW3s6C@r5vLai7)Uy_KdWgMA znLJ6lXwTQACE5TvpW5?*Jr5K{1?e%@Uza_797qfGenWQUO*N7ApDg3|foJ?j4^42K z$kI6`)Qt6DMzbfRv?;PeSxCsf&jhkX1Aw$pCm>rC1q=ZCOp_I}XVOBDv!(yiWCc2# zGM}^HOF)i!-h94D<@5zYrk@1lgq95C()*l}*9Ecx{y=u!%^40yCE#&jIp78$#$mz^}Te06x?3aXxUk-G$ZZUV_#1%629UvVlDPb7r4#y28KdA5o zkR914Az=u1r;ZWeIc6Vz-E8!s%x@6lImVj-D+BA9=Zck1zq(r1@jHb>f|HWtu|)tT z4oXaOe78o%2O|}0le$*s);B3GEiIvs18;l7Si8WFq+VH7i-gn{(-JYYC!`L2F(GAy z!|!96E+KVDN&*(Pg3yE8MrTY%gn^9hIr9E;SE_u`=ghZTC0hifo6Z6j2JZhDNG(>@y4*tDQb(Xz|kNg*M5` zG}L?-yp|J+oj-P^QR;ZN{Q?1aM))LD|eW`6t7bE{T;Fw9Le8q2ft)oL0DXY7v*{?$b+nX%z+V=(4OS>*hhk;YH?!s_izGW9nlI9OnF~QCcz6uTFhe2(}Z=%z&3;JP}~&9wu|#r#R#YE);VS z2anDMYVl@lgxfV8vL{ z!CGh2H^yze23Z@(JfIO}WU9+ej(va6~}E!|MAr;FuTs9(DW~ z9BY-&@`M^@c0;%EDy9aO4P(`abbSi0zS*O8w0@#k|qrAh>z>=}S9l%KkC?6JSWPx)d4yJKOVw}Y`b{qLB*v6X=7inZ>LpBjIx*Hl0 z8EI@&9LxkW+ycjjTa7bXV;@L&gTYXm0LOGNZ{tYUA#fpPkDzGd9zqQvcUfBXE7>_9 zKW&`p+th6wgDe^vC9GlUhfPSh&1tR8?51vQqUqbrt<^ANo4JjjFrT)R0V2iNovTvN0i}nY{Oz7v4#u0F|p_~><2j$jm4Net@p*{uN z)8_2{F`f+AO)zwVL^acf_iv2EOjPp}++nC@ys8r{IrGr*x) z-+`mWi(9TfkpWw_0w-$|}5c)tL)#?SVuBsL$gy-DG zAxMx6E{3t9-w!qYI@C8_!_3Jx8|JypC{Oqb-36703#XXb9o*VM)3>7=({XG^x6ug$ zi8F0J^IT|@CzwWHWKY}&*IRO!!{X}5!r@8irCH#bA{=qOJ(#4}d@ z%p(;aAw3ml_Xq$>;D3!KbdbCp?m0mq(zZNOD;YFn4fP+K^Xnup!kT7aXb!#p=5$`edS znLP~CqPffxj)i%D7CFl>UYKR27G2;~aMH!t4WEKzc5*uLY-!J4)NBQguA_0vH{Jxt zK`*&|;MfVMFgm&&g5AMMM_LY!akAU1!QZ+`O*HLgaLREqqCCOCgixf%#iJgLSz*^A zgs`zd=sZF#7}8_fa?|G;2k&~pYSmeU*xfMg*HIoYWo;`(8-%Q0G!`R73rm~cLa@&R zu6ujiB8e@q@r&ut>vTdzh8z){2_hiEbmhgPbxj*f5$0 zgJT;Cn&q2C>eoA%vj)_6g~8nQ&DjH@wIQbOK)3du89NXQ6{NMGYAv}&^G{K_TzG-fwQM^6v#xNJ(D|R75GR02_NxfSAZN1?LNxfYUWQ7u8G%Ia4gj6mFsaz0}<<^GL zWq!jElCo_G$rObK+9{qxsD+i^y9i0W3kXTQT1mESI6_u_+9uO?lv@ioV@J7NS%a+E zdQ`N2V~}}rRDEpyd`Dy7IrzbLux6xd2RQ7wM@4HMX7*^e*1_~0<2Gg|qY73h!L?ks zz+s?|iZ<#Ek<%syYfz*s8C(Oi$Eax6DukdhI$Hm6h&gL)eXX*YJ=X2&G1Qukp>Q0b zD0BAMXxG#fjBqPaXsWe#A@)s#nlQO`)Qrt=8-3H{xVHw7hEC6LYhI>rrd#W1#^Un> zGb__={P?2X5cs(9)G&Kj#W}pE89UC686#_)+qj7sjtoqDbkvs~Y*)agf#WobjYDvx zYcaT9RxK+Hx8^6*Z7f2vky{XgsYXW|juFy49C=Y5U|LxV(lmrHpNx&>=4`y%C_K`B z|HEK&xxryULYC7Js%xHZ6m49jOuGNTNUgD%^{U%=X_Va%&Izl)$x(T(d8Ag@jGe&u z?yL!J*P_v?T>W38O}~ltwOBKDqFY;SW=(YKe~d9tLWFmoNp5Yf89T}Cx;d78bb4Ym z+#?%OY|CGByY{5hWdmM|c6n!58wk{|KSJ#}-dr0HYH5wU2s8F|)GbqvIZQy9Bi4cA zm~)tMn2sXG$?mW?R|+^xIc=l0J!aM$ZsQ*K=Fr7k`UGrLU&b+*#g%_8Qk!qaPIkKj zU!k-2m>lgIgHSVuwB4rf6t_`wJX#8EOc~81jp^VpImrpcbpxDSKO4Sk`z*V80=NgW zlj}6NHfE2k7>@~7A&sMr7a6j$;p|y;qCHlwwZ#|>uAQYhe@B!j7_I?wHZ42ZZWEHZUI7P(8y(|`fJ{NlRG%W1 zup);;2^^X_Fxv2Z({3GKGTNEh)7{2G$kY;p<>D4NERL$MR&Uu$32pNlI2kt|H9iAQ z+6^VTYD~2}H9XoFju0(~vQd+5;5hkPH8E;Tvj-LHl>!dlfHa#JlBFW&e5TzNmNOKb zs!{DoeYa`O%Brs!W_Fg_)$VPFBbG1J#u9|+Y;yeH0aptgPBCgkYD3NJS#H-3;cz@} zo}Lx$3Yf`M3=Ko5lNH*Jkc_R8Wi3RA9f42}EA|*diB`xx%i)N#Lh}%kvG)+_XUY1! z<8UNcp}h$8L@37NT^WdWO+ZK{xPXwXK)u;^?7Ij_*{=w7VJ1eaIS=-E!I9boGkdPv z*aBH2%Tn|-$6R}Qgsrt0Gi#pPH4(DL=InXVt}hU3Vugy&<7lI-8$vR6&I8#cgs|Mq zi@_-MoA36R&u*de4uqs~zW3}5+91@*ik)T20zQcGK%k8!s`b7dIT|5Z_6~$(*#$nZ zvulG8mb&+2FwOgY;PwCu<@aJd7T86nAS6?LijXWY-$Gl~1|h3r`kaNP-=g~Z$%SUO zMfDBeMRF8j-wn@62FLM+i628{EjY}L&7+NL2-SuhMnBq_>MDdI>KRt{P# zjexbGN0cWRw%U5z&^N3#XMI@TxCS8&1K~8xU?0h1tXE*8KRC{n*puM=VI?>gW4&YR z7d|piuC8xX`dH?P0}FIxFL3OK0v0z1TpKHEID+vLI2sOZZ5XLFFn!m$jnq%1r97>- ze`B z;#0HRy81@(jkY2S=m3tj!y<)=UVx*8EKfDQ07p;3kU%?vHc5NI+?d)YfQvADB*u6k z5M{mb{fdxOr-9mRw%v>dngXtoT^}PGob*z9L5VH$;Gqa#u5tXC^(pr#*`K#%^{S8zJFvw2rt9=ML#Mvf*xU%*ERK8mZvuk?3j|YzsK1v+V53w-a$z z2@wb}4VGu@6I zy~C}&!_TT0KX*7@g1m$y@AD<(r66xTGI`3r{>6rw4T601_xG5ycGh=A?{zqaS)1GB zeGbQ~R_IrRMoQU!>$C`4%hL$qyb7V{FVqg(n2r#~5N3w9k^03i%vrnZYjw=5-R`Qx z4>%kzK513%9Wc9n#%*xcXKv%IgSMy9X}#I>-^rt(5?{)G zl^*a6xJa2UBhnZNj$Vaaz-ad@FaH>jXHwFYJlYzA|U+aeIE1 zOVza!9KKiD8$)p`yDV3uBNWeu7y-^!Rxz#<;AB=`Gbd(iC~a~vy$8IT_1fdGyG@mAR5gv<*Q z$T2wEV3h;+G&sW=BDcYD z_+ZY&@CiO8zSOrW*(1L!^8>89cd>dats| z3`}4w9wHOG2BQ3R5FaAtQ^??X6tZG(S+NqSZ-SW5+mxFtj+IVlU^+kW5UDj2#MHAu zd>)0=n{CDBMdm&SM4YSQiOg@l;&Y+2_xp%oh6|O#qmT}h7X_avsfFg%A z08XQXoau8NKS=zEo4AcxnJgbs7QOStR zzPaLaBdgO=$#Ww`t?)r?jSu>Ndmx+JL&;xI^4=OIID{SX{7}eXAAHc|Uj#CP5kSgE zDjWr51;!{$2NnbWCXf%26`uxV`WXsmDV(F?=W7LJ#_vJkFt46^D!nKIYr#?Q5kDS- z^oLIo&*8B{=@D72U5Y0%xLfhW!r;FGQtxYp$5cE}`UwTh_>79kjZAn}$^Qyj!AsDi z>i0@NH&S#NAI$Fu75{^V@)`b735Zm<3S@@=Qu4n->fMGORe#3^N9lbaJ48oj%+CWK zsgM;LrFbIq84F}OHjod| z2PhP;F#Tb{|1ppLf2c-J#Iq5+Bh4o_(zNp+rv=_u@kCl^F_5CA_+b1BGW352XsVSI zD5Sz_C0_$%6mN6$Au{+0J~(`L16crXee=nU^o@g%Grz+?@<){(F%*0r!GGd4v+&u{ z>CEONk~61o@WFPU2eKNMmHY=r;vq8llj0wRjQUx{=SEHnzd_FOeh1P_cXU{d?fF9` zcobSuP@3Kk-^>y6M)Qc zfZ_)!90Fv8hXR>#Dv%G6@-!ePhcQY{L&IGcr<^i$9IF>2-3Lpz! z1LTHLug<1faK^Guv9uH*8`Y22Q^2v=HQ)!Tw1Wr`(c@h1eSMYz#)BjijTIqFF!`w(y zf1u?54Yd6KzmYNO{g)MI7k}!3{-@JzLJNso6mA2uZ?`Ml0pyb#xyw4D>Yvt~BG#L3iYGP*!OLDy_eRETz0k zmm67|3QA7oToj~uBI9eg9OjRA471GLV&+emidn8%Q|S;{(=f&7MwVF{@+!b?D*msK zWj(L-h~&EinSW2k_i|zKq097DibTyE^nGb-8!@7Wk3EPk_u|ox=4%I{hZaZ&tVk$cIS% ztw0v=8ITRytK$ER=oZ@L5EMAQo&j<<_?t@bU&r+SgMw($TPTP4JCK&q&}w#7dI1C& zQHW+xND~%TJdu16g+&z>Q}INmFAn5FRu;&pN(!q3nLbEi4TZr#Jn0Swm_bb-jZquO z>9mECw*vCXjm(fw6Gili78Kygjnr!iVt#QTw!A-xk3{r819XysASOssI0(q+QOJto zSkS6?8kO+mM&|QGr-Gao{^!SnvceNsIvye$@*0R0cpb!tNcj{pcyePkTfi=Oiwqtj z6PO@o^fu_9V?i7jVv9#Rt))Hi5dS$Agb}XN8vh&%{&OrS=Zk-i1vwrZe|bE}RqdZ+ z!GDeg|K2GdwhMnRfz;=smHRl5&Pks8p?N?q_Wv9U(klNP3;uH~_|LK6|KnprzU2IK zEcnl{;6KNL{~QZqD){GE@IM|a{{Q`0FzoLi3$D$+zkG%`Jwt1$trOLR)VB$;&u2QgB)Xjn*Dhqs+ zQQRh6673fOz86afmxZ<%a7Dxteh{k(KMG?B;3pABxGFXfeimL!0oOz#K=faVW*k_G zX50|I%h=FmZ0Irw{w4NOu!n-+aB#}j+nF(f{803xJto2QFj#tQL7-By-I7T-`B(stF+GIG9|4)go-AzK7`854L6{ zItYfZgWxm;rA4*%5L8_c!Q}N2_=;=_PEb&90|fqJ(gp}7Zh+t_1p%V&rw~Ma3c>78 zAqW&#D7Z{P>x~eU7g-x2n7I*xdlXa@%{M{NY!d`4HbGEX+@|0b1wA%H@RV4x8G^-| zAt<~Bf@&gm3j|%aK(LvDAYp8Uz;i1EgSJ8tEH+TEo`QgF5Y!Zj+aTz_4T1v{gbLs7 z5cq6|V9a(1!o^++_D~SK1A;nY#106C?||Sm1(BlKP6(>*gkbVc2L9@>x zSn)Xo&x+d=+@hez9tfI>C3_%Pya$59dm(5pV)sJObuR>)DQGE-eGqu=gJ94;2wIB` z6s)HpU_S(HMdE%4`tOI}00r%Z?-vmGd;!6jFCge3_ENBig5U!XbP^*DKrs9O1g9zJ zBB~w4N7aK6Og;!ftjMO|1O@dDLGZkobO?fphak90K@U;)O9-OAgkbiU5cCvRD7Z{P z>%$PliLApA%sdRiJqqGQ^RFOi_7wyxzJee@+@|0b1wD>H&`&Hm0>R=V5ET9zfe#f?c2Z1T}Qm}`D;PVhn z7bDI?F#J3OrzsGk+64%zUVvco1qiZ4HU%drsCN;9cf_QN5KO!X!Bq-oi@KK}h`I#9 z>`M^L6;~*@OhN1KA($_+zK3Au_YmBp;C<2jG6c;oL$Km91PjD%3T{!*;|c_e)-AcB zP0+NK>q39jqV>Y1aU}-VjWfyvi;4O9^!;YLAP>X-5ABDG_CITLH4ky)XD!~<47W#$ ztlM5hAE;4FbobIdwTN|xuWP1<9uOk-dFVUD5O00g|LWtqhd$G)1pcSRw6w&8l(o~h z>m^)wdgJR&(LSFZWJTKl?dHjKUYfpE4>+GJf7-(XBtATFv;Y6z+Txs355z*MdFXSi z+yCiqd{Sb_fK;hz|A)M}WxuAdK?gBh4hQIqtU?Fi|CX-b8qAPmoXrX(jNy*LDyvew-wr0>@>t4((_vA&qTT#FE} zuYX>Hh{;}hTm9Oyb!WWvg}N(x6uwGkzihPn#Tbwp%)boa7$Mqw>(^Y@mf_1}_U-%3 zW!+Du^;a}0WSac4FEjjfPuJB0$_x5Ewyt^XK0z1n2I#f5e(PYOmtQXMC66-kq%9I} z&e!Yl)B%_A?#qW^7vlMSCW(*DGa0_xvz~lPhex9P+L(`hJC{Z-W2e z2cCi|EzkD%x@7qd{@1h2jAyN1D8-^GG4DL^as(eQm6+ekzoKNtl?-FVv5=+XDWPQO zJ_rAYVLl~Qe*6M{rILB`Y6n|d81%6cmR2%e%KJpge3Xp$A9$sZkFS#P)?Y7#`S<}D z&8tehcg*Agkg*XMzm8v(tg=d*PLq~Ww&wj|21|lmun!yfl#=nn0!JZWWj!*9ZHdxlPU8D?NYLEATuljav{L( z;^k#V2Y~W15)c1}VWuq$%CBT~m0loZo=O(2qkJls1L2~J_3+j+Gb|6{?J)LNeU)Ja zgyTSb+)7pv;do2pa5PY|O5k~2kxxSJkO@OefFpF((`Qf#bb zRUu1OvS*cy*8;o|W?we};vfGzQ|lf%`YfDUTDWZ{-+m?TL@GE_EfSMgcl;r?&8IE7E%v13}HUK zm8?F(XBmknUdh}DAE1o=-$w}>AUp?QS|dTp8Y0XKRkQ}32mc(6Kp#U!YxGyLXAoWs z8Lcru$r>Zf{8;%UAj^9e#9Q{P=pdlg|4qQKB@KY%fmCb?Vq#X{RV8btWQ~9ml&rau zu>up7tOdd&Kzt@ahJV%#pHY?scU_cTE4F{M6275?t-+5`vdJoO8}O_kTlA*VYpZ0e z=vzwGPRUr&sY=#f$ym{8O7o;3EBnX?K-YjJeVQ#YO5X2#zH$eZ^ zv0$u6f(@XJpsi3`0bB}N1maIh-vPY~iW7&I>;7T9KmG=2GH43uP0(AQsi0{f6ZAG{ zI%oz+c!-~u>tX4`5Pk_X95ezn5;O`l8Z-tp7R29+WPmb3<3KNiUIC2I&)x@&$38=5m_|8mwdQI23^t5PyXi0a^rFA}+4bpGvQXV0};n zP(x4~P+L$fP#7p2R2x(s6a=aS`W+^@1G)>k4LSq*7IYSL4)h)9Jm>=GBIpw6d(dUj z70?f$A3G=iGC&1Dw>TC50{Rtn9dr$J z74#E`_gfdEz*iA2h}Pr-eT481&^FK<(3_xPpqD@cLHwCY3TQOaZ9)N?LEAycK_@`j zpi`i4K&L_J{J|lATiG9!2pR}V0u2GBfcS&f{3!TiWWEzL9K=;`Bxn?f3mX@*u_%zg zN#xHaGeP4(uYksbCW0n`UWeWrpqD|ffhx2AC&95-gVuq#m*?J{+w%3u@Kex6kOx}C zop2Z`5(v7^IApL1#LeqMhJpO~QD4MyU&(!>0V)9Ei%$_yF_0Ii1c-ae-}&Rl4^SYl z7V}3~D?zJ3r$J{x--6DAE`Yd^d=GR5v>Wsp=yMSF>Ke!i`U5=og53KpMMal^wxMDI zYTTCb$2|Oz7I#?-L9^2F;SVYc;x8iegO(zE62u>$?E~!x*@Zlevk*52#N88jOWYkz zfqEJ!1#|~-cR{uu^XATI7s}yJO3PcdfPk6UN;WbsT~uD9_iV#-WNjLUyk7xjf+m3I z#~C2%#pTivOs z&ru-LZxQEq>EZbfBiP>|%I?;i6yAz(W2kVH91%lz>+$@8W4B&0_$DM=TwivBl{`!#wxw_c+CXL|IE-MTBCzmdHMYKYb~0M!T8 z1JO$1pfFHP5T_+hPpoPU;8UPVpo*Zckt3(9a-aZE8IT`{@6kRW`hHMIAHRDT%bRYIS(v!ggrn`kOp#q=&F>HC*}v0LX;=4C@A+}D-OOShy$(^s5FQ! zKNxwG1+tQXpz?Zt8R0Lcey*2DuMAlg5Gx%7st&3Oss;)L)dI0DY&;vyhQ@%{s3=eb zh;69@iUd6k;_GoV$PHo|#&-lY0@3K*LC=G_(HLD2prv1c;%1O-Wo#U*#GMgW0n`b^ zOgeyaTe>-T+tN)DZUTB1WLvy3!mK1LZ#5*Z@iC(OAb$pt%|IvV464VOx9H>30 z9f&Py18NOwtHMNDmR4p3%Y$fnxglx+BeYx{#ODmK#8bCe^4BV740p~?bSD3n8WgU&~$`NkgZQ0jvDfBgGxe20B0&0^Q86Ya7dSK z4d-{ku;1PR*@fB#z6lu%Wg*ndT^Sa_KDR5$`H2;nLmAS{12PTe3xNwj^yCjf?}G+8 zg!dPE5qxt}`3t>6fw%TUhNpM3825!<1?!<>-J&maFQ-Vmua}6bEuAg^t4TfY^KbZ+ zs#;9fUI+^b4M7YC8fKY{#Wlvg*5Tj&Ra8tUdQSv=sr&k8Lcs?Lrv{DRJ!R3Ym!S}f zq)^Bb?V+H3BnDD%AM|{omoKgB)YdhQp3TuaFXlp_&P^zIL*a$tO%DBX;fH9Y5QYJb z8pGZh>&1)zdjHJaVL377#Z~4V1qD_zc-7MK?PKu&w`woc35h@p!di>+hf#-tA{yYI zsq`wpwy4O;4i^qUFDfLgR!D?nhDfG@{g(_|gvECHe94ygt&-~2Mr-zq_YdotIBu`} zmEKyv4%2<5hxy+{hQ(0uo{I(RG+4XvhOR}0gw|r?3yJMtq2N;D6u`fd2j*w!RbSb4 z=;eh!+(y22LoB^8QS^x3T5E=H$Dr34dL^JY)HR}I+>K*%tnzDx;M6HmOg*B%qb(7^ zU+Y1}2y8j1_qB-o8mTXc8%>o zS;Zd;j~})PWkkr93>8zGIZKN8V|rcO5SZ)b^md`|?&TBfkLh7rKJnu*y|usn_o9Dj zA3v(y>s7}hIm{4J7h6BE@wo1*U&${9AJ={K-|~xz$Mx!l+Y`1#c{1AH{_D!V#fv9( ztXTAGtBIj(;%spZ>9vKzH~|B#f*mlGXY3XAPQbS+6_A#>zH7{ocaMK@MAu3{;spi! z4^F4%duvSm%%_Lznw|8dXg$#B>u>+X=`QWRZr$UTz2{IO%43Ne#a86q*8U6Bi%0yC z96ry#p@-EZ)~O>Z&br@A(motgq47y1t%JtGDlb7yzq$4MsQx>;zP&)#dO=bek_lqP zNtk&dSem&{?9Lr?iq3QC+EhsB;~$CVv!QSpY+Y(U-EkBI3hPNRT2_8-XpsZX~~r(?D?hC-B7u>XYi z^5oEul75b-lfucYOhphYZ~rar-a{+ZYf!Lg6coasfKgFZ^n^kk`)_5psk!4l-+env zT7_HHdPIfm$fU|J_TTgFroDTr$<4V#tfW@nz3`J-YGHT8{J3q7J)ua??5=G{YSTZHIFY? zVNxgdWVp1WrRp6}+hPB??e_-^dvU}c&j&yS6A(w=9+CMC>hP6#=Nr_akT?YJxBvR~ zd*R30%w2FhQWXy0P~#}{G{%t}j%Cp+_TK^vI{JRf;el(q=jiKV?rFUrzNOB82H6)E z(PvOt718MormLkQt~rKqd2xfnexl&F=pOs;XIHP;?C!1SOa6kaB4z(A7Vd9RH~UXv zzuxEV)tgT4{nlzqE&A;+@oguJ>u_sES^O==wKa=)iyPl!!iW?_&%(TYMUS&6=2kiB z{S)$6o74LJHH)koTSL9EmpN%7`6-Q^!sQ4Z2R zZ+y}@@tZk6tBSxJYSyyaS5lllr>KnzFLqDfa1s>D@os$$?4C&hv9;5V0{z_Gbc z^E%mf+2!A@hQS^1a#C67=k@CT9#!D#kF1J^XnJ0M9~TyWJP#Auf6RNz(7?EHg=fBq zoYY8~ECMfJpxA%qJ0$Y0IxUxU?4uN5|D`CRD2iC{VZS4<>X#a+TB46Oh(So|Z~rCn zwO`l0RpHm4IS5tW$Hjaoptm+s&xwk%HFbXX9vmN52=iNY><{>)i)$D3o?YgF$7Gpd z|IKjk@e!W$D>(<~T4=f8a;S=9GU~!Cg13Gg&@U(>T-Vkj4Ob%j?~jj&D7JCh_<5al zZIg=GEf!qFdN3Xvdn%qws(fNWixK-|rf7v@n<#t<)teQJ%{vsjPu{iMS?<#`G($}Y z3q(UG;4a^}@AXn*$R*vo;%+E&8}nxuuwyJ2DD4b$1c;#Tbzkw#CEX7fp&0hJ|A_hJ z#jh1Tb}S#JmUlzsG%Xd%*njO@KRz@=Gz<$uU9d;u#D`U@*Z0^D*niI4`;Q9GZe4X` z3N#`^>V(w6o19p|G9I6W)sFBof3AA{dZ7^^n45pCDQaEDh}$OOFY7@SJHSZT7d)b^ zcFaMs`m!Eg#{P5NlfTU0sA%_ooaQ(iqt-XVW?aGe^AMANLe=a)RDS)JE}dt-HoPzJf z)ZbC4{TJESRw-TTjd$XgqfoUB<xYEf zMA56L<8e{%7wj;8i4red#SY_cl-P0$&9|Cmw?MyHSDZyG?&Mv&f&41r0z?kf5gNG( zBJSrLP$g%HIJQ};i0^)eU#S>bXH>vG;uyzWMFlU2m7o z=^XX<;WJWqkNbYjsuoz_?QS#00VK7nrs||DFY9Z!!vFEvtA<&X^7Xg>_Iz-e@h{AL za~tP8Db67i)nB-qbcdX(h_Y|r-(X*rd!CrQdbWy8^`KhHW~xU{iPQf=?QS)4SUa$| zr|&ndcl-=r4wMaKqu>%js7RSA&sf(4GBymTUNplojvGRym$%jaji6W_BXs{<*ZQ7C zrtHd5j2A1|q`b|2R68HkIBy#tvhtHh3fk?_nLb)7%G}ib{M$B>v-Gnm9RnA3jUJb? zMkp5_Csy}xdV63_QNvOB{F`FKO}NbhaTSBpe`iy96A?k6YO#2TqrE;;1{va^4~tiV@YJz<0$S9WO@%1d)ChliA~Ek;f+u6bF!0PAtCK(?YukuBGq?_;+d} z+Y|6&+~n=aH#eg|HPV7b+Z??|8|smLD0^8%{h=3k-E5C>EHXWurTp#PK_C3_=HR}A zQ|nEe()eD(E%%CP-9`Ezy07c|=lJdtS^&GpHGk+a+8S{UVr`Q!?(0GI_H?kWK>cYc zE~(iuC!s-q-Ffbf8H3&o&sXxnCaZ!NdLKFE6R!jO?LYBeXLW&s;S~l)6u$!`j$7G#52dJv4oLFy6EK;N*=aI=&?E{N~yi02+^xxDy;H#!$~kgc<CBiqjdS?+zAYJ)Axd%(p~DdN`lO z&q>B$qw_$TXWt`!@^IF$6MEWq7wtTper2xWw4YnsvRU7p9lo*D74)T=iGJ%QhPa&7 z?Q{>a4iKANcI84uTbI)%YSls~sEXyyAMX(J!<@b|3ZMcH^-?bT<97k*u_v*NGVc@X z=PpF0l{FH*Je^I-*dFs|M}4TWkMN^qgQkQ7isr6i>S%V$)%)WF33SMZU$y zOY831_F7x3Q>+8N^dztCjioOsTh z**be!*yg$(8s5^aH_;F=zpt}oJ}cZvoThSDC|l=4zyBWk+`fo}9OWe8RRk5t6hQ$0 zx1q;@Sh#fKD|1#3`#eW)spv_CEvnSS4Ka&r=Noh>N8w8`w4Yt-1+jw4H&t%Snk4o} zaXSYAWKc^_1^tsQ#arg&2CB?QL`u%dPAW&s`dShv|r}Cf7_U& zkR}@Tw@aNM#y}a@*ULAtMXSYXigt?qfHKFCO$lV<=~X{yz4rDOIobS>Fryf{`nM!} zVFQKAyBBtN%6|a&-H{<-b#O%KC2kaV`ifS?oVBr|vAXU-=PI|6{qG+-Se{$7`1tEBU^bI^Xv;-_7wV)=TU&cM1OAkwb%g3G&!BwE{HFt`vb7zLJkG)qRNmvP z(#{8dw?ItojY?Q;C>6*SKDyqFvD#ZYpqAr5nFl|mD_8=#_7vy*VXXQkoV|>qDe^=` zX5y@cuMRLKe6=#-6hYa}8W_HE2(hF`R|ao~j_RUENq9sIvgL`WqwkhoSKk`?4Xhj% zf(-(`^bu2`pbZhrN@B2765oQWV_&^bo_VO~*omzK)N6-?@%3Fxm9F(!nG%1@zEadD zr^`!-;8G~5VwyZG3ZE7~a{KIgV|2|M5*`xO6md|%?|?$R0qsOffLK||=_mX4&!raA zK1EWl3xCeknj;+q728kA68DKEU63z@;i}7yJGE-C^^b-CgkVz@Sl#`GMqOWmRy$T zx2p3f%S)_1ujPd$^I=hDUA^G0j~V>on5!+*mI{P})p~r6G8Z+XL<4_J|FXXMSYIqA zz5Ja%b^g4V;7rVV%Bp3i-e;awJ9vKnan5^|vH-KmJ;3_a`SbdU^P(u`HV_WLdHnmv zU-;K4`A*GZ+<#b0orf4x#u@Jam~+f+*s z^@==^j1uk3qMC_fU|DC-1F5y4@WoHjunG0eyZ^K|o7nM{4@Am7BAPH#oPG-6BjPFm z&Wr8k0BVCdOl%H(qMe6KErx_Y(Qex+TWIhGZ<&(ATjvY*U#o`+)s>eK1)}sEp;Ikl0um^-#x})&X(2&9xEr zsvxeX7@7<6IGF7fKvpMxv;P#z)=?uv7|UcB>~@ge6>&X;!{&OMLxtYp8&{dsk;W9*ZLzt_Ew_Orj)lX)wi zcL8~DLh*#1_Wz>#%H4d#>In3>^0Qo{_zC*!zu@nAtn?SNnzEnj|G#U$|6zwd!B!wo zP5!n++5`NFmtWZs|IVg-AI+D2=H1u(l)~jF`&{gAOyvKgzJF+tK5A{sGhIE&cBYgt z8aRX0+3USNPJhv{fwMRMPl!FJ_IBfQMtRT{^_Hr}87OBXFi!$g^Eae>xHbbwcp)$ zKSyB$V#*@sQp3ugzdqA=S5D0F8R9PMbrTwupfN48OzhZUzgErB@E0|oLA~kSfmSK{-Qmz0clY1TQCJ|vnrED0 z^3{|tKG?)<7Wa|p(I-i;Gfs_plH~1yP$SX0F;pLUj%2AOiA*Tq9O)hEJ@OpM(pxGH zQQ?v2NS1Ml`2`c-8I?;+~^~ERir+qXsN5Q(**DN2;Ivjs}w-1$)cl%ml z8#I|W_~eBg549^vn6cs>l197_ z1rE7=71BrkEBK|Ca}+*93|~^_OzpHHeo%JToS0KfM5`7Ua{q<~&-FY`CJq1W!rY`B zjiMsG1?p94sm$WYOLJRx-jVjN9EG}wDTky7Pqlbyw(o&`IWe)~0Fp+GRtneJcIn^l z^W3h# zI7i87akwS!MNJfetuVT0Eth9R-6oVDS@+}LcH@;BSJrSr#Ib0(7~2CG?p`hix576r z)_1bLdiHYhMGxdZOk8W_4A*~LF50{Rt^3PG{npSbC^BEb!F5>?+683=i|p1=k}l;d zo@xVGJ<+fYCgo)~=eS^6NTRJxVY8?=Sh2V(c#tPA^tuw659cUDTfrfARHPZET^JVIICIa6i%Z$q0 zfJOmmM9m0(yZp}Ldv$GWEqUukY2g33dh>ZW@Z)c4HWD@4!FJ!RmUoFdx8L6;uGmMP zqbzmVwX(gJzTF$& zAMh(sz6|?`@b)m%bg{J?ECRE(cLr6g@`?PybuG|98{&49-`#YkFYQoU-putAhuTA5 zg3zDC*|K#@k=r)h z@Da{D|2|o<)chsqc^$fzEN%a%BBq1$BYqp#0hLv~=p)=6aUN6$Ke*tT=Cbl#-}`<1 z{YprtzOZT}CU=DWzJvl>@nfNKz6Hls4ugXF0(k!xv7;j@sWL1migv<@%>~X& zqYH|!CDwwqJ+`j!?`k`4_+L3LzEijEtv1~zJ*`vrxuD{KEk{~*vu41TL|`oHmn!PU zIy3e6cZf}~7*bYiao*X@8KAG+A?kN?W~lzJQxHyD0;c+Y<%p?Wz8i`|6PSwk$Q}R5 z6-1Tiae5pt8a|IMSqO8};(_{iFSP&pXWn+D#q01cB7FXxM|GsHnAY7{+^F!m_2Y?* z+{XX&ywWTmu72Xzg8%Eu1=no7Ma>sriU0BpQ(Jwz=Hl0H^1BNhYSwmi-y^-ZaPO@7 zz00gik)4P8){aEPa5zlwc63IoXL|FCSQ-(N&=|3~7d!-KyS>o6I9QDX*xy{lp&R6P z5@lBJm9uy==jeuu)6#R4cvmv=pO#^q>pFb|1w9zOKfMZ^~9+M znLm6q&r|iBgZc+t9^rp3L7w`HV)4#0SZaggQK)rlT(To>{nv6l`s)w;KEUz#FCyFr zKOGVW```=E+lR#OeK4XP^=|OE{VH79+OPU*jl{;jSQ8$z zS%sdpS@n)WD6@|otF;=<>bWjoQC;oB5c8NVEMlxJtT!)aKV}z;7;6{niyvpa-VZf- z%oY}U))v;PNhP&qtX{loxc2(_`8hRt%*Ga}t&Oc!lgI3Bp=0e}d2jLg{^&GouWI#= z+^&|huCP94yNcA-cGa6VJAOzIXZm9j&ixgHHEwc!u-N(DD{k-3Wzd5sX7?{q?o$AubHSj99-Ow(}SX@`oLGW#~5$Ye(K3e8j43Vf{9$ zZ?=0)r{JiI#c-Sdev-4i|Km%O#XMxt0(OR~Q84gkROT_}cUjuJ)l@>X8SJd&f9|w2 zvymM0*$?e!ZNyFs-&ym!vgTs)U}sRJabVyO~%cdn`cB!vh&#EIvSp(ev@Rq>^%5slKgs{Un3OJAtG=%IFz3f_tt*w^ Iqi&o354&JQR{#J2 diff --git a/app/package.json b/app/package.json index 258f4148..16130e2e 100644 --- a/app/package.json +++ b/app/package.json @@ -15,6 +15,7 @@ "@hookform/resolvers": "^3.9.1", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-avatar": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", diff --git a/app/src/components/ui/checkbox.tsx b/app/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..ddbdd01d --- /dev/null +++ b/app/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/app/src/routes/_layout/prs/$prId/route.tsx b/app/src/routes/_layout/prs/$prId/route.tsx index f2fb8b18..b0c9a5aa 100644 --- a/app/src/routes/_layout/prs/$prId/route.tsx +++ b/app/src/routes/_layout/prs/$prId/route.tsx @@ -1,7 +1,41 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { queries } from "@/lib/api/queries/queries"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { PRDetailsDialog } from "../-components/pr-details-dialog"; +import { AzureAvatar } from "@/components/azure-avatar"; +import { AccordionContent, AccordionTrigger } from "@/components/ui/accordion"; +import { Accordion, AccordionItem } from "@/components/ui/accordion"; +import { Dialog, DialogDescription, DialogTitle } from "@/components/ui/dialog"; +import { PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { DialogContent } from "@/components/ui/dialog"; +import BranchLink from "@/components/branch-link"; +import { PRLink } from "@/components/pr-link"; +import { DialogFooter, DialogHeader } from "@/components/ui/dialog"; +import { mutations } from "@/lib/api/mutations/mutations"; +import { + ListPullRequest, + User, + Thread as PullRequestThread, +} from "@/lib/api/queries/pullRequests"; +import dayjs from "dayjs"; +import { + ClipboardCopy, + MessageCircleCodeIcon, + CodeXmlIcon, +} from "lucide-react"; +import React from "react"; +import { toast } from "sonner"; +import { PRNotificationSettings } from "../-components/pr-notification-settings"; +import { Popover } from "@/components/ui/popover"; +import { + Select, + SelectItem, + SelectContent, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import Markdown from "react-markdown"; export const Route = createFileRoute("/_layout/prs/$prId")({ loader: ({ context }) => @@ -21,12 +55,20 @@ function PRDetailsDialog() { const { data: differs } = useSuspenseQuery(queries.differs()); const { data: allProjects } = useSuspenseQuery(queries.listProjects()); - const [selectedProject, setSelectedProject] = React.useState(null); - const [selectedActivity, setSelectedActivity] = React.useState(null); - const [timeReportMode, setTimeReportMode] = React.useState<"review" | "develop" | null>(null); + const [selectedProject, setSelectedProject] = React.useState( + null, + ); + const [selectedActivity, setSelectedActivity] = React.useState( + null, + ); + const [timeReportMode, setTimeReportMode] = React.useState< + "review" | "develop" | null + >(null); - const differ = differs.find(d => d.repoName === pr?.repoName); - const connectedProjects = allProjects.filter(p => differ?.milltimeProjectIds.includes(p.projectId)); + const differ = differs.find((d) => d.repoName === pr?.repoName); + const connectedProjects = allProjects.filter((p) => + differ?.milltimeProjectIds.includes(p.projectId), + ); const { data: activities } = useSuspenseQuery({ ...queries.listActivities(selectedProject ?? ""), @@ -72,13 +114,13 @@ function PRDetailsDialog() { const text = getTimeReportText(timeReportMode); copyToClipboard(text); - const project = projects.find(p => p.projectId === selectedProject); + const project = projects.find((p) => p.projectId === selectedProject); if (!project) { toast.error("Project not found"); return; } - const activity = activities?.find(a => a.activity === selectedActivity); + const activity = activities?.find((a) => a.activity === selectedActivity); if (!activity) { toast.error("Activity not found"); return; @@ -113,15 +155,18 @@ function PRDetailsDialog() { - { - if (open) { - setTimeReportMode("review"); - } else { - setTimeReportMode(null); - setSelectedProject(null); - setSelectedActivity(null); - } - }}> + { + if (open) { + setTimeReportMode("review"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }} + >
- { - if (open) { - setTimeReportMode("develop"); - } else { - setTimeReportMode(null); - setSelectedProject(null); - setSelectedActivity(null); - } - }}> + { + if (open) { + setTimeReportMode("develop"); + } else { + setTimeReportMode(null); + setSelectedProject(null); + setSelectedActivity(null); + } + }} + >