Chore/project wildcard route sort last visited org (#35680)

* Project wildcard route, sort last visited org first

* Add search and filter status

* Smol fix
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-05-16 15:42:02 +08:00
1 parent 0880e0e1d0
commit 5c99584001
4 files changed
+141 -91

No files matched your search

@@ -1,7 +1,6 @@
import { Plus } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import NoSearchResults from 'components/ui/NoSearchResults'
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
@@ -19,7 +18,7 @@ import ProjectCard from './ProjectCard'
import ShimmeringCard from './ShimmeringCard'
export interface ProjectListProps {
forOrganization?: Organization
organization?: Organization
rewriteHref?: (projectRef: string) => string
search?: string
filterStatus?: string[]
@@ -28,13 +27,13 @@ export interface ProjectListProps {
const ProjectList = ({
search = '',
forOrganization,
organization: organization_,
rewriteHref,
filterStatus,
resetFilterStatus,
}: ProjectListProps) => {
const { slug } = useParams()
const organization = useSelectedOrganization() ?? forOrganization
const selectedOrganization = useSelectedOrganization()
const organization = organization_ ?? selectedOrganization
const {
data: allProjects = [],
@@ -50,9 +49,7 @@ const ProjectList = ({
} = usePermissionsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery()
const orgProjects = allProjects.filter(
(x) => x.organization_slug === slug || x.organization_slug === organization?.slug
)
const orgProjects = allProjects.filter((x) => x.organization_slug === organization?.slug)
const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
const hasFilterStatusApplied = filterStatus !== undefined && filterStatus.length !== 2
@@ -17,6 +17,7 @@ import {
interface HomePageActionsProps {
search: string
filterStatus: string[]
hideNewProject?: boolean
setSearch: (value: string) => void
setFilterStatus: (value: string[]) => void
}
@@ -24,6 +25,7 @@ interface HomePageActionsProps {
const HomePageActions = ({
search,
filterStatus,
hideNewProject = false,
setSearch,
setFilterStatus,
}: HomePageActionsProps) => {
@@ -32,7 +34,7 @@ const HomePageActions = ({
return (
<div className="flex flex-col gap-2 md:gap-3 md:flex-row">
{projectCreationEnabled && (
{projectCreationEnabled && !hideNewProject && (
<Button asChild type="primary">
<Link href={`/new/${slug}`}>New project</Link>
</Button>
+60 -55
View File
@@ -8,7 +8,7 @@ import CardButton from 'components/ui/CardButton'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { Button } from 'ui'
import { Button, cn } from 'ui'
const Header = () => {
return (
@@ -27,6 +27,7 @@ const Header = () => {
)
}
// [Joshen] Thinking we can deprecate this page in favor of /organizations
const GenericOrganizationPage: NextPage = () => {
const router = useRouter()
@@ -51,64 +52,68 @@ const GenericOrganizationPage: NextPage = () => {
return (
<>
<Header />
<div className="flex flex-col mx-auto w-full max-w-5xl space-y-3">
<h3 className="mt-8 text-2xl">Select an organization to continue</h3>
<div className="flex flex-col mx-auto w-full space-y-3">
<h3 className="mt-8 text-2xl w-full max-w-5xl mx-auto">
Select an organization to continue
</h3>
<div
className="flex-grow py-6 space-y-8 overflow-y-auto"
className="flex-grow py-6 overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 49px - 64px)' }}
>
{isLoading ? (
<ul
className={[
'mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3',
].join(' `')}
>
<ShimmeringCard />
<ShimmeringCard />
</ul>
) : organizations?.length === 0 ? (
<div className="col-span-4 space-y-4 rounded-lg border border-dashed border-muted p-6 text-center">
<div className="space-y-1">
<p>You are not part of any organizations yet</p>
<p className="text-sm text-foreground-light">
Get started by creating a new organization.
</p>
<div className="w-full max-w-5xl mx-auto flex flex-col gap-y-8">
{isLoading ? (
<ul
className={cn(
'mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
)}
>
<ShimmeringCard />
<ShimmeringCard />
</ul>
) : organizations?.length === 0 ? (
<div className="col-span-4 space-y-4 rounded-lg border border-dashed border-muted p-6 text-center">
<div className="space-y-1">
<p>You are not part of any organizations yet</p>
<p className="text-sm text-foreground-light">
Get started by creating a new organization.
</p>
</div>
<div>
<Button asChild icon={<Plus />}>
<Link href="/new">New organization</Link>
</Button>
</div>
</div>
<div>
<Button asChild icon={<Plus />}>
<Link href="/new">New organization</Link>
</Button>
</div>
</div>
) : (
<ul
className={[
'mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3',
].join(' `')}
>
{organizations?.map((organization) => (
<li key={organization.slug} className="col-span-1">
<CardButton
linkHref={urlRewriterFactory(routeSlug)(organization.slug)}
title={
<div className="flex w-full flex-row justify-between gap-1">
<span className="flex-shrink truncate">{organization.name}</span>
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-foreground-light">
{organization.slug}
</span>
</div>
}
/>
</li>
))}
</ul>
)}
) : (
<ul
className={cn(
'w-full mx-auto grid grid-cols-1 gap-4',
'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
)}
>
{organizations?.map((organization) => (
<li key={organization.slug} className="col-span-1">
<CardButton
linkHref={urlRewriterFactory(routeSlug)(organization.slug)}
title={
<div className="flex w-full flex-row justify-between gap-1">
<span className="flex-shrink truncate">{organization.name}</span>
</div>
}
footer={
<div className="flex items-end justify-between">
<span className="text-sm lowercase text-foreground-light">
{organization.slug}
</span>
</div>
}
/>
</li>
))}
</ul>
)}
</div>
</div>
</div>
</>
@@ -1,15 +1,18 @@
import { partition } from 'lodash'
import { AlertTriangleIcon, Boxes } from 'lucide-react'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Fragment } from 'react'
import { Fragment, useMemo, useState } from 'react'
import { IS_PLATFORM } from 'common'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common'
import { ProjectList } from 'components/interfaces/Home/ProjectList'
import HomePageActions from 'components/interfaces/HomePageActions'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui'
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge } from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
const Header = () => {
@@ -37,8 +40,19 @@ const GenericProjectPage: NextPage = () => {
const router = useRouter()
const { routeSlug, ...queryParams } = router.query
const [search, setSearch] = useState('')
const [filterStatus, setFilterStatus] = useState<string[]>([
PROJECT_STATUS.ACTIVE_HEALTHY,
PROJECT_STATUS.INACTIVE,
])
const [lastVisitedOrgSlug] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
)
const {
data: organizations,
data: organizations = [],
isLoading: isLoadingOrganizations,
isError: isErrorOrganizations,
} = useOrganizationsQuery({
@@ -62,34 +76,66 @@ const GenericProjectPage: NextPage = () => {
}
}
const [[lastVisitedOrganization], otherOrganizations] = useMemo(
() => partition(organizations, (org) => org.slug === lastVisitedOrgSlug),
[lastVisitedOrgSlug, organizations]
)
return (
<>
<Header />
<div className="flex flex-col mx-auto w-full max-w-5xl">
<h1 className="mt-8 text-2xl">Select a project to continue</h1>
<div className="flex flex-col mx-auto w-full">
<h1 className="mt-8 text-2xl max-w-5xl mx-auto w-full">Select a project to continue</h1>
<div
className="flex-grow py-6 space-y-8 overflow-y-auto"
className="flex-grow py-6 overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 49px - 64px)' }}
>
{isLoadingOrganizations ? (
<OrganizationLoadingState />
) : isErrorOrganizations ? (
<OrganizationErrorState />
) : (
organizations.map((organization) => (
<Fragment key={organization.id}>
<h2 className="flex items-center gap-2">
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
{organization.name}
</h2>
<ProjectList
forOrganization={organization}
rewriteHref={urlRewriterFactory(routeSlug)}
search=""
/>
</Fragment>
))
)}
<div className="w-full max-w-5xl mx-auto flex flex-col gap-y-8">
<HomePageActions
hideNewProject
search={search}
setSearch={setSearch}
filterStatus={filterStatus}
setFilterStatus={setFilterStatus}
/>
{isLoadingOrganizations ? (
<OrganizationLoadingState />
) : isErrorOrganizations ? (
<OrganizationErrorState />
) : (
<>
{!!lastVisitedOrganization && (
<>
<h2 className="flex items-center gap-2">
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
{lastVisitedOrganization.name}
<Badge variant="default">Recently visited</Badge>
</h2>
<ProjectList
search={search}
filterStatus={filterStatus}
organization={lastVisitedOrganization}
rewriteHref={urlRewriterFactory(routeSlug)}
/>
</>
)}
{otherOrganizations.map((organization) => (
<Fragment key={organization.id}>
<h2 className="flex items-center gap-2">
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
{organization.name}
</h2>
<ProjectList
search={search}
filterStatus={filterStatus}
organization={organization}
rewriteHref={urlRewriterFactory(routeSlug)}
/>
</Fragment>
))}
</>
)}
</div>
</div>
</div>
</>