From 7f741caa4b548807803f5f6e4a1e8cea273ed87d Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Mon, 24 Aug 2026 21:36:09 +0200 Subject: [PATCH] feat: Update card sizing logic for responsive organization chart and bump version to 1.0.6 --- README.md | 2 +- config/package-solution.json | 2 +- package.json | 2 +- .../components/OrganizationChart.module.scss | 20 +++++ .../components/OrganizationChart.tsx | 73 +++++++++++++++++-- tests/validate-project.js | 3 + 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7a8c0f8..5dd4465 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus - Profilbild, Anzeigename, Position, Abteilung und optional Kontaktdaten - Verlinkung auf die SharePoint-Profilseite - kurzzeitiger In-Memory-Profilcache zur Reduzierung wiederholter REST-Aufrufe -- responsive Baumdarstellung mit SharePoint-Themefarben +- responsive Baumdarstellung mit automatisch kompakteren Karten bei breiten Organisationsebenen - durchgehende Verbindungslinien und SharePoint-Chevrons zum Auf- und Zuklappen ## Datenquelle und Voraussetzungen diff --git a/config/package-solution.json b/config/package-solution.json index 4a2f71f..ea06489 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -4,7 +4,7 @@ "name": "organization-chart-webpart", "title": "organization-chart-webpart", "id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e", - "version": "1.0.5.0", + "version": "1.0.6.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 83c7516..5ce4533 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "organization-chart-webpart", - "version": "1.0.5", + "version": "1.0.6", "private": true, "main": "lib/index.js", "engines": { "node": ">=6.9.0 <9.0.0" }, diff --git a/src/webparts/organizationChart/components/OrganizationChart.module.scss b/src/webparts/organizationChart/components/OrganizationChart.module.scss index 161f2e4..2252323 100644 --- a/src/webparts/organizationChart/components/OrganizationChart.module.scss +++ b/src/webparts/organizationChart/components/OrganizationChart.module.scss @@ -54,6 +54,26 @@ .status { color: "[theme:bodyText, default:#323130]"; padding: 16px; } .error { color: "[theme:errorText, default:#a80000]"; padding: 16px; } +.compactTree .tree li, .compactTree li { padding-left: 5px; padding-right: 5px; } +.compactTree .personCard, .compactTree .cardContent { min-height: 82px; } +.compactTree .cardContent { padding: 9px; } +.compactTree .photoWrap { flex-basis: 44px; height: 44px; margin-right: 9px; } +.compactTree .initials { font-size: 15px; line-height: 44px; } +.compactTree .photo { height: 44px; width: 44px; } +.compactTree .name { font-size: 14px; line-height: 18px; } +.compactTree .jobTitle, .compactTree .department, .compactTree .email, .compactTree .location { font-size: 11px; line-height: 15px; } + +.denseTree .tree li, .denseTree li { padding-left: 3px; padding-right: 3px; } +.denseTree .personCard, .denseTree .cardContent { min-height: 72px; } +.denseTree .cardContent { padding: 7px; } +.denseTree .photoWrap { flex-basis: 36px; height: 36px; margin-right: 7px; } +.denseTree .initials { font-size: 13px; line-height: 36px; } +.denseTree .photo { height: 36px; width: 36px; } +.denseTree .personText { padding-bottom: 13px; } +.denseTree .name { font-size: 13px; line-height: 17px; } +.denseTree .jobTitle, .denseTree .department, .denseTree .email, .denseTree .location { font-size: 10px; line-height: 14px; } +.denseTree .reportCount { bottom: 6px; font-size: 10px; right: 7px; } + @media (max-width: 600px) { .tree { display: block; min-width: 0; width: 100%; } .tree ul { display: block; padding: 12px 0 0 18px; } diff --git a/src/webparts/organizationChart/components/OrganizationChart.tsx b/src/webparts/organizationChart/components/OrganizationChart.tsx index a7b4856..607cdf4 100644 --- a/src/webparts/organizationChart/components/OrganizationChart.tsx +++ b/src/webparts/organizationChart/components/OrganizationChart.tsx @@ -5,23 +5,43 @@ import { IOrganizationChartProps } from './IOrganizationChartProps'; export interface IOrganizationChartState { collapsed: { [account: string]: boolean }; + cardWidth: number; } export default class OrganizationChart extends React.Component { + private viewportElement: HTMLDivElement; + public constructor(props: IOrganizationChartProps) { super(props); - this.state = { collapsed: {} }; + this.state = { collapsed: {}, cardWidth: 230 }; + } + + public componentDidMount(): void { + this.updateCardWidth(); + window.addEventListener('resize', this.onWindowResize); + } + + public componentDidUpdate(): void { + this.updateCardWidth(); + } + + public componentWillUnmount(): void { + window.removeEventListener('resize', this.onWindowResize); } public render(): React.ReactElement { + const densityClass: string = this.state.cardWidth <= 170 + ? styles.denseTree + : (this.state.cardWidth <= 205 ? styles.compactTree : ''); + const treeClassName: string = styles.tree + (densityClass ? ' ' + densityClass : ''); return (
{this.props.title &&

{this.props.title}

}
{this.props.data.truncated &&
{this.props.truncatedLabel}
} -
-
+
{ this.viewportElement = element; }}> +
    {this.renderNode(this.props.data.root, 1)}
