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
|
- Profilbild, Anzeigename, Position, Abteilung und optional Kontaktdaten
|
||||||
- Verlinkung auf die SharePoint-Profilseite
|
- Verlinkung auf die SharePoint-Profilseite
|
||||||
- kurzzeitiger In-Memory-Profilcache zur Reduzierung wiederholter REST-Aufrufe
|
- 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
|
- durchgehende Verbindungslinien und SharePoint-Chevrons zum Auf- und Zuklappen
|
||||||
|
|
||||||
## Datenquelle und Voraussetzungen
|
## Datenquelle und Voraussetzungen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "organization-chart-webpart",
|
"name": "organization-chart-webpart",
|
||||||
"title": "organization-chart-webpart",
|
"title": "organization-chart-webpart",
|
||||||
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
|
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
|
||||||
"version": "1.0.5.0",
|
"version": "1.0.6.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": true
|
"skipFeatureDeployment": true
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "organization-chart-webpart",
|
"name": "organization-chart-webpart",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"engines": { "node": ">=6.9.0 <9.0.0" },
|
"engines": { "node": ">=6.9.0 <9.0.0" },
|
||||||
|
|||||||
@@ -54,6 +54,26 @@
|
|||||||
.status { color: "[theme:bodyText, default:#323130]"; padding: 16px; }
|
.status { color: "[theme:bodyText, default:#323130]"; padding: 16px; }
|
||||||
.error { color: "[theme:errorText, default:#a80000]"; 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) {
|
@media (max-width: 600px) {
|
||||||
.tree { display: block; min-width: 0; width: 100%; }
|
.tree { display: block; min-width: 0; width: 100%; }
|
||||||
.tree ul { display: block; padding: 12px 0 0 18px; }
|
.tree ul { display: block; padding: 12px 0 0 18px; }
|
||||||
|
|||||||
@@ -5,23 +5,43 @@ import { IOrganizationChartProps } from './IOrganizationChartProps';
|
|||||||
|
|
||||||
export interface IOrganizationChartState {
|
export interface IOrganizationChartState {
|
||||||
collapsed: { [account: string]: boolean };
|
collapsed: { [account: string]: boolean };
|
||||||
|
cardWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class OrganizationChart extends React.Component<IOrganizationChartProps, IOrganizationChartState> {
|
export default class OrganizationChart extends React.Component<IOrganizationChartProps, IOrganizationChartState> {
|
||||||
|
private viewportElement: HTMLDivElement;
|
||||||
|
|
||||||
public constructor(props: IOrganizationChartProps) {
|
public constructor(props: IOrganizationChartProps) {
|
||||||
super(props);
|
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> {
|
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 (
|
return (
|
||||||
<section className={styles.organizationChart} aria-label={this.props.title}>
|
<section className={styles.organizationChart} aria-label={this.props.title}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
{this.props.title && <h2 className={styles.heading}>{this.props.title}</h2>}
|
{this.props.title && <h2 className={styles.heading}>{this.props.title}</h2>}
|
||||||
</div>
|
</div>
|
||||||
{this.props.data.truncated && <div className={styles.notice} role='status'>{this.props.truncatedLabel}</div>}
|
{this.props.data.truncated && <div className={styles.notice} role='status'>{this.props.truncatedLabel}</div>}
|
||||||
<div className={styles.viewport}>
|
<div className={styles.viewport} ref={(element: HTMLDivElement): void => { this.viewportElement = element; }}>
|
||||||
<div className={styles.tree}>
|
<div className={treeClassName}>
|
||||||
<ul role='tree'>{this.renderNode(this.props.data.root, 1)}</ul>
|
<ul role='tree'>{this.renderNode(this.props.data.root, 1)}</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,8 +97,51 @@ export default class OrganizationChart extends React.Component<IOrganizationChar
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
return profile.profileUrl
|
return profile.profileUrl
|
||||||
? <a className={styles.personCard} href={profile.profileUrl}>{content}</a>
|
? <a className={styles.personCard} style={{ width: this.state.cardWidth }} href={profile.profileUrl}>{content}</a>
|
||||||
: <div className={styles.personCard}>{content}</div>;
|
: <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 {
|
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.');
|
component.indexOf('') >= 0, 'Header count removal or modern toggle icons are missing.');
|
||||||
assert(styles.indexOf('.tree li:only-child { padding-top: 0; }') >= 0,
|
assert(styles.indexOf('.tree li:only-child { padding-top: 0; }') >= 0,
|
||||||
'Single-child connector spacing must remain continuous.');
|
'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.');
|
assert(component.indexOf("role='presentation'") >= 0, 'Decorative profile images must be hidden from assistive technology.');
|
||||||
['bodyText', 'neutralSecondary', 'neutralLighter', 'themePrimary', 'neutralTertiary', 'white', 'neutralLight', 'focusBorder', 'errorText']
|
['bodyText', 'neutralSecondary', 'neutralLighter', 'themePrimary', 'neutralTertiary', 'white', 'neutralLight', 'focusBorder', 'errorText']
|
||||||
.forEach(function(slot) {
|
.forEach(function(slot) {
|
||||||
|
|||||||
Reference in New Issue
Block a user