mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
Shift selection and detailIndi to URL as source of truth
This commit is contained in:
@@ -95,7 +95,7 @@ We will gradually eliminate React state variables in `src/app.tsx` and derive th
|
|||||||
* [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
* [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||||
* [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
* [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||||
* [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
* [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
|
||||||
* [ ] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them directly from URL params (`indi`, `gen`, and `detail`); update display selectors. Remove React states. Ensure that chart selection changes (`onSelection` callback) explicitly clear or update the `detail` query parameter to match the new selection to avoid getting stuck on the old details viewport. Also update the detail-only selection handler (`onDetailSelection`) to update the `detail` query parameter in the URL.
|
* [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them directly from URL params (`indi`, `gen`, and `detail`); update display selectors. Remove React states. Ensure that chart selection changes (`onSelection` callback) explicitly clear or update the `detail` query parameter to match the new selection to avoid getting stuck on the old details viewport. Also update the detail-only selection handler (`onDetailSelection`) to update the `detail` query parameter in the URL.
|
||||||
* [ ] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from `?sidePanel=` parameter. Remove React state. Enhance URL args helpers to allow generating path/query target objects with replaced values, and ensure layout settings (`sidePanel`) and configuration changes use `replace: true` to prevent polluting the browser history stack.
|
* [ ] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from `?sidePanel=` parameter. Remove React state. Enhance URL args helpers to allow generating path/query target objects with replaced values, and ensure layout settings (`sidePanel`) and configuration changes use `replace: true` to prevent polluting the browser history stack.
|
||||||
* [ ] **Step 3.4:** Extract `config` state. Parse display settings on render using `argsToConfig` helper. Remove state. In `ViewPage`, run `updateChartWithConfig(config, data)` synchronously during the render pass (e.g. in the `useMemo` that derives query parameters/config) to update the in-memory chart data before it is rendered by the `<Chart>` child component. Run E2E and visual tests to verify no rendering regressions occurred.
|
* [ ] **Step 3.4:** Extract `config` state. Parse display settings on render using `argsToConfig` helper. Remove state. In `ViewPage`, run `updateChartWithConfig(config, data)` synchronously during the render pass (e.g. in the `useMemo` that derives query parameters/config) to update the in-memory chart data before it is rendered by the `<Chart>` child component. Run E2E and visual tests to verify no rendering regressions occurred.
|
||||||
|
|
||||||
|
|||||||
73
src/app.tsx
73
src/app.tsx
@@ -83,10 +83,7 @@ export function App() {
|
|||||||
const [loadingStatus, setLoadingStatus] = useState('Loading…');
|
const [loadingStatus, setLoadingStatus] = useState('Loading…');
|
||||||
/** Loaded data. */
|
/** Loaded data. */
|
||||||
const [data, setData] = useState<TopolaData>();
|
const [data, setData] = useState<TopolaData>();
|
||||||
/** Selected individual. */
|
|
||||||
const [selection, setSelection] = useState<IndiInfo>();
|
|
||||||
/** Selected individual which should be displayed in the details pane. */
|
|
||||||
const [detailIndi, setDetailIndi] = useState<string>();
|
|
||||||
/** Error to display. */
|
/** Error to display. */
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
/** Whether the side panel is shown. */
|
/** Whether the side panel is shown. */
|
||||||
@@ -125,6 +122,12 @@ export function App() {
|
|||||||
const showWikiTreeMenus = args.showWikiTreeMenus;
|
const showWikiTreeMenus = args.showWikiTreeMenus;
|
||||||
/** Freeze animations after initial chart render. */
|
/** Freeze animations after initial chart render. */
|
||||||
const freezeAnimation = args.freezeAnimation;
|
const freezeAnimation = args.freezeAnimation;
|
||||||
|
/** The currently selected individual. Fallback to default individual from loaded data if not specified. */
|
||||||
|
const updatedSelection = useMemo(() => {
|
||||||
|
return data ? getSelection(data.chartData, args.selection) : undefined;
|
||||||
|
}, [data, args.selection]);
|
||||||
|
/** The individual displayed in the details pane. */
|
||||||
|
const detailIndi = args.detail || updatedSelection?.id;
|
||||||
|
|
||||||
/** Prevents the Google Drive "Open with" state from being processed more than once. */
|
/** Prevents the Google Drive "Open with" state from being processed more than once. */
|
||||||
const stateProcessed = useRef(false);
|
const stateProcessed = useRef(false);
|
||||||
@@ -132,6 +135,8 @@ export function App() {
|
|||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
/** Incremented with each load request to ensure only the latest asynchronous load result is applied. */
|
/** Incremented with each load request to ensure only the latest asynchronous load result is applied. */
|
||||||
const fetchIdRef = useRef(0);
|
const fetchIdRef = useRef(0);
|
||||||
|
/** Tracks the currently loaded selection to check if new data needs to be fetched. */
|
||||||
|
const loadedSelectionRef = useRef<IndiInfo>();
|
||||||
|
|
||||||
/** Manages the mount lifecycle ref to avoid setting state on unmounted components. */
|
/** Manages the mount lifecycle ref to avoid setting state on unmounted components. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -141,20 +146,6 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateDisplay = useCallback(
|
|
||||||
(newSelection: IndiInfo) => {
|
|
||||||
if (
|
|
||||||
!selection ||
|
|
||||||
selection.id !== newSelection.id ||
|
|
||||||
selection.generation !== newSelection.generation
|
|
||||||
) {
|
|
||||||
setSelection(newSelection);
|
|
||||||
setDetailIndi(newSelection.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[selection],
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateChartWithConfig(config: Config, data: TopolaData | undefined) {
|
function updateChartWithConfig(config: Config, data: TopolaData | undefined) {
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -196,7 +187,7 @@ export function App() {
|
|||||||
const newSource = {spec: newSourceSpec, selection: newSelection};
|
const newSource = {spec: newSourceSpec, selection: newSelection};
|
||||||
const oldSouce = {
|
const oldSouce = {
|
||||||
spec: sourceSpec,
|
spec: sourceSpec,
|
||||||
selection: selection,
|
selection: loadedSelectionRef.current,
|
||||||
};
|
};
|
||||||
switch (newSource.spec.source) {
|
switch (newSource.spec.source) {
|
||||||
case DataSourceEnum.UPLOADED:
|
case DataSourceEnum.UPLOADED:
|
||||||
@@ -231,7 +222,7 @@ export function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sourceSpec, selection, data, wikiTreeDataSource],
|
[sourceSpec, data, wikiTreeDataSource],
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadData = useCallback(
|
const loadData = useCallback(
|
||||||
@@ -321,8 +312,7 @@ export function App() {
|
|||||||
await googleDriveService.signOut();
|
await googleDriveService.signOut();
|
||||||
setHasGoogleToken(false);
|
setHasGoogleToken(false);
|
||||||
setData(undefined);
|
setData(undefined);
|
||||||
setSelection(undefined);
|
loadedSelectionRef.current = undefined;
|
||||||
setDetailIndi(undefined);
|
|
||||||
// Purge sessionStorage keys starting with "google-drive:"
|
// Purge sessionStorage keys starting with "google-drive:"
|
||||||
clearGoogleDriveCache();
|
clearGoogleDriveCache();
|
||||||
navigate({pathname: '/'}, {replace: true});
|
navigate({pathname: '/'}, {replace: true});
|
||||||
@@ -353,8 +343,6 @@ export function App() {
|
|||||||
setState(AppState.LOADING);
|
setState(AppState.LOADING);
|
||||||
// Set state from URL parameters.
|
// Set state from URL parameters.
|
||||||
setSourceSpec(args.sourceSpec);
|
setSourceSpec(args.sourceSpec);
|
||||||
setSelection(args.selection);
|
|
||||||
setDetailIndi(args.selection?.id);
|
|
||||||
setConfig(args.config);
|
setConfig(args.config);
|
||||||
const currentFetchId = ++fetchIdRef.current;
|
const currentFetchId = ++fetchIdRef.current;
|
||||||
setLoadingStatus('Loading…');
|
setLoadingStatus('Loading…');
|
||||||
@@ -375,6 +363,10 @@ export function App() {
|
|||||||
`Rendering chart (${data.chartData.indis.length.toLocaleString()} people)…`,
|
`Rendering chart (${data.chartData.indis.length.toLocaleString()} people)…`,
|
||||||
);
|
);
|
||||||
setData(data);
|
setData(data);
|
||||||
|
loadedSelectionRef.current = getSelection(
|
||||||
|
data.chartData,
|
||||||
|
args.selection,
|
||||||
|
);
|
||||||
updateChartWithConfig(args.config, data);
|
updateChartWithConfig(args.config, data);
|
||||||
setShowSidePanel(args.showSidePanel);
|
setShowSidePanel(args.showSidePanel);
|
||||||
setState(AppState.SHOWING_CHART);
|
setState(AppState.SHOWING_CHART);
|
||||||
@@ -398,12 +390,11 @@ export function App() {
|
|||||||
// Update selection if it has changed in the URL.
|
// Update selection if it has changed in the URL.
|
||||||
const loadMoreFromWikitree =
|
const loadMoreFromWikitree =
|
||||||
args.sourceSpec.source === DataSourceEnum.WIKITREE &&
|
args.sourceSpec.source === DataSourceEnum.WIKITREE &&
|
||||||
(!selection || selection.id !== args.selection?.id);
|
(!loadedSelectionRef.current ||
|
||||||
|
loadedSelectionRef.current.id !== args.selection?.id);
|
||||||
setState(
|
setState(
|
||||||
loadMoreFromWikitree ? AppState.LOADING_MORE : AppState.SHOWING_CHART,
|
loadMoreFromWikitree ? AppState.LOADING_MORE : AppState.SHOWING_CHART,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
updateDisplay(getSelection(data!.chartData, args.selection));
|
|
||||||
if (loadMoreFromWikitree) {
|
if (loadMoreFromWikitree) {
|
||||||
const currentFetchId = ++fetchIdRef.current;
|
const currentFetchId = ++fetchIdRef.current;
|
||||||
try {
|
try {
|
||||||
@@ -417,8 +408,7 @@ export function App() {
|
|||||||
}
|
}
|
||||||
const newSelection = getSelection(data.chartData, args.selection);
|
const newSelection = getSelection(data.chartData, args.selection);
|
||||||
setData(data);
|
setData(data);
|
||||||
setSelection(newSelection);
|
loadedSelectionRef.current = newSelection;
|
||||||
setDetailIndi(newSelection.id);
|
|
||||||
setState(AppState.SHOWING_CHART);
|
setState(AppState.SHOWING_CHART);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -442,17 +432,7 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [
|
}, [location, state, data, navigate, intl, isNewData, loadData]);
|
||||||
location,
|
|
||||||
state,
|
|
||||||
selection,
|
|
||||||
data,
|
|
||||||
navigate,
|
|
||||||
intl,
|
|
||||||
isNewData,
|
|
||||||
loadData,
|
|
||||||
updateDisplay,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Clean up object URLs created for uploaded images/files when the dataset
|
// Clean up object URLs created for uploaded images/files when the dataset
|
||||||
// changes or the app unmounts to prevent memory leaks.
|
// changes or the app unmounts to prevent memory leaks.
|
||||||
@@ -483,6 +463,7 @@ export function App() {
|
|||||||
updateUrl({
|
updateUrl({
|
||||||
indi: selection.id,
|
indi: selection.id,
|
||||||
gen: String(selection.generation),
|
gen: String(selection.generation),
|
||||||
|
detail: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -490,7 +471,9 @@ export function App() {
|
|||||||
* Shows the individual in the details pane.
|
* Shows the individual in the details pane.
|
||||||
*/
|
*/
|
||||||
function onDetailSelection(selection: IndiInfo) {
|
function onDetailSelection(selection: IndiInfo) {
|
||||||
setDetailIndi(selection.id);
|
updateUrl({
|
||||||
|
detail: selection.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPrint() {
|
function onPrint() {
|
||||||
@@ -580,10 +563,10 @@ export function App() {
|
|||||||
switch (state) {
|
switch (state) {
|
||||||
case AppState.SHOWING_CHART:
|
case AppState.SHOWING_CHART:
|
||||||
case AppState.LOADING_MORE: {
|
case AppState.LOADING_MORE: {
|
||||||
if (!data) {
|
if (!data || !updatedSelection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const updatedSelection = getSelection(data.chartData, selection);
|
const selection = updatedSelection;
|
||||||
return (
|
return (
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<ErrorPopup
|
<ErrorPopup
|
||||||
@@ -597,7 +580,7 @@ export function App() {
|
|||||||
<SidebarPushable>
|
<SidebarPushable>
|
||||||
<SidePanel
|
<SidePanel
|
||||||
data={data}
|
data={data}
|
||||||
selectedIndiId={detailIndi || updatedSelection.id}
|
selectedIndiId={detailIndi || selection.id}
|
||||||
config={config}
|
config={config}
|
||||||
expanded={showSidePanel}
|
expanded={showSidePanel}
|
||||||
onToggle={onToggleSidePanel}
|
onToggle={onToggleSidePanel}
|
||||||
@@ -607,7 +590,7 @@ export function App() {
|
|||||||
updateUrl(configToArgs(config));
|
updateUrl(configToArgs(config));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SidebarPusher>{renderChart(updatedSelection)}</SidebarPusher>
|
<SidebarPusher>{renderChart(selection)}</SidebarPusher>
|
||||||
</SidebarPushable>
|
</SidebarPushable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user