docs(storybook): make Expanded OrgChart a preview-only page
Deploy Storybook / deploy (push) Successful in 59s
Deploy Storybook / deploy (push) Successful in 59s
Remove the data, layout, interaction and employee-popup setting panels from the Expanded OrgChart Story. The chart now renders a fixed sample (7 companies x 20, TB, all expanded, collapse/zoom/pan on, Drag & Drop off, HM Dialog) right under the page title. Story ID, OrgChart packages and public API are unchanged. Align the Story browser checks (P7, P8, P9, P9.2-P9.5, geometry, popup parity, hierarchy, HM Story) with the preview: setting-only scenarios are removed, chart rendering, collapse, Zoom/Pan/Fit and HM Dialog checks run on the fixed state. Move/Drag & Drop coverage stays in the Vanilla Core checks (verify-p9-3-core, verify-p9-4-core). Verified: tsc, test:guard 94/94, node --check on 11 scripts, changed browser sections PASS. Pre-existing P7 story-menu, P8 Department long-name, P9 Expanded tb/runner failures remain separate issues. verify-p9-5-popup-parity was not run (needs the Vanilla example server).
This commit is contained in:
1 parent
4a54cfbeb2
commit
f4ddd5ac4b
14 files changed
+163
-1134
No files matched your search
@@ -13,48 +13,3 @@
|
||||
gap: 0.75rem;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-settings {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-controls {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-instance {
|
||||
flex: 1 1 12rem;
|
||||
}
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-popup-setting {
|
||||
flex: 0 1 24rem;
|
||||
width: 100%;
|
||||
}
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-companies { margin: 0; }
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-company-trigger {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-company-summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
/* Popover is portaled outside StoryPage. Only this Story's content layout is
|
||||
composed here; the HM panel surface, padding and widget states stay intact. */
|
||||
.hm-orgchart-story-company-popover {
|
||||
max-height: var(--radix-popover-content-available-height);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.hm-orgchart-story-company-options {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1,115 +1,32 @@
|
||||
import { StrictMode, useEffect, useId, useMemo, useState } from 'react';
|
||||
import { StrictMode, useMemo, useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import {
|
||||
Button, Checkbox, FilterBar, FilterBarActions, Label,
|
||||
Popover, PopoverContent, PopoverTrigger, Separator,
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch,
|
||||
} from '@hm-design/ui';
|
||||
import { OrgChart, type OrgChartProps } from '@hm-design/org-chart-react/hm-dialog';
|
||||
import { OrgChart } from '@hm-design/org-chart-react/hm-dialog';
|
||||
import {
|
||||
companyColorPresets, createOrgChartExampleDataset, JsonOrgChartDataSource,
|
||||
type CompanyId, type OrgChartAutoLayoutOptions, type OrgChartDataset, type OrgChartExampleSelection, type OrgChartInteractionOptions,
|
||||
type CompanyId, type OrgChartAutoLayoutOptions, type OrgChartDataset, type OrgChartInteractionOptions,
|
||||
} from '@hm-design/org-chart';
|
||||
import engineUrl from '@hm-design/org-chart/engine.css?url';
|
||||
import coreUrl from '@hm-design/org-chart/style.css?url';
|
||||
import { StoryPage, StoryPageHeader, StorySection } from '@/components/story-page';
|
||||
import { StoryPage, StoryPageHeader } from '@/components/story-page';
|
||||
import large from '../../examples/org-chart/data-expanded-large.json';
|
||||
import './org-chart-stories.css';
|
||||
import '../center/center-stories.css';
|
||||
import './org-chart-expanded-story.css';
|
||||
|
||||
const template = large as OrgChartDataset;
|
||||
const companies = Object.values(companyColorPresets);
|
||||
const allCompanies = companies.map(company => company.companyId as CompanyId);
|
||||
const allCompanies = Object.values(companyColorPresets).map(company => company.companyId as CompanyId);
|
||||
const compositionStylesheets = { engine: engineUrl, core: coreUrl };
|
||||
const topNodes = { ptc: { title: '기술총괄', name: '가상책임자 5' } };
|
||||
type DataSettings = Required<Pick<OrgChartExampleSelection, 'employeeCount' | 'companyIds'>>;
|
||||
const initialSettings: DataSettings = { employeeCount: 20, companyIds: allCompanies };
|
||||
// Preview only: fixed sample (7 companies × 20 = 140) with the former default settings.
|
||||
const layout: OrgChartAutoLayoutOptions = { type: 'tb', hybridStartDepth: 2, initialVisibleDepth: 'all' };
|
||||
const interactions: OrgChartInteractionOptions = { collapse: true, zoom: true, pan: true };
|
||||
|
||||
/** Existing Expanded Story only: public FilterBar layout/actions match data
|
||||
* conditions. The preview selects samples rather than searching employees. */
|
||||
/** Existing Expanded Story only: a chart preview without setting panels. */
|
||||
function ExpandedPreview() {
|
||||
const id = useId();
|
||||
const [draft, setDraft] = useState<DataSettings>(initialSettings);
|
||||
const [applied, setApplied] = useState<DataSettings>(initialSettings);
|
||||
const [dataRevision, setDataRevision] = useState(0);
|
||||
const [layout, setLayout] = useState<OrgChartAutoLayoutOptions>({ type: 'tb', hybridStartDepth: 2, initialVisibleDepth: 'all' });
|
||||
const [popup, setPopup] = useState<OrgChartProps['employeePopup']>('hm-dialog');
|
||||
const [interactions, setInteractions] = useState<OrgChartInteractionOptions>({ collapse: true, zoom: true, pan: true });
|
||||
const [dragDropEnabled, setDragDropEnabled] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const dataset = useMemo(() => createOrgChartExampleDataset(template, {
|
||||
scope: 'per-company',
|
||||
employeeCount: applied.employeeCount,
|
||||
// Company selection is view state. Keep every company in the canonical
|
||||
// session source so hiding/revealing a company retains successful moves.
|
||||
companyIds: allCompanies,
|
||||
}), [applied.employeeCount, dataRevision]);
|
||||
const source = useMemo(() => new JsonOrgChartDataSource(dataset), [dataset]);
|
||||
// Core owns edits. This read model updates counts/options without replacing
|
||||
// dataSource and reloading the unchanged fixture over a successful move.
|
||||
const [session, setSession] = useState(() => ({ baseline: dataset, current: dataset }));
|
||||
const currentDataset = session.baseline === dataset ? session.current : dataset;
|
||||
const organizationCompanies = useMemo(() => {
|
||||
const byId = new Map(currentDataset.organizations.map(organization => [organization.id, organization]));
|
||||
const companyById = new Map<string, CompanyId>();
|
||||
for (const organization of currentDataset.organizations) {
|
||||
let current = organization;
|
||||
while (!current.companyId && current.parentId && byId.has(current.parentId)) current = byId.get(current.parentId)!;
|
||||
if (current.companyId) companyById.set(organization.id, current.companyId as CompanyId);
|
||||
}
|
||||
return companyById;
|
||||
}, [currentDataset]);
|
||||
const groupChoices = useMemo(() => {
|
||||
const parents = new Set(currentDataset.organizations.map(organization => organization.parentId));
|
||||
return currentDataset.organizations.filter(organization => parents.has(organization.id)).map(organization => {
|
||||
const companyId = organizationCompanies.get(organization.id);
|
||||
if (!companyId || !applied.companyIds.includes(companyId)) return null;
|
||||
const company = companies.find(item => item.companyId === companyId);
|
||||
const prefix = company ? `${company.companyName} · ` : '';
|
||||
return { id: organization.id, name: organization.name.startsWith(prefix) ? organization.name : `${prefix}${organization.name}` };
|
||||
}).filter((organization): organization is { id: string; name: string } => organization !== null);
|
||||
}, [currentDataset, organizationCompanies, applied.companyIds]);
|
||||
useEffect(() => {
|
||||
const valid = new Set(groupChoices.map(organization => organization.id));
|
||||
setLayout(current => {
|
||||
const previous = current.hybridOrganizationIds ?? [];
|
||||
const ids = previous.filter(organizationId => valid.has(organizationId));
|
||||
return ids.length === previous.length ? current : { ...current, hybridOrganizationIds: ids };
|
||||
});
|
||||
}, [groupChoices]);
|
||||
const predicted = draft.companyIds.length * draft.employeeCount;
|
||||
const displayedTotal = new Set(currentDataset.employees.filter(employee => {
|
||||
const companyId = organizationCompanies.get(employee.organizationId);
|
||||
return companyId !== undefined && applied.companyIds.includes(companyId);
|
||||
}).map(employee => employee.id)).size;
|
||||
const sessionTotal = new Set(currentDataset.employees.map(employee => employee.id)).size;
|
||||
const companyEmployeeCounts = useMemo(() => {
|
||||
const employeeIds = new Map<CompanyId, Set<string>>(allCompanies.map(companyId => [companyId, new Set<string>()]));
|
||||
for (const employee of currentDataset.employees) {
|
||||
const companyId = organizationCompanies.get(employee.organizationId);
|
||||
if (companyId) employeeIds.get(companyId)?.add(employee.id);
|
||||
}
|
||||
return new Map([...employeeIds].map(([companyId, ids]) => [companyId, ids.size]));
|
||||
}, [currentDataset, organizationCompanies]);
|
||||
const companyCountSummary = applied.companyIds.map(companyId => `${companies.find(company => company.companyId === companyId)!.companyName} ${companyEmployeeCounts.get(companyId) ?? 0}명`).join(' · ');
|
||||
const pending = draft.employeeCount !== applied.employeeCount
|
||||
|| draft.companyIds.join() !== applied.companyIds.join();
|
||||
const selectedCompanyCount = draft.companyIds.length;
|
||||
const companySummary = selectedCompanyCount === allCompanies.length ? `전체 회사 (${allCompanies.length}개)`
|
||||
: selectedCompanyCount === 1 ? companies.find(company => company.companyId === draft.companyIds[0])!.companyName
|
||||
: selectedCompanyCount > 1 ? `${selectedCompanyCount}개 회사 선택` : '회사를 선택하세요';
|
||||
const allCompaniesChecked = selectedCompanyCount === allCompanies.length ? true
|
||||
: selectedCompanyCount > 0 ? 'indeterminate' : false;
|
||||
const changeCompanies = (companyIds: readonly CompanyId[]) => {
|
||||
const selected = allCompanies.filter(companyId => companyIds.includes(companyId));
|
||||
setError(null);
|
||||
// Company filtering is immediate; pending employee-count changes still belong
|
||||
// to the existing Data Apply action and P7 dataset generation contract.
|
||||
setDraft(current => ({ ...current, companyIds: selected }));
|
||||
setApplied(current => ({ ...current, companyIds: selected }));
|
||||
};
|
||||
const source = useMemo(() => new JsonOrgChartDataSource(createOrgChartExampleDataset(template, {
|
||||
scope: 'per-company', employeeCount: 20, companyIds: allCompanies,
|
||||
})), []);
|
||||
|
||||
return <StoryPage className="hm-orgchart-story-page hm-orgchart-story-expanded">
|
||||
<StoryPageHeader
|
||||
@@ -120,122 +37,9 @@ function ExpandedPreview() {
|
||||
<div className="hm-orgchart-story hm-orgchart-story-lead">
|
||||
{error ? <p role="alert" className="hm-orgchart-story-note">{error}</p> : null}
|
||||
<div className="hm-orgchart-story-preview sb-center">
|
||||
<OrgChart dataSource={source} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" autoLayout={layout} interactions={interactions} dragDropEnabled={dragDropEnabled} selectedCompanyIds={applied.companyIds} companyTopNodes={topNodes} employeePopup={popup} className="hm-orgchart-story-chart" onMove={result => { if (result.moved) setSession({ baseline: dataset, current: result.data }); }} onLifecycleError={cause => setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
|
||||
<OrgChart dataSource={source} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" autoLayout={layout} interactions={interactions} dragDropEnabled={false} selectedCompanyIds={allCompanies} companyTopNodes={topNodes} employeePopup="hm-dialog" className="hm-orgchart-story-chart" onLifecycleError={cause => setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
|
||||
</div>
|
||||
</div>
|
||||
<StorySection title="데이터 설정">
|
||||
<fieldset className="hm-orgchart-story-settings">
|
||||
<FilterBar layout="list-extended">
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-companies-label`} htmlFor={`${id}-companies`}>회사 선택</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button id={`${id}-companies`} type="button" variant="cancel" appearance="outline"
|
||||
className="hm-orgchart-story-company-trigger" aria-labelledby={`${id}-companies-label ${id}-companies-summary`}>
|
||||
<span id={`${id}-companies-summary`} className="hm-orgchart-story-company-summary">{companySummary}</span>
|
||||
<ChevronDownIcon aria-hidden="true" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" aria-labelledby={`${id}-companies-label`} data-component="OrgChartCompanySelection" className="hm-orgchart-story-company-popover">
|
||||
<fieldset className="hm-orgchart-story-company-options">
|
||||
<legend className="sr-only">표시할 회사 · 복수 선택 가능</legend>
|
||||
<div className="hm-orgchart-story-company">
|
||||
<Checkbox id={`${id}-companies-all`} checked={allCompaniesChecked} onCheckedChange={checked => changeCompanies(checked === true ? allCompanies : [])} />
|
||||
<Label htmlFor={`${id}-companies-all`}>전체 선택</Label>
|
||||
</div>
|
||||
<Separator />
|
||||
{companies.map(company => <div key={company.companyId} className="hm-orgchart-story-company">
|
||||
<Checkbox id={`${id}-${company.companyId}`} checked={draft.companyIds.includes(company.companyId as CompanyId)} onCheckedChange={checked => {
|
||||
changeCompanies(checked === true ? [...draft.companyIds, company.companyId as CompanyId] : draft.companyIds.filter(value => value !== company.companyId));
|
||||
}} />
|
||||
<Label htmlFor={`${id}-${company.companyId}`}>{company.companyName}</Label>
|
||||
</div>)}
|
||||
</fieldset>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-size-label`} htmlFor={`${id}-size`}>회사당 직원 수</Label>
|
||||
<Select value={String(draft.employeeCount)} onValueChange={value => setDraft(current => ({ ...current, employeeCount: Number(value) as DataSettings['employeeCount'] }))}>
|
||||
<SelectTrigger id={`${id}-size`} aria-labelledby={`${id}-size-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{[20, 100, 300].map(count => <SelectItem key={count} value={String(count)}>{count}명</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<FilterBarActions>
|
||||
<Button variant="primary" appearance="solid" onClick={() => { setError(null); setApplied({ ...draft, companyIds: [...draft.companyIds] }); setDataRevision(current => current + 1); }}>데이터 적용</Button>
|
||||
<Button variant="cancel" appearance="outline" onClick={() => { setError(null); setDraft(initialSettings); setApplied(initialSettings); setDataRevision(current => current + 1); }}>초기화</Button>
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
{/* Not shown: kept only as the data hook that the OrgChart browser checks (P8, P9-2/3/4) read. */}
|
||||
<p hidden data-component="OrgChartExampleCounts">
|
||||
표시 중: {applied.companyIds.length}개 회사 · 현재 소속 기준 직원 합계 {displayedTotal.toLocaleString('ko-KR')}명 / 전체 {sessionTotal.toLocaleString('ko-KR')}명
|
||||
{companyCountSummary ? ` · ${companyCountSummary}` : ''}
|
||||
</p>
|
||||
{/* Live feedback only while the employee count differs from the applied data. */}
|
||||
<p className="hm-orgchart-story-note" role="status" aria-live="polite">
|
||||
{pending ? `적용 예정: ${draft.companyIds.length}개 회사 · 회사당 ${draft.employeeCount}명 · 총 ${predicted.toLocaleString('ko-KR')}명 · 데이터 적용을 눌러 반영하세요.` : ''}
|
||||
</p>
|
||||
</fieldset>
|
||||
</StorySection>
|
||||
<StorySection title="레이아웃 설정">
|
||||
<fieldset className="hm-orgchart-story-settings">
|
||||
<div className="hm-orgchart-story-controls">
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-layout-label`} htmlFor={`${id}-layout`}>조직도 표시 유형</Label>
|
||||
<Select value={layout.type} onValueChange={value => setLayout(current => ({ ...current, type: value as OrgChartAutoLayoutOptions['type'], hybridOrganizationIds: [] }))}>
|
||||
<SelectTrigger id={`${id}-layout`} aria-labelledby={`${id}-layout-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="tb">세로형 TB</SelectItem><SelectItem value="lr">가로형 LR</SelectItem><SelectItem value="hybrid">혼합형 Hybrid</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-hybrid-label`} htmlFor={`${id}-hybrid`}>혼합형 시작 Depth</Label>
|
||||
<Select value={String(layout.hybridStartDepth)} disabled={layout.type !== 'hybrid'} onValueChange={value => setLayout(current => ({ ...current, hybridStartDepth: Number(value) as 2 | 3 | 4 }))}>
|
||||
<SelectTrigger id={`${id}-hybrid`} aria-labelledby={`${id}-hybrid-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{[2, 3, 4].map(depth => <SelectItem key={depth} value={String(depth)}>Depth {depth}부터 세로 배치</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-group-hybrid-label`} htmlFor={`${id}-group-hybrid`}>특정 그룹 세로 배치</Label>
|
||||
<Select value={layout.hybridOrganizationIds?.[0] ?? 'none'} disabled={layout.type !== 'tb' || !applied.companyIds.length} onValueChange={value => setLayout(current => ({ ...current, hybridOrganizationIds: value === 'none' ? [] : [value] }))}>
|
||||
<SelectTrigger id={`${id}-group-hybrid`} aria-labelledby={`${id}-group-hybrid-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="none">사용 안 함</SelectItem>{groupChoices.map(organization => <SelectItem key={organization.id} value={organization.id}>{organization.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-depth-label`} htmlFor={`${id}-depth`}>초기 표시 Depth</Label>
|
||||
<Select value={String(layout.initialVisibleDepth)} onValueChange={value => setLayout(current => ({ ...current, initialVisibleDepth: value === 'all' ? 'all' : Number(value) as 1 | 2 | 3 }))}>
|
||||
<SelectTrigger id={`${id}-depth`} aria-labelledby={`${id}-depth-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{[1, 2, 3].map(depth => <SelectItem key={depth} value={String(depth)}>{depth}Depth까지</SelectItem>)}<SelectItem value="all">전체 펼침</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</StorySection>
|
||||
<StorySection title="인터랙션·직원 정보">
|
||||
<div className="hm-orgchart-story-settings">
|
||||
<fieldset className="hm-orgchart-story-companies">
|
||||
<div className="hm-orgchart-story-company">
|
||||
<Switch id={`${id}-drag-drop`} checked={dragDropEnabled} onCheckedChange={setDragDropEnabled} />
|
||||
<Label htmlFor={`${id}-drag-drop`}>Drag & Drop</Label>
|
||||
</div>
|
||||
{([
|
||||
['collapse', '접기·펼치기 사용'], ['zoom', 'Zoom 사용'], ['pan', 'Pan 사용'],
|
||||
] as const).map(([key, label]) => <div key={key} className="hm-orgchart-story-company">
|
||||
<Checkbox id={`${id}-interaction-${key}`} checked={interactions[key]} onCheckedChange={checked => setInteractions(current => ({ ...current, [key]: checked === true }))} />
|
||||
<Label htmlFor={`${id}-interaction-${key}`}>{label}</Label>
|
||||
</div>)}
|
||||
</fieldset>
|
||||
<div className="hm-orgchart-story-controls">
|
||||
<div className="hm-orgchart-story-instance hm-orgchart-story-popup-setting">
|
||||
<Label id={`${id}-popup-label`} htmlFor={`${id}-popup`}>직원 정보 팝업</Label>
|
||||
<Select value={popup} onValueChange={value => setPopup(value as OrgChartProps['employeePopup'])}>
|
||||
<SelectTrigger id={`${id}-popup`} aria-labelledby={`${id}-popup-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="hm-dialog">HMDesign Dialog</SelectItem><SelectItem value="core">Core 기본 팝업</SelectItem><SelectItem value="none">사용 안 함</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user