@@ -62,7 +62,7 @@ import {
6262 EnvironmentParamSchema ,
6363 v3ProjectSettingsPath ,
6464} from "~/utils/pathBuilder" ;
65- import { useEffect , useState } from "react" ;
65+ import React , { useEffect , useState } from "react" ;
6666import { Select , SelectItem } from "~/components/primitives/Select" ;
6767import { Switch } from "~/components/primitives/Switch" ;
6868import { type BranchTrackingConfig } from "~/v3/github" ;
@@ -77,6 +77,7 @@ import { DateTime } from "~/components/primitives/DateTime";
7777import { TextLink } from "~/components/primitives/TextLink" ;
7878import { cn } from "~/utils/cn" ;
7979import { ProjectSettingsPresenter } from "~/services/projectSettingsPresenter.server" ;
80+ import { type BuildSettings } from "~/v3/buildSettings" ;
8081
8182export const meta : MetaFunction = ( ) => {
8283 return [
@@ -120,7 +121,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
120121 }
121122 }
122123
123- const { gitHubApp } = resultOrFail . value ;
124+ const { gitHubApp, buildSettings } = resultOrFail . value ;
124125
125126 const session = await getSession ( request . headers . get ( "Cookie" ) ) ;
126127 const openGitHubRepoConnectionModal = session . get ( "gitHubAppInstalled" ) === true ;
@@ -134,6 +135,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
134135 githubAppInstallations : gitHubApp . installations ,
135136 connectedGithubRepository : gitHubApp . connectedRepository ,
136137 openGitHubRepoConnectionModal,
138+ buildSettings,
137139 } ,
138140 { headers }
139141 ) ;
@@ -155,6 +157,38 @@ const UpdateGitSettingsFormSchema = z.object({
155157 . transform ( ( val ) => val === "on" ) ,
156158} ) ;
157159
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+
158192export function createSchema (
159193 constraints : {
160194 getSlugMatch ?: ( slug : string ) => { isMatch : boolean ; projectSlug : string } ;
@@ -188,6 +222,7 @@ export function createSchema(
188222 } ) ,
189223 ConnectGitHubRepoFormSchema ,
190224 UpdateGitSettingsFormSchema ,
225+ UpdateBuildSettingsFormSchema ,
191226 z . object ( {
192227 action : z . literal ( "disconnect-repo" ) ,
193228 } ) ,
@@ -376,6 +411,31 @@ export const action: ActionFunction = async ({ request, params }) => {
376411 success : true ,
377412 } ) ;
378413 }
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+ }
379439 default : {
380440 submission . value satisfies never ;
381441 return redirectBackWithErrorMessage ( request , "Failed to process request" ) ;
@@ -389,6 +449,7 @@ export default function Page() {
389449 connectedGithubRepository,
390450 githubAppEnabled,
391451 openGitHubRepoConnectionModal,
452+ buildSettings,
392453 } = useTypedLoaderData < typeof loader > ( ) ;
393454 const project = useProject ( ) ;
394455 const organization = useOrganization ( ) ;
@@ -511,22 +572,31 @@ export default function Page() {
511572 </ div >
512573
513574 { 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 >
528591 </ 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 >
530600 ) }
531601
532602 < div >
@@ -1033,3 +1103,115 @@ function ConnectedGitHubRepoForm({
10331103 </ >
10341104 ) ;
10351105}
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