@@ -77,8 +97,51 @@ export default class OrganizationChart extends React.Component ); return profile.profileUrl - ? {content} - :
{content}
; + ? {content} + :
{content}
; + } + + private onWindowResize = (): void => { + this.updateCardWidth(); + } + + private updateCardWidth(): void { + if (!this.viewportElement) { return; } + const availableWidth: number = this.viewportElement.clientWidth - 8; + if (availableWidth <= 0) { return; } + + let cardWidth: number; + if (availableWidth <= 600) { + cardWidth = Math.max(144, Math.min(260, availableWidth - 30)); + } else { + const maximumBreadth: number = this.getMaximumVisibleBreadth(); + const horizontalSpacePerCard: number = 16; + cardWidth = Math.floor((availableWidth - (maximumBreadth * horizontalSpacePerCard)) / maximumBreadth); + cardWidth = Math.max(144, Math.min(230, cardWidth)); + } + + if (cardWidth !== this.state.cardWidth) { + this.setState({ cardWidth: cardWidth }); + } + } + + private getMaximumVisibleBreadth(): number { + const countsByLevel: number[] = []; + this.countVisibleNodes(this.props.data.root, 0, countsByLevel); + let maximum: number = 1; + for (let i: number = 0; i < countsByLevel.length; i++) { + maximum = Math.max(maximum, countsByLevel[i]); + } + return maximum; + } + + private countVisibleNodes(node: IOrganizationNode, level: number, countsByLevel: number[]): void { + countsByLevel[level] = (countsByLevel[level] || 0) + 1; + const key: string = node.profile.accountName.toLowerCase(); + if (this.state.collapsed[key] === true) { return; } + for (let i: number = 0; i < node.children.length; i++) { + this.countVisibleNodes(node.children[i], level + 1, countsByLevel); + } } private toggle(key: string): void { diff --git a/tests/validate-project.js b/tests/validate-project.js index d4079fa..87d0d0f 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -47,6 +47,9 @@ assert(component.indexOf('peopleCountLabel') < 0 && component.indexOf('' component.indexOf('') >= 0, 'Header count removal or modern toggle icons are missing.'); assert(styles.indexOf('.tree li:only-child { padding-top: 0; }') >= 0, 'Single-child connector spacing must remain continuous.'); +assert(component.indexOf('getMaximumVisibleBreadth') >= 0 && component.indexOf('cardWidth') >= 0 && + styles.indexOf('.compactTree') >= 0 && styles.indexOf('.denseTree') >= 0, + 'Responsive organization-chart card sizing is missing.'); assert(component.indexOf("role='presentation'") >= 0, 'Decorative profile images must be hidden from assistive technology.'); ['bodyText', 'neutralSecondary', 'neutralLighter', 'themePrimary', 'neutralTertiary', 'white', 'neutralLight', 'focusBorder', 'errorText'] .forEach(function(slot) {