Canvas: Form to choose source repository

Change-Id: I48011d6374e036ead934815ed8e88dc0d1bb914e
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 3eed93e..22353f8 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -54,7 +54,7 @@
     ingress?: Ingress[];
     expose?: PortDomain[];
     volume?: string[];
-    preBuildCommands?: string[];
+    preBuildCommands?: { bin: string }[];
 };
 
 export type Volume = {
@@ -124,7 +124,7 @@
                         auth: { enabled: false },
                     })),
                     expose: findExpose(n),
-                    preBuildCommands: [n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))],
+                    preBuildCommands: n.data.preBuildCommands ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd })) : [],
                 };
             }),
             volume: nodes.filter((n) => n.type === "volume").map((n): Volume => ({
@@ -214,7 +214,7 @@
 export function CreateValidators(): Validator {
     return SortingValidator(
         CombineValidators(
-            EmptyValidator, 
+            EmptyValidator,
             GitRepositoryValidator,
             ServiceValidator,
             GatewayHTTPSValidator,
@@ -228,7 +228,7 @@
     if (nodes.length > 0) {
         return [];
     }
-    return [{   
+    return [{
         id: "no-nodes",
         type: "FATAL",
         message: "Start by importing application source code",
@@ -254,7 +254,7 @@
         message: "Connect to service",
         onHighlight: (store) => store.setHighlightCategory("Services", true),
         onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
-} satisfies Message));
+    } satisfies Message));
     return noAddress.concat(noApp);
 }
 
@@ -269,8 +269,8 @@
         onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
         onClick: (store) => {
             store.updateNode(n.id, { selected: true });
-            store.updateNodeData<"app">(n.id, { 
-                activeField: "name" ,
+            store.updateNodeData<"app">(n.id, {
+                activeField: "name",
             });
         },
     }));
@@ -291,8 +291,8 @@
         onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
         onClick: (store) => {
             store.updateNode(n.id, { selected: true });
-            store.updateNodeData<"app">(n.id, { 
-                activeField: "type" ,
+            store.updateNodeData<"app">(n.id, {
+                activeField: "type",
             });
         },
     }));
@@ -324,7 +324,7 @@
             },
             onLooseHighlight: (store) => {
                 store.updateNode(n.id, { selected: false });
-                store.setHighlightCategory("gateways", false);    
+                store.setHighlightCategory("gateways", false);
             },
         }));
     });
@@ -339,7 +339,7 @@
             nodeId: n.id,
             message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
             onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-            onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),        
+            onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
         };
     })).filter((m) => m !== undefined);
     return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 664ba6e..ee6f6d2 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -74,7 +74,7 @@
   "golang:1.24.0",
   "hugo:latest",
   "php:8.2-apache",
-  "nextjs:deno-2.0.0", 
+  "nextjs:deno-2.0.0",
   "node-23.1.0"
 ] as const;
 export type ServiceType = typeof ServiceTypes[number];
@@ -83,6 +83,11 @@
   type: ServiceType;
   repository: {
     id: string;
+  } | {
+    id: string;
+    branch: string;
+  } | {
+    id: string;
     branch: string;
     rootDir: string;
   };
@@ -173,11 +178,11 @@
         return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
       } else if (handle === "repository") {
         if (!n.data || !n.data.repository || !n.data.repository.id) {
-            return true;
+          return true;
         }
         return false;
       }
-      return false;   
+      return false;
     case "github":
       if (n.data !== undefined && n.data.address) {
         return true;
@@ -202,7 +207,7 @@
         return false;
       }
       if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
-          return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
+        return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
       }
       return true;
     case undefined: throw new Error("MUST NOT REACH!");
@@ -244,7 +249,7 @@
 export function nodeEnvVarNames(n: AppNode): string[] {
   switch (n.type) {
     case "app": return [
-      `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`, 
+      `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
       ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
     ];
     case "github": return [];
@@ -371,16 +376,18 @@
 const v: Validator = CreateValidators();
 
 export const useStateStore = create<AppState>((set, get): AppState => {
-  set({ env: {
-    "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
-    "networks": [{
-      "name": "Public",
-      "domain": "v1.dodo.cloud",
-    }, {
-      "name": "Private",
-      "domain": "p.v1.dodo.cloud",
-    }],
-  }});
+  set({
+    env: {
+      "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
+      "networks": [{
+        "name": "Public",
+        "domain": "v1.dodo.cloud",
+      }, {
+        "name": "Private",
+        "domain": "p.v1.dodo.cloud",
+      }],
+    }
+  });
   console.log(get().env);
   const setN = (nodes: AppNode[]) => {
     set({
@@ -390,18 +397,18 @@
   };
   function updateNodeData<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>): void {
     setN(get().nodes.map((n) => {
-        if (n.id !== id) {
-          return n;
-        }
-        const nd = {
-          ...n,
-          data: {
-            ...n.data,
-            ...d,
-          },
-        };
-        return nd;
-      })
+      if (n.id !== id) {
+        return n;
+      }
+      const nd = {
+        ...n,
+        data: {
+          ...n.data,
+          ...d,
+        },
+      };
+      return nd;
+    })
     );
   };
   function updateNode<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>): void {
@@ -429,7 +436,7 @@
         updateNodeData<"gateway-https">(sn.id, {
           network: tn.data.domain,
         });
-      }else if (sn.type === "gateway-tcp") {
+      } else if (sn.type === "gateway-tcp") {
         updateNodeData<"gateway-tcp">(sn.id, {
           network: tn.data.domain,
         });
@@ -628,12 +635,12 @@
     onConnect,
     refreshEnv: async () => {
       return get().env;
-        const resp = await fetch("/env");
-        if (!resp.ok) {
-          throw new Error("failed to fetch env config");
-        }
-        set({ env: envSchema.parse(await resp.json()) });
-        return get().env;
+      const resp = await fetch("/env");
+      if (!resp.ok) {
+        throw new Error("failed to fetch env config");
+      }
+      set({ env: envSchema.parse(await resp.json()) });
+      return get().env;
     },
     setProject: (projectId) => set({ projectId }),
     setProjects: (projects) => set({ projects }),