11"use client" ;
22
3- import { useState , useEffect , useCallback } from "react" ;
3+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
44import SettingsSidebar from "@/app/components/settings/SettingsSidebar" ;
55import PageHeader from "@/app/components/PageHeader" ;
66import { useAuth } from "@/app/lib/context/AuthContext" ;
7- import { usePaginatedList , useInfiniteScroll } from "@/app/hooks" ;
87import {
8+ useDebouncedValue ,
9+ useInfiniteScroll ,
10+ useOnboardingActivation ,
11+ usePaginatedList ,
12+ } from "@/app/hooks" ;
13+ import {
14+ DeleteOrganizationModal ,
15+ DeleteProjectModal ,
16+ EditOrganizationModal ,
17+ OnboardingCredentials ,
918 OnboardingForm ,
1019 OnboardingSuccess ,
1120 OrganizationList ,
21+ OrganizationListSkeleton ,
1222 ProjectList ,
1323 StepIndicator ,
1424 UserList ,
15- OnboardingCredentials ,
1625} from "@/app/components/settings/onboarding" ;
26+ import { useToast } from "@/app/hooks/useToast" ;
1727import {
28+ ActiveStatus ,
1829 Organization ,
1930 Project ,
2031 ProjectListResponse ,
@@ -25,41 +36,15 @@ import { ArrowLeftIcon } from "@/app/components/icons";
2536import { DEFAULT_PAGE_LIMIT } from "@/app/lib/constants" ;
2637import { TabNavigation } from "@/app/components/ui" ;
2738
39+ const SEARCH_DEBOUNCE_MS = 300 ;
40+
2841const PROJECT_TABS = [
2942 { id : "users" , label : "Users" } ,
3043 { id : "credentials" , label : "Credentials" } ,
3144] ;
3245
3346type View = "loading" | "list" | "projects" | "users" | "form" | "success" ;
3447
35- function OrganizationListSkeleton ( ) {
36- return (
37- < div className = "animate-pulse" >
38- < div className = "flex items-center justify-between mb-4" >
39- < div >
40- < div className = "h-5 w-36 bg-neutral-200 rounded mb-2" />
41- < div className = "h-3 w-24 bg-neutral-100 rounded" />
42- </ div >
43- < div className = "h-9 w-40 bg-neutral-200 rounded-full" />
44- </ div >
45- < div className = "space-y-2.5" >
46- { [ 1 , 2 , 3 ] . map ( ( i ) => (
47- < div
48- key = { i }
49- className = "flex items-center justify-between gap-3 p-4 rounded-lg bg-bg-primary shadow-[0_2px_6px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)]"
50- >
51- < div className = "min-w-0 flex-1" >
52- < div className = "h-4 w-40 max-w-full bg-neutral-200 rounded mb-2" />
53- < div className = "h-3 w-28 bg-neutral-100 rounded" />
54- </ div >
55- < div className = "h-4 w-4 shrink-0 bg-neutral-100 rounded" />
56- </ div >
57- ) ) }
58- </ div >
59- </ div >
60- ) ;
61- }
62-
6348export default function OnboardingPage ( ) {
6449 const { activeKey } = useAuth ( ) ;
6550 const [ view , setView ] = useState < View > ( "loading" ) ;
@@ -71,6 +56,34 @@ export default function OnboardingPage() {
7156 null ,
7257 ) ;
7358 const [ activeProjectTab , setActiveProjectTab ] = useState ( "users" ) ;
59+ const [ orgToDelete , setOrgToDelete ] = useState < Organization | null > ( null ) ;
60+ const [ isDeletingOrg , setIsDeletingOrg ] = useState ( false ) ;
61+ const [ orgToEdit , setOrgToEdit ] = useState < Organization | null > ( null ) ;
62+ const [ projectToDelete , setProjectToDelete ] = useState < Project | null > ( null ) ;
63+ const [ isDeletingProject , setIsDeletingProject ] = useState ( false ) ;
64+ const [ orgSearchInput , setOrgSearchInput ] = useState ( "" ) ;
65+ const [ projectSearchInput , setProjectSearchInput ] = useState ( "" ) ;
66+ const [ orgActiveStatus , setOrgActiveStatus ] =
67+ useState < ActiveStatus > ( "active" ) ;
68+ const [ projectActiveStatus , setProjectActiveStatus ] =
69+ useState < ActiveStatus > ( "active" ) ;
70+ const toast = useToast ( ) ;
71+ const debouncedOrgSearch = useDebouncedValue (
72+ orgSearchInput . trim ( ) ,
73+ SEARCH_DEBOUNCE_MS ,
74+ ) ;
75+ const debouncedProjectSearch = useDebouncedValue (
76+ projectSearchInput . trim ( ) ,
77+ SEARCH_DEBOUNCE_MS ,
78+ ) ;
79+
80+ const orgExtraParams = useMemo ( ( ) => {
81+ const params : Record < string , string > = {
82+ is_active : orgActiveStatus === "active" ? "true" : "false" ,
83+ } ;
84+ if ( debouncedOrgSearch ) params . search = debouncedOrgSearch ;
85+ return params ;
86+ } , [ debouncedOrgSearch , orgActiveStatus ] ) ;
7487
7588 const {
7689 items : organizations ,
@@ -82,6 +95,7 @@ export default function OnboardingPage() {
8295 } = usePaginatedList < Organization > ( {
8396 endpoint : "/api/organization" ,
8497 limit : DEFAULT_PAGE_LIMIT ,
98+ extraParams : orgExtraParams ,
8599 } ) ;
86100
87101 const scrollRef = useInfiniteScroll ( {
@@ -90,46 +104,81 @@ export default function OnboardingPage() {
90104 isLoading : isLoadingOrgs || isLoadingMore ,
91105 } ) ;
92106
107+ const initialOrgViewDecidedRef = useRef ( false ) ;
93108 useEffect ( ( ) => {
94- if ( isLoadingOrgs ) {
95- setView ( "loading" ) ;
96- return ;
97- }
98- if ( view === "loading" ) {
99- setView ( organizations . length > 0 ? "list" : "form" ) ;
100- }
109+ if ( initialOrgViewDecidedRef . current ) return ;
110+ if ( isLoadingOrgs ) return ;
111+ initialOrgViewDecidedRef . current = true ;
112+ setView ( organizations . length > 0 ? "list" : "form" ) ;
101113 } , [ isLoadingOrgs , organizations . length ] ) ;
102114
103- const fetchProjects = useCallback (
104- async ( org : Organization ) => {
105- setSelectedOrg ( org ) ;
106- setView ( "projects" ) ;
107- setIsLoadingProjects ( true ) ;
108- setProjects ( [ ] ) ;
115+ const buildProjectsQuery = ( search : string , status : ActiveStatus ) => {
116+ const params = new URLSearchParams ( {
117+ is_active : status === "active" ? "true" : "false" ,
118+ } ) ;
119+ if ( search ) params . set ( "search" , search ) ;
120+ return params . toString ( ) ;
121+ } ;
109122
123+ const loadProjects = useCallback (
124+ async ( orgId : number , search : string , status : ActiveStatus ) => {
125+ setIsLoadingProjects ( true ) ;
110126 try {
127+ const qs = buildProjectsQuery ( search , status ) ;
111128 const result = await apiFetch < ProjectListResponse > (
112- `/api/organization/${ org . id } /projects` ,
129+ `/api/organization/${ orgId } /projects? ${ qs } ` ,
113130 activeKey ?. key ?? "" ,
114131 ) ;
115-
116132 if ( result . success && result . data ) {
117133 setProjects ( result . data ) ;
134+ } else {
135+ setProjects ( [ ] ) ;
118136 }
119137 } catch {
120- // keep empty list
138+ setProjects ( [ ] ) ;
121139 } finally {
122140 setIsLoadingProjects ( false ) ;
123141 }
124142 } ,
125143 [ activeKey ] ,
126144 ) ;
127145
146+ const fetchProjects = useCallback (
147+ async ( org : Organization ) => {
148+ setSelectedOrg ( org ) ;
149+ setView ( "projects" ) ;
150+ setProjectSearchInput ( "" ) ;
151+ setProjectActiveStatus ( "active" ) ;
152+ setProjects ( [ ] ) ;
153+ await loadProjects ( org . id , "" , "active" ) ;
154+ } ,
155+ [ loadProjects ] ,
156+ ) ;
157+
158+ useEffect ( ( ) => {
159+ if ( view !== "projects" || ! selectedOrg ) return ;
160+ void loadProjects (
161+ selectedOrg . id ,
162+ debouncedProjectSearch ,
163+ projectActiveStatus ,
164+ ) ;
165+ } , [
166+ debouncedProjectSearch ,
167+ projectActiveStatus ,
168+ view ,
169+ selectedOrg ,
170+ loadProjects ,
171+ ] ) ;
172+
128173 const refreshProjects = useCallback ( async ( ) => {
129174 if ( ! selectedOrg ) return ;
130175 try {
176+ const qs = buildProjectsQuery (
177+ debouncedProjectSearch ,
178+ projectActiveStatus ,
179+ ) ;
131180 const result = await apiFetch < ProjectListResponse > (
132- `/api/organization/${ selectedOrg . id } /projects` ,
181+ `/api/organization/${ selectedOrg . id } /projects? ${ qs } ` ,
133182 activeKey ?. key ?? "" ,
134183 ) ;
135184 if ( result . success && result . data ) {
@@ -138,7 +187,7 @@ export default function OnboardingPage() {
138187 } catch {
139188 // keep current list
140189 }
141- } , [ selectedOrg , activeKey ] ) ;
190+ } , [ selectedOrg , activeKey , debouncedProjectSearch , projectActiveStatus ] ) ;
142191
143192 const handleSuccess = ( data : OnboardResponseData ) => {
144193 setOnboardData ( data ) ;
@@ -165,6 +214,7 @@ export default function OnboardingPage() {
165214 setSelectedOrg ( null ) ;
166215 setSelectedProject ( null ) ;
167216 setProjects ( [ ] ) ;
217+ setProjectSearchInput ( "" ) ;
168218 setView ( "list" ) ;
169219 } ;
170220
@@ -173,6 +223,71 @@ export default function OnboardingPage() {
173223 setView ( "projects" ) ;
174224 } ;
175225
226+ const handleConfirmDeleteOrg = async ( hardDelete : boolean ) => {
227+ if ( ! orgToDelete ) return ;
228+ setIsDeletingOrg ( true ) ;
229+ try {
230+ await apiFetch (
231+ `/api/organization/${ orgToDelete . id } ` ,
232+ activeKey ?. key ?? "" ,
233+ {
234+ method : "DELETE" ,
235+ body : JSON . stringify ( { hard_delete : hardDelete } ) ,
236+ } ,
237+ ) ;
238+ toast . success (
239+ hardDelete
240+ ? `"${ orgToDelete . name } " permanently deleted`
241+ : `"${ orgToDelete . name } " deactivated` ,
242+ ) ;
243+ setOrgToDelete ( null ) ;
244+ refetchOrganizations ( ) ;
245+ } catch ( e ) {
246+ toast . error (
247+ e instanceof Error ? e . message : "Failed to delete organization" ,
248+ ) ;
249+ } finally {
250+ setIsDeletingOrg ( false ) ;
251+ }
252+ } ;
253+
254+ const handleConfirmDeleteProject = async ( hardDelete : boolean ) => {
255+ if ( ! projectToDelete ) return ;
256+ setIsDeletingProject ( true ) ;
257+ try {
258+ await apiFetch (
259+ `/api/project/${ projectToDelete . id } ` ,
260+ activeKey ?. key ?? "" ,
261+ {
262+ method : "DELETE" ,
263+ body : JSON . stringify ( { hard_delete : hardDelete } ) ,
264+ } ,
265+ ) ;
266+ toast . success (
267+ hardDelete
268+ ? `"${ projectToDelete . name } " permanently deleted`
269+ : `"${ projectToDelete . name } " deactivated` ,
270+ ) ;
271+ setProjectToDelete ( null ) ;
272+ await refreshProjects ( ) ;
273+ } catch ( e ) {
274+ toast . error ( e instanceof Error ? e . message : "Failed to delete project" ) ;
275+ } finally {
276+ setIsDeletingProject ( false ) ;
277+ }
278+ } ;
279+
280+ const {
281+ activatingOrgId,
282+ activatingProjectId,
283+ activateOrg : handleActivateOrg ,
284+ activateProject : handleActivateProject ,
285+ } = useOnboardingActivation ( {
286+ apiKey : activeKey ?. key ?? "" ,
287+ onOrgActivated : refetchOrganizations ,
288+ onProjectActivated : refreshProjects ,
289+ } ) ;
290+
176291 return (
177292 < div className = "w-full h-screen flex flex-col bg-bg-primary" >
178293 < div className = "flex flex-1 overflow-hidden" >
@@ -191,9 +306,18 @@ export default function OnboardingPage() {
191306 { view === "list" && (
192307 < OrganizationList
193308 organizations = { organizations }
309+ isLoading = { isLoadingOrgs }
194310 isLoadingMore = { isLoadingMore }
195311 onNewOrg = { ( ) => setView ( "form" ) }
196312 onSelectOrg = { fetchProjects }
313+ onDeleteOrg = { setOrgToDelete }
314+ onEditOrg = { setOrgToEdit }
315+ onActivateOrg = { handleActivateOrg }
316+ activatingOrgId = { activatingOrgId }
317+ search = { orgSearchInput }
318+ onSearchChange = { setOrgSearchInput }
319+ activeStatus = { orgActiveStatus }
320+ onActiveStatusChange = { setOrgActiveStatus }
197321 />
198322 ) }
199323
@@ -205,6 +329,13 @@ export default function OnboardingPage() {
205329 onBack = { handleBackToOrgs }
206330 onSelectProject = { handleSelectProject }
207331 onProjectAdded = { refreshProjects }
332+ search = { projectSearchInput }
333+ onSearchChange = { setProjectSearchInput }
334+ activeStatus = { projectActiveStatus }
335+ onActiveStatusChange = { setProjectActiveStatus }
336+ onDeleteProject = { setProjectToDelete }
337+ onActivateProject = { handleActivateProject }
338+ activatingProjectId = { activatingProjectId }
208339 />
209340 ) }
210341
@@ -313,6 +444,38 @@ export default function OnboardingPage() {
313444 </ div >
314445 </ div >
315446 </ div >
447+
448+ { orgToDelete && (
449+ < DeleteOrganizationModal
450+ organization = { orgToDelete }
451+ isDeleting = { isDeletingOrg }
452+ onCancel = { ( ) => {
453+ if ( ! isDeletingOrg ) setOrgToDelete ( null ) ;
454+ } }
455+ onConfirm = { handleConfirmDeleteOrg }
456+ />
457+ ) }
458+
459+ { projectToDelete && (
460+ < DeleteProjectModal
461+ project = { projectToDelete }
462+ isDeleting = { isDeletingProject }
463+ onCancel = { ( ) => {
464+ if ( ! isDeletingProject ) setProjectToDelete ( null ) ;
465+ } }
466+ onConfirm = { handleConfirmDeleteProject }
467+ />
468+ ) }
469+
470+ { orgToEdit && (
471+ < EditOrganizationModal
472+ open
473+ organization = { orgToEdit }
474+ apiKey = { activeKey ?. key ?? "" }
475+ onClose = { ( ) => setOrgToEdit ( null ) }
476+ onOrganizationUpdated = { refetchOrganizations }
477+ />
478+ ) }
316479 </ div >
317480 ) ;
318481}
0 commit comments