@@ -17,23 +17,18 @@ import {
1717 Loader2 ,
1818} from 'lucide-react' ;
1919import { Button } from '@mcpmux/ui' ;
20- import {
21- type WorkspaceBinding ,
22- type WorkspaceBindingInput ,
23- } from '@/lib/api/workspaceBindings' ;
2420import { uploadWorkspaceIcon } from '@/lib/api/workspaceAppearances' ;
2521import { ServerIcon } from '@/components/ServerIcon' ;
2622import { EmojiPickerButton } from '@/components/emoji-picker-button.component' ;
2723import { isStarterFeatureSet , type FeatureSet } from '@/lib/api/featureSets' ;
2824import { createMachine , getHostname , type Machine } from '@/lib/api/machines' ;
2925import type { Space } from '@/lib/api/spaces' ;
3026import { MachineProfileEditor } from '@/components/machine-profile-editor' ;
31-
32- export type SaveStatus =
33- | { kind : 'idle' }
34- | { kind : 'saving' }
35- | { kind : 'saved' }
36- | { kind : 'error' ; message : string } ;
27+ import {
28+ isWorkspaceFileIcon ,
29+ type RootValidationState ,
30+ type SaveStatus ,
31+ } from './workspace-binding-form.helpers' ;
3732
3833/**
3934 * Small pill shown in the Routing section header during edit-mode autosave.
@@ -79,137 +74,6 @@ export function SaveStatusPill({
7974 ) ;
8075}
8176
82- /**
83- * Structural equality between two binding inputs. The autosave effect
84- * uses this to skip writes when the user re-toggled their way back to
85- * the last-saved state.
86- */
87- export function normalizeLabel ( label : string | null | undefined ) : string | null {
88- const trimmed = label ?. trim ( ) ?? '' ;
89- return trimmed . length > 0 ? trimmed : null ;
90- }
91-
92- export function normalizeIcon ( icon : string | null | undefined ) : string | null {
93- const trimmed = icon ?. trim ( ) ?? '' ;
94- return trimmed . length > 0 ? trimmed : null ;
95- }
96-
97- /**
98- * Last path segment of a workspace root, normalized for cross-platform matching.
99- */
100- export function folderName ( root : string ) : string {
101- const segments = root . replace ( / \\ / g, '/' ) . replace ( / \/ $ / , '' ) . split ( '/' ) ;
102- return segments [ segments . length - 1 ] ?? root ;
103- }
104-
105- /**
106- * Bindings on other machines (or scopes) that can seed a new create-from-live row.
107- * Same folder name is enough; identical absolute paths count when machine differs.
108- */
109- export function findAdoptableSiblingBindings (
110- allBindings : WorkspaceBinding [ ] ,
111- workspaceRoot : string ,
112- targetMachineId : string | null ,
113- ) : WorkspaceBinding [ ] {
114- const currentFolder = folderName ( workspaceRoot ) . toLowerCase ( ) ;
115- const normalizedRoot = workspaceRoot . toLowerCase ( ) ;
116- return allBindings . filter ( ( binding ) => {
117- if ( folderName ( binding . workspace_root ) . toLowerCase ( ) !== currentFolder ) return false ;
118- const samePath = binding . workspace_root . toLowerCase ( ) === normalizedRoot ;
119- if ( ! samePath ) return true ;
120- return ( binding . machine_id ?? null ) !== targetMachineId ;
121- } ) ;
122- }
123-
124- /**
125- * Space, feature sets, label, and icon to copy from an adopt source binding.
126- */
127- export function adoptBindingSeed (
128- source : WorkspaceBinding ,
129- workspaceRoot : string ,
130- ) : Pick < WorkspaceBinding , 'space_id' | 'feature_set_ids' | 'label' | 'icon' > {
131- const trimmedLabel = source . label ?. trim ( ) ?? '' ;
132- return {
133- space_id : source . space_id ,
134- feature_set_ids : source . feature_set_ids ,
135- label : trimmedLabel . length > 0 ? trimmedLabel : folderName ( workspaceRoot ) ,
136- icon : source . icon ,
137- } ;
138- }
139-
140- /** True when the icon value is an uploaded file ref or URL, not a plain emoji. */
141- function isWorkspaceFileIcon ( icon : string ) : boolean {
142- const trimmed = icon . trim ( ) ;
143- return trimmed . startsWith ( 'local:' ) || trimmed . startsWith ( 'http://' ) || trimmed . startsWith ( 'https://' ) ;
144- }
145-
146- export type RootValidationState =
147- | { state : 'idle' }
148- | { state : 'checking' }
149- | { state : 'ok' ; normalized : string }
150- | { state : 'error' ; reason : string ; duplicate ?: boolean } ;
151-
152- /** True when two bindings would collide on the partial unique indexes. */
153- export function bindingScopeConflicts (
154- existing : WorkspaceBinding ,
155- root : string ,
156- machineId : string | null ,
157- clientId : string | null | undefined ,
158- ) : boolean {
159- if ( existing . workspace_root !== root ) return false ;
160- return (
161- ( existing . machine_id ?? null ) === machineId &&
162- ( existing . client_id ?? null ) === ( clientId ?? null )
163- ) ;
164- }
165-
166- /** Map empty machine picker value to null for API payloads. */
167- export function bindingMachineId ( value : string ) : string | null {
168- return value . trim ( ) ? value : null ;
169- }
170-
171- /** Build a workspace binding input from lifted form field values. */
172- export function buildBindingPayload ( params : {
173- root : string ;
174- label : string ;
175- icon : string ;
176- spaceId : string ;
177- fsIds : string [ ] ;
178- machineId : string ;
179- clientId ?: string ;
180- resolvedMachineId : string | null ;
181- } ) : WorkspaceBindingInput {
182- return {
183- workspace_root : params . root . trim ( ) ,
184- label : params . label . trim ( ) || null ,
185- icon : params . icon . trim ( ) || null ,
186- space_id : params . spaceId ,
187- feature_set_ids : params . fsIds ,
188- machine_id : params . resolvedMachineId ,
189- client_id : params . resolvedMachineId ? null : params . clientId ,
190- } ;
191- }
192-
193- export function sameBindingInput (
194- a : WorkspaceBindingInput ,
195- b : {
196- workspace_root : string ;
197- label ?: string | null ;
198- icon ?: string | null ;
199- space_id : string ;
200- feature_set_ids : string [ ] ;
201- machine_id ?: string | null ;
202- }
203- ) : boolean {
204- if ( a . workspace_root . trim ( ) !== b . workspace_root . trim ( ) ) return false ;
205- if ( normalizeLabel ( a . label ) !== normalizeLabel ( b . label ) ) return false ;
206- if ( normalizeIcon ( a . icon ) !== normalizeIcon ( b . icon ) ) return false ;
207- if ( a . space_id !== b . space_id ) return false ;
208- if ( ( a . machine_id ?? null ) !== ( b . machine_id ?? null ) ) return false ;
209- if ( a . feature_set_ids . length !== b . feature_set_ids . length ) return false ;
210- return a . feature_set_ids . every ( ( id , i ) => id === b . feature_set_ids [ i ] ) ;
211- }
212-
21377/**
21478 * Space picker and feature-set multiselect for workspace binding routing.
21579 */
0 commit comments