Skip to content
Merged
Show file tree
Hide file tree
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
86 changes: 2 additions & 84 deletions src/app/(dashboard)/@bar/default.tsx
Original file line number Diff line number Diff line change
@@ -1,92 +1,10 @@
"use client";

import {
Badge,
Breadcrumb,
Button,
Container,
Flex,
MenuItem,
MenuSeparator,
Text,
TextInput,
useService,
useUserSession
} from "@code0-tech/pictor";
import DUserMenu from "@code0-tech/pictor/dist/components/d-user/DUserMenu";
import {IconBuilding, IconFolders, IconInbox, IconLogout, IconSearch} from "@tabler/icons-react";
import React from "react";
import Image from "next/image";
import {UserService} from "@edition/user/User.service";
import {useRouter} from "next/navigation";
import Link from "next/link";
import {ApplicationBarView} from "@edition/dashboard/application/ApplicationBarView";

const Page = () => {

const currentSession = useUserSession()
const userService = useService(UserService)
const router = useRouter()
const [loading, startTransition] = React.useTransition()


const userMenu = React.useMemo(() => {

if (!currentSession?.token) {
return null
}

const userLogout = () => {
startTransition(async () => {
await userService.usersLogout({
userSessionId: currentSession.id!!
}).then(payload => {
window.localStorage.removeItem("ide_code-zero_session")
router.push("/login")
})
})
}

const namespaceIndex = currentSession.user?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1]

return <DUserMenu userId={currentSession.user?.id!!}>
<Link href={"/organizations"}>
<MenuItem>
<IconBuilding size={16}/>Organizations
</MenuItem>
</Link>
<Link href={`/namespace/${namespaceIndex}`}>
<MenuItem>
<IconFolders size={16}/>Personal Workspace
</MenuItem>
</Link>
<MenuSeparator/>
<MenuItem onSelect={userLogout}>
<IconLogout size={16}/>Logout
</MenuItem>
</DUserMenu>
}, [currentSession])

return <Container>
<Flex style={{gap: "0.7rem", flexDirection: "column"}} py={0.7} w={"100%"}>
<Flex align={"center"} justify={"space-between"}>
<Flex align={"center"} style={{gap: "1.3rem"}}>
<Image src={"/CodeZero_Logo.png"} alt={"CodeZero Banner"} width={160} height={0}
style={{width: '38px', height: 'auto'}}/>
<Breadcrumb>
<Text>Application</Text>
</Breadcrumb>
</Flex>
<Flex align={"center"} style={{gap: "0.7rem"}}>
<TextInput disabled left={<IconSearch size={16}/>} right={<Badge>⌘K</Badge>} rightType={"icon"}
placeholder={"Search..."}/>
<Button>
<IconInbox size={16}/>
</Button>
{userMenu}
</Flex>
</Flex>
</Flex>
</Container>
return <ApplicationBarView/>
}

export default Page
86 changes: 86 additions & 0 deletions src/packages/ce/src/dashboard/application/ApplicationBarView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client"

import {
Badge,
Breadcrumb, Button,
Container,
Flex,
MenuItem,
MenuSeparator,
Text, TextInput,
useService,
useUserSession
} from "@code0-tech/pictor";
import {UserService} from "@edition/user/User.service";
import {useRouter} from "next/navigation";
import React from "react";
import DUserMenu from "@code0-tech/pictor/dist/components/d-user/DUserMenu";
import Link from "next/link";
import {IconBuilding, IconFolders, IconInbox, IconLogout, IconSearch} from "@tabler/icons-react";
import Image from "next/image";
import {ApplicationBreadcrumbView} from "@edition/dashboard/application/ApplicationBreadcrumbView";

