Skip to content

Commit fadda96

Browse files
committed
refactor(workspaces): Phase 2 — Panel header identity + machine badge
Autonomous decisions: - PanelIdentityHeader lives in workspace-binding-panel.component.tsx - Icon upload removed from BindingForm until Phase 3 ScopeFields; create-from-live icon autosave stays in BindingForm - Machine badge onClick is a no-op stub for Phase 3 scope expand wiring - Path always shown under label input when workspaceRoot is known Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 138dba7 commit fadda96

3 files changed

Lines changed: 155 additions & 220 deletions

File tree

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

Lines changed: 1 addition & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
AlertCircle,
1313
Check,
1414
ChevronDown,
15-
FolderOpen,
1615
FolderSearch,
1716
Loader2,
1817
} from 'lucide-react';
@@ -24,14 +23,11 @@ import {
2423
import {
2524
deleteWorkspaceAppearance,
2625
upsertWorkspaceAppearance,
27-
uploadWorkspaceIcon,
2826
} from '@/lib/api/workspaceAppearances';
2927
import { isStarterFeatureSet, type FeatureSet } from '@/lib/api/featureSets';
3028
import { createMachine, getHostname, type Machine } from '@/lib/api/machines';
3129
import type { Space } from '@/lib/api/spaces';
32-
import { ServerIcon } from '@/components/ServerIcon';
3330
import { MachineProfileEditor } from '@/components/machine-profile-editor';
34-
import { EmojiPickerButton } from '@/components/emoji-picker-button.component';
3531

3632
export type SaveStatus =
3733
| { kind: 'idle' }
@@ -161,12 +157,9 @@ export function BindingForm({
161157
featureSets,
162158
machines,
163159
localMachineId,
164-
initial,
160+
initial: _initial,
165161
initialUnmappedIcon,
166-
label,
167-
setLabel,
168162
icon,
169-
setIcon,
170163
spaceId,
171164
setSpaceId,
172165
fsIds,
@@ -181,8 +174,6 @@ export function BindingForm({
181174
onFormSubmit,
182175
onCancel,
183176
onError,
184-
onPersistIcon,
185-
onIconChange,
186177
t,
187178
}: {
188179
mode: 'create' | 'edit' | 'create-from-live';
@@ -192,10 +183,7 @@ export function BindingForm({
192183
localMachineId: string | null;
193184
initial?: WorkspaceBinding | null;
194185
initialUnmappedIcon?: string | null;
195-
label: string;
196-
setLabel: (value: string) => void;
197186
icon: string;
198-
setIcon: (value: string) => void;
199187
spaceId: string;
200188
setSpaceId: (value: string) => void;
201189
fsIds: string[];
@@ -210,8 +198,6 @@ export function BindingForm({
210198
onFormSubmit: (machineTargets: (string | null)[]) => Promise<void>;
211199
onCancel: () => void;
212200
onError: (message: string) => void;
213-
onPersistIcon?: (nextIcon: string) => Promise<void>;
214-
onIconChange?: (icon: string | null) => void;
215201
t: TFunction<['workspaces', 'common']>;
216202
}) {
217203
const rootRef = useRef<HTMLInputElement | null>(null);
@@ -225,7 +211,6 @@ export function BindingForm({
225211
const [newMachineIcon, setNewMachineIcon] = useState('');
226212
const [newMachineHostname, setNewMachineHostname] = useState('');
227213
const [creatingMachine, setCreatingMachine] = useState(false);
228-
const [iconFilePath, setIconFilePath] = useState('');
229214
const isEdit = mode === 'edit';
230215

231216
const rootEditable = mode !== 'create-from-live';
@@ -326,31 +311,6 @@ export function BindingForm({
326311
mode === 'create-from-live' ? normalizeIcon(initialUnmappedIcon) : null
327312
);
328313

329-
/** Persist icon immediately after upload so the card updates without waiting for autosave. */
330-
const persistIconNow = async (nextIcon: string) => {
331-
const workspaceRoot = root.trim();
332-
if (!workspaceRoot) return;
333-
334-
if (mode === 'edit' && initial && canSubmit && onPersistIcon) {
335-
await onPersistIcon(nextIcon);
336-
return;
337-
}
338-
339-
if (mode === 'create-from-live') {
340-
const normalizedIcon = normalizeIcon(nextIcon);
341-
if (normalizedIcon) {
342-
await upsertWorkspaceAppearance({
343-
workspace_root: workspaceRoot,
344-
icon: normalizedIcon,
345-
});
346-
lastSavedAppearanceRef.current = normalizedIcon;
347-
} else {
348-
await deleteWorkspaceAppearance(workspaceRoot);
349-
lastSavedAppearanceRef.current = null;
350-
}
351-
}
352-
};
353-
354314
const submitLabel =
355315
mode === 'create-from-live' ? t('form.saveBinding') : t('form.createBinding');
356316

@@ -384,134 +344,6 @@ export function BindingForm({
384344

385345
return (
386346
<div className="space-y-5">
387-
<FormField label={t('form.label')} hint={t('form.labelHint')}>
388-
<input
389-
type="text"
390-
value={label}
391-
onChange={(e) => setLabel(e.target.value)}
392-
placeholder={t('form.labelPlaceholder')}
393-
className="w-full px-3 py-2 rounded-lg text-sm bg-[rgb(var(--background))] border border-[rgb(var(--border))] focus:outline-none focus:ring-2 focus:ring-primary-500"
394-
data-testid="workspace-binding-label-input"
395-
/>
396-
</FormField>
397-
398-
<FormField label={t('form.icon')} hint={t('form.iconHint')}>
399-
<div className="space-y-2.5">
400-
<div className="flex items-start gap-3">
401-
<div className="w-14 h-14 rounded-xl border border-[rgb(var(--border-subtle))] bg-[rgb(var(--background))] flex items-center justify-center flex-shrink-0">
402-
{icon.trim() ? (
403-
<ServerIcon icon={icon.trim()} className="h-9 w-9 object-contain" fallback="📁" />
404-
) : (
405-
<FolderOpen className="h-6 w-6 text-[rgb(var(--muted))]" />
406-
)}
407-
</div>
408-
<div className="flex-1 min-w-0 space-y-2">
409-
<div className="flex items-center gap-2">
410-
<EmojiPickerButton
411-
value={icon.trim().length <= 2 ? icon.trim() : ''}
412-
onChange={(emoji) => {
413-
setIcon(emoji);
414-
onIconChange?.(emoji);
415-
}}
416-
/>
417-
<input
418-
type="text"
419-
value={icon}
420-
onChange={(e) => {
421-
const next = e.target.value;
422-
setIcon(next);
423-
onIconChange?.(normalizeIcon(next));
424-
}}
425-
placeholder={t('form.iconPlaceholder')}
426-
className="min-w-0 flex-1 h-10 px-3 rounded-lg text-sm bg-[rgb(var(--background))] border border-[rgb(var(--border))] focus:outline-none focus:ring-2 focus:ring-primary-500"
427-
data-testid="workspace-binding-icon-input"
428-
/>
429-
</div>
430-
<div className="flex items-center gap-2 flex-wrap">
431-
{isTauri() ? (
432-
<Button
433-
variant="secondary"
434-
size="sm"
435-
onClick={async () => {
436-
try {
437-
const picked = await pickPath({
438-
directory: false,
439-
multiple: false,
440-
title: t('form.pickIconTitle'),
441-
filters: [
442-
{
443-
name: t('form.imagesFilter'),
444-
extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif'],
445-
},
446-
],
447-
});
448-
if (typeof picked !== 'string' || picked.length === 0) return;
449-
const localRef = await uploadWorkspaceIcon(picked);
450-
setIcon(localRef);
451-
onIconChange?.(localRef);
452-
await persistIconNow(localRef);
453-
} catch (e) {
454-
onError(e instanceof Error ? e.message : String(e));
455-
}
456-
}}
457-
data-testid="workspace-binding-icon-upload"
458-
>
459-
{t('form.upload')}
460-
</Button>
461-
) : (
462-
<>
463-
<input
464-
type="text"
465-
value={iconFilePath}
466-
onChange={(e) => setIconFilePath(e.target.value)}
467-
placeholder="Enter absolute path"
468-
className="min-w-0 flex-1 px-3 py-2 rounded-lg text-sm bg-[rgb(var(--background))] border border-[rgb(var(--border))] focus:outline-none focus:ring-2 focus:ring-primary-500"
469-
data-testid="workspace-binding-icon-path-input"
470-
/>
471-
<Button
472-
variant="secondary"
473-
size="sm"
474-
disabled={!iconFilePath.trim()}
475-
onClick={async () => {
476-
const picked = iconFilePath.trim();
477-
if (!picked) return;
478-
try {
479-
const localRef = await uploadWorkspaceIcon(picked);
480-
setIcon(localRef);
481-
onIconChange?.(localRef);
482-
await persistIconNow(localRef);
483-
setIconFilePath('');
484-
} catch (e) {
485-
onError(e instanceof Error ? e.message : String(e));
486-
}
487-
}}
488-
data-testid="workspace-binding-icon-upload"
489-
>
490-
{t('form.upload')}
491-
</Button>
492-
</>
493-
)}
494-
<Button
495-
variant="ghost"
496-
size="sm"
497-
onClick={() => {
498-
setIcon('');
499-
onIconChange?.(null);
500-
void persistIconNow('').catch((e) =>
501-
onError(e instanceof Error ? e.message : String(e))
502-
);
503-
}}
504-
disabled={!icon.trim()}
505-
data-testid="workspace-binding-icon-clear"
506-
>
507-
{t('form.clear')}
508-
</Button>
509-
</div>
510-
</div>
511-
</div>
512-
</div>
513-
</FormField>
514-
515347
<FormField label={t('form.machine')} hint={t('form.machineHint')}>
516348
{isEdit ? (
517349
<div className="space-y-2">

0 commit comments

Comments
 (0)