fix import for ConstellationState

This commit is contained in:
Jan-Henrik Bruhn 2026-01-16 10:04:40 +01:00
parent 00c7adc41d
commit 23c65ffbb1
4 changed files with 111 additions and 76 deletions

View file

@ -1,7 +1,8 @@
import { useState, useEffect, KeyboardEvent } from 'react'; import { useState, useEffect, KeyboardEvent } from "react";
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from "@mui/icons-material/Save";
import TangibleForm from './TangibleForm'; import TangibleForm from "./TangibleForm";
import type { TangibleConfig, TangibleMode, LabelConfig, ConstellationState } from '../../types'; import type { TangibleConfig, TangibleMode, LabelConfig } from "../../types";
import type { ConstellationState } from "../../types/timeline";
interface Props { interface Props {
tangible: TangibleConfig; tangible: TangibleConfig;
@ -16,28 +17,34 @@ interface Props {
hardwareId?: string; hardwareId?: string;
filterLabels?: string[]; filterLabels?: string[];
stateId?: string; stateId?: string;
} },
) => void; ) => void;
onCancel: () => void; onCancel: () => void;
} }
const EditTangibleInline = ({ tangible, labels, states, onSave, onCancel }: Props) => { const EditTangibleInline = ({
const [name, setName] = useState(''); tangible,
const [mode, setMode] = useState<TangibleMode>('filter'); labels,
const [description, setDescription] = useState(''); states,
const [hardwareId, setHardwareId] = useState(''); onSave,
onCancel,
}: Props) => {
const [name, setName] = useState("");
const [mode, setMode] = useState<TangibleMode>("filter");
const [description, setDescription] = useState("");
const [hardwareId, setHardwareId] = useState("");
const [filterLabels, setFilterLabels] = useState<string[]>([]); const [filterLabels, setFilterLabels] = useState<string[]>([]);
const [stateId, setStateId] = useState(''); const [stateId, setStateId] = useState("");
// Sync state with tangible prop // Sync state with tangible prop
useEffect(() => { useEffect(() => {
if (tangible) { if (tangible) {
setName(tangible.name); setName(tangible.name);
setMode(tangible.mode); setMode(tangible.mode);
setDescription(tangible.description || ''); setDescription(tangible.description || "");
setHardwareId(tangible.hardwareId || ''); setHardwareId(tangible.hardwareId || "");
setFilterLabels(tangible.filterLabels || []); setFilterLabels(tangible.filterLabels || []);
setStateId(tangible.stateId || ''); setStateId(tangible.stateId || "");
} }
}, [tangible]); }, [tangible]);
@ -45,12 +52,12 @@ const EditTangibleInline = ({ tangible, labels, states, onSave, onCancel }: Prop
if (!name.trim()) return; if (!name.trim()) return;
// Validate mode-specific fields // Validate mode-specific fields
if (mode === 'filter' && filterLabels.length === 0) { if (mode === "filter" && filterLabels.length === 0) {
alert('Filter mode requires at least one label'); alert("Filter mode requires at least one label");
return; return;
} }
if ((mode === 'state' || mode === 'stateDial') && !stateId) { if ((mode === "state" || mode === "stateDial") && !stateId) {
alert('State mode requires a state selection'); alert("State mode requires a state selection");
return; return;
} }
@ -59,16 +66,16 @@ const EditTangibleInline = ({ tangible, labels, states, onSave, onCancel }: Prop
mode, mode,
description: description.trim() || undefined, description: description.trim() || undefined,
hardwareId: hardwareId.trim() || undefined, hardwareId: hardwareId.trim() || undefined,
filterLabels: mode === 'filter' ? filterLabels : undefined, filterLabels: mode === "filter" ? filterLabels : undefined,
stateId: (mode === 'state' || mode === 'stateDial') ? stateId : undefined, stateId: mode === "state" || mode === "stateDial" ? stateId : undefined,
}); });
}; };
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
handleSave(); handleSave();
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
onCancel(); onCancel();
} }
@ -117,9 +124,13 @@ const EditTangibleInline = ({ tangible, labels, states, onSave, onCancel }: Prop
{/* Keyboard Shortcut Hint */} {/* Keyboard Shortcut Hint */}
<div className="text-xs text-gray-500 text-center"> <div className="text-xs text-gray-500 text-center">
<kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs"> <kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">
{navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+Enter {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter
</kbd>{' '} </kbd>{" "}
to save, <kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Esc</kbd> to cancel to save,{" "}
<kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">
Esc
</kbd>{" "}
to cancel
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import { useState, useRef, KeyboardEvent } from 'react'; import { useState, useRef, KeyboardEvent } from "react";
import TangibleForm from './TangibleForm'; import TangibleForm from "./TangibleForm";
import type { TangibleMode, LabelConfig, ConstellationState } from '../../types'; import type { TangibleMode, LabelConfig } from "../../types";
import type { ConstellationState } from "../../types/timeline";
interface Props { interface Props {
labels: LabelConfig[]; labels: LabelConfig[];
@ -16,12 +17,12 @@ interface Props {
} }
const QuickAddTangibleForm = ({ labels, states, onAdd }: Props) => { const QuickAddTangibleForm = ({ labels, states, onAdd }: Props) => {
const [name, setName] = useState(''); const [name, setName] = useState("");
const [hardwareId, setHardwareId] = useState(''); const [hardwareId, setHardwareId] = useState("");
const [mode, setMode] = useState<TangibleMode>('filter'); const [mode, setMode] = useState<TangibleMode>("filter");
const [description, setDescription] = useState(''); const [description, setDescription] = useState("");
const [filterLabels, setFilterLabels] = useState<string[]>([]); const [filterLabels, setFilterLabels] = useState<string[]>([]);
const [stateId, setStateId] = useState(''); const [stateId, setStateId] = useState("");
const nameInputRef = useRef<HTMLInputElement>(null); const nameInputRef = useRef<HTMLInputElement>(null);
@ -32,12 +33,12 @@ const QuickAddTangibleForm = ({ labels, states, onAdd }: Props) => {
} }
// Validate mode-specific fields // Validate mode-specific fields
if (mode === 'filter' && filterLabels.length === 0) { if (mode === "filter" && filterLabels.length === 0) {
alert('Filter mode requires at least one label'); alert("Filter mode requires at least one label");
return; return;
} }
if ((mode === 'state' || mode === 'stateDial') && !stateId) { if ((mode === "state" || mode === "stateDial") && !stateId) {
alert('State mode requires a state selection'); alert("State mode requires a state selection");
return; return;
} }
@ -46,33 +47,33 @@ const QuickAddTangibleForm = ({ labels, states, onAdd }: Props) => {
mode, mode,
description, description,
hardwareId: hardwareId.trim() || undefined, hardwareId: hardwareId.trim() || undefined,
filterLabels: mode === 'filter' ? filterLabels : undefined, filterLabels: mode === "filter" ? filterLabels : undefined,
stateId: (mode === 'state' || mode === 'stateDial') ? stateId : undefined, stateId: mode === "state" || mode === "stateDial" ? stateId : undefined,
}); });
// Reset form // Reset form
setName(''); setName("");
setHardwareId(''); setHardwareId("");
setMode('filter'); setMode("filter");
setDescription(''); setDescription("");
setFilterLabels([]); setFilterLabels([]);
setStateId(''); setStateId("");
nameInputRef.current?.focus(); nameInputRef.current?.focus();
}; };
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
} else if (e.key === 'Escape') { } else if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
setName(''); setName("");
setHardwareId(''); setHardwareId("");
setMode('filter'); setMode("filter");
setDescription(''); setDescription("");
setFilterLabels([]); setFilterLabels([]);
setStateId(''); setStateId("");
nameInputRef.current?.blur(); nameInputRef.current?.blur();
} }
}; };

