Skip to content

Commit 2aa1ede

Browse files
authored
feat: org settings page (#1683)
* feat: org settings page * Discard changes to fake-snippets-api/routes/api/orgs/list_members.ts * feat(OrganizationHeader): add showActions prop to control action button visibility
1 parent c1c9f23 commit 2aa1ede

9 files changed

Lines changed: 640 additions & 55 deletions

File tree

fake-snippets-api/lib/db/seed.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,4 +1733,9 @@ export const SquareWaveModule = () => (
17331733
started_at: null,
17341734
completed_at: null,
17351735
})
1736+
1737+
db.addOrganization({
1738+
name: "test-organization",
1739+
owner_account_id: account_id,
1740+
})
17361741
}

fake-snippets-api/routes/api/orgs/add_member.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ export default withRouteSpec({
55
methods: ["GET", "POST"],
66
commonParams: z.object({
77
org_id: z.string(),
8-
account_id: z.string(),
8+
account_id: z.string().optional(),
9+
github_username: z.string().optional(),
910
}),
1011
auth: "session",
1112
jsonResponse: z.object({}),
1213
})(async (req, ctx) => {
13-
const { org_id, account_id } = req.commonParams
14+
const { org_id, account_id, github_username } = req.commonParams
1415

1516
const org = ctx.db.getOrg({ org_id }, ctx.auth)
1617

@@ -28,7 +29,12 @@ export default withRouteSpec({
2829
})
2930
}
3031

31-
const account = ctx.db.accounts.find((acc) => acc.account_id === account_id)
32+
let account = ctx.db.accounts.find((acc) => acc.account_id === account_id)
33+
if (!account) {
34+
account = ctx.db.accounts.find(
35+
(acc) => acc.github_username === github_username,
36+
)
37+
}
3238

3339
if (!account) {
3440
return ctx.error(404, {
@@ -39,7 +45,7 @@ export default withRouteSpec({
3945

4046
ctx.db.addOrganizationAccount({
4147
org_id,
42-
account_id,
48+
account_id: account.account_id,
4349
})
4450

4551
return ctx.json({})

src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ const ReleasesPage = lazyImport(() => import("@/pages/releases"))
8585
const ReleaseDetailPage = lazyImport(() => import("@/pages/release-detail"))
8686
const ReleaseBuildsPage = lazyImport(() => import("@/pages/release-builds"))
8787
const ReleasePreviewPage = lazyImport(() => import("@/pages/preview-release"))
88+
const OrganizationSettingsPage = lazyImport(
89+
() => import("@/pages/organization-settings"),
90+
)
8891

8992
class ErrorBoundary extends React.Component<
9093
{ children: React.ReactNode },
@@ -265,6 +268,12 @@ function App() {
265268
{/* Organization creation route */}
266269
<Route path="/orgs/new" component={CreateOrganizationPage} />
267270

271+
{/* Organization settings route */}
272+
<Route
273+
path="/:orgname/settings"
274+
component={OrganizationSettingsPage}
275+
/>
276+
268277
{/* Profile fallback route - handles both organizations and users */}
269278
<Route path="/:username" component={ProfileRouter} />
270279

src/components/organization/OrganizationCard.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
6969
const handleSettingsClick = (e: React.MouseEvent) => {
7070
e.preventDefault()
7171
e.stopPropagation()
72-
// TODO: Navigate to organization settings
73-
console.log("Navigate to organization settings:", organization.name)
72+
window.location.href = `/${organization.name}/settings`
7473
}
7574

7675
const cardContent = (

src/components/organization/OrganizationHeader.tsx

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
import React from "react"
1+
import React, { useState } from "react"
22
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
33
import { Button } from "@/components/ui/button"
44
import { Building2, Users, Package, Lock, Globe2, Settings } from "lucide-react"
55
import { cn } from "@/lib/utils"
66
import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
77
import { useGlobalStore } from "@/hooks/use-global-store"
8+
import { useLocation } from "wouter"
89

910
interface OrganizationHeaderProps {
1011
organization: PublicOrgSchema
1112
isCurrentUserOrganization?: boolean
1213
className?: string
14+
showActions?: boolean
1315
}
1416

1517
export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
1618
organization,
1719
className,
20+
showActions = true,
1821
}) => {
1922
const session = useGlobalStore((s) => s.session)
23+
const [, navigate] = useLocation()
24+
const canManageOrg =
25+
organization.user_permissions?.can_manage_org ||
26+
organization.owner_account_id === session?.account_id
27+
28+
const handleSettingsClick = () => {
29+
navigate(`/${organization.name}/settings`)
30+
}
2031
return (
2132
<div className={cn("bg-white border-b border-gray-200", className)}>
2233
<div className="container mx-auto px-6 py-6">
@@ -40,17 +51,21 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
4051
</Avatar>
4152

4253
<div>
43-
{organization.owner_account_id === session?.account_id && (
44-
<div className="flex flex-col items-center gap-3 mb-3">
45-
<h1 className="font-bold text-gray-900 text-xl">
46-
{organization.name}
47-
</h1>
48-
<Button variant="outline" size="sm">
54+
<div className="flex flex-col items-center gap-3 mb-3">
55+
<h1 className="font-bold text-gray-900 text-xl">
56+
{organization.name}
57+
</h1>
58+
{canManageOrg && showActions && (
59+
<Button
60+
variant="outline"
61+
size="sm"
62+
onClick={handleSettingsClick}
63+
>
4964
<Settings className="mr-2 h-4 w-4" />
5065
Settings
5166
</Button>
52-
</div>
53-
)}
67+
)}
68+
</div>
5469

5570
<div className="grid grid-cols-2 md:flex flex-wrap justify-center gap-4 text-sm">
5671
<div className="flex items-center gap-1.5 text-gray-600">
@@ -63,20 +78,6 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
6378
<span className="font-medium">2</span>
6479
<span>packages</span>
6580
</div>
66-
<div className="flex items-center gap-1.5 text-gray-600">
67-
<Building2 className="h-3.5 w-3.5" />
68-
<span>Organization</span>
69-
</div>
70-
<div className="flex items-center gap-1.5 text-gray-600">
71-
{organization.is_personal_org ? (
72-
<Lock className="h-3.5 w-3.5" />
73-
) : (
74-
<Globe2 className="h-3.5 w-3.5" />
75-
)}
76-
<span>
77-
{organization.is_personal_org ? "Personal" : "Public"}
78-
</span>
79-
</div>
8081
</div>
8182
</div>
8283
</div>
@@ -106,8 +107,8 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
106107
<h1 className="font-bold text-gray-900 text-2xl md:text-3xl truncate">
107108
{organization.name}
108109
</h1>
109-
{organization.owner_account_id === session?.account_id && (
110-
<Button variant="outline">
110+
{canManageOrg && showActions && (
111+
<Button variant="outline" onClick={handleSettingsClick}>
111112
<Settings className="mr-2 h-4 w-4" />
112113
Settings
113114
</Button>
@@ -129,16 +130,6 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
129130
<Building2 className="h-4 w-4" />
130131
<span>Organization</span>
131132
</div>
132-
<div className="flex items-center gap-2">
133-
{organization.is_personal_org ? (
134-
<Lock className="h-4 w-4" />
135-
) : (
136-
<Globe2 className="h-4 w-4" />
137-
)}
138-
<span>
139-
{organization.is_personal_org ? "Personal" : "Public"}
140-
</span>
141-
</div>
142133
</div>
143134
</div>
144135
</div>

src/components/organization/OrganizationMembers.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Link } from "wouter"
33
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
44
import { Badge } from "@/components/ui/badge"
55
import { Button } from "@/components/ui/button"
6-
import { Users, Crown, Shield, User } from "lucide-react"
6+
import { Users, Crown, Shield, User, Loader2 } from "lucide-react"
77
import { timeAgo } from "@/lib/utils/timeAgo"
88
import { cn } from "@/lib/utils"
99
import { Account } from "fake-snippets-api/lib/db/schema"
@@ -56,34 +56,36 @@ export const OrganizationMembers: React.FC<OrganizationMembersProps> = ({
5656
>
5757
<div className="mb-4">
5858
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
59-
<Users className="h-5 w-5" />
6059
Members
6160
</h2>
6261
</div>
63-
<div className="text-center py-6 sm:py-8 text-gray-500">
64-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div>
65-
<p className="mt-2 text-sm">Loading members...</p>
62+
<div className="text-center py-20 text-gray-500 grid place-items-center">
63+
<Loader2 className="w-8 h-8 text-blue-600 dark:text-blue-400 animate-spin" />
64+
<p className="mt-2 text-sm select-none">Loading members...</p>
6665
</div>
6766
</div>
6867
)
6968
}
7069
return (
7170
<div
7271
className={cn(
73-
"bg-white rounded-lg border border-gray-200 p-4 sm:p-6",
72+
"bg-white rounded-lg border border-gray-200 p-4 py-20 sm:p-6",
7473
className,
7574
)}
7675
>
7776
<div className="mb-4">
7877
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
79-
<Users className="h-5 w-5" />
8078
Members ({members.length})
8179
</h2>
8280
</div>
8381

8482
<div className="space-y-2 sm:space-y-3">
85-
{members.map((member, i) => (
86-
<Link key={i} href={`/${member.github_username}`} className="block">
83+
{members.map((member) => (
84+
<Link
85+
key={member.account_id || member.github_username}
86+
href={`/${member.github_username}`}
87+
className="block"
88+
>
8789
<div className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
8890
<div className="flex items-center gap-3 min-w-0 flex-1">
8991
<Avatar className="h-10 w-10 sm:h-12 sm:w-12 flex-shrink-0">

src/hooks/use-add-org-member-mutation.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,38 @@ import { useGlobalStore } from "@/hooks/use-global-store"
44

55
export const useAddOrgMemberMutation = ({
66
onSuccess,
7-
}: { onSuccess?: () => void } = {}) => {
7+
onError,
8+
}: { onSuccess?: () => void; onError?: (error: any) => void } = {}) => {
89
const axios = useAxios()
910
const session = useGlobalStore((s) => s.session)
1011
const queryClient = useQueryClient()
1112

1213
return useMutation(
1314
["addOrgMember"],
14-
async ({ orgId, accountId }: { orgId: string; accountId: string }) => {
15+
async ({
16+
orgId,
17+
accountId,
18+
githubUsername,
19+
}: {
20+
orgId: string
21+
accountId?: string
22+
githubUsername?: string
23+
}) => {
1524
if (!session) throw new Error("No session")
1625

17-
await axios.post("/orgs/add_member", {
26+
const payload: any = {
1827
org_id: orgId,
19-
account_id: accountId,
20-
})
28+
}
29+
30+
if (accountId) {
31+
payload.account_id = accountId
32+
}
33+
34+
if (githubUsername) {
35+
payload.github_username = githubUsername
36+
}
37+
38+
await axios.post("/orgs/add_member", payload)
2139
},
2240
{
2341
onSuccess: () => {
@@ -26,6 +44,7 @@ export const useAddOrgMemberMutation = ({
2644
},
2745
onError: (error: any) => {
2846
console.error("Error adding organization member:", error)
47+
onError?.(error)
2948
},
3049
},
3150
)

src/hooks/use-list-org-members.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const useListOrgMembers = ({
2121
enabled: Boolean(orgId || orgName),
2222
retry: false,
2323
refetchOnWindowFocus: false,
24+
keepPreviousData: true,
2425
},
2526
)
2627
}

0 commit comments

Comments
 (0)