feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { createOrgChart } from '@hm-design/org-chart';
|
||||
import type { OrgChartDataSource, OrgChartHandle } from '@hm-design/org-chart';
|
||||
import type { OrgChartProps } from './types.js';
|
||||
|
||||
interface Instance {
|
||||
readonly handle: OrgChartHandle;
|
||||
readonly generation: number;
|
||||
source: OrgChartDataSource;
|
||||
operation: number;
|
||||
}
|
||||
|
||||
/** A React host for the exact same Core used by Vanilla HTML. */
|
||||
export function OrgChart({
|
||||
dataSource,
|
||||
aggregationPolicy,
|
||||
stylesheetUrls,
|
||||
displayMode,
|
||||
organizationLinks,
|
||||
onEmployeeClick,
|
||||
className,
|
||||
style,
|
||||
onStateChange,
|
||||
onLifecycleError,
|
||||
}: OrgChartProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const instanceRef = useRef<Instance | null>(null);
|
||||
const generationRef = useRef(0);
|
||||
const committedRef = useRef({ dataSource, onStateChange, onLifecycleError, onEmployeeClick });
|
||||
|
||||
// Publish committed values before Core effects run, without letting callback
|
||||
// identity changes or abandoned concurrent renders alter Core ownership.
|
||||
useLayoutEffect(() => {
|
||||
committedRef.current = { dataSource, onStateChange, onLifecycleError, onEmployeeClick };
|
||||
}, [dataSource, onStateChange, onLifecycleError, onEmployeeClick]);
|
||||
|
||||
const engineStylesheetUrl = stylesheetUrls.engine;
|
||||
const coreStylesheetUrl = stylesheetUrls.core;
|
||||
const hasEmployeeCallback = typeof onEmployeeClick === 'function';
|
||||
|
||||
// Ownership changes are synchronous with the commit: an old request cannot
|
||||
// notify between a committed unmount/source change and a passive cleanup.
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const generation = ++generationRef.current;
|
||||
let handle: OrgChartHandle | undefined;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
if (generationRef.current !== generation) return;
|
||||
try { committedRef.current.onLifecycleError?.(error); }
|
||||
catch { /* Consumer errors must not leave rejected lifecycle promises. */ }
|
||||
};
|
||||
|
||||
try {
|
||||
const source = committedRef.current.dataSource;
|
||||
handle = createOrgChart({
|
||||
dataSource: source,
|
||||
aggregationPolicy,
|
||||
stylesheetUrls: { engine: engineStylesheetUrl, core: coreStylesheetUrl },
|
||||
displayMode,
|
||||
organizationLinks,
|
||||
onEmployeeClick: hasEmployeeCallback ? (employee, context) => {
|
||||
if (generationRef.current === generation) committedRef.current.onEmployeeClick?.(employee, context);
|
||||
} : undefined,
|
||||
});
|
||||
const instance: Instance = { handle, source, generation, operation: 1 };
|
||||
instanceRef.current = instance;
|
||||
unsubscribe = handle.subscribe(state => {
|
||||
if (instanceRef.current === instance && generationRef.current === generation) {
|
||||
committedRef.current.onStateChange?.(state);
|
||||
}
|
||||
});
|
||||
// Core mount performs the first load. The source effect below compares
|
||||
// identity against this record and does not load the initial source again.
|
||||
const operation = instance.operation;
|
||||
void handle.mount(container).catch(error => {
|
||||
if (instanceRef.current === instance && instance.operation === operation) reportError(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reportError(error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Invalidate callbacks and promise continuations before terminal disposal.
|
||||
if (generationRef.current === generation) ++generationRef.current;
|
||||
if (instanceRef.current?.generation === generation) instanceRef.current = null;
|
||||
try { unsubscribe?.(); }
|
||||
catch (error) {
|
||||
try { committedRef.current.onLifecycleError?.(error); }
|
||||
catch { /* A consumer callback cannot prevent terminal Core disposal. */ }
|
||||
}
|
||||
finally {
|
||||
try { handle?.destroy(); }
|
||||
catch (error) {
|
||||
try { committedRef.current.onLifecycleError?.(error); }
|
||||
catch { /* Cleanup still completes when the consumer callback throws. */ }
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [aggregationPolicy, engineStylesheetUrl, coreStylesheetUrl, displayMode, organizationLinks, hasEmployeeCallback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const instance = instanceRef.current;
|
||||
if (!instance || instance.source === dataSource) return;
|
||||
instance.source = dataSource;
|
||||
const operation = ++instance.operation;
|
||||
// Core reload synchronously starts a new revision, aborting/ignoring earlier
|
||||
// source requests even when their Promise or engine import is still pending.
|
||||
void instance.handle.reload(dataSource).catch(error => {
|
||||
if (instanceRef.current !== instance || generationRef.current !== instance.generation || instance.operation !== operation) return;
|
||||
try { committedRef.current.onLifecycleError?.(error); }
|
||||
catch { /* Consumer errors must not become unhandled Promise rejections. */ }
|
||||
});
|
||||
}, [dataSource]);
|
||||
|
||||
return <div ref={containerRef} className={className} style={style} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
|
||||
export { OrgChart } from './OrgChart.js';
|
||||
export type { OrgChartProps } from './types.js';
|
||||
export type * from '@hm-design/org-chart';
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { OrgChartOptions, OrgChartState } from '@hm-design/org-chart';
|
||||
|
||||
/** The Core owns data, rendering, organization links and employee information
|
||||
* interactions; React owns its dedicated container. */
|
||||
export interface OrgChartProps extends Omit<OrgChartOptions, 'container' | 'onStateChange'> {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
/** Latest committed callback. Controlled data/style failures arrive as error states. */
|
||||
onStateChange?: (state: OrgChartState) => void;
|
||||
/** Unexpected create/mount/reload/cleanup errors, rather than controlled error states. */
|
||||
onLifecycleError?: (error: unknown) => void;
|
||||
}
|
||||
Reference in new issue
Block a user