Sync relation map selections
This commit is contained in:
127
src/App.jsx
127
src/App.jsx
@@ -176,8 +176,25 @@ const initialGates = {
|
||||
const channelName = 'program-flow-state';
|
||||
const contentStorageKey = 'program-flow-content';
|
||||
const programStateStorageKey = 'program-flow-gates';
|
||||
const selectedRelationStorageKey = 'program-flow-selected-relation';
|
||||
const selectedProgramStorageKey = 'program-flow-selected-program';
|
||||
const serverStateEndpoint = '/api/state';
|
||||
const disabledFlowStep = '__disabled__';
|
||||
|
||||
function createSelectionSignal(id) {
|
||||
return JSON.stringify({ id, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
function readSelectionSignal(value) {
|
||||
if (!value) return '';
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed?.id || '';
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const programTypeOptions = {
|
||||
internal: {
|
||||
label: '사내프로그램',
|
||||
@@ -2543,21 +2560,55 @@ export default function App() {
|
||||
const targetName = targetPrograms.map((program) => program.name).join(' / ');
|
||||
return storedTarget?.linkLabel || `${fromProgram.name} 결과물을 ${targetName}로 연계`;
|
||||
};
|
||||
const focusRelation = (relationId, shouldScroll = true) => {
|
||||
setSelectedRelationId(relationId);
|
||||
if (shouldScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
relationBlockRefs.current[relationId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
const broadcastRelationSelection = (relationId) => {
|
||||
try {
|
||||
window.localStorage.setItem(selectedRelationStorageKey, createSelectionSignal(relationId));
|
||||
window.opener?.postMessage?.({ type: 'program-flow-selected-relation', relationId }, window.location.origin);
|
||||
const channel = new BroadcastChannel(channelName);
|
||||
channel.postMessage({ selectedRelationId: relationId });
|
||||
channel.close();
|
||||
} catch {
|
||||
window.localStorage.setItem(selectedRelationStorageKey, createSelectionSignal(relationId));
|
||||
}
|
||||
};
|
||||
const selectRelation = (fromId, toId) => {
|
||||
const relationId = getRelationId(fromId, toId);
|
||||
setSelectedRelationId(relationId);
|
||||
requestAnimationFrame(() => {
|
||||
relationBlockRefs.current[relationId]?.scrollIntoView({
|
||||
focusRelation(relationId, !isRelationMapWindow);
|
||||
broadcastRelationSelection(relationId);
|
||||
};
|
||||
const focusProgram = (programId, shouldScroll = true) => {
|
||||
if (shouldScroll) {
|
||||
programSectionRefs.current[programId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
const scrollProgramIntoView = (programId) => {
|
||||
programSectionRefs.current[programId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
const broadcastProgramSelection = (programId) => {
|
||||
try {
|
||||
window.localStorage.setItem(selectedProgramStorageKey, createSelectionSignal(programId));
|
||||
window.opener?.postMessage?.({ type: 'program-flow-selected-program', programId }, window.location.origin);
|
||||
const channel = new BroadcastChannel(channelName);
|
||||
channel.postMessage({ selectedProgramId: programId });
|
||||
channel.close();
|
||||
} catch {
|
||||
window.localStorage.setItem(selectedProgramStorageKey, createSelectionSignal(programId));
|
||||
}
|
||||
};
|
||||
const selectProgramFromMap = (programId) => {
|
||||
focusProgram(programId, !isRelationMapWindow);
|
||||
broadcastProgramSelection(programId);
|
||||
};
|
||||
const renderRelationBlock = (fromProgram) => {
|
||||
const successorGroups = Object.values(
|
||||
@@ -2661,11 +2712,18 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
const channel = new BroadcastChannel(channelName);
|
||||
channel.onmessage = (event) => {
|
||||
if (!event.data?.states) return;
|
||||
setProgramStates(event.data.states);
|
||||
if (event.data?.states) {
|
||||
setProgramStates(event.data.states);
|
||||
}
|
||||
if (event.data?.selectedRelationId) {
|
||||
focusRelation(event.data.selectedRelationId, !isRelationMapWindow);
|
||||
}
|
||||
if (event.data?.selectedProgramId) {
|
||||
focusProgram(event.data.selectedProgramId, !isRelationMapWindow);
|
||||
}
|
||||
};
|
||||
return () => channel.close();
|
||||
}, []);
|
||||
}, [isRelationMapWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
@@ -2736,11 +2794,50 @@ export default function App() {
|
||||
if (event.key === programStateStorageKey) {
|
||||
setProgramStates(readStoredProgramStates());
|
||||
}
|
||||
if (event.key === selectedRelationStorageKey && event.newValue) {
|
||||
focusRelation(readSelectionSignal(event.newValue), !isRelationMapWindow);
|
||||
}
|
||||
if (event.key === selectedProgramStorageKey && event.newValue) {
|
||||
focusProgram(readSelectionSignal(event.newValue), !isRelationMapWindow);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', syncStoredContent);
|
||||
return () => window.removeEventListener('storage', syncStoredContent);
|
||||
}, []);
|
||||
}, [isRelationMapWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRelationMapWindow) return undefined;
|
||||
const syncProgramMessage = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type === 'program-flow-selected-program' && event.data.programId) {
|
||||
focusProgram(event.data.programId, true);
|
||||
}
|
||||
if (event.data?.type === 'program-flow-selected-relation' && event.data.relationId) {
|
||||
focusRelation(event.data.relationId, true);
|
||||
}
|
||||
};
|
||||
let lastRelationSignal = window.localStorage.getItem(selectedRelationStorageKey) || '';
|
||||
let lastProgramSignal = window.localStorage.getItem(selectedProgramStorageKey) || '';
|
||||
const syncSelectionSignals = () => {
|
||||
const relationSignal = window.localStorage.getItem(selectedRelationStorageKey) || '';
|
||||
const programSignal = window.localStorage.getItem(selectedProgramStorageKey) || '';
|
||||
if (relationSignal && relationSignal !== lastRelationSignal) {
|
||||
lastRelationSignal = relationSignal;
|
||||
focusRelation(readSelectionSignal(relationSignal), true);
|
||||
}
|
||||
if (programSignal && programSignal !== lastProgramSignal) {
|
||||
lastProgramSignal = programSignal;
|
||||
focusProgram(readSelectionSignal(programSignal), true);
|
||||
}
|
||||
};
|
||||
const timer = window.setInterval(syncSelectionSignals, 300);
|
||||
window.addEventListener('message', syncProgramMessage);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener('message', syncProgramMessage);
|
||||
};
|
||||
}, [isRelationMapWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(programStateStorageKey, JSON.stringify(programStates));
|
||||
@@ -3329,7 +3426,7 @@ export default function App() {
|
||||
<main className="min-h-screen bg-white text-slate-900">
|
||||
<RelationTreePanel
|
||||
programs={programs}
|
||||
onProgramClick={openProgramWindow}
|
||||
onProgramClick={selectProgramFromMap}
|
||||
onOpenComparePair={openComparePair}
|
||||
onRelationSelect={selectRelation}
|
||||
selectedRelationId={selectedRelationId}
|
||||
@@ -3379,7 +3476,7 @@ export default function App() {
|
||||
>
|
||||
<RelationTreePanel
|
||||
programs={programs}
|
||||
onProgramClick={scrollProgramIntoView}
|
||||
onProgramClick={selectProgramFromMap}
|
||||
onOpenComparePair={openComparePair}
|
||||
onRelationSelect={selectRelation}
|
||||
selectedRelationId={selectedRelationId}
|
||||
|
||||
Reference in New Issue
Block a user