export const ApplicationBarView: React.FC = () => {
const currentSession = useUserSession()
const userService = useService(UserService)
const router = useRouter()
const [loading, startTransition] = React.useTransition()


const userMenu = React.useMemo(() => {

if (!currentSession?.token) {
return null
}

const userLogout = () => {
startTransition(async () => {
await userService.usersLogout({
userSessionId: currentSession.id!!
}).then(payload => {
window.localStorage.removeItem("ide_code-zero_session")
router.push("/login")
})
})
}

const namespaceIndex = currentSession.user?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1]

return <DUserMenu userId={currentSession.user?.id!!}>
<Link href={"/organizations"}>
<MenuItem>
<IconBuilding size={16}/>Organizations
</MenuItem>
</Link>
<Link href={`/namespace/${namespaceIndex}`}>
<MenuItem>
<IconFolders size={16}/>Personal Workspace
</MenuItem>
</Link>
<MenuSeparator/>
<MenuItem onSelect={userLogout}>
<IconLogout size={16}/>Logout
</MenuItem>
</DUserMenu>
}, [currentSession])

return <Container>
<Flex style={{gap: "0.7rem", flexDirection: "column"}} py={0.7} w={"100%"}>
<Flex align={"center"} justify={"space-between"}>
<Flex align={"center"} style={{gap: "1.3rem"}}>
<Image src={"/CodeZero_Logo.png"} alt={"CodeZero Banner"} width={160} height={0}
style={{width: '38px', height: 'auto'}}/>
<ApplicationBreadcrumbView/>
</Flex>
<Flex align={"center"} style={{gap: "0.7rem"}}>
<TextInput disabled left={<IconSearch size={16}/>} right={<Badge>⌘K</Badge>} rightType={"icon"}
placeholder={"Search..."}/>
<Button>
<IconInbox size={16}/>
</Button>
{userMenu}
</Flex>
</Flex>
</Flex>
</Container>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"use client"

import React from "react";
import {Badge, Breadcrumb, Text, useService, useStore} from "@code0-tech/pictor";
import {useParams, usePathname, useRouter} from "next/navigation";
import type {Namespace, NamespaceProject, NamespaceRole, Runtime, User} from "@code0-tech/sagittarius-graphql-types";
import {NamespaceService} from "@edition/namespace/Namespace.service";
import {RuntimeService} from "@edition/runtime/Runtime.service";
import {UserService} from "@edition/user/User.service";
import {ProjectService} from "@edition/project/Project.service";
import {RoleService} from "@edition/role/Role.service";
import {OrganizationService} from "@edition/organization/Organization.service";

