@@ -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" ;
@@ -120,7 +120,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
120120 }
121121 }
122122
123- const { gitHubApp } = resultOrFail . value ;
123+ const { gitHubApp, buildSettings } = resultOrFail . value ;
124124
125125 const session = await getSession ( request . headers . get ( "Cookie" ) ) ;
126126 const openGitHubRepoConnectionModal = session . get ( "gitHubAppInstalled" ) === true ;
@@ -134,6 +134,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
134134 githubAppInstallations : gitHubApp . installations ,
135135 connectedGithubRepository : gitHubApp . connectedRepository ,
136136 openGitHubRepoConnectionModal,
137+ buildSettings,
137138 } ,
138139 { headers }
139140 ) ;
@@ -155,6 +156,13 @@ const UpdateGitSettingsFormSchema = z.object({
155156 . transform ( ( val ) => val === "on" ) ,
156157} ) ;
157158
159+ const UpdateBuildSettingsFormSchema = z . object ( {
160+ action : z . literal ( "update-build-settings" ) ,
161+ rootDirectory : z . string ( ) . trim ( ) . optional ( ) ,
162+ installCommand : z . string ( ) . trim ( ) . optional ( ) ,
163+ triggerConfigFile : z . string ( ) . trim ( ) . optional ( ) ,
164+ } ) ;
165+
158166export function createSchema (
159167 constraints : {
160168 getSlugMatch ?: ( slug : string ) => { isMatch : boolean ; projectSlug : string } ;
@@ -188,6 +196,7 @@ export function createSchema(
188196 } ) ,
189197 ConnectGitHubRepoFormSchema ,
190198 UpdateGitSettingsFormSchema ,
199+ UpdateBuildSettingsFormSchema ,
191200 z . object ( {
192201 action : z . literal ( "disconnect-repo" ) ,
193202 } ) ,
@@ -376,6 +385,31 @@ export const action: ActionFunction = async ({ request, params }) => {
376385 success : true ,
377386 } ) ;
378387 }
388+ case "update-build-settings" : {
389+ const { rootDirectory, installCommand, triggerConfigFile } = submission . value ;
390+
391+ const resultOrFail = await projectSettingsService . updateBuildSettings ( projectId , {
392+ rootDirectory : rootDirectory || undefined ,
393+ installCommand : installCommand || undefined ,
394+ triggerConfigFile : triggerConfigFile || undefined ,
395+ } ) ;
396+
397+ if ( resultOrFail . isErr ( ) ) {
398+ switch ( resultOrFail . error . type ) {
399+ case "other" :
400+ default : {
401+ resultOrFail . error . type satisfies "other" ;
402+
403+ logger . error ( "Failed to update build settings" , {
404+ error : resultOrFail . error ,
405+ } ) ;
406+ return redirectBackWithErrorMessage ( request , "Failed to update build settings" ) ;
407+ }
408+ }
409+ }
410+
411+ return redirectBackWithSuccessMessage ( request , "Build settings updated successfully" ) ;
412+ }
379413 default : {
380414 submission . value satisfies never ;
381415 return redirectBackWithErrorMessage ( request , "Failed to process request" ) ;
@@ -389,6 +423,7 @@ export default function Page() {
389423 connectedGithubRepository,
390424 githubAppEnabled,
391425 openGitHubRepoConnectionModal,
426+ buildSettings,
392427 } = useTypedLoaderData < typeof loader > ( ) ;
393428 const project = useProject ( ) ;
394429 const organization = useOrganization ( ) ;
@@ -511,22 +546,31 @@ export default function Page() {
511546 </ div >
512547
513548 { 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- ) }
549+ < React . Fragment >
550+ < div >
551+ < Header2 spacing > Git settings</ Header2 >
552+ < div className = "w-full rounded-sm border border-grid-dimmed p-4" >
553+ { connectedGithubRepository ? (
554+ < ConnectedGitHubRepoForm connectedGitHubRepo = { connectedGithubRepository } />
555+ ) : (
556+ < GitHubConnectionPrompt
557+ gitHubAppInstallations = { githubAppInstallations ?? [ ] }
558+ organizationSlug = { organization . slug }
559+ projectSlug = { project . slug }
560+ environmentSlug = { environment . slug }
561+ openGitHubRepoConnectionModal = { openGitHubRepoConnectionModal }
562+ />
563+ ) }
564+ </ div >
528565 </ div >
529- </ div >
566+
567+ < div >
568+ < Header2 spacing > Build settings</ Header2 >
569+ < div className = "w-full rounded-sm border border-grid-dimmed p-4" >
570+ < BuildSettingsForm buildSettings = { buildSettings ?? { } } />
571+ </ div >
572+ </ div >
573+ </ React . Fragment >
530574 ) }
531575
532576 < div >
@@ -1033,3 +1077,120 @@ function ConnectedGitHubRepoForm({
10331077 </ >
10341078 ) ;
10351079}
1080+
1081+ type BuildSettings = {
1082+ rootDirectory ?: string ;
1083+ installCommand ?: string ;
1084+ triggerConfigFile ?: string ;
1085+ } ;
1086+
1087+ function BuildSettingsForm ( { buildSettings } : { buildSettings : BuildSettings } ) {
1088+ const lastSubmission = useActionData ( ) as any ;
1089+ const navigation = useNavigation ( ) ;
1090+
1091+ const [ hasBuildSettingsChanges , setHasBuildSettingsChanges ] = useState ( false ) ;
1092+ const [ buildSettingsValues , setBuildSettingsValues ] = useState ( {
1093+ rootDirectory : buildSettings ?. rootDirectory || "" ,
1094+ installCommand : buildSettings ?. installCommand || "" ,
1095+ triggerConfigFile : buildSettings ?. triggerConfigFile || "" ,
1096+ } ) ;
1097+
1098+ useEffect ( ( ) => {
1099+ const hasChanges =
1100+ buildSettingsValues . rootDirectory !== ( buildSettings ?. rootDirectory || "" ) ||
1101+ buildSettingsValues . installCommand !== ( buildSettings ?. installCommand || "" ) ||
1102+ buildSettingsValues . triggerConfigFile !== ( buildSettings ?. triggerConfigFile || "" ) ;
1103+ setHasBuildSettingsChanges ( hasChanges ) ;
1104+ } , [ buildSettingsValues , buildSettings ] ) ;
1105+
1106+ const [ buildSettingsForm , fields ] = useForm ( {
1107+ id : "update-build-settings" ,
1108+ lastSubmission : lastSubmission ,
1109+ shouldRevalidate : "onSubmit" ,
1110+ onValidate ( { formData } ) {
1111+ return parse ( formData , {
1112+ schema : UpdateBuildSettingsFormSchema ,
1113+ } ) ;
1114+ } ,
1115+ } ) ;
1116+
1117+ const isBuildSettingsLoading =
1118+ navigation . formData ?. get ( "action" ) === "update-build-settings" &&
1119+ ( navigation . state === "submitting" || navigation . state === "loading" ) ;
1120+
1121+ return (
1122+ < Form method = "post" { ...buildSettingsForm . props } >
1123+ < Fieldset >
1124+ < InputGroup fullWidth >
1125+ < Label htmlFor = { fields . rootDirectory . id } > Root directory</ Label >
1126+ < Input
1127+ { ...conform . input ( fields . rootDirectory , { type : "text" } ) }
1128+ defaultValue = { buildSettings ?. rootDirectory || "" }
1129+ placeholder = "/"
1130+ onChange = { ( e ) => {
1131+ setBuildSettingsValues ( ( prev ) => ( {
1132+ ...prev ,
1133+ rootDirectory : e . target . value ,
1134+ } ) ) ;
1135+ } }
1136+ />
1137+ < Hint > The directory that contains your code.</ Hint >
1138+ < FormError id = { fields . rootDirectory . errorId } > { fields . rootDirectory . error } </ FormError >
1139+ </ InputGroup >
1140+ < InputGroup fullWidth >
1141+ < Label htmlFor = { fields . installCommand . id } > Install command</ Label >
1142+ < Input
1143+ { ...conform . input ( fields . installCommand , { type : "text" } ) }
1144+ defaultValue = { buildSettings ?. installCommand || "" }
1145+ placeholder = "e.g., `npm install`, or `bun install`"
1146+ onChange = { ( e ) => {
1147+ setBuildSettingsValues ( ( prev ) => ( {
1148+ ...prev ,
1149+ installCommand : e . target . value ,
1150+ } ) ) ;
1151+ } }
1152+ />
1153+ < Hint >
1154+ Command to install your project dependencies. This is auto-detected by default.
1155+ </ Hint >
1156+ < FormError id = { fields . installCommand . errorId } > { fields . installCommand . error } </ FormError >
1157+ </ InputGroup >
1158+ < InputGroup fullWidth >
1159+ < Label htmlFor = { fields . triggerConfigFile . id } > Trigger config file</ Label >
1160+ < Input
1161+ { ...conform . input ( fields . triggerConfigFile , { type : "text" } ) }
1162+ defaultValue = { buildSettings ?. triggerConfigFile || "" }
1163+ placeholder = "trigger.config.ts"
1164+ onChange = { ( e ) => {
1165+ setBuildSettingsValues ( ( prev ) => ( {
1166+ ...prev ,
1167+ triggerConfigFile : e . target . value ,
1168+ } ) ) ;
1169+ } }
1170+ />
1171+ < Hint >
1172+ Path to your trigger configuration file, relative to the specified root directory.
1173+ </ Hint >
1174+ < FormError id = { fields . triggerConfigFile . errorId } >
1175+ { fields . triggerConfigFile . error }
1176+ </ FormError >
1177+ </ InputGroup >
1178+ < FormError > { buildSettingsForm . error } </ FormError >
1179+ < FormButtons
1180+ confirmButton = {
1181+ < Button
1182+ type = "submit"
1183+ name = "action"
1184+ value = "update-build-settings"
1185+ variant = "secondary/small"
1186+ disabled = { isBuildSettingsLoading || ! hasBuildSettingsChanges }
1187+ LeadingIcon = { isBuildSettingsLoading ? SpinnerWhite : undefined }
1188+ >
1189+ Save
1190+ </ Button >
1191+ }
1192+ />
1193+ </ Fieldset >
1194+ </ Form >
1195+ ) ;
1196+ }
0 commit comments