feat: Update card sizing logic for responsive organization chart and bump version to 1.0.6
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
+1
-1
@@ -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" },
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -5,23 +5,43 @@ import { IOrganizationChartProps } from './IOrganizationChartProps';
|
||||
|
||||
export interface IOrganizationChartState {
|
||||
collapsed: { [account: string]: boolean };
|
||||
cardWidth: number;
|
||||
}
|
||||
|
||||
export default class OrganizationChart extends React.Component<IOrganizationChartProps, IOrganizationChartState> {
|
||||
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<IOrganizationChartProps> {
|
||||
const densityClass: string = this.state.cardWidth <= 170
|
||||
? styles.denseTree
|
||||
: (this.state.cardWidth <= 205 ? styles.compactTree : '');
|
||||
const treeClassName: string = styles.tree + (densityClass ? ' ' + densityClass : '');
|
||||
return (
|
||||
<section className={styles.organizationChart} aria-label={this.props.title}>
|
||||
<div className={styles.header}>
|
||||
{this.props.title && <h2 className={styles.heading}>{this.props.title}</h2>}
|
||||
</div>
|
||||
{this.props.data.truncated && <div className={styles.notice} role='status'>{this.props.truncatedLabel}</div>}
|
||||
<div className={styles.viewport}>
|
||||
<div className={styles.tree}>
|
||||
<div className={styles.viewport} ref={(element: HTMLDivElement): void => { this.viewportElement = element; }}>
|
||||
<div className={treeClassName}>
|
||||
<ul role='tree'>{this.renderNode(this.props.data.root, 1)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,8 +97,51 @@ export default class OrganizationChart extends React.Component<IOrganizationChar
|
||||
</span>
|
||||
);
|
||||
return profile.profileUrl
|
||||
? <a className={styles.personCard} href={profile.profileUrl}>{content}</a>
|
||||
: <div className={styles.personCard}>{content}</div>;
|
||||
? <a className={styles.personCard} style={{ width: this.state.cardWidth }} href={profile.profileUrl}>{content}</a>
|
||||
: <div className={styles.personCard} style={{ width: this.state.cardWidth }}>{content}</div>;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user