@@ -6,21 +6,16 @@ import { RpcApi } from "@/app/store/wshclientapi";
66import { TabRpcClient } from "@/app/store/wshrpcutil" ;
77import { atoms , globalStore } from "@/store/global" ;
88import * as WOS from "@/store/wos" ;
9+ import { formatRelativeTime } from "@/util/util" ;
910import { useEffect , useState } from "react" ;
1011
1112const MaxAppNameLength = 50 ;
1213const AppNameRegex = / ^ [ a - z A - Z 0 - 9 _ - ] + $ / ;
1314
14- export function AppSelectionModal ( ) {
15- const [ apps , setApps ] = useState < string [ ] > ( [ ] ) ;
16- const [ loading , setLoading ] = useState ( true ) ;
15+ function CreateNewWaveApp ( { onCreateApp } : { onCreateApp : ( appName : string ) => Promise < void > } ) {
1716 const [ newAppName , setNewAppName ] = useState ( "" ) ;
18- const [ error , setError ] = useState ( "" ) ;
1917 const [ inputError , setInputError ] = useState ( "" ) ;
20-
21- useEffect ( ( ) => {
22- loadApps ( ) ;
23- } , [ ] ) ;
18+ const [ isCreating , setIsCreating ] = useState ( false ) ;
2419
2520 const validateAppName = ( name : string ) => {
2621 if ( ! name . trim ( ) ) {
@@ -39,10 +34,83 @@ export function AppSelectionModal() {
3934 return true ;
4035 } ;
4136
37+ const handleCreate = async ( ) => {
38+ const trimmedName = newAppName . trim ( ) ;
39+ if ( ! validateAppName ( trimmedName ) ) {
40+ return ;
41+ }
42+
43+ setIsCreating ( true ) ;
44+ try {
45+ await onCreateApp ( trimmedName ) ;
46+ } finally {
47+ setIsCreating ( false ) ;
48+ }
49+ } ;
50+
51+ return (
52+ < div className = "min-h-[80px]" >
53+ < h3 className = "text-base font-medium mb-1 text-muted-foreground" > Create New WaveApp</ h3 >
54+ < div className = "relative" >
55+ < div className = "flex w-full" >
56+ < input
57+ type = "text"
58+ value = { newAppName }
59+ onChange = { ( e ) => {
60+ const value = e . target . value ;
61+ setNewAppName ( value ) ;
62+ validateAppName ( value ) ;
63+ } }
64+ onKeyDown = { ( e ) => {
65+ if ( e . key === "Enter" && ! e . nativeEvent . isComposing && newAppName . trim ( ) && ! inputError ) {
66+ handleCreate ( ) ;
67+ }
68+ } }
69+ placeholder = "my-app"
70+ maxLength = { MaxAppNameLength }
71+ className = { `flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${
72+ inputError ? "border-error" : "border-border focus:border-accent"
73+ } `}
74+ autoFocus
75+ disabled = { isCreating }
76+ />
77+ < button
78+ onClick = { handleCreate }
79+ disabled = { ! newAppName . trim ( ) || ! ! inputError || isCreating }
80+ className = { `px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
81+ ! newAppName . trim ( ) || inputError || isCreating
82+ ? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
83+ : "bg-accent text-black hover:bg-accent-hover cursor-pointer"
84+ } `}
85+ >
86+ Create
87+ </ button >
88+ </ div >
89+ { inputError && (
90+ < div className = "absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap" >
91+ < i className = "fa-solid fa-circle-exclamation" > </ i >
92+ < span > { inputError } </ span >
93+ </ div >
94+ ) }
95+ </ div >
96+ </ div >
97+ ) ;
98+ }
99+
100+ export function AppSelectionModal ( ) {
101+ const [ apps , setApps ] = useState < AppInfo [ ] > ( [ ] ) ;
102+ const [ loading , setLoading ] = useState ( true ) ;
103+ const [ error , setError ] = useState ( "" ) ;
104+
105+ useEffect ( ( ) => {
106+ loadApps ( ) ;
107+ } , [ ] ) ;
108+
42109 const loadApps = async ( ) => {
43110 try {
44111 const appList = await RpcApi . ListAllEditableAppsCommand ( TabRpcClient ) ;
45- setApps ( appList || [ ] ) ;
112+ const sortedApps = ( appList || [ ] ) . sort ( ( a , b ) => b . modtime - a . modtime ) ;
113+ setApps ( sortedApps ) ;
46114 } catch ( err ) {
47115 console . error ( "Failed to load apps:" , err ) ;
48116 setError ( "Failed to load apps" ) ;
@@ -61,25 +129,8 @@ export function AppSelectionModal() {
61129 globalStore . set ( atoms . builderAppId , appId ) ;
62130 } ;
63131
64- const handleCreateNew = async ( ) => {
65- const trimmedName = newAppName . trim ( ) ;
66-
67- if ( ! trimmedName ) {
68- setError ( "WaveApp name cannot be empty" ) ;
69- return ;
70- }
71-
72- if ( trimmedName . length > MaxAppNameLength ) {
73- setError ( `WaveApp name must be ${ MaxAppNameLength } characters or less` ) ;
74- return ;
75- }
76-
77- if ( ! AppNameRegex . test ( trimmedName ) ) {
78- setError ( "WaveApp name can only contain letters, numbers, hyphens, and underscores" ) ;
79- return ;
80- }
81-
82- const draftAppId = `draft/${ trimmedName } ` ;
132+ const handleCreateNew = async ( appName : string ) => {
133+ const draftAppId = `draft/${ appName } ` ;
83134 const builderId = globalStore . get ( atoms . builderId ) ;
84135 const oref = WOS . makeORef ( "builder" , builderId ) ;
85136 await RpcApi . SetRTInfoCommand ( TabRpcClient , {
@@ -111,9 +162,9 @@ export function AppSelectionModal() {
111162 }
112163
113164 return (
114- < FlexiModal className = "min-w-[600px] w-[600px] max-h-[80vh ] overflow-y-auto" >
165+ < FlexiModal className = "min-w-[600px] w-[600px] max-h-[90vh ] overflow-y-auto" >
115166 < div className = "w-full px-2 pt-0 pb-4" >
116- < h2 className = "text-2xl mb-6 " > Select a WaveApp to Edit</ h2 >
167+ < h2 className = "text-2xl mb-2 " > Select a WaveApp to Edit</ h2 >
117168
118169 { error && (
119170 < div className = "mb-6 px-4 py-3 bg-panel rounded" >
@@ -125,18 +176,23 @@ export function AppSelectionModal() {
125176 ) }
126177
127178 { apps . length > 0 && (
128- < div className = "mb-6 " >
129- < h3 className = "text-base font-medium mb-3 text-muted-foreground" > Existing WaveApps</ h3 >
130- < div className = "space-y-2 max-h-[200px ] overflow-y-auto" >
131- { apps . map ( ( appId ) => (
179+ < div className = "mb-2 " >
180+ < h3 className = "text-base font-medium mb-1 text-muted-foreground" > Existing WaveApps</ h3 >
181+ < div className = "space-y-2 max-h-[220px ] overflow-y-auto" >
182+ { apps . map ( ( appInfo ) => (
132183 < button
133- key = { appId }
134- onClick = { ( ) => handleSelectApp ( appId ) }
135- className = "w-full text-left px-4 py-3 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
184+ key = { appInfo . appid }
185+ onClick = { ( ) => handleSelectApp ( appInfo . appid ) }
186+ className = "w-full text-left px-4 py-1.5 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
136187 >
137188 < div className = "flex items-center gap-3" >
138- < i className = "fa-solid fa-cube" > </ i >
139- < span > { getAppDisplayName ( appId ) } </ span >
189+ < i className = "fa-solid fa-cube self-center" > </ i >
190+ < div className = "flex flex-col" >
191+ < span > { getAppDisplayName ( appInfo . appid ) } </ span >
192+ < span className = "text-[11px] text-muted mt-0.5" >
193+ Last updated: { formatRelativeTime ( appInfo . modtime ) }
194+ </ span >
195+ </ div >
140196 </ div >
141197 </ button >
142198 ) ) }
@@ -145,62 +201,14 @@ export function AppSelectionModal() {
145201 ) }
146202
147203 { apps . length > 0 && (
148- < div className = "flex items-center gap-4 my-6 " >
204+ < div className = "flex items-center gap-4 my-2 " >
149205 < div className = "flex-1 border-t border-border" > </ div >
150206 < span className = "text-muted-foreground text-sm" > or</ span >
151207 < div className = "flex-1 border-t border-border" > </ div >
152208 </ div >
153209 ) }
154210
155- < div className = "min-h-[80px]" >
156- < h3 className = "text-base font-medium mb-4 text-muted-foreground" > Create New WaveApp</ h3 >
157- < div className = "relative" >
158- < div className = "flex w-full" >
159- < input
160- type = "text"
161- value = { newAppName }
162- onChange = { ( e ) => {
163- const value = e . target . value ;
164- setNewAppName ( value ) ;
165- validateAppName ( value ) ;
166- } }
167- onKeyDown = { ( e ) => {
168- if (
169- e . key === "Enter" &&
170- ! e . nativeEvent . isComposing &&
171- newAppName . trim ( ) &&
172- ! inputError
173- ) {
174- handleCreateNew ( ) ;
175- }
176- } }
177- placeholder = "my-app"
178- maxLength = { MaxAppNameLength }
179- className = { `flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${
180- inputError ? "border-error" : "border-border focus:border-accent"
181- } `}
182- autoFocus
183- />
184- < button
185- onClick = { handleCreateNew }
186- disabled = { ! newAppName . trim ( ) || ! ! inputError }
187- className = { `px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
188- ! newAppName . trim ( ) || inputError
189- ? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
190- : "bg-accent text-black hover:bg-accent-hover cursor-pointer"
191- } `}
192- >
193- Create
194- </ button >
195- </ div >
196- { inputError && (
197- < div className = "absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap" >
198- < i className = "fa-solid fa-circle-exclamation" > </ i >
199- < span > { inputError } </ span >
200- </ div >
201- ) }
202- </ div >
203- </ div >
211+ < CreateNewWaveApp onCreateApp = { handleCreateNew } />
204212 </ div >
205213 </ FlexiModal >
206214 ) ;
0 commit comments