@@ -62,7 +62,7 @@ import {
62
62
EnvironmentParamSchema ,
63
63
v3ProjectSettingsPath ,
64
64
} from "~/utils/pathBuilder" ;
65
- import { useEffect , useState } from "react" ;
65
+ import React , { useEffect , useState } from "react" ;
66
66
import { Select , SelectItem } from "~/components/primitives/Select" ;
67
67
import { Switch } from "~/components/primitives/Switch" ;
68
68
import { type BranchTrackingConfig } from "~/v3/github" ;
@@ -77,6 +77,7 @@ import { DateTime } from "~/components/primitives/DateTime";
77
77
import { TextLink } from "~/components/primitives/TextLink" ;
78
78
import { cn } from "~/utils/cn" ;
79
79
import { ProjectSettingsPresenter } from "~/services/projectSettingsPresenter.server" ;
80
+ import { type BuildSettings } from "~/v3/buildSettings" ;
80
81
81
82
export const meta : MetaFunction = ( ) => {
82
83
return [
@@ -120,7 +121,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
120
121
}
121
122
}
122
123
123
- const { gitHubApp } = resultOrFail . value ;
124
+ const { gitHubApp, buildSettings } = resultOrFail . value ;
124
125
125
126
const session = await getSession ( request . headers . get ( "Cookie" ) ) ;
126
127
const openGitHubRepoConnectionModal = session . get ( "gitHubAppInstalled" ) === true ;
@@ -134,6 +135,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
134
135
githubAppInstallations : gitHubApp . installations ,
135
136
connectedGithubRepository : gitHubApp . connectedRepository ,
136
137
openGitHubRepoConnectionModal,
138
+ buildSettings,
137
139
} ,
138
140
{ headers }
139
141
) ;
@@ -155,6 +157,38 @@ const UpdateGitSettingsFormSchema = z.object({
155
157
. transform ( ( val ) => val === "on" ) ,
156
158
} ) ;
157
159
160
+ const UpdateBuildSettingsFormSchema = z . object ( {
161
+ action : z . literal ( "update-build-settings" ) ,
162
+ triggerConfigFilePath : z
163
+ . string ( )
164
+ . trim ( )
165
+ . optional ( )
166
+ . transform ( ( val ) => ( val ? val . replace ( / ^ \/ + / , "" ) : val ) )
167
+ . refine ( ( val ) => ! val || val . length <= 255 , {
168
+ message : "Config file path must not exceed 255 characters" ,
169
+ } ) ,
170
+ installDirectory : z
171
+ . string ( )
172
+ . trim ( )
173
+ . optional ( )
174
+ . transform ( ( val ) => ( val ? val . replace ( / ^ \/ + / , "" ) : val ) )
175
+ . refine ( ( val ) => ! val || val . length <= 255 , {
176
+ message : "Install directory must not exceed 255 characters" ,
177
+ } ) ,
178
+ installCommand : z
179
+ . string ( )
180
+ . trim ( )
181
+ . optional ( )
182
+ . refine ( ( val ) => ! val || ! val . includes ( "\n" ) , {
183
+ message : "Install command must be a single line" ,
184
+ } )
185
+ . refine ( ( val ) => ! val || val . length <= 500 , {
186
+ message : "Install command must not exceed 500 characters" ,
187
+ } ) ,
188
+ } ) ;
189
+
190
+ type UpdateBuildSettingsFormSchema = z . infer < typeof UpdateBuildSettingsFormSchema > ;
191
+
158
192
export function createSchema (
159
193
constraints : {
160
194
getSlugMatch ?: ( slug : string ) => { isMatch : boolean ; projectSlug : string } ;
@@ -188,6 +222,7 @@ export function createSchema(
188
222
} ) ,
189
223
ConnectGitHubRepoFormSchema ,
190
224
UpdateGitSettingsFormSchema ,
225
+ UpdateBuildSettingsFormSchema ,
191
226
z . object ( {
192
227
action : z . literal ( "disconnect-repo" ) ,
193
228
} ) ,
@@ -376,6 +411,31 @@ export const action: ActionFunction = async ({ request, params }) => {
376
411
success : true ,
377
412
} ) ;
378
413
}
414
+ case "update-build-settings" : {
415
+ const { installDirectory, installCommand, triggerConfigFilePath } = submission . value ;
416
+
417
+ const resultOrFail = await projectSettingsService . updateBuildSettings ( projectId , {
418
+ installDirectory : installDirectory || undefined ,
419
+ installCommand : installCommand || undefined ,
420
+ triggerConfigFilePath : triggerConfigFilePath || undefined ,
421
+ } ) ;
422
+
423
+ if ( resultOrFail . isErr ( ) ) {
424
+ switch ( resultOrFail . error . type ) {
425
+ case "other" :
426
+ default : {
427
+ resultOrFail . error . type satisfies "other" ;
428
+
429
+ logger . error ( "Failed to update build settings" , {
430
+ error : resultOrFail . error ,
431
+ } ) ;
432
+ return redirectBackWithErrorMessage ( request , "Failed to update build settings" ) ;
433
+ }
434
+ }
435
+ }
436
+
437
+ return redirectBackWithSuccessMessage ( request , "Build settings updated successfully" ) ;
438
+ }
379
439
default : {
380
440
submission . value satisfies never ;
381
441
return redirectBackWithErrorMessage ( request , "Failed to process request" ) ;
@@ -389,6 +449,7 @@ export default function Page() {
389
449
connectedGithubRepository,
390
450
githubAppEnabled,
391
451
openGitHubRepoConnectionModal,
452
+ buildSettings,
392
453
} = useTypedLoaderData < typeof loader > ( ) ;
393
454
const project = useProject ( ) ;
394
455
const organization = useOrganization ( ) ;
@@ -511,22 +572,31 @@ export default function Page() {
511
572
</ div >
512
573
513
574
{ githubAppEnabled && (
514
- < div >
515
- < Header2 spacing > Git settings</ Header2 >
516
- < div className = "w-full rounded-sm border border-grid-dimmed p-4" >
517
- { connectedGithubRepository ? (
518
- < ConnectedGitHubRepoForm connectedGitHubRepo = { connectedGithubRepository } />
519
- ) : (
520
- < GitHubConnectionPrompt
521
- gitHubAppInstallations = { githubAppInstallations ?? [ ] }
522
- organizationSlug = { organization . slug }
523
- projectSlug = { project . slug }
524
- environmentSlug = { environment . slug }
525
- openGitHubRepoConnectionModal = { openGitHubRepoConnectionModal }
526
- />
527
- ) }
575
+ < React . Fragment >
576
+ < div >
577
+ < Header2 spacing > Git settings</ Header2 >
578
+ < div className = "w-full rounded-sm border border-grid-dimmed p-4" >
579
+ { connectedGithubRepository ? (
580
+ < ConnectedGitHubRepoForm connectedGitHubRepo = { connectedGithubRepository } />
581
+ ) : (
582
+ < GitHubConnectionPrompt
583
+ gitHubAppInstallations = { githubAppInstallations ?? [ ] }
584
+ organizationSlug = { organization . slug }
585
+ projectSlug = { project . slug }
586
+ environmentSlug = { environment . slug }
587
+ openGitHubRepoConnectionModal = { openGitHubRepoConnectionModal }
588
+ />
589
+ ) }
590
+ </ div >
528
591
</ div >
529
- </ div >
592
+
593
+ < div >
594
+ < Header2 spacing > Build settings</ Header2 >
595
+ < div className = "w-full rounded-sm border border-grid-dimmed p-4" >
596
+ < BuildSettingsForm buildSettings = { buildSettings ?? { } } />
597
+ </ div >
598
+ </ div >
599
+ </ React . Fragment >
530
600
) }
531
601
532
602
< div >
@@ -1033,3 +1103,115 @@ function ConnectedGitHubRepoForm({
1033
1103
</ >
1034
1104
) ;
1035
1105
}
1106
+
1107
+ function BuildSettingsForm ( { buildSettings } : { buildSettings : BuildSettings } ) {
1108
+ const lastSubmission = useActionData ( ) as any ;
1109
+ const navigation = useNavigation ( ) ;
1110
+
1111
+ const [ hasBuildSettingsChanges , setHasBuildSettingsChanges ] = useState ( false ) ;
1112
+ const [ buildSettingsValues , setBuildSettingsValues ] = useState ( {
1113
+ installDirectory : buildSettings ?. installDirectory || "" ,
1114
+ installCommand : buildSettings ?. installCommand || "" ,
1115
+ triggerConfigFilePath : buildSettings ?. triggerConfigFilePath || "" ,
1116
+ } ) ;
1117
+
1118
+ useEffect ( ( ) => {
1119
+ const hasChanges =
1120
+ buildSettingsValues . installDirectory !== ( buildSettings ?. installDirectory || "" ) ||
1121
+ buildSettingsValues . installCommand !== ( buildSettings ?. installCommand || "" ) ||
1122
+ buildSettingsValues . triggerConfigFilePath !== ( buildSettings ?. triggerConfigFilePath || "" ) ;
1123
+ setHasBuildSettingsChanges ( hasChanges ) ;
1124
+ } , [ buildSettingsValues , buildSettings ] ) ;
1125
+
1126
+ const [ buildSettingsForm , fields ] = useForm ( {
1127
+ id : "update-build-settings" ,
1128
+ lastSubmission : lastSubmission ,
1129
+ shouldRevalidate : "onSubmit" ,
1130
+ onValidate ( { formData } ) {
1131
+ return parse ( formData , {
1132
+ schema : UpdateBuildSettingsFormSchema ,
1133
+ } ) ;
1134
+ } ,
1135
+ } ) ;
1136
+
1137
+ const isBuildSettingsLoading =
1138
+ navigation . formData ?. get ( "action" ) === "update-build-settings" &&
1139
+ ( navigation . state === "submitting" || navigation . state === "loading" ) ;
1140
+
1141
+ return (
1142
+ < Form method = "post" { ...buildSettingsForm . props } >
1143
+ < Fieldset >
1144
+ < InputGroup fullWidth >
1145
+ < Label htmlFor = { fields . triggerConfigFilePath . id } > Trigger config file</ Label >
1146
+ < Input
1147
+ { ...conform . input ( fields . triggerConfigFilePath , { type : "text" } ) }
1148
+ defaultValue = { buildSettings ?. triggerConfigFilePath || "" }
1149
+ placeholder = "trigger.config.ts"
1150
+ onChange = { ( e ) => {
1151
+ setBuildSettingsValues ( ( prev ) => ( {
1152
+ ...prev ,
1153
+ triggerConfigFilePath : e . target . value ,
1154
+ } ) ) ;
1155
+ } }
1156
+ />
1157
+ < Hint >
1158
+ Path to your Trigger configuration file, relative to the root directory of your repo.
1159
+ </ Hint >
1160
+ < FormError id = { fields . triggerConfigFilePath . errorId } >
1161
+ { fields . triggerConfigFilePath . error }
1162
+ </ FormError >
1163
+ </ InputGroup >
1164
+
1165
+ < InputGroup fullWidth >
1166
+ < Label htmlFor = { fields . installCommand . id } > Install command</ Label >
1167
+ < Input
1168
+ { ...conform . input ( fields . installCommand , { type : "text" } ) }
1169
+ defaultValue = { buildSettings ?. installCommand || "" }
1170
+ placeholder = "e.g., `npm install`, or `bun install`"
1171
+ onChange = { ( e ) => {
1172
+ setBuildSettingsValues ( ( prev ) => ( {
1173
+ ...prev ,
1174
+ installCommand : e . target . value ,
1175
+ } ) ) ;
1176
+ } }
1177
+ />
1178
+ < Hint > Command to install your project dependencies. Auto-detected by default.</ Hint >
1179
+ < FormError id = { fields . installCommand . errorId } > { fields . installCommand . error } </ FormError >
1180
+ </ InputGroup >
1181
+ < InputGroup fullWidth >
1182
+ < Label htmlFor = { fields . installDirectory . id } > Install directory</ Label >
1183
+ < Input
1184
+ { ...conform . input ( fields . installDirectory , { type : "text" } ) }
1185
+ defaultValue = { buildSettings ?. installDirectory || "" }
1186
+ placeholder = ""
1187
+ onChange = { ( e ) => {
1188
+ setBuildSettingsValues ( ( prev ) => ( {
1189
+ ...prev ,
1190
+ installDirectory : e . target . value ,
1191
+ } ) ) ;
1192
+ } }
1193
+ />
1194
+ < Hint > The directory where the install command is run in. Auto-detected by default.</ Hint >
1195
+ < FormError id = { fields . installDirectory . errorId } >
1196
+ { fields . installDirectory . error }
1197
+ </ FormError >
1198
+ </ InputGroup >
1199
+ < FormError > { buildSettingsForm . error } </ FormError >
1200
+ < FormButtons
1201
+ confirmButton = {
1202
+ < Button
1203
+ type = "submit"
1204
+ name = "action"
1205
+ value = "update-build-settings"
1206
+ variant = "secondary/small"
1207
+ disabled = { isBuildSettingsLoading || ! hasBuildSettingsChanges }
1208
+ LeadingIcon = { isBuildSettingsLoading ? SpinnerWhite : undefined }
1209
+ >
1210
+ Save
1211
+ </ Button >
1212
+ }
1213
+ />
1214
+ </ Fieldset >
1215
+ </ Form >
1216
+ ) ;
1217
+ }
0 commit comments