Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1c8bd1a
added a link to the individual contributor page from the teams name list
marton-balazs-kovacs Apr 2, 2025
f73105d
logic and example to turn off tooltips. also made the labelled checkb…
marton-balazs-kovacs Apr 3, 2025
e8c6795
made all the tooltips that are not tied to an icon dynamic. I left th…
marton-balazs-kovacs Apr 3, 2025
627e239
changed getTeam query to return individual contributorId, and cleaned…
marton-balazs-kovacs Apr 4, 2025
2220bce
changed teamId to id in getTeam for consistency
marton-balazs-kovacs Apr 4, 2025
5820963
addded link to individual contributor page from the tasklog page
marton-balazs-kovacs Apr 4, 2025
8ec7db2
showteammodal does not show null value for missing firstname and last…
marton-balazs-kovacs Apr 4, 2025
11f6348
added missing tooltip property to the signup session created by the s…
marton-balazs-kovacs Apr 4, 2025
d728df1
refactored breadcrumb component
marton-balazs-kovacs Apr 4, 2025
4952522
refactored breadcrumbs
marton-balazs-kovacs Apr 4, 2025
6a31156
factored out the breadcrumb label
marton-balazs-kovacs Apr 5, 2025
39b2201
added a context to store previously fetched breadcrumb names
marton-balazs-kovacs Apr 6, 2025
4220173
tried to make breadcrumb segments dynamicly growing and shrinking but…
marton-balazs-kovacs Apr 7, 2025
40917d5
added a general validator to validate breadcrumb paths. if a path is …
marton-balazs-kovacs Apr 7, 2025
8067eec
Delete db/migrations/20250402072357_new_setup/migration.sql
doomlab Apr 9, 2025
5c63853
Delete db/migrations/20250402171933_tooltips/migration.sql
doomlab Apr 9, 2025
f6a5bb2
Delete db/migrations/migration_lock.toml
doomlab Apr 9, 2025
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
1 change: 1 addition & 0 deletions db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ model User {
email String @unique
language String @default("en-US")
gravatar String?
tooltips Boolean @default(true)
// relational information
projects ProjectMember[]
forms Form[]
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"node-fetch": "3.3.2",
"nodemailer": "6.9.14",
"passport-orcid": "0.0.4",
"path-to-regexp": "8.2.0",
"postcss": "8.4.27",
"react": "18.2.0",
"react-circular-progressbar": "2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/auth/components/TosForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const TosForm = (props: TosFormProps) => {
label="I have read and agree to these terms."
className="checkbox checkbox-primary border-2"
labelProps={{ className: "text-lg" }}
></LabeledCheckboxField>
/>
</Form>
<div className="divider pt-4 pb-4"></div>
<div className="flex flex-row justify-center">
Expand Down
2 changes: 1 addition & 1 deletion src/auth/mutations/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ct
// This throws an error if credentials are invalid
const user = await authenticateUser(email, password)

await ctx.session.$create({ userId: user.id, role: user.role as Role })
await ctx.session.$create({ userId: user.id, role: user.role as Role, tooltips: user.tooltips })

return user
})
6 changes: 5 additions & 1 deletion src/auth/mutations/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export default resolver.pipe(resolver.zod(Signup), async ({ email, password, use
size: WidgetSize.SMALL,
}))

await ctx.session.$create({ userId: user.id, role: user.role as Role })
await ctx.session.$create({
userId: user.id,
role: user.role as Role,
tooltips: true,
})