View file

@ -1,4 +1,5 @@
import type { TangibleMode, LabelConfig, ConstellationState } from '../../types'; import type { TangibleMode, LabelConfig } from "../../types";
import type { ConstellationState } from "../../types/timeline";
interface Props { interface Props {
name: string; name: string;
@ -74,49 +75,60 @@ const TangibleForm = ({
type="radio" type="radio"
name="mode" name="mode"
value="filter" value="filter"
checked={mode === 'filter'} checked={mode === "filter"}
onChange={(e) => onModeChange(e.target.value as TangibleMode)} onChange={(e) => onModeChange(e.target.value as TangibleMode)}
className="mr-2" className="mr-2"
/> />
<span className="text-sm text-gray-700">Filter mode (activate label filters)</span> <span className="text-sm text-gray-700">
Filter mode (activate label filters)
</span>
</label> </label>
<label className="flex items-center cursor-pointer"> <label className="flex items-center cursor-pointer">
<input <input
type="radio" type="radio"
name="mode" name="mode"
value="state" value="state"
checked={mode === 'state'} checked={mode === "state"}
onChange={(e) => onModeChange(e.target.value as TangibleMode)} onChange={(e) => onModeChange(e.target.value as TangibleMode)}
className="mr-2" className="mr-2"
/> />
<span className="text-sm text-gray-700">State mode (switch to timeline state)</span> <span className="text-sm text-gray-700">
State mode (switch to timeline state)
</span>
</label> </label>
<label className="flex items-center cursor-pointer"> <label className="flex items-center cursor-pointer">
<input <input
type="radio" type="radio"
name="mode" name="mode"
value="stateDial" value="stateDial"
checked={mode === 'stateDial'} checked={mode === "stateDial"}
onChange={(e) => onModeChange(e.target.value as TangibleMode)} onChange={(e) => onModeChange(e.target.value as TangibleMode)}
className="mr-2" className="mr-2"
/> />
<span className="text-sm text-gray-700">State dial mode (clock-like, deferred)</span> <span className="text-sm text-gray-700">
State dial mode (clock-like, deferred)
</span>
</label> </label>
</div> </div>
</div> </div>
{/* Mode-specific fields */} {/* Mode-specific fields */}
{mode === 'filter' && ( {mode === "filter" && (
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">
Filter Labels * (select one or more) Filter Labels * (select one or more)
</label> </label>
<div className="border border-gray-300 rounded-md p-2 max-h-40 overflow-y-auto"> <div className="border border-gray-300 rounded-md p-2 max-h-40 overflow-y-auto">
{labels.length === 0 ? ( {labels.length === 0 ? (
<p className="text-xs text-gray-500 italic">No labels available</p> <p className="text-xs text-gray-500 italic">
No labels available
</p>
) : ( ) : (
labels.map((label) => ( labels.map((label) => (
<label key={label.id} className="flex items-center py-1 cursor-pointer"> <label
key={label.id}
className="flex items-center py-1 cursor-pointer"
>
<input <input
type="checkbox" type="checkbox"
checked={filterLabels.includes(label.id)} checked={filterLabels.includes(label.id)}
@ -124,7 +136,9 @@ const TangibleForm = ({
if (e.target.checked) { if (e.target.checked) {
onFilterLabelsChange([...filterLabels, label.id]); onFilterLabelsChange([...filterLabels, label.id]);
} else { } else {
onFilterLabelsChange(filterLabels.filter((id) => id !== label.id)); onFilterLabelsChange(
filterLabels.filter((id) => id !== label.id),
);
} }
}} }}
className="mr-2" className="mr-2"
@ -141,7 +155,7 @@ const TangibleForm = ({
</div> </div>
)} )}
{(mode === 'state' || mode === 'stateDial') && ( {(mode === "state" || mode === "stateDial") && (
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1"> <label className="block text-xs font-medium text-gray-700 mb-1">
Timeline State * Timeline State *

View file

@ -1,5 +1,6 @@
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from "@mui/icons-material/Delete";
import type { TangibleConfig, LabelConfig, ConstellationState } from '../../types'; import type { TangibleConfig, LabelConfig } from "../../types";
import type { ConstellationState } from "../../types/timeline";
interface Props { interface Props {
tangibles: TangibleConfig[]; tangibles: TangibleConfig[];
@ -9,7 +10,12 @@ interface Props {
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
const TangibleManagementList = ({ tangibles, states, onEdit, onDelete }: Props) => { const TangibleManagementList = ({
tangibles,
states,
onEdit,
onDelete,
}: Props) => {
if (tangibles.length === 0) { if (tangibles.length === 0) {
return ( return (
<div className="text-center py-12 text-gray-500"> <div className="text-center py-12 text-gray-500">
@ -21,17 +27,17 @@ const TangibleManagementList = ({ tangibles, states, onEdit, onDelete }: Props)
const getModeDisplay = (tangible: TangibleConfig) => { const getModeDisplay = (tangible: TangibleConfig) => {
switch (tangible.mode) { switch (tangible.mode) {
case 'filter': { case "filter": {
const labelCount = tangible.filterLabels?.length || 0; const labelCount = tangible.filterLabels?.length || 0;
return `Filter (${labelCount} label${labelCount !== 1 ? 's' : ''})`; return `Filter (${labelCount} label${labelCount !== 1 ? "s" : ""})`;
} }
case 'state': { case "state": {
const state = states.find(s => s.id === tangible.stateId); const state = states.find((s) => s.id === tangible.stateId);
return `State: ${state?.label || 'Unknown'}`; return `State: ${state?.label || "Unknown"}`;
} }
case 'stateDial': { case "stateDial": {
const dialState = states.find(s => s.id === tangible.stateId); const dialState = states.find((s) => s.id === tangible.stateId);
return `State Dial: ${dialState?.label || 'Unknown'}`; return `State Dial: ${dialState?.label || "Unknown"}`;
} }
default: default:
return tangible.mode; return tangible.mode;
@ -48,7 +54,7 @@ const TangibleManagementList = ({ tangibles, states, onEdit, onDelete }: Props)
role="button" role="button"
tabIndex={0} tabIndex={0}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === "Enter" || e.key === " ") {
e.preventDefault(); e.preventDefault();
onEdit(tangible); onEdit(tangible);
} }
@ -62,7 +68,10 @@ const TangibleManagementList = ({ tangibles, states, onEdit, onDelete }: Props)
</h4> </h4>
{tangible.hardwareId && ( {tangible.hardwareId && (
<p className="text-xs text-gray-600 mt-1"> <p className="text-xs text-gray-600 mt-1">
Hardware: <code className="bg-gray-100 px-1 rounded">{tangible.hardwareId}</code> Hardware:{" "}
<code className="bg-gray-100 px-1 rounded">
{tangible.hardwareId}
</code>
</p> </p>
)} )}
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">