From 983a4af63c05fde52b40fda4f845e3b0aeebf15c Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Mon, 24 Aug 2026 22:50:06 +0200 Subject: [PATCH] feat: Add configurable card design options and enhance person details in organization chart --- README.md | 3 + config/package-solution.json | 2 +- package.json | 2 +- .../OrganizationChartWebPart.manifest.json | 5 +- .../OrganizationChartWebPart.ts | 24 +++- .../components/IOrganizationChartProps.ts | 10 ++ .../components/OrganizationChart.module.scss | 30 +++-- .../components/OrganizationChart.tsx | 107 +++++++++++++++--- src/webparts/organizationChart/loc/de-de.js | 11 ++ src/webparts/organizationChart/loc/en-us.js | 11 ++ .../organizationChart/loc/myStrings.d.ts | 11 ++ tests/validate-project.js | 10 +- 12 files changed, 191 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 5dd4465..6f5ad3f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus - standardmäßig vollständig aufgeklappter Baum - einzelne Zweige können auf- und zugeklappt werden - Profilbild, Anzeigename, Position, Abteilung und optional Kontaktdaten +- Teams-ähnliche Personenkarten mit Text links und Profilbild rechts +- aufklappbare Personendetails direkt in der Karte +- konfigurierbare Kartenfarben und wahlweise abgerundete Ecken - Verlinkung auf die SharePoint-Profilseite - kurzzeitiger In-Memory-Profilcache zur Reduzierung wiederholter REST-Aufrufe - responsive Baumdarstellung mit automatisch kompakteren Karten bei breiten Organisationsebenen diff --git a/config/package-solution.json b/config/package-solution.json index ea06489..0929d3a 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.6.0", + "version": "1.1.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 5ce4533..617b690 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "organization-chart-webpart", - "version": "1.0.6", + "version": "1.1.0", "private": true, "main": "lib/index.js", "engines": { "node": ">=6.9.0 <9.0.0" }, diff --git a/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json b/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json index c1eb385..1498c17 100644 --- a/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json +++ b/src/webparts/organizationChart/OrganizationChartWebPart.manifest.json @@ -23,7 +23,10 @@ "maxPeople": 100, "cacheMinutes": 15, "showPictures": true, - "showContactDetails": false + "showContactDetails": false, + "cardBackgroundColor": "", + "cardAccentColor": "", + "roundedCards": false } }] } diff --git a/src/webparts/organizationChart/OrganizationChartWebPart.ts b/src/webparts/organizationChart/OrganizationChartWebPart.ts index 147065a..9257a19 100644 --- a/src/webparts/organizationChart/OrganizationChartWebPart.ts +++ b/src/webparts/organizationChart/OrganizationChartWebPart.ts @@ -34,6 +34,9 @@ export interface IOrganizationChartWebPartProps { cacheMinutes: number; showPictures: boolean; showContactDetails: boolean; + cardBackgroundColor: string; + cardAccentColor: string; + roundedCards: boolean; } export default class OrganizationChartWebPart extends BaseClientSideWebPart { @@ -61,9 +64,19 @@ export default class OrganizationChartWebPart extends BaseClientSideWebPart { private viewportElement: HTMLDivElement; + private effectiveCardWidth: number = 230; public constructor(props: IOrganizationChartProps) { super(props); - this.state = { collapsed: {}, cardWidth: 230 }; + this.state = { collapsed: {}, expandedDetails: {}, cardWidth: 230 }; } public componentDidMount(): void { @@ -30,9 +32,11 @@ export default class OrganizationChart extends React.Component { - const densityClass: string = this.state.cardWidth <= 170 + const effectiveCardWidth: number = this.getEffectiveCardWidth(); + this.effectiveCardWidth = effectiveCardWidth; + const densityClass: string = effectiveCardWidth <= 170 ? styles.denseTree - : (this.state.cardWidth <= 205 ? styles.compactTree : ''); + : (effectiveCardWidth <= 205 ? styles.compactTree : ''); const treeClassName: string = styles.tree + (densityClass ? ' ' + densityClass : ''); return (
@@ -77,28 +81,65 @@ export default class OrganizationChart extends React.Component + + ); + return ( +
+ {content} + {detailsExpanded && this.renderDetails(profile)} +
+ ); + } + + private renderDetails(profile: IUserProfile): JSX.Element { + return ( +
+ {profile.department && this.renderDetail(this.props.departmentLabel, profile.department, styles.department)} + {this.props.showContactDetails && (profile.office || profile.location) && + this.renderDetail(this.props.officeLabel, profile.office || profile.location, styles.location)} + {this.props.showContactDetails && profile.email && + this.renderDetail(this.props.emailLabel, profile.email, styles.email)} + {this.props.showContactDetails && (profile.workPhone || profile.cellPhone) && + this.renderDetail(this.props.phoneLabel, profile.workPhone || profile.cellPhone, styles.phone)} + {profile.directReports.length > 0 && + this.renderDetail(this.props.reportsLabel, String(profile.directReports.length), styles.reportCount)} + {profile.profileUrl && {this.props.profileLabel}} +
+ ); + } + + private renderDetail(label: string, value: string, className: string): JSX.Element { + return ( +
+ {label} + {value} +
); - return profile.profileUrl - ? {content} - :
{content}
; } private onWindowResize = (): void => { @@ -117,7 +158,7 @@ export default class OrganizationChart extends React.Component= 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 && +assert(component.indexOf('getMaximumVisibleBreadth') >= 0 && component.indexOf('getBreadthCardWidth') >= 0 && + component.indexOf('maximumBreadth === 4') >= 0 && component.indexOf('cardWidth') >= 0 && styles.indexOf('.compactTree') >= 0 && styles.indexOf('.denseTree') >= 0, 'Responsive organization-chart card sizing is missing.'); +assert(component.indexOf('expandedDetails') >= 0 && component.indexOf('toggleDetails') >= 0 && + component.indexOf('renderDetails') >= 0 && component.indexOf("className={styles.cardContent}") >= 0, + 'Teams-style expandable person cards are missing.'); +assert(webPart.indexOf("PropertyPaneTextField('cardBackgroundColor'") >= 0 && + webPart.indexOf("PropertyPaneTextField('cardAccentColor'") >= 0 && + webPart.indexOf("PropertyPaneToggle('roundedCards'") >= 0, + 'Configurable card design options are 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) {