await db.widget.createMany({
data: [...widgets, ...smallWidgets],
Expand Down
4 changes: 2 additions & 2 deletions src/contributors/components/ContributorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { LabelSelectField } from "src/core/components/fields/LabelSelectField"
import { useQuery } from "@blitzjs/rpc"
import { MemberPrivileges } from "@prisma/client"
import LabeledTextField from "src/core/components/fields/LabeledTextField"
import { Tooltip } from "react-tooltip"
import AddRoleInput from "src/roles/components/AddRoleInput"
import getProjectManagerUserIds from "src/projectmembers/queries/getProjectManagerUserIds"
import TooltipWrapper from "src/core/components/TooltipWrapper"

interface ContributorFormProps<S extends z.ZodType<any, any>> extends FormProps<S> {
projectId: number
Expand All @@ -33,7 +33,7 @@ export function ContributorForm<S extends z.ZodType<any, any>>(props: Contributo

return (
<Form<S> {...formProps}>
<Tooltip
<TooltipWrapper
id="priv-tooltip"
content={
isLastProjectManager
Expand Down
145 changes: 24 additions & 121 deletions src/core/components/BreadCrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,43 @@
import { useRouter } from "next/router"
import Link from "next/link"
import { invoke } from "@blitzjs/rpc"
import getProject from "src/projects/queries/getProject"
import getTask from "src/tasks/queries/getTask"
import { useState, useEffect, useCallback } from "react"
import getElement from "src/elements/queries/getElement"
import getProjectMember from "src/projectmembers/queries/getProjectMember"
import getTeam from "src/teams/queries/getTeam"
import { BreadcrumbList } from "src/core/components/BreadcrumbList"
import { useBreadcrumbNames } from "../hooks/useBreadCrumbNames"
import { BreadcrumbLabel } from "./BreadcrumbLabel"
import { getAllValidRoutes } from "../utils/getAllValidRoutes"
import { match } from "path-to-regexp"

const validPatterns = getAllValidRoutes()

export const isValidPath = (href: string): boolean => {
return validPatterns.some((pattern) => {
try {
return match(pattern)(href)
} catch (e) {
console.warn("Invalid pattern:", pattern)
return false
}
})
}

export const Breadcrumbs = () => {
const router = useRouter()
const pathSegments = router.asPath.split("/").filter((segment) => segment)
const [namesCache, setNamesCache] = useState<Record<string, string>>({})

// ✅ Use useCallback to prevent unnecessary re-renders
const fetchNameIfNeeded = useCallback(
async (type: "project" | "task" | "element" | "team" | "contributor", id: string) => {
if (namesCache[id]) {
return // Use cached name if available
}

try {
let data

if (type === "project") {
data = await invoke(getProject, { id: parseInt(id, 10) }) // No ctx needed
} else if (type === "task") {
data = await invoke(getTask, { id: parseInt(id, 10) }) // No ctx needed
} else if (type == "element") {
data = await invoke(getElement, { id: parseInt(id, 10) }) // No ctx needed
} else if (type == "team") {
data = await invoke(getTeam, { id: parseInt(id, 10) }) // No ctx needed
} else if (type == "contributor") {
data = await invoke(getProjectMember, {
where: { id: parseInt(id, 10) },
include: { users: true },
})
console.log(`[API RESPONSE] ProjectMember Data for ID ${id}:`, data)

if (data && data.users.length > 0) {
const user = data.users[0]
const fullName =
user.firstName && user.lastName
? `${user.firstName} ${user.lastName}`
: user.username || "Unknown"
data = { name: fullName } // Reshape data to match the expected format
} else {
console.warn(`[WARNING] No valid user found for ProjectMember ID ${id}`)
}
}
const pathSegments = router.asPath.split("/").filter(Boolean)
const namesCache = useBreadcrumbNames(pathSegments)

if (data?.name) {
console.log(`[UPDATE] Storing ${type} name for ID ${id}:`, data.name)
setNamesCache((prev) => ({ ...prev, [id]: data.name })) // Store in cache
} else {
console.error(`[ERROR] No valid name found in response for ${type} ID ${id}`, data)
}
} catch (error) {
console.error(`[ERROR] Failed to fetch ${type} name for ID ${id}:`, error)
}
},
[namesCache]
) // ✅ Only re-create when namesCache changes

useEffect(() => {
pathSegments.forEach((segment, index) => {
const prevSegment = pathSegments[index - 1]
if (prevSegment === "projects" && !isNaN(Number(segment))) {
void fetchNameIfNeeded("project", segment)
} else if (prevSegment === "tasks" && !isNaN(Number(segment))) {
void fetchNameIfNeeded("task", segment)
} else if (prevSegment === "elements" && !isNaN(Number(segment))) {
void fetchNameIfNeeded("element", segment)
} else if (prevSegment === "teams" && !isNaN(Number(segment))) {
void fetchNameIfNeeded("team", segment)
} else if (prevSegment === "contributors" && !isNaN(Number(segment))) {
void fetchNameIfNeeded("contributor", segment)
}
})
}, [pathSegments, fetchNameIfNeeded]) // ✅ Now fetchNameIfNeeded is properly included

// Function to truncate long names with "..."
const truncateLabel = (label: string, maxLength: number = 20) => {
return label.length > maxLength ? label.substring(0, maxLength) + "..." : label
}

// Generate breadcrumb objects
const breadcrumbs = pathSegments.map((segment, index) => {
const href = "/" + pathSegments.slice(0, index + 1).join("/")
const prevSegment = pathSegments[index - 1]

let label = decodeURIComponent(segment).replace(/[-_]/g, " ")
if (prevSegment === "projects" && namesCache[segment]) {
label = namesCache[segment] ?? "" // Use cached project name
} else if (prevSegment === "tasks" && namesCache[segment]) {
label = namesCache[segment] ?? "" // Use cached task name
} else if (prevSegment === "elements" && namesCache[segment]) {
label = namesCache[segment] ?? "" // Use cached task name
} else if (prevSegment === "teams" && namesCache[segment]) {
label = namesCache[segment] ?? "" // Use cached task name
} else if (prevSegment === "contributors" && namesCache[segment]) {
label = namesCache[segment] ?? "" // Use cached task name
} else {
label = label.replace(/\b\w/g, (char) => char.toUpperCase()) // Proper Case
}

// Truncate long names
label = truncateLabel(label, 20)
const prev = pathSegments[index - 1]

return {
label,
label: <BreadcrumbLabel segment={segment} prevSegment={prev} namesCache={namesCache} />,
href,
isLast: index === pathSegments.length - 1,
isValid: isValidPath(href),
}
})

return (
<div className="text-md breadcrumbs">
<ul>
<li>
<Link href="/main" className="hover:underline">
Home
</Link>
</li>
{breadcrumbs.map((crumb, index) => (
<li key={index}>
{crumb.isLast ? (
<span className="font-bold text-base-content">{crumb.label}</span>
) : (
<Link href={crumb.href} className="hover:underline">
{crumb.label}
</Link>
)}
</li>
))}
</ul>
<BreadcrumbList items={breadcrumbs} />
</div>
)
}
27 changes: 27 additions & 0 deletions src/core/components/BreadcrumbCacheContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createContext, useContext, useState } from "react"

type BreadcrumbCache = Record<string, string>

const BreadcrumbCacheContext = createContext<{
names: BreadcrumbCache
setName: (key: string, label: string) => void
}>({
names: {},
setName: () => {},
})

export const BreadcrumbCacheProvider = ({ children }: { children: React.ReactNode }) => {
const [names, setNames] = useState<BreadcrumbCache>({})

const setName = (key: string, label: string) => {
setNames((prev) => (prev[key] ? prev : { ...prev, [key]: label }))
}

return (
<BreadcrumbCacheContext.Provider value={{ names, setName }}>
{children}
</BreadcrumbCacheContext.Provider>
)
}

export const useBreadcrumbCache = () => useContext(BreadcrumbCacheContext)
36 changes: 36 additions & 0 deletions src/core/components/BreadcrumbLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { segmentToTypeMap } from "../hooks/useBreadCrumbNames"

const isNumeric = (value: string) => /^\d+$/.test(value)

export const BreadcrumbLabel = ({
segment,
prevSegment,
namesCache,
maxLength = 40,
}: {
segment: string
prevSegment?: string
namesCache: Record<string, string>
maxLength?: number
}) => {
const type = prevSegment ? segmentToTypeMap[prevSegment] : undefined
const fallback = segment.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())

const isDynamic = type && isNumeric(segment)
const compositeKey = type ? `${type}:${segment}` : segment
const label = namesCache[compositeKey]

const displayLabel = label ?? fallback
const truncated =
displayLabel.length > maxLength ? displayLabel.slice(0, maxLength) + "..." : displayLabel

return (
<span
aria-label={displayLabel}
title={displayLabel}
className={label || !isDynamic ? "" : "skeleton h-4 w-20 inline-block rounded"}
>
{label || !isDynamic ? truncated : null}
</span>
)
}
25 changes: 25 additions & 0 deletions src/core/components/BreadcrumbList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Link from "next/link"
import { BreadcrumbItem } from "../types"

export const BreadcrumbList = ({ items }: { items: BreadcrumbItem[] }) => (
<ul>
<li>
<Link href="/main" className="hover:underline">
Home
</Link>
</li>
{items.map((crumb, index) => (
<li key={index}>
{crumb.isLast ? (
<span className="font-bold text-base-content">{crumb.label}</span>
) : crumb.isValid ? (
<Link href={crumb.href} className="hover:underline">
{crumb.label}
</Link>
) : (
<span className="text-base-content">{crumb.label}</span>
)}
</li>
))}
</ul>
)
8 changes: 6 additions & 2 deletions src/core/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode } from "react"
import { Tooltip } from "react-tooltip"
import clsx from "clsx"
import { v4 as uuidv4 } from "uuid"
import TooltipWrapper from "./TooltipWrapper"

interface CardProps {
title: string
Expand All @@ -22,7 +22,11 @@ const Card = ({ title, children, tooltipContent, actions, className }: CardProps
{title}
</div>
{tooltipContent && (
<Tooltip id={tooltipId} content={tooltipContent} className="z-[1099] ourtooltips" />
<TooltipWrapper
id={tooltipId}
content={tooltipContent}
className="z-[1099] ourtooltips"
/>
)}
{children}
{actions && <div className="card-actions justify-end">{actions}</div>}
Expand Down
8 changes: 6 additions & 2 deletions src/core/components/CollapseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode } from "react"
import { Tooltip } from "react-tooltip"
import clsx from "clsx"
import { v4 as uuidv4 } from "uuid"
import TooltipWrapper from "./TooltipWrapper"

interface CollapseCardProps {
title: string
Expand All @@ -27,7 +27,11 @@ const CollapseCard = ({
<div className="collapse-title text-xl font-medium">
<div className="card-title">{title}</div>
{tooltipContent && (
<Tooltip id={tooltipId} content={tooltipContent} className="z-[1099] ourtooltips" />
<TooltipWrapper
id={tooltipId}
content={tooltipContent}
className="z-[1099] ourtooltips"
/>
)}
</div>

Expand Down
Loading