Skip to content

Commit f5cf27a

Browse files
committed
refactor(frontend): fix react-refresh lint warnings, add rollout plan doc
Move non-component exports out of ServerDefinitionModal.tsx and workspace-binding-form.component.tsx into dedicated helper modules so react-refresh/only-export-components stops flagging them. Also adds the planning doc for the declare-root-before-grant resolver gate. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 38f1df1 commit f5cf27a

7 files changed

Lines changed: 378 additions & 208 deletions

File tree

apps/desktop/src/components/ServerDefinitionModal.tsx

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { X, Copy, Check, Loader2, Save } from 'lucide-react';
4-
import type { ServerViewModel, ServerDefinition } from '../types/registry';
4+
import type { ServerViewModel } from '../types/registry';
55
import { MonacoJsonEditor } from './monaco-json-editor.component';
66
import { updateClonedServerDefinition, updateServerInConfig } from '@/lib/api/spaces';
7+
import {
8+
buildEditableEntry,
9+
canEditServerDefinition,
10+
extractDefinition,
11+
} from './server-definition-modal.helpers';
712

813
const EDITOR_MOUNT_TIMEOUT_MS = 10_000;
914

@@ -15,67 +20,6 @@ interface ServerDefinitionModalProps {
1520
onSaved?: () => void;
1621
}
1722

18-
const RUNTIME_SERVER_FIELDS = [
19-
'is_installed',
20-
'enabled',
21-
'oauth_connected',
22-
'input_values',
23-
'connection_status',
24-
'missing_required_inputs',
25-
'last_error',
26-
'created_at',
27-
'installation_source',
28-
'env_overrides',
29-
'args_append',
30-
'extra_headers',
31-
'default_params',
32-
] as const;
33-
34-
/** Extract only ServerDefinition fields, stripping runtime state */
35-
function extractDefinition(server: ServerViewModel): ServerDefinition {
36-
const copy = { ...server };
37-
for (const key of RUNTIME_SERVER_FIELDS) {
38-
delete (copy as Record<string, unknown>)[key];
39-
}
40-
return copy as ServerDefinition;
41-
}
42-
43-
/**
44-
* Build the standard MCP config format (the shape that lives under a
45-
* `mcpServers` key in a space JSON file) from a server's current view model.
46-
* This is the editable subset — no id/source/badges or other derived fields.
47-
*/
48-
function buildEditableEntry(server: ServerViewModel): Record<string, unknown> {
49-
const entry: Record<string, unknown> = {};
50-
51-
if (server.transport.type === 'stdio') {
52-
entry.command = server.transport.command;
53-
entry.args = server.transport.args;
54-
entry.env = server.transport.env;
55-
} else {
56-
entry.url = server.transport.url;
57-
entry.headers = server.transport.headers;
58-
}
59-
60-
entry.name = server.name;
61-
if (server.description) entry.description = server.description;
62-
if (server.icon) entry.icon = server.icon;
63-
if (server.alias) entry.alias = server.alias;
64-
if (server.auth && server.auth.type !== 'none') entry.auth = server.auth;
65-
if (server.transport.metadata.inputs.length > 0) {
66-
entry.metadata = { inputs: server.transport.metadata.inputs };
67-
}
68-
69-
return entry;
70-
}
71-
72-
/** Whether the Definition editor allows in-place edits for this server. */
73-
export function canEditServerDefinition(server: ServerViewModel): boolean {
74-
return (
75-
server.source.type === 'UserSpace' || server.installation_source?.type === 'manual_entry'
76-
);
77-
}
78-
7923
export function ServerDefinitionModal({
8024
server,
8125
spaceId,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { ServerViewModel, ServerDefinition } from '../types/registry';
2+
3+
const RUNTIME_SERVER_FIELDS = [
4+
'is_installed',
5+
'enabled',
6+
'oauth_connected',
7+
'input_values',
8+
'connection_status',
9+
'missing_required_inputs',
10+
'last_error',
11+
'created_at',
12+
'installation_source',
13+
'env_overrides',
14+
'args_append',
15+
'extra_headers',
16+
'default_params',
17+
] as const;
18+
19+
/** Extract only ServerDefinition fields, stripping runtime state */
20+
export function extractDefinition(server: ServerViewModel): ServerDefinition {
21+
const copy = { ...server };
22+
for (const key of RUNTIME_SERVER_FIELDS) {
23+
delete (copy as Record<string, unknown>)[key];
24+
}
25+
return copy as ServerDefinition;
26+
}
27+
28+
/**
29+
* Build the standard MCP config format (the shape that lives under a
30+
* `mcpServers` key in a space JSON file) from a server's current view model.
31+
* This is the editable subset — no id/source/badges or other derived fields.
32+
*/
33+
export function buildEditableEntry(server: ServerViewModel): Record<string, unknown> {
34+
const entry: Record<string, unknown> = {};
35+
36+
if (server.transport.type === 'stdio') {
37+
entry.command = server.transport.command;
38+
entry.args = server.transport.args;
39+
entry.env = server.transport.env;
40+
} else {
41+
entry.url = server.transport.url;
42+
entry.headers = server.transport.headers;
43+
}
44+
45+
entry.name = server.name;
46+
if (server.description) entry.description = server.description;
47+
if (server.icon) entry.icon = server.icon;
48+
if (server.alias) entry.alias = server.alias;
49+
if (server.auth && server.auth.type !== 'none') entry.auth = server.auth;
50+
if (server.transport.metadata.inputs.length > 0) {
51+
entry.metadata = { inputs: server.transport.metadata.inputs };
52+
}
53+
54+
return entry;
55+
}
56+
57+
/** Whether the Definition editor allows in-place edits for this server. */
58+
export function canEditServerDefinition(server: ServerViewModel): boolean {
59+
return (
60+
server.source.type === 'UserSpace' || server.installation_source?.type === 'manual_entry'
61+
);
62+
}

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ import type { FeaturesUpdatedEvent } from '@/lib/api/serverManager';
7272
import { ServerLogViewer } from '@/components/ServerLogViewer';
7373
import { ConfigEditorModal } from '@/components/ConfigEditorModal';
7474
import { CustomServerPanel } from './CustomServerPanel';
75-
import { ServerDefinitionModal, canEditServerDefinition } from '@/components/ServerDefinitionModal';
75+
import { ServerDefinitionModal } from '@/components/ServerDefinitionModal';
76+
import { canEditServerDefinition } from '@/components/server-definition-modal.helpers';
7677
import { SourceBadge } from '@/components/SourceBadge';
7778
import type { ClonedInstalledServer } from '@/lib/api/serverClone';
7879
import { listCloneDependents } from '@/lib/api/serverClone';

apps/desktop/src/features/workspaces/workspace-binding-form.component.tsx

Lines changed: 5 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,18 @@ import {
1717
Loader2,
1818
} from 'lucide-react';
1919
import { Button } from '@mcpmux/ui';
20-
import {
21-
type WorkspaceBinding,
22-
type WorkspaceBindingInput,
23-
} from '@/lib/api/workspaceBindings';
2420
import { uploadWorkspaceIcon } from '@/lib/api/workspaceAppearances';
2521
import { ServerIcon } from '@/components/ServerIcon';
2622
import { EmojiPickerButton } from '@/components/emoji-picker-button.component';
2723
import { isStarterFeatureSet, type FeatureSet } from '@/lib/api/featureSets';
2824
import { createMachine, getHostname, type Machine } from '@/lib/api/machines';
2925
import type { Space } from '@/lib/api/spaces';
3026
import { 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

Comments
 (0)