feat(org-chart): support standalone React integration and update guides
This commit is contained in:
1 parent
5acaf8a295
commit
b637f86631
19 files changed
+1455
-64
No files matched your search
@@ -5,21 +5,13 @@ import {
|
||||
Avatar, AvatarFallback, AvatarImage, Button, Dialog, DialogBody, DialogClose,
|
||||
DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@hm-design/ui';
|
||||
import type { Employee, OrgChartEmployeeClickContext } from '@hm-design/org-chart';
|
||||
|
||||
export interface EmployeeDialogSelection {
|
||||
employee: Employee;
|
||||
context: OrgChartEmployeeClickContext;
|
||||
}
|
||||
import type { OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js';
|
||||
|
||||
const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const;
|
||||
|
||||
/** Lazy React-only adapter. The Core and Vanilla remain independent of HM UI. */
|
||||
export default function HMEmployeeDialog({ selection, onClose }: {
|
||||
selection: EmployeeDialogSelection | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const returnTarget = useRef<EmployeeDialogSelection | null>(null);
|
||||
/** Explicit React-only adapter. The Core and default entry stay UI-independent. */
|
||||
export default function HMEmployeeDialog({ selection, onClose }: OrgChartEmployeePopupProps) {
|
||||
const returnTarget = useRef<OrgChartEmployeeSelection | null>(null);
|
||||
const returnRoot = useRef<ShadowRoot | null>(null);
|
||||
const returnContainer = useRef<HTMLElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { lazy, Suspense, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { createOrgChart } from '@hm-design/org-chart';
|
||||
import type { OrgChartAutoLayoutOptions, OrgChartDataSource, OrgChartHandle, OrgChartInteractionOptions, OrgChartOptions } from '@hm-design/org-chart';
|
||||
import type { OrgChartProps } from './types.js';
|
||||
import type { EmployeeDialogSelection } from './HMEmployeeDialog.js';
|
||||
|
||||
const HMEmployeeDialog = lazy(() => import('./HMEmployeeDialog.js'));
|
||||
import type { OrgChartEmployeeSelection, OrgChartProps } from './types.js';
|
||||
|
||||
function annotateEmployeeCards(shadow: ShadowRoot, popup: NonNullable<OrgChartProps['employeePopup']>, customCallback: boolean) {
|
||||
const actionable = customCallback || popup !== 'none';
|
||||
@@ -33,7 +30,7 @@ interface Instance {
|
||||
dragDropEnabled: boolean;
|
||||
readonly employeeClick: NonNullable<OrgChartOptions['onEmployeeClick']>;
|
||||
employeeCallbackEnabled: boolean;
|
||||
popupMode: 'core' | 'hm-dialog' | 'none';
|
||||
popupMode: NonNullable<OrgChartProps['employeePopup']>;
|
||||
operation: number;
|
||||
}
|
||||
|
||||
@@ -56,6 +53,7 @@ export function OrgChart({
|
||||
onEmployeeClick,
|
||||
onMove,
|
||||
employeePopup = 'core',
|
||||
employeePopupAdapter: EmployeePopupAdapter,
|
||||
className,
|
||||
style,
|
||||
onStateChange,
|
||||
@@ -64,19 +62,28 @@ export function OrgChart({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const instanceRef = useRef<Instance | null>(null);
|
||||
const generationRef = useRef(0);
|
||||
const [employeeSelection, setEmployeeSelection] = useState<EmployeeDialogSelection | null>(null);
|
||||
const committedRef = useRef({ dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup });
|
||||
const [employeeSelection, setEmployeeSelection] = useState<OrgChartEmployeeSelection | null>(null);
|
||||
const requestedAdapter = employeePopup === 'hm-dialog' || employeePopup === 'adapter';
|
||||
const effectivePopup = requestedAdapter && !EmployeePopupAdapter ? 'core' : employeePopup;
|
||||
const committedRef = useRef({ dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup: effectivePopup });
|
||||
|
||||
// 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, onMove, employeePopup };
|
||||
}, [dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup]);
|
||||
committedRef.current = { dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, employeePopup: effectivePopup };
|
||||
}, [dataSource, onStateChange, onLifecycleError, onEmployeeClick, onMove, effectivePopup]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (layoutMode !== 'auto' || !requestedAdapter || EmployeePopupAdapter || onEmployeeClick) return;
|
||||
try {
|
||||
committedRef.current.onLifecycleError?.(new Error('OrgChart popup adapter is missing. Use @hm-design/org-chart-react/hm-dialog or supply employeePopupAdapter. Falling back to the Core popup.'));
|
||||
} catch { /* Consumer error reporting cannot prevent Core rendering. */ }
|
||||
}, [layoutMode, requestedAdapter, EmployeePopupAdapter, Boolean(onEmployeeClick)]);
|
||||
|
||||
const engineStylesheetUrl = stylesheetUrls.engine;
|
||||
const coreStylesheetUrl = stylesheetUrls.core;
|
||||
const hasEmployeeCallback = typeof onEmployeeClick === 'function';
|
||||
const popupMode = layoutMode === 'auto' ? employeePopup : 'core';
|
||||
const popupMode = layoutMode === 'auto' ? effectivePopup : 'core';
|
||||
const adapterOwnsPopup = popupMode !== 'core';
|
||||
const hasAdapterCallback = hasEmployeeCallback || popupMode !== 'core';
|
||||
const fixedHasEmployeeCallback = layoutMode !== 'auto' && hasEmployeeCallback;
|
||||
@@ -103,7 +110,7 @@ export function OrgChart({
|
||||
if (generationRef.current !== generation) return;
|
||||
const callback = committedRef.current.onEmployeeClick;
|
||||
if (callback) callback(employee, context);
|
||||
else if (committedRef.current.employeePopup === 'hm-dialog') setEmployeeSelection({ employee, context });
|
||||
else if (committedRef.current.employeePopup === 'hm-dialog' || committedRef.current.employeePopup === 'adapter') setEmployeeSelection({ employee, context });
|
||||
};
|
||||
handle = createOrgChart({
|
||||
dataSource: source,
|
||||
@@ -197,7 +204,7 @@ export function OrgChart({
|
||||
if (shadow && layoutMode === 'auto') {
|
||||
annotateEmployeeCards(shadow, popupMode, hasEmployeeCallback);
|
||||
}
|
||||
}, [dataSource, selectedCompanyIds, popupMode, hasEmployeeCallback, layoutMode, adapterOwnsPopup]);
|
||||
}, [dataSource, selectedCompanyIds, popupMode, hasEmployeeCallback, layoutMode, adapterOwnsPopup, EmployeePopupAdapter]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const instance = instanceRef.current;
|
||||
@@ -259,8 +266,7 @@ export function OrgChart({
|
||||
|
||||
return <>
|
||||
<div ref={containerRef} className={className} style={style} />
|
||||
{popupMode === 'hm-dialog' && !hasEmployeeCallback ? <Suspense fallback={null}>
|
||||
<HMEmployeeDialog selection={employeeSelection} onClose={() => setEmployeeSelection(null)} />
|
||||
</Suspense> : null}
|
||||
{(popupMode === 'hm-dialog' || popupMode === 'adapter') && EmployeePopupAdapter && !hasEmployeeCallback
|
||||
? <EmployeePopupAdapter selection={employeeSelection} onClose={() => setEmployeeSelection(null)} /> : null}
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { OrgChart as CoreOrgChart } from './OrgChart.js';
|
||||
import HMEmployeeDialog from './HMEmployeeDialog.js';
|
||||
import type { OrgChartProps } from './types.js';
|
||||
|
||||
/** HM integration is explicit at the import and popup setting boundaries. */
|
||||
export function OrgChart(props: OrgChartProps) {
|
||||
return <CoreOrgChart {...props} employeePopupAdapter={props.employeePopupAdapter ?? HMEmployeeDialog} />;
|
||||
}
|
||||
|
||||
export { HMEmployeeDialog };
|
||||
export type { OrgChartProps, OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js';
|
||||
export type * from '@hm-design/org-chart';
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
|
||||
export { OrgChart } from './OrgChart.js';
|
||||
export type { OrgChartProps } from './types.js';
|
||||
export type { OrgChartProps, OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js';
|
||||
export type * from '@hm-design/org-chart';
|
||||
@@ -1,15 +1,29 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { OrgChartOptions, OrgChartState } from '@hm-design/org-chart';
|
||||
import type { ComponentType, CSSProperties } from 'react';
|
||||
import type { Employee, OrgChartEmployeeClickContext, OrgChartOptions, OrgChartState } from '@hm-design/org-chart';
|
||||
|
||||
export interface OrgChartEmployeeSelection {
|
||||
employee: Employee;
|
||||
context: OrgChartEmployeeClickContext;
|
||||
}
|
||||
|
||||
/** Host-owned popup UI. Adapters own presentation and focus restoration. */
|
||||
export interface OrgChartEmployeePopupProps {
|
||||
selection: OrgChartEmployeeSelection | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/** The Core owns data, organization rendering and interactions; React owns its
|
||||
* dedicated container and the optional Portal-based HM employee information UI. */
|
||||
* dedicated container and optional host-owned employee information UI. */
|
||||
export interface OrgChartProps extends Omit<OrgChartOptions, 'container' | 'onStateChange'> {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
/** Auto only. Opt into the public HM Dialog (requires @hm-design/ui + its
|
||||
* Theme), retain the Vanilla popup, or suppress the default information UI.
|
||||
/** Auto only. Explicitly select an HM/custom adapter, retain the Vanilla
|
||||
* popup, or suppress the default information UI.
|
||||
* An explicit onEmployeeClick callback always owns the popup instead. */
|
||||
employeePopup?: 'core' | 'hm-dialog' | 'none';
|
||||
employeePopup?: 'core' | 'hm-dialog' | 'adapter' | 'none';
|
||||
/** Explicit UI integration. Supplying an adapter does not enable it: select
|
||||
* employeePopup="adapter" (or "hm-dialog" for the HM entry) as well. */
|
||||
employeePopupAdapter?: ComponentType<OrgChartEmployeePopupProps>;
|
||||
/** 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. */
|
||||
|
||||
Reference in new issue
Block a user