mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-08-04 18:11:49 +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';
|
} from '../datasource/load_data';
|
||||||
import {
|
import {
|
||||||
loadWikiTree,
|
loadWikiTree,
|
||||||
PRIVATE_ID_PREFIX,
|
|
||||||
WikiTreeDataSource,
|
WikiTreeDataSource,
|
||||||
WikiTreeSourceSpec,
|
WikiTreeSourceSpec,
|
||||||
} from '../datasource/wikitree';
|
} from '../datasource/wikitree';
|
||||||
import {DonatsoChart} from '../donatso-chart';
|
import {DonatsoChart} from '../donatso-chart';
|
||||||
import {useGoogleAuth} from '../hooks/use_google_auth';
|
import {useGoogleAuth} from '../hooks/use_google_auth';
|
||||||
|
import {useUrlState} from '../hooks/use_url_state';
|
||||||
import {useWebMcpBridge} from '../hooks/use_webmcp_bridge';
|
import {useWebMcpBridge} from '../hooks/use_webmcp_bridge';
|
||||||
import {GoogleAuthModal} from '../menu/google_auth_modal';
|
import {GoogleAuthModal} from '../menu/google_auth_modal';
|
||||||
import {TopBar} from '../menu/top_bar';
|
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 {SidePanel} from '../sidepanel/side-panel';
|
||||||
import {analyticsEvent} from '../util/analytics';
|
import {analyticsEvent} from '../util/analytics';
|
||||||
import {TopolaError} from '../util/error';
|
import {TopolaError} from '../util/error';
|
||||||
import {getI18nMessage} from '../util/error_i18n';
|
import {getI18nMessage} from '../util/error_i18n';
|
||||||
import {idToIndiMap, TopolaData} from '../util/gedcom_util';
|
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 {
|
export enum AppState {
|
||||||
INITIAL,
|
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. */
|
/** Tracks whether the user has a valid cached Google Drive OAuth access token and provides a state setter. */
|
||||||
const {hasGoogleToken, setHasGoogleToken} = useGoogleAuth();
|
const {hasGoogleToken, setHasGoogleToken} = useGoogleAuth();
|
||||||
|
|
||||||
|
const {
|
||||||
|
chartType,
|
||||||
|
standalone,
|
||||||
|
showWikiTreeMenus,
|
||||||
|
freezeAnimation,
|
||||||
|
showSidePanel,
|
||||||
|
config,
|
||||||
|
selection: urlSelection,
|
||||||
|
detail: urlDetail,
|
||||||
|
onSelection,
|
||||||
|
onDetailSelection,
|
||||||
|
onToggleSidePanel,
|
||||||
|
onConfigChange,
|
||||||
|
} = useUrlState();
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
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(() => {
|
useMemo(() => {
|
||||||
updateChartWithConfig(config, data);
|
updateChartWithConfig(config, data);
|
||||||
}, [config, data]);
|
}, [config, data]);
|
||||||
|
|
||||||
/** The currently selected individual. Fallback to default individual from loaded data if not specified. */
|
/** The currently selected individual. Fallback to default individual from loaded data if not specified. */
|
||||||
const updatedSelection = useMemo(() => {
|
const updatedSelection = useMemo(() => {
|
||||||
return data ? getSelection(data.chartData, args.selection) : undefined;
|
return data ? getSelection(data.chartData, urlSelection) : undefined;
|
||||||
}, [data, args.selection]);
|
}, [data, urlSelection]);
|
||||||
|
|
||||||
/** The individual displayed in the details pane. */
|
/** 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. */
|
/** Tracks whether the component is currently mounted to prevent state updates after unmount. */
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
@@ -151,15 +151,7 @@ export function ViewPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleSidePanel() {
|
/** Sets error message after data load failure. */
|
||||||
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) {
|
function setErrorMessage(message: string) {
|
||||||
@@ -402,39 +394,6 @@ export function ViewPage() {
|
|||||||
|
|
||||||
useWebMcpBridge(data, detailIndi, onSelection);
|
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() {
|
function onPrint() {
|
||||||
analyticsEvent('print');
|
analyticsEvent('print');
|
||||||
printChart();
|
printChart();
|
||||||
@@ -543,9 +502,7 @@ export function ViewPage() {
|
|||||||
config={config}
|
config={config}
|
||||||
expanded={showSidePanel}
|
expanded={showSidePanel}
|
||||||
onToggle={onToggleSidePanel}
|
onToggle={onToggleSidePanel}
|
||||||
onConfigChange={(config) => {
|
onConfigChange={onConfigChange}
|
||||||
updateUrl(configToArgs(config), {replace: true});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SidebarPusher>{renderChart(selection)}</SidebarPusher>
|
<SidebarPusher>{renderChart(selection)}</SidebarPusher>
|
||||||
</SidebarPushable>
|
</SidebarPushable>
|
||||||
|
|||||||
Reference in New Issue
Block a user