From 8eb65173a6079fd01ca9848e061ded41b7f636d3 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 12 Jan 2026 23:17:25 +0000 Subject: [PATCH] added project page to org dialog moved sprint creation/management there --- .../frontend/src/components/create-issue.tsx | 8 +- .../src/components/organisations-dialog.tsx | 131 ++++++++++++++++-- .../frontend/src/components/ui/dialog.tsx | 2 +- packages/frontend/src/pages/App.tsx | 131 +++++++++--------- todo.md | 7 +- 5 files changed, 202 insertions(+), 77 deletions(-) diff --git a/packages/frontend/src/components/create-issue.tsx b/packages/frontend/src/components/create-issue.tsx index 900d167..95b6655 100644 --- a/packages/frontend/src/components/create-issue.tsx +++ b/packages/frontend/src/components/create-issue.tsx @@ -116,13 +116,19 @@ export function CreateIssue({ console.error(actionErr); } }, - onError: (error) => { + onError: async (error) => { setError(error); setSubmitting(false); toast.error(`Error creating issue: ${error}`, { dismissible: false, }); + + try { + await errorAction?.(error); + } catch (actionErr) { + console.error(actionErr); + } }, }); } catch (err) { diff --git a/packages/frontend/src/components/organisations-dialog.tsx b/packages/frontend/src/components/organisations-dialog.tsx index fa548b8..48e398c 100644 --- a/packages/frontend/src/components/organisations-dialog.tsx +++ b/packages/frontend/src/components/organisations-dialog.tsx @@ -3,14 +3,19 @@ import { ISSUE_STATUS_MAX_LENGTH, type OrganisationMemberResponse, type OrganisationResponse, + type ProjectRecord, + type ProjectResponse, + type SprintRecord, } from "@issue/shared"; import { ChevronDown, ChevronUp, EllipsisVertical, Plus, X } from "lucide-react"; -import type { ReactNode } from "react"; -import { useCallback, useEffect, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { AddMemberDialog } from "@/components/add-member-dialog"; +import { CreateSprint } from "@/components/create-sprint"; import { OrganisationSelect } from "@/components/organisation-select"; +import { ProjectSelect } from "@/components/project-select"; import { useAuthenticatedSession } from "@/components/session-provider"; +import SmallSprintDisplay from "@/components/small-sprint-display"; import SmallUserDisplay from "@/components/small-user-display"; import StatusTag from "@/components/status-tag"; import { Button } from "@/components/ui/button"; @@ -35,12 +40,24 @@ function OrganisationsDialog({ selectedOrganisation, setSelectedOrganisation, refetchOrganisations, + projects, + selectedProject, + sprints, + onSelectedProjectChange, + onCreateProject, + onCreateSprint, }: { trigger?: ReactNode; organisations: OrganisationResponse[]; selectedOrganisation: OrganisationResponse | null; setSelectedOrganisation: (organisation: OrganisationResponse | null) => void; refetchOrganisations: (options?: { selectOrganisationId?: number }) => Promise; + projects: ProjectResponse[]; + selectedProject: ProjectResponse | null; + sprints: SprintRecord[]; + onSelectedProjectChange: (project: ProjectResponse | null) => void; + onCreateProject: (project: ProjectRecord) => void | Promise; + onCreateSprint: (sprint: SprintRecord) => void | Promise; }) { const { user } = useAuthenticatedSession(); @@ -79,6 +96,20 @@ function OrganisationsDialog({ selectedOrganisation?.OrganisationMember.role === "owner" || selectedOrganisation?.OrganisationMember.role === "admin"; + const formatDate = (value: Date | string) => + new Date(value).toLocaleDateString(undefined, { month: "short", day: "numeric" }); + const getSprintDateRange = (sprint: SprintRecord) => { + if (!sprint.startDate || !sprint.endDate) return ""; + return `${formatDate(sprint.startDate)} - ${formatDate(sprint.endDate)}`; + }; + const isCurrentSprint = (sprint: SprintRecord) => { + if (!sprint.startDate || !sprint.endDate) return false; + const today = new Date(); + const start = new Date(sprint.startDate); + const end = new Date(sprint.endDate); + return start <= today && today <= end; + }; + const refetchMembers = useCallback(async () => { if (!selectedOrganisation) return; try { @@ -436,15 +467,15 @@ function OrganisationsDialog({ )} - + Organisations
{selectedOrganisation ? ( - -
+ +
Info Users + Projects Issues
@@ -582,6 +614,88 @@ function OrganisationsDialog({
+ +
+
+ +
+
+ {selectedProject ? ( + <> +

+ {selectedProject.Project.name} +

+
+

+ Key: {selectedProject.Project.key} +

+

+ Creator: {selectedProject.User.name} +

+
+ + ) : ( +

+ Select a project to view details. +

+ )} +
+
+ {selectedProject ? ( +
+ {sprints.map((sprint) => { + const dateRange = getSprintDateRange(sprint); + const isCurrent = isCurrentSprint(sprint); + + return ( +
+ + {dateRange && ( + + {dateRange} + + )} +
+ ); + })} + {isAdmin && ( + + Create sprint{" "} + + + } + /> + )} +
+ ) : ( +

+ Select a project to view sprints. +

+ )} +
+
+
+
+
+

Issue Statuses

@@ -659,7 +773,7 @@ function OrganisationsDialog({ {isAdmin && (isCreatingStatus ? ( <> -
+
{ if (e.key === "Enter") { void handleCreateStatus(); @@ -712,6 +826,7 @@ function OrganisationsDialog({ setIsCreatingStatus(true); setStatusError(null); }} + className="flex gap-2 w-full min-w-0" > Create status @@ -721,7 +836,7 @@ function OrganisationsDialog({ ) : ( -
+
([]); const [sprints, setSprints] = useState([]); - const isAdmin = - selectedOrganisation?.OrganisationMember.role === "owner" || - selectedOrganisation?.OrganisationMember.role === "admin"; - const deepLinkParams = useMemo(() => { const params = new URLSearchParams(window.location.search); const orgSlug = params.get("o")?.trim().toLowerCase() ?? ""; @@ -394,6 +390,43 @@ export default function App() { } }, [deepLinkParams, selectedOrganisation, selectedProject]); + const handleProjectChange = (project: ProjectResponse | null) => { + setSelectedProject(project); + localStorage.setItem("selectedProjectId", `${project?.Project.id}`); + setSelectedIssue(null); + updateUrlParams({ + projectKey: project?.Project.key.toLowerCase() ?? null, + issueNumber: null, + }); + }; + + const handleProjectCreate = async (project: ProjectRecord) => { + if (!selectedOrganisation) return; + + toast.success(`Created Project ${project.name}`, { + dismissible: false, + }); + + await refetchProjects(selectedOrganisation.Organisation.id, { + selectProjectId: project.id, + }); + }; + + const handleSprintCreate = async (sprint: SprintRecord) => { + if (!selectedProject) return; + + toast.success( + <> + Created sprint {sprint.name} + , + { + dismissible: false, + }, + ); + + await refetchSprints(selectedProject.Project.id); + }; + return (
{/* header area */} @@ -427,71 +460,33 @@ export default function App() { projects={projects} selectedProject={selectedProject} organisationId={selectedOrganisation?.Organisation.id} - onSelectedProjectChange={(project) => { - setSelectedProject(project); - localStorage.setItem("selectedProjectId", `${project?.Project.id}`); - setSelectedIssue(null); - updateUrlParams({ - projectKey: project?.Project.key.toLowerCase() ?? null, - issueNumber: null, - }); - }} - onCreateProject={async (project) => { - if (!selectedOrganisation) return; - - toast.success(`Created Project ${project.name}`, { - dismissible: false, - }); - - await refetchProjects(selectedOrganisation.Organisation.id, { - selectProjectId: project.id, - }); - }} + onSelectedProjectChange={handleProjectChange} + onCreateProject={handleProjectCreate} showLabel /> )} {selectedOrganisation && selectedProject && ( - <> - { - if (!selectedProject) return; - toast.success( - `Created ${issueID(selectedProject.Project.key, issueNumber)}`, - { - dismissible: false, - }, - ); - await refetchIssues(); - }} - errorAction={async (errorMessage) => { - toast.error(`Error creating issue: ${errorMessage}`, { + { + if (!selectedProject) return; + toast.success( + `Created ${issueID(selectedProject.Project.key, issueNumber)}`, + { dismissible: false, - }); - }} - /> - {isAdmin && ( - { - if (!selectedProject) return; - toast.success( - <> - Created sprint{" "} - {sprint.name} - , - { - dismissible: false, - }, - ); - await refetchSprints(selectedProject?.Project.id); - }} - /> - )} - + }, + ); + await refetchIssues(); + }} + errorAction={async (errorMessage) => { + toast.error(`Error creating issue: ${errorMessage}`, { + dismissible: false, + }); + }} + /> )}
@@ -510,6 +505,12 @@ export default function App() { selectedOrganisation={selectedOrganisation} setSelectedOrganisation={setSelectedOrganisation} refetchOrganisations={refetchOrganisations} + projects={projects} + selectedProject={selectedProject} + sprints={sprints} + onSelectedProjectChange={handleProjectChange} + onCreateProject={handleProjectCreate} + onCreateSprint={handleSprintCreate} /> diff --git a/todo.md b/todo.md index fe2d586..a031b44 100644 --- a/todo.md +++ b/todo.md @@ -1,10 +1,13 @@ # HIGH PRIORITY -- projects - - project management menu (will this be accessed from the organisations-dialog? or will it be a separate menu in the user select) +- projects menu + - delete project - sprints + - prevent overlapping sprints in the same project - timeline display - display sprints + - edit sprint + - delete sprint - issues - sprint - edit title & description