1- import { useReducer , useCallback , useMemo , Suspense } from "react" ;
2- import { Link } from "@tanstack/react-router" ;
3- import { Result , useAtomValue , useAtomRefresh , useAtomSet , sourcesAtom , detectSource } from "@executor/react" ;
1+ import { useState , useCallback , useMemo } from "react" ;
2+ import { Link , useNavigate } from "@tanstack/react-router" ;
3+ import { Result , useAtomValue , useAtomSet , sourcesAtom , detectSource } from "@executor/react" ;
44import { useScope } from "../lib/use-scope" ;
55import type { SourcePlugin , SourcePreset } from "@executor/react" ;
66import { openApiSourcePlugin } from "@executor/plugin-openapi/react" ;
@@ -27,123 +27,51 @@ const KIND_TO_PLUGIN_KEY: Record<string, string> = {
2727 googleDiscovery : "googleDiscovery" ,
2828} ;
2929
30- // ---------------------------------------------------------------------------
31- // State machine
32- // ---------------------------------------------------------------------------
33-
34- type State = {
35- step : "list" | "detecting" | "adding" ;
36- url : string ;
37- error : string | null ;
38- pluginKey : string | null ;
39- initialUrl : string | undefined ;
40- } ;
41-
42- type Action =
43- | { type : "set-url" ; url : string }
44- | { type : "detect-start" }
45- | { type : "detect-ok" ; pluginKey : string ; url : string }
46- | { type : "detect-no-match" }
47- | { type : "detect-unknown-kind" ; kind : string }
48- | { type : "detect-fail" }
49- | { type : "add-manual" ; pluginKey : string }
50- | { type : "add-preset" ; pluginKey : string ; url : string }
51- | { type : "back" } ;
52-
53- const init : State = { step : "list" , url : "" , error : null , pluginKey : null , initialUrl : undefined } ;
54-
55- function reducer ( state : State , action : Action ) : State {
56- switch ( action . type ) {
57- case "set-url" :
58- return { ...state , step : "list" , url : action . url , error : null } ;
59- case "detect-start" :
60- return { ...state , step : "detecting" , error : null } ;
61- case "detect-ok" :
62- return { ...state , step : "adding" , pluginKey : action . pluginKey , initialUrl : action . url } ;
63- case "detect-no-match" :
64- return { ...state , step : "list" , error : "Could not detect a source type from this URL. Try adding manually." } ;
65- case "detect-unknown-kind" :
66- return { ...state , step : "list" , error : `Detected source type "${ action . kind } " but no plugin is available for it.` } ;
67- case "detect-fail" :
68- return { ...state , step : "list" , error : "Detection failed. Try adding a source manually." } ;
69- case "add-manual" :
70- return { ...state , step : "adding" , pluginKey : action . pluginKey , initialUrl : undefined } ;
71- case "add-preset" :
72- return { ...state , step : "adding" , pluginKey : action . pluginKey , initialUrl : action . url } ;
73- case "back" :
74- return init ;
75- default :
76- return state ;
77- }
78- }
79-
8030// ---------------------------------------------------------------------------
8131// Page
8232// ---------------------------------------------------------------------------
8333
8434export function SourcesPage ( ) {
85- const [ state , dispatch ] = useReducer ( reducer , init ) ;
35+ const [ url , setUrl ] = useState ( "" ) ;
36+ const [ detecting , setDetecting ] = useState ( false ) ;
37+ const [ error , setError ] = useState < string | null > ( null ) ;
38+
8639 const scopeId = useScope ( ) ;
8740 const sources = useAtomValue ( sourcesAtom ( scopeId ) ) ;
88- const refreshSources = useAtomRefresh ( sourcesAtom ( scopeId ) ) ;
8941 const doDetect = useAtomSet ( detectSource , { mode : "promise" } ) ;
42+ const navigate = useNavigate ( ) ;
9043
9144 const handleDetect = useCallback ( async ( ) => {
92- const trimmed = state . url . trim ( ) ;
45+ const trimmed = url . trim ( ) ;
9346 if ( ! trimmed ) return ;
94- dispatch ( { type : "detect-start" } ) ;
47+ setDetecting ( true ) ;
48+ setError ( null ) ;
9549 try {
9650 const results = await doDetect ( {
9751 path : { scopeId } ,
9852 payload : { url : trimmed } ,
9953 } ) ;
10054 if ( results . length === 0 ) {
101- dispatch ( { type : "detect-no-match" } ) ;
55+ setError ( "Could not detect a source type from this URL. Try adding manually." ) ;
56+ setDetecting ( false ) ;
10257 return ;
10358 }
10459 const pluginKey = KIND_TO_PLUGIN_KEY [ results [ 0 ] . kind ] ;
10560 if ( pluginKey ) {
106- dispatch ( { type : "detect-ok" , pluginKey, url : trimmed } ) ;
61+ void navigate ( {
62+ to : "/sources/add/$pluginKey" ,
63+ params : { pluginKey } ,
64+ search : { url : trimmed } ,
65+ } ) ;
10766 } else {
108- dispatch ( { type : "detect-unknown-kind" , kind : results [ 0 ] . kind } ) ;
67+ setError ( `Detected source type " ${ results [ 0 ] . kind } " but no plugin is available for it.` ) ;
10968 }
11069 } catch {
111- dispatch ( { type : "detect-fail" } ) ;
70+ setError ( "Detection failed. Try adding a source manually." ) ;
71+ } finally {
72+ setDetecting ( false ) ;
11273 }
113- } , [ state . url , doDetect ] ) ;
114-
115- // ---------------------------------------------------------------------------
116- // Adding view
117- // ---------------------------------------------------------------------------
118-
119- if ( state . step === "adding" && state . pluginKey ) {
120- const plugin = sourcePlugins . find ( ( p ) => p . key === state . pluginKey ) ;
121- if ( ! plugin ) return null ;
122- const AddComponent = plugin . add ;
123- return (
124- < div className = "min-h-0 flex-1 overflow-y-auto" >
125- < div className = "mx-auto max-w-4xl px-6 py-10 lg:px-10 lg:py-14" >
126- < Suspense fallback = { < p className = "text-sm text-muted-foreground" > Loading…</ p > } >
127- < AddComponent
128- initialUrl = { state . initialUrl }
129- onComplete = { ( ) => {
130- dispatch ( { type : "back" } ) ;
131- refreshSources ( ) ;
132- } }
133- onCancel = { ( ) => dispatch ( { type : "back" } ) }
134- />
135- </ Suspense >
136- </ div >
137- </ div >
138- ) ;
139- }
140-
141- // ---------------------------------------------------------------------------
142- // List view
143- // ---------------------------------------------------------------------------
144-
145- const isDetecting = state . step === "detecting" ;
146- const error = state . step === "list" ? state . error : null ;
74+ } , [ url , doDetect , navigate , scopeId ] ) ;
14775
14876 return (
14977 < div className = "min-h-0 flex-1 overflow-y-auto" >
@@ -166,19 +94,22 @@ export function SourcesPage() {
16694 < div className = "flex gap-2" >
16795 < input
16896 type = "url"
169- value = { state . url }
170- onChange = { ( e ) => dispatch ( { type : "set-url" , url : e . target . value } ) }
97+ value = { url }
98+ onChange = { ( e ) => {
99+ setUrl ( e . target . value ) ;
100+ setError ( null ) ;
101+ } }
171102 onKeyDown = { ( e ) => { if ( e . key === "Enter" ) handleDetect ( ) ; } }
172103 placeholder = "Paste a URL to auto-detect source type..."
173- disabled = { isDetecting }
104+ disabled = { detecting }
174105 className = "flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
175106 />
176107 < button
177108 onClick = { handleDetect }
178- disabled = { isDetecting || ! state . url . trim ( ) }
109+ disabled = { detecting || ! url . trim ( ) }
179110 className = "inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none"
180111 >
181- { isDetecting ? "Detecting..." : "Detect" }
112+ { detecting ? "Detecting..." : "Detect" }
182113 </ button >
183114 </ div >
184115 { error && (
@@ -187,21 +118,22 @@ export function SourcesPage() {
187118 < div className = "mt-3 flex items-center gap-2 text-xs text-muted-foreground" >
188119 < span > Or add manually:</ span >
189120 { sourcePlugins . map ( ( p ) => (
190- < button
121+ < Link
191122 key = { p . key }
192- onClick = { ( ) => dispatch ( { type : "add-manual" , pluginKey : p . key } ) }
123+ to = "/sources/add/$pluginKey"
124+ params = { { pluginKey : p . key } }
193125 className = "rounded-md border border-border px-2 py-1 text-xs font-medium transition-colors hover:bg-muted"
194126 >
195127 { p . label }
196- </ button >
128+ </ Link >
197129 ) ) }
198130 </ div >
199131 </ div >
200132 </ div >
201133
202134 < McpInstallCard className = "mb-8 rounded-2xl border border-border bg-card/80 p-5" />
203135
204- < PresetGrid plugins = { sourcePlugins } dispatch = { dispatch } />
136+ < PresetGrid plugins = { sourcePlugins } />
205137
206138 { Result . match ( sources , {
207139 onInitial : ( ) => (
@@ -272,7 +204,6 @@ export function SourcesPage() {
272204
273205function PresetGrid ( props : {
274206 plugins : readonly SourcePlugin [ ] ;
275- dispatch : React . Dispatch < Action > ;
276207} ) {
277208 const allPresets = useMemo ( ( ) => {
278209 const out : { preset : SourcePreset ; pluginKey : string ; pluginLabel : string } [ ] = [ ] ;
@@ -296,9 +227,11 @@ function PresetGrid(props: {
296227 </ div >
297228 < div className = "grid gap-2 sm:grid-cols-2 lg:grid-cols-3" >
298229 { allPresets . map ( ( { preset, pluginKey, pluginLabel } ) => (
299- < button
230+ < Link
300231 key = { `${ pluginKey } -${ preset . id } ` }
301- onClick = { ( ) => props . dispatch ( { type : "add-preset" , pluginKey, url : preset . url } ) }
232+ to = "/sources/add/$pluginKey"
233+ params = { { pluginKey } }
234+ search = { { url : preset . url } }
302235 className = "flex items-start gap-3 rounded-xl border border-border bg-card px-4 py-3 text-left transition-colors hover:border-primary/25 hover:bg-card/90"
303236 >
304237 < div className = "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground overflow-hidden" >
@@ -319,7 +252,7 @@ function PresetGrid(props: {
319252 </ div >
320253 < p className = "mt-0.5 text-xs text-muted-foreground line-clamp-1" > { preset . summary } </ p >
321254 </ div >
322- </ button >
255+ </ Link >
323256 ) ) }
324257 </ div >
325258 </ section >
0 commit comments