export const ApplicationBreadcrumbView: React.FC = () => {

const path = usePathname()
const router = useRouter()
const params = useParams()

const namespaceIndex = params.namespaceId as string | undefined
const runtimeIndex = params.runtimeId as string | undefined
const userIndex = params.userId as string | undefined
const projectIndex = params.projectId as string | undefined
const roleIndex = params.roleId as string | undefined

const namespaceId = namespaceIndex ? `gid://sagittarius/Namespace/${namespaceIndex}` as Namespace['id'] : undefined
const runtimeId = runtimeIndex ? `gid://sagittarius/Runtime/${runtimeIndex}` as Runtime['id'] : undefined
const userId = userIndex ? `gid://sagittarius/User/${userIndex}` as User['id'] : undefined
const projectId = projectIndex ? `gid://sagittarius/NamespaceProject/${projectIndex}` as NamespaceProject['id'] : undefined
const roleId = roleIndex ? `gid://sagittarius/NamespaceRole/${roleIndex}` as NamespaceRole['id'] : undefined

const namespaceService = useService(NamespaceService)
const namespaceStore = useStore(NamespaceService)
const organizationService = useService(OrganizationService)
const organizationStore = useStore(OrganizationService)
const runtimeService = useService(RuntimeService)
const runtimeStore = useStore(RuntimeService)
const userService = useService(UserService)
const userStore = useStore(UserService)
const projectService = useService(ProjectService)
const projectStore = useStore(ProjectService)
const roleService = useService(RoleService)
const roleStore = useStore(RoleService)

const namespace = React.useMemo(() => namespaceId ? namespaceService.getById(namespaceId) : undefined, [namespaceStore, namespaceId])
const namespaceOrganization = React.useMemo(() => namespace?.parent?.__typename === "Organization" ? organizationService.getById(namespace?.parent?.id) : undefined, [organizationStore, namespace])
const namespaceUser = React.useMemo(() => namespace?.parent?.__typename === "User" ? userService.getById(namespace?.parent?.id) : undefined, [userStore, namespace])
const runtime = React.useMemo(() => runtimeId ? runtimeService.getById(runtimeId) : undefined, [runtimeStore, runtimeId])
const user = React.useMemo(() => userId ? userService.getById(userId) : undefined, [userStore, userId])
const project = React.useMemo(() => projectId ? projectService.getById(projectId, namespaceId ? {namespaceId} : undefined) : undefined, [projectStore, projectId, namespaceId])
const role = React.useMemo(() => roleId ? roleService.getById(roleId, namespaceId ? {namespaceId} : undefined) : undefined, [roleStore, roleId, namespaceId])

const pathSegments = React.useMemo(() => path.split("/").filter(pathSegment => pathSegment.length > 0), [path])

const namespaceLabel = React.useMemo(() => {
if (namespaceOrganization?.name) return namespaceOrganization.name
if (namespaceUser?.username) return namespaceUser.username
const fallbackName = [namespaceUser?.firstname, namespaceUser?.lastname].filter(Boolean).join(" ")
if (fallbackName) return fallbackName
return namespaceIndex ? `Namespace ${namespaceIndex}` : undefined
}, [namespaceOrganization, namespaceUser, namespaceIndex])

const runtimeLabel = runtime?.name ?? (runtimeIndex ? `Runtime ${runtimeIndex}` : undefined)
const userLabel = React.useMemo(() => {
if (user?.username) return user.username
const fallbackName = [user?.firstname, user?.lastname].filter(Boolean).join(" ")
if (fallbackName) return fallbackName
return userIndex ? `User ${userIndex}` : undefined
}, [user, userIndex])
const projectLabel = project?.name ?? (projectIndex ? `Project ${projectIndex}` : undefined)
const roleLabel = role?.name ?? (roleIndex ? `Role ${roleIndex}` : undefined)

const isRoutablePath = React.useCallback((segments: string[]) => {
if (segments.length === 0) return true

const [first, second, third, fourth, fifth, sixth, seventh] = segments

if (first === "settings") {
return segments.length === 1
}

if (first === "users") {
if (segments.length === 1) return true
if (segments.length === 2) return second === "invite" || second === userIndex
return false
}

if (first === "runtimes") {
if (segments.length === 1) return true
if (segments.length === 2) return second === "create"
if (segments.length === 3) return second === runtimeIndex && third === "settings"
return false
}

if (first === "organizations") {
if (segments.length === 1) return true
return segments.length === 2 && second === "create"
}

if (first !== "namespace") {
return segments.length === 1
}

if (segments.length === 1) return true
if (segments.length === 2) return second === namespaceIndex
if (segments.length === 3) {
return second === namespaceIndex && ["settings", "projects", "members", "roles", "runtimes"].includes(third)
|| second === namespaceIndex && third === projectIndex
}
if (segments.length === 4) {
if (second !== namespaceIndex) return false
if (third === "projects") return fourth === "create"
if (third === "members") return fourth === "add"
if (third === "roles") return fourth === "create"
if (third === "runtimes") return fourth === "create"
if (third === projectIndex) return fourth === "@tabs"
Comment on lines +112 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat project tab routes as routable without @tabs

Next.js parallel route segment names (like @tabs) do not appear in the URL returned by usePathname(), so for real URLs such as /namespace/:namespaceId/:projectId/flow the pathSegments array will never contain "@tabs". With the current checks, those paths are marked non-routable and resolveDestination trims them back to /namespace/:namespaceId/:projectId, causing breadcrumb entries for Flow (and flowId) pages to link to the project root instead of the current tab. This breaks breadcrumb navigation for all pages rendered under the @tabs slot.

Useful? React with 👍 / 👎.

return false
}
if (segments.length === 5) {
if (second !== namespaceIndex) return false
if (third === "roles") return fourth === roleIndex && fifth === "settings"
if (third === "runtimes") return fourth === runtimeIndex && fifth === "settings"
if (third === projectIndex) return fourth === "@tabs" && fifth === "flow"
return false
}
if (segments.length === 6) {
if (second !== namespaceIndex) return false
if (third === projectIndex) return fourth === "@tabs" && fifth === "flow" && sixth !== undefined
return false
}
if (segments.length === 7) {
if (second !== namespaceIndex) return false
return third === projectIndex && fourth === "@tabs" && fifth === "flow" && sixth !== undefined
&& ["@files", "@flow"].includes(seventh ?? "")
}
return false
}, [namespaceIndex, runtimeIndex, userIndex, projectIndex, roleIndex])

const collectionSegments = React.useMemo(() => ({
namespace: namespaceIndex,
runtimes: runtimeIndex,
users: userIndex,
roles: roleIndex,
projects: projectIndex,
}), [namespaceIndex, runtimeIndex, userIndex, roleIndex, projectIndex])

const getSegmentLabel = React.useCallback((segment: string, index: number) => {
const previous = pathSegments[index - 1]
if (segment === namespaceIndex && previous === "namespace") {
return {label: namespaceLabel ?? segment, isBadge: true}
}
if (segment === runtimeIndex && previous === "runtimes") {
return {label: runtimeLabel ?? segment, isBadge: true}
}
if (segment === userIndex && previous === "users") {
return {label: userLabel ?? segment, isBadge: true}
}
if (segment === roleIndex && previous === "roles") {
return {label: roleLabel ?? segment, isBadge: true}
}
if (segment === projectIndex && previous === namespaceIndex) {
return {label: projectLabel ?? segment, isBadge: true}
}
return {label: segment.charAt(0).toUpperCase() + segment.slice(1), isBadge: false}
}, [namespaceIndex, runtimeIndex, userIndex, roleIndex, projectIndex, namespaceLabel, runtimeLabel, userLabel, roleLabel, projectLabel, pathSegments])

const resolveDestination = React.useCallback((segments: string[]) => {
let destinationSegments = segments
while (destinationSegments.length > 0 && !isRoutablePath(destinationSegments)) {
destinationSegments = destinationSegments.slice(0, -1)
}
return destinationSegments
}, [isRoutablePath])

const breadcrumbs = React.useMemo(() => {
return pathSegments.flatMap((segment, index) => {
const nextSegment = pathSegments[index + 1]
if (collectionSegments[segment as keyof typeof collectionSegments] && nextSegment === collectionSegments[segment as keyof typeof collectionSegments]) {
return []
}

const destinationSegments = resolveDestination(pathSegments.slice(0, index + 1))
if (destinationSegments.length === 0) {
return []
}

const {label, isBadge} = getSegmentLabel(segment, index)

return [{
destination: `/${destinationSegments.join("/")}`,
label,
isBadge,
}]
})
}, [pathSegments, collectionSegments, getSegmentLabel, resolveDestination])

return <Breadcrumb>
{breadcrumbs.map((crumb, index) => {
return crumb.isBadge ? <Badge key={`${crumb.destination}-${index}`} color={"secondary"} border onClick={() => router.push(crumb.destination)}>
<Text>
{crumb.label}
</Text>
</Badge> : <Text key={`${crumb.destination}-${index}`} onClick={() => router.push(crumb.destination)}>
{crumb.label}
</Text>
})}
</Breadcrumb>
}