Files
HMDesign/packages/org-chart/src/core/expanded-company.ts
T
shkim4 064517ec46 feat(org-chart): implement expandable layout and multi-company navigation
Integrate P5-1B through P5-4 shared Core layout, collapse, zoom/pan/fit, company roots and keyboard accessibility. Include React/HTML examples, virtual 20/100/300 employee fixtures, focused checks and the additive data contract.
2026-10-12 21:59:09 +09:00

41 lines
3.0 KiB
TypeScript

import { getCompanyColorPreset } from '../company-color-presets.js';
import { isSafeImageUrl, type OrgChartDataset, type OrganizationTreeNode, type ValidationIssue } from '../data/index.js';
import type { OrgChartTopNode } from './types.js';
export function resolveExpandedCompanies(data: OrgChartDataset, roots: OrganizationTreeNode[], fallback: string) {
const companies = new Map<string, string>();
const issues: ValidationIssue[] = [];
const visit = (node: OrganizationTreeNode, inherited?: string) => {
const org = node.organization;
const leader = data.employees.find(employee => employee.id === org.leaderId);
const company = org.companyId ?? inherited ?? (leader && getCompanyColorPreset(leader.affiliationId) ? leader.affiliationId : fallback);
if (!getCompanyColorPreset(company)) issues.push({ code: 'COMPANY_PRESET', path: `organizations.${org.id}.companyId`, message: 'Auto company requires a registered color preset.' });
if (inherited && company !== inherited) issues.push({ code: 'COMPANY_HIERARCHY', path: `organizations.${org.id}.companyId`, message: 'A child cannot belong to another company tree.' });
companies.set(org.id, company);
node.children.forEach(child => visit(child, company));
};
roots.forEach(root => visit(root));
return { companies, issues, companyIds: [...new Set(roots.map(root => companies.get(root.organization.id)!))] };
}
export function validateTopNodes(data: OrgChartDataset, roots: OrganizationTreeNode[], companies: ReadonlyMap<string, string>, configs: Readonly<Record<string, OrgChartTopNode>>): ValidationIssue[] {
const issues: ValidationIssue[] = [];
for (const [id, config] of Object.entries(configs)) {
const path = `companyTopNodes.${id}`;
if (!getCompanyColorPreset(id) || !config || typeof config !== 'object' || Array.isArray(config)) {
issues.push({ code: 'TOP_NODE_CONFIG', path, message: 'Expected a registered company and top node object.' }); continue;
}
for (const [key, value] of Object.entries(config)) {
const valid = key === 'display' ? ['card', 'titleOnly', 'hidden'].includes(value as string)
: ['employeeId', 'profileImage'].includes(key) && value === null ? true
: ['employeeId', 'profileImage', 'title', 'name', 'organizationName'].includes(key) && typeof value === 'string' && !!value.trim() && !/[<>\u0000-\u001f]/u.test(value);
if (!valid || (key === 'profileImage' && typeof value === 'string' && !isSafeImageUrl(value))) issues.push({ code: 'TOP_NODE_CONFIG', path: `${path}.${key}`, message: 'Invalid top node presentation value.' });
}
const companyRoots = roots.filter(root => companies.get(root.organization.id) === id);
if (config.employeeId && companyRoots.length && !companyRoots.some(root => data.employees.some(employee => employee.id === config.employeeId && employee.organizationId === root.organization.id))) {
issues.push({ code: 'TOP_NODE_REFERENCE', path, message: 'Top employee must belong to a root of this company.' });
}
}
return issues;
}