修复UI编辑器状态高亮循环定位
高亮状态绑定到具体节点与状态字段 循环定位时重建状态提示并滚入Inspector可视区 补充多节点连续定位的回归测试
This commit is contained in:
+23
-5
@@ -1,14 +1,17 @@
|
||||
import { Boxes, LockKeyhole, Trash2, Unlock } from 'lucide-react';
|
||||
import type {
|
||||
InputHTMLAttributes,
|
||||
import {
|
||||
type InputHTMLAttributes,
|
||||
ReactNode,
|
||||
SelectHTMLAttributes,
|
||||
TextareaHTMLAttributes,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { FontSamplePreview } from '../../../../features/ui-editor/components/FontSamplePreview';
|
||||
import { SpriteImagePreview } from '../../../../features/ui-editor/components/SpriteImagePreview';
|
||||
import type { StageStatusField } from '../../../../features/ui-editor/stageStatusOverview';
|
||||
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
|
||||
import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDesignImageId';
|
||||
import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole';
|
||||
import { uiEditorPrivateFontFamily } from '../../../../features/ui-editor/useUiEditorFontFaces';
|
||||
@@ -307,6 +310,7 @@ function NodeInspector({
|
||||
onKeepChildrenUnchangedChange: (value: boolean) => void;
|
||||
onMetadataChange: UiEditorInspectorProjection['setNodeMetadata'];
|
||||
highlightedStatusField: {
|
||||
nodeId: NodeId;
|
||||
field: StageStatusField;
|
||||
requestId: number;
|
||||
} | null;
|
||||
@@ -412,11 +416,13 @@ function NodeInspector({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<NodeStageSelect
|
||||
key={`layout-status-${node.id}-${highlightedStatusField?.requestId ?? 'idle'}`}
|
||||
label="布局状态"
|
||||
value={node.metadata.layout_status}
|
||||
disabled={isReadOnly}
|
||||
highlight={
|
||||
highlightedStatusField?.field === 'layout_status'
|
||||
highlightedStatusField?.nodeId === node.id &&
|
||||
highlightedStatusField.field === 'layout_status'
|
||||
? highlightedStatusField
|
||||
: null
|
||||
}
|
||||
@@ -425,11 +431,13 @@ function NodeInspector({
|
||||
}}
|
||||
/>
|
||||
<NodeStageSelect
|
||||
key={`components-status-${node.id}-${highlightedStatusField?.requestId ?? 'idle'}`}
|
||||
label="组件状态"
|
||||
value={node.metadata.components_status}
|
||||
disabled={isReadOnly}
|
||||
highlight={
|
||||
highlightedStatusField?.field === 'components_status'
|
||||
highlightedStatusField?.nodeId === node.id &&
|
||||
highlightedStatusField.field === 'components_status'
|
||||
? highlightedStatusField
|
||||
: null
|
||||
}
|
||||
@@ -805,6 +813,7 @@ function NodeStageSelect({
|
||||
onChange: (value: SelectedNode['metadata']['layout_status']) => void;
|
||||
}) {
|
||||
const readOnly = useInspectorReadOnly();
|
||||
const statusRowRef = useRef<HTMLLabelElement>(null);
|
||||
const kind =
|
||||
typeof value === 'string'
|
||||
? value
|
||||
@@ -818,9 +827,18 @@ function NodeStageSelect({
|
||||
: value.Blocked
|
||||
: null;
|
||||
const attentionTone = kind === 'Blocked' ? 'blocked' : 'review';
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlight) return;
|
||||
statusRowRef.current?.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [highlight]);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={highlight?.requestId}
|
||||
ref={statusRowRef}
|
||||
data-status-attention={highlight ? attentionTone : undefined}
|
||||
data-status-attention-request={highlight?.requestId}
|
||||
className={`block rounded-lg p-2 text-[10px] text-(--platform-text-soft) transition ${
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function UiEditorPage({
|
||||
uiTrees={session.input.uiTrees}
|
||||
onFocusStatusNode={(treeId, nodeId) => {
|
||||
session.input.focusNode(treeId, nodeId);
|
||||
session.input.highlightStatusField('layout_status');
|
||||
session.input.highlightStatusField(nodeId, 'layout_status');
|
||||
}}
|
||||
/>
|
||||
) : session.input.activeStep === 'visual-binding' ? (
|
||||
@@ -192,7 +192,7 @@ export default function UiEditorPage({
|
||||
sprites={session.input.sprites}
|
||||
onFocusStatusNode={(treeId, nodeId) => {
|
||||
session.input.focusNode(treeId, nodeId);
|
||||
session.input.highlightStatusField('components_status');
|
||||
session.input.highlightStatusField(nodeId, 'components_status');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -64,6 +64,7 @@ import { useUiEditorNodeFocus } from './useUiEditorNodeFocus';
|
||||
const ASSET_BATCH_SIZE = 5;
|
||||
|
||||
type StatusFieldHighlight = {
|
||||
nodeId: NodeId;
|
||||
field: StageStatusField;
|
||||
requestId: number;
|
||||
};
|
||||
@@ -684,8 +685,9 @@ export function useUiEditorSession(
|
||||
setHighlightedStatusField(null);
|
||||
}
|
||||
|
||||
function highlightStatusField(field: StageStatusField) {
|
||||
function highlightStatusField(nodeId: NodeId, field: StageStatusField) {
|
||||
setHighlightedStatusField((current) => ({
|
||||
nodeId,
|
||||
field,
|
||||
requestId: (current?.requestId ?? 0) + 1,
|
||||
}));
|
||||
|
||||
@@ -424,15 +424,19 @@ describe('UiEditorPage', () => {
|
||||
act(() => {
|
||||
otherNodeId = result.current.input.insertNode(rootId, 'page')?.value;
|
||||
result.current.input.selectNode(rootId);
|
||||
result.current.input.highlightStatusField('layout_status');
|
||||
result.current.input.highlightStatusField(rootId, 'layout_status');
|
||||
});
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
nodeId: rootId,
|
||||
field: 'layout_status',
|
||||
requestId: 1,
|
||||
});
|
||||
|
||||
act(() => result.current.input.highlightStatusField('layout_status'));
|
||||
act(() =>
|
||||
result.current.input.highlightStatusField(rootId, 'layout_status'),
|
||||
);
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
nodeId: rootId,
|
||||
field: 'layout_status',
|
||||
requestId: 2,
|
||||
});
|
||||
@@ -441,7 +445,10 @@ describe('UiEditorPage', () => {
|
||||
expect(result.current.inspector.highlightedStatusField).toBeNull();
|
||||
|
||||
act(() => {
|
||||
result.current.input.highlightStatusField('components_status');
|
||||
result.current.input.highlightStatusField(
|
||||
otherNodeId!,
|
||||
'components_status',
|
||||
);
|
||||
result.current.inspector.setNodeMetadata({
|
||||
components_status: 'NoProblem',
|
||||
});
|
||||
@@ -449,6 +456,32 @@ describe('UiEditorPage', () => {
|
||||
expect(result.current.inspector.highlightedStatusField).toBeNull();
|
||||
});
|
||||
|
||||
it('retargets the status highlight to every overview navigation result', async () => {
|
||||
const { result } = await renderLoadedSession(stateWithPages(['page']));
|
||||
const rootId = 'page-root';
|
||||
let otherNodeId: string | undefined;
|
||||
act(() => {
|
||||
otherNodeId = result.current.input.insertNode(rootId, 'page')?.value;
|
||||
result.current.input.focusNode('page', rootId);
|
||||
result.current.input.highlightStatusField(rootId, 'layout_status');
|
||||
});
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
nodeId: rootId,
|
||||
field: 'layout_status',
|
||||
requestId: 1,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.input.focusNode('page', otherNodeId!);
|
||||
result.current.input.highlightStatusField(otherNodeId!, 'layout_status');
|
||||
});
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
nodeId: otherNodeId,
|
||||
field: 'layout_status',
|
||||
requestId: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('shares exclusive child visibility between tree actions and final preview state', async () => {
|
||||
const { result } = await renderLoadedSession(stateWithPages(['page']));
|
||||
const rootId = 'page-root';
|
||||
|
||||
Reference in New Issue
Block a user