mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
Extract useUrlState hook from view_page.tsx
This commit is contained in:
106
src/hooks/use_url_state.ts
Normal file
106
src/hooks/use_url_state.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import {useLocation, useNavigate} from 'react-router';
|
||||
import {IndiInfo} from 'topola';
|
||||
import {PRIVATE_ID_PREFIX} from '../datasource/wikitree';
|
||||
import {Config, configToArgs} from '../sidepanel/config/config';
|
||||
import {analyticsEvent} from '../util/analytics';
|
||||
import {getArguments, getUrlForArgs} from '../util/url_args';
|
||||
|
||||
/**
|
||||
* Custom React hook to manage and synchronize URL routing and query parameter state
|
||||
* for the genealogy chart workspace.
|
||||
*/
|
||||
export function useUrlState() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Parse all URL arguments memoized on location changes.
|
||||
const args = useMemo(() => getArguments(location), [location]);
|
||||
|
||||
/**
|
||||
* Helper to update the browser URL query parameters.
|
||||
*/
|
||||
const updateUrl = useCallback(
|
||||
(
|
||||
newArgs: Record<string, string | (string | null)[] | null | undefined>,
|
||||
options?: {replace?: boolean},
|
||||
) => {
|
||||
navigate(getUrlForArgs(location, newArgs), options);
|
||||
},
|
||||
[location, navigate],
|
||||
);
|
||||
|
||||
/**
|
||||
* Called when the user clicks an individual box in the chart.
|
||||
* Updates the browser URL with the new primary individual selection.
|
||||
*/
|
||||
const onSelection = useCallback(
|
||||
(selection: IndiInfo) => {
|
||||
// Don't allow selecting WikiTree private profiles.
|
||||
if (selection.id.startsWith(PRIVATE_ID_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
analyticsEvent('selection_changed');
|
||||
updateUrl({
|
||||
indi: selection.id,
|
||||
gen: String(selection.generation),
|
||||
detail: null,
|
||||
});
|
||||
},
|
||||
[updateUrl],
|
||||
);
|
||||
|
||||
/**
|
||||
* Called when the user shift+clicks an individual box in the chart.
|
||||
* Shows the individual in the details pane.
|
||||
*/
|
||||
const onDetailSelection = useCallback(
|
||||
(selection: IndiInfo) => {
|
||||
updateUrl({
|
||||
detail: selection.id,
|
||||
});
|
||||
},
|
||||
[updateUrl],
|
||||
);
|
||||
|
||||
/**
|
||||
* Toggles the side panel expansion state.
|
||||
*/
|
||||
const onToggleSidePanel = useCallback(() => {
|
||||
const newShowSidePanel = !args.showSidePanel;
|
||||
updateUrl(
|
||||
{
|
||||
sidePanel: newShowSidePanel ? 'true' : 'false',
|
||||
},
|
||||
{replace: true},
|
||||
);
|
||||
}, [args.showSidePanel, updateUrl]);
|
||||
|
||||
/**
|
||||
* Updates the URL parameters based on configuration updates.
|
||||
*/
|
||||
const onConfigChange = useCallback(
|
||||
(config: Config) => {
|
||||
updateUrl(configToArgs(config), {replace: true});
|
||||
},
|
||||
[updateUrl],
|
||||
);
|
||||
|
||||
return {
|
||||
args,
|
||||
chartType: args.chartType,
|
||||
standalone: args.standalone,
|
||||
showWikiTreeMenus: args.showWikiTreeMenus,
|
||||
freezeAnimation: args.freezeAnimation,
|
||||
showSidePanel: args.showSidePanel,
|
||||
config: args.config,
|
||||
sourceSpec: args.sourceSpec,
|
||||
selection: args.selection,
|
||||
detail: args.detail,
|
||||
updateUrl,
|
||||
onSelection,
|
||||
onDetailSelection,
|
||||
onToggleSidePanel,
|
||||
onConfigChange,
|
||||
};
|
||||
}
|
||||
@@ -39,22 +39,22 @@ import {
|
||||
} from '../datasource/load_data';
|
||||
import {
|
||||
loadWikiTree,
|
||||
PRIVATE_ID_PREFIX,
|
||||
WikiTreeDataSource,
|
||||
WikiTreeSourceSpec,
|
||||
} from '../datasource/wikitree';
|
||||
import {DonatsoChart} from '../donatso-chart';
|
||||
import {useGoogleAuth} from '../hooks/use_google_auth';
|
||||
import {useUrlState} from '../hooks/use_url_state';
|
||||
import {useWebMcpBridge} from '../hooks/use_webmcp_bridge';
|
||||
import {GoogleAuthModal} from '../menu/google_auth_modal';
|
||||
import {TopBar} from '../menu/top_bar';
|
||||
import {Config, configToArgs, Ids, Sex} from '../sidepanel/config/config';
|
||||
import {Config, Ids, Sex} from '../sidepanel/config/config';
|
||||
import {SidePanel} from '../sidepanel/side-panel';
|
||||
import {analyticsEvent} from '../util/analytics';
|
||||
import {TopolaError} from '../util/error';
|
||||
import {getI18nMessage} from '../util/error_i18n';
|
||||
import {idToIndiMap, TopolaData} from '../util/gedcom_util';
|
||||
import {DataSourceSpec, getArguments, getUrlForArgs} from '../util/url_args';
|
||||
import {DataSourceSpec, getArguments} from '../util/url_args';
|
||||
|
||||
export enum AppState {
|
||||
INITIAL,
|
||||
@@ -92,36 +92,36 @@ export function ViewPage() {
|
||||
/** Tracks whether the user has a valid cached Google Drive OAuth access token and provides a state setter. */
|
||||
const {hasGoogleToken, setHasGoogleToken} = useGoogleAuth();
|
||||
|
||||
const {
|
||||
chartType,
|
||||
standalone,
|
||||
showWikiTreeMenus,
|
||||
freezeAnimation,
|
||||
showSidePanel,
|
||||
config,
|
||||
selection: urlSelection,
|
||||
detail: urlDetail,
|
||||
onSelection,
|
||||
onDetailSelection,
|
||||
onToggleSidePanel,
|
||||
onConfigChange,
|
||||
} = useUrlState();
|
||||
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const args = useMemo(() => getArguments(location), [location]);
|
||||
/** Type of displayed chart. */
|
||||
const chartType = args.chartType;
|
||||
/** Whether the app is in standalone mode, i.e. showing 'open file' menus. */
|
||||
const standalone = args.standalone;
|
||||
/**
|
||||
* Whether the app should display WikiTree-specific menus when showing data
|
||||
* from WikiTree.
|
||||
*/
|
||||
const showWikiTreeMenus = args.showWikiTreeMenus;
|
||||
/** Freeze animations after initial chart render. */
|
||||
const freezeAnimation = args.freezeAnimation;
|
||||
/** Whether the side panel is shown. */
|
||||
const showSidePanel = args.showSidePanel;
|
||||
/** Configuration settings for chart display options (e.g. colors, hiding IDs). */
|
||||
const config = args.config;
|
||||
|
||||
useMemo(() => {
|
||||
updateChartWithConfig(config, data);
|
||||
}, [config, data]);
|
||||
|
||||
/** 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]);
|
||||
return data ? getSelection(data.chartData, urlSelection) : undefined;
|
||||
}, [data, urlSelection]);
|
||||
|
||||
/** The individual displayed in the details pane. */
|
||||
const detailIndi = args.detail || updatedSelection?.id;
|
||||
const detailIndi = urlDetail || updatedSelection?.id;
|
||||
|
||||
/** Tracks whether the component is currently mounted to prevent state updates after unmount. */
|
||||
const isMountedRef = useRef(true);
|
||||
@@ -151,15 +151,7 @@ export function ViewPage() {
|
||||
});
|
||||
}
|
||||
|
||||
function onToggleSidePanel() {
|
||||
const newShowSidePanel = !showSidePanel;
|
||||
updateUrl(
|
||||
{
|
||||
sidePanel: newShowSidePanel ? 'true' : 'false',
|
||||
},
|
||||
{replace: true},
|
||||
);
|
||||
}
|
||||
/** Sets error message after data load failure. */
|
||||
|
||||
/** Sets error message after data load failure. */
|
||||
function setErrorMessage(message: string) {
|
||||
@@ -402,39 +394,6 @@ export function ViewPage() {
|
||||
|
||||
useWebMcpBridge(data, detailIndi, onSelection);
|
||||
|
||||
function updateUrl(
|
||||
args: Record<string, string | (string | null)[] | null | undefined>,
|
||||
options?: {replace?: boolean},
|
||||
) {
|
||||
navigate(getUrlForArgs(location, args), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks an individual box in the chart.
|
||||
* Updates the browser URL.
|
||||
*/
|
||||
function onSelection(selection: IndiInfo) {
|
||||
// Don't allow selecting WikiTree private profiles.
|
||||
if (selection.id.startsWith(PRIVATE_ID_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
analyticsEvent('selection_changed');
|
||||
updateUrl({
|
||||
indi: selection.id,
|
||||
gen: String(selection.generation),
|
||||
detail: null,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Called when the user shift+clicks an individual box in the chart.
|
||||
* Shows the individual in the details pane.
|
||||
*/
|
||||
function onDetailSelection(selection: IndiInfo) {
|
||||
updateUrl({
|
||||
detail: selection.id,
|
||||
});
|
||||
}
|
||||
|
||||
function onPrint() {
|
||||
analyticsEvent('print');
|
||||
printChart();
|
||||
@@ -543,9 +502,7 @@ export function ViewPage() {
|
||||
config={config}
|
||||
expanded={showSidePanel}
|
||||
onToggle={onToggleSidePanel}
|
||||
onConfigChange={(config) => {
|
||||
updateUrl(configToArgs(config), {replace: true});
|
||||
}}
|
||||
onConfigChange={onConfigChange}
|
||||
/>
|
||||
<SidebarPusher>{renderChart(selection)}</SidebarPusher>
|
||||
</SidebarPushable>
|
||||
|
||||
Reference in New Issue
Block a user