Füge neue Ereignis- und Alle anzeigen-Links hinzu, aktualisiere die Version und erweitere die Datenstruktur für die Ereignisse

This commit is contained in:
Torsten Brendgen
2026-08-24 19:38:18 +02:00
parent 664dba3b56
commit c0cfcf2111
13 changed files with 73 additions and 93 deletions
+1
View File
@@ -8,6 +8,7 @@ SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es orientiert si
- serverseitige Expansion von Serienterminen über `Lists.asmx` und `ExpandRecurrence=TRUE`
- Berücksichtigung normaler Termine, Serienvorkommen, geänderter Ausnahmen und gelöschter Vorkommen
- Layouts **Filmstreifen** und **Kompakt**
- SharePoint-nahe Ereigniszeilen mit Datumsfeld sowie **Neues Ereignis**- und **Alle anzeigen**-Links
- Filterung nach Kategorie und Zeitraum ab heute
- Anzeige von Datum, Uhrzeit, Ganztägigkeit, Ort, Bild und Serienkennzeichnung
- responsive Darstellung mit SharePoint-Schrift und Themefarben
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "modern-recurring-events-webpart",
"title": "modern-recurring-events-webpart",
"id": "f1082b4a-bbdf-4d31-852d-97e0685c20e2",
"version": "1.0.0.0",
"version": "1.0.1.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "modern-recurring-events-webpart",
"version": "1.0.0",
"version": "1.0.1",
"private": true,
"main": "lib/index.js",
"engines": { "node": ">=6.9.0 <9.0.0" },
@@ -13,12 +13,15 @@ export class RecurringEventsDataService {
public getLists(): Promise<IEventsListOption[]> {
const url: string = this.webUrl +
'/_api/web/lists?$select=Id,Title,RootFolder/ServerRelativeUrl&$expand=RootFolder&$filter=Hidden eq false and BaseTemplate eq 106&$orderby=Title';
'/_api/web/lists?$select=Id,Title,DefaultViewUrl,DefaultNewFormUrl,RootFolder/ServerRelativeUrl' +
'&$expand=RootFolder&$filter=Hidden eq false and BaseTemplate eq 106&$orderby=Title';
return this.getJson(url).then((payload: any): IEventsListOption[] => // tslint:disable-line:no-any
this.results(payload).map((list: any): IEventsListOption => ({ // tslint:disable-line:no-any
id: this.cleanGuid(list.Id),
title: String(list.Title || list.Id),
serverRelativeUrl: String(list.RootFolder && list.RootFolder.ServerRelativeUrl || '')
serverRelativeUrl: String(list.RootFolder && list.RootFolder.ServerRelativeUrl || ''),
defaultViewUrl: String(list.DefaultViewUrl || ''),
defaultNewFormUrl: String(list.DefaultNewFormUrl || '')
})));
}
@@ -4,6 +4,8 @@ export interface IEventsListOption {
id: string;
title: string;
serverRelativeUrl: string;
defaultViewUrl: string;
defaultNewFormUrl: string;
}
export interface IRecurringEvent {
@@ -50,14 +50,19 @@ export default class RecurringEventsWebPart extends BaseClientSideWebPart<IRecur
this.service.getEvents(list, range, this.properties.category || '', normalizeMaxEvents(this.properties.maxEvents))
.then((events: IRecurringEvent[]): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (!events.length) { this.renderStatus(strings.EmptyMessage, false); return; }
const listUrl: string = list.serverRelativeUrl.replace(/\/$/, '');
const element: React.ReactElement<IRecurringEventsProps> = React.createElement(RecurringEvents, {
events: events,
title: this.properties.title || strings.DefaultTitle,
layout: this.properties.layout === 'compact' ? 'compact' : 'filmstrip',
locale: this.context.pageContext.cultureInfo.currentUICultureName || 'en-us',
allDayLabel: strings.AllDayLabel,
recurringLabel: strings.RecurringLabel
recurringLabel: strings.RecurringLabel,
emptyMessage: strings.EmptyMessage,
newEventLabel: strings.NewEventLabel,
newEventUrl: list.defaultNewFormUrl || listUrl + '/NewForm.aspx',
seeAllLabel: strings.SeeAllLabel,
seeAllUrl: list.defaultViewUrl || listUrl
});
ReactDom.render(element, this.domElement);
}).catch((error: Error): void => {
@@ -7,4 +7,9 @@ export interface IRecurringEventsProps {
locale: string;
allDayLabel: string;
recurringLabel: string;
emptyMessage: string;
newEventLabel: string;
newEventUrl: string;
seeAllLabel: string;
seeAllUrl: string;
}
@@ -4,79 +4,31 @@
color: "[theme:bodyText, default:#323130]";
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.heading {
color: "[theme:bodyText, default:#323130]";
font-size: 24px;
font-weight: 300;
line-height: 32px;
margin: 0 0 20px;
}
.filmstrip {
display: flex;
gap: 16px;
list-style: none;
margin: 0;
overflow-x: auto;
padding: 0 0 8px;
scroll-snap-type: x proximity;
}
.card {
background: "[theme:white, default:#ffffff]";
border: 1px solid "[theme:neutralLight, default:#edebe9]";
box-sizing: border-box;
flex: 0 0 calc((100% - 32px) / 3);
min-width: 240px;
scroll-snap-align: start;
}
.cardLink, .compactLink {
color: "[theme:bodyText, default:#323130]";
display: flex;
text-decoration: none;
&:hover { color: "[theme:bodyText, default:#323130]"; }
&:focus { outline: 2px solid "[theme:focusBorder, default:#605e5c]"; outline-offset: -2px; }
}
.cardLink { display: block; height: 100%; }
.image, .imageFallback {
background-color: "[theme:themePrimary, default:#0078d4]";
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: block;
height: 150px;
position: relative;
}
.imageFallback { background: linear-gradient(135deg, "[theme:themePrimary, default:#0078d4]", "[theme:themeDark, default:#005a9e]"); }
.calendarIcon { color: #fff; font-size: 52px; left: 20px; opacity: .8; position: absolute; top: 16px; }
.cardBody { display: flex; min-height: 112px; padding: 16px; }
.cardContent, .compactContent { display: block; min-width: 0; }
.dateBadge {
color: "[theme:themePrimary, default:#0078d4]";
display: flex;
flex: 0 0 48px;
flex-direction: column;
margin-right: 14px;
text-align: center;
}
.month { font-size: 12px; font-weight: 600; line-height: 18px; text-transform: uppercase; }
.day { color: "[theme:bodyText, default:#323130]"; font-size: 28px; font-weight: 300; line-height: 32px; }
.title { display: block; font-size: 17px; font-weight: 600; line-height: 22px; overflow-wrap: break-word; }
.dateLine, .location { color: "[theme:neutralSecondary, default:#605e5c]"; display: block; font-size: 13px; line-height: 19px; margin-top: 4px; }
.recurring { color: "[theme:themePrimary, default:#0078d4]"; display: block; font-size: 12px; margin-top: 6px; }
.commandBar { align-items: center; display: flex; justify-content: space-between; margin-bottom: 18px; min-height: 32px; }
.commandLink { align-items: center; color: "[theme:themePrimary, default:#0078d4]"; display: inline-flex; font-size: 14px; min-height: 32px; padding: 0 4px; text-decoration: none; }
.commandLink:hover { color: "[theme:themeDark, default:#005a9e]"; text-decoration: underline; }
.commandLink:focus { outline: 2px solid "[theme:focusBorder, default:#605e5c]"; outline-offset: 1px; }
.addIcon { font-size: 22px; font-weight: 300; line-height: 20px; margin-right: 7px; }
.heading { color: "[theme:bodyText, default:#323130]"; font-size: 24px; font-weight: 300; line-height: 32px; margin: 0 0 14px; }
.empty { color: "[theme:neutralSecondary, default:#605e5c]"; font-size: 14px; padding: 16px 8px; }
.filmstrip { display: flex; gap: 18px; list-style: none; margin: 0; overflow-x: auto; padding: 0 0 8px; scroll-snap-type: x proximity; }
.card { background: "[theme:white, default:#ffffff]"; border-bottom: 1px solid "[theme:neutralLight, default:#edebe9]"; box-sizing: border-box; flex: 0 0 280px; scroll-snap-align: start; }
.compact { list-style: none; margin: 0; padding: 0; }
.compactItem { border-bottom: 1px solid "[theme:neutralLight, default:#edebe9]"; }
.compactLink { align-items: center; min-height: 72px; padding: 12px 8px; }
.compactContent { flex: 1 1 auto; }
.recurringIcon { color: "[theme:themePrimary, default:#0078d4]"; flex: 0 0 30px; font-size: 20px; text-align: center; }
.cardLink, .compactLink { align-items: center; color: "[theme:bodyText, default:#323130]"; display: flex; min-height: 72px; padding: 10px 8px; text-decoration: none; }
.cardLink:hover, .compactLink:hover { background: "[theme:neutralLighterAlt, default:#faf9f8]"; color: "[theme:bodyText, default:#323130]"; }
.cardLink:focus, .compactLink:focus { outline: 2px solid "[theme:focusBorder, default:#605e5c]"; outline-offset: -2px; }
.cardContent, .compactContent { display: block; flex: 1 1 auto; min-width: 0; }
.dateBadge { color: "[theme:themePrimary, default:#0078d4]"; display: flex; flex: 0 0 48px; flex-direction: column; margin-right: 12px; text-align: center; }
.month { font-size: 11px; font-weight: 600; line-height: 16px; text-transform: uppercase; }
.day { color: "[theme:bodyText, default:#323130]"; font-size: 22px; font-weight: 300; line-height: 28px; }
.title { display: block; font-size: 14px; font-weight: 600; line-height: 20px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dateLine, .location { color: "[theme:neutralSecondary, default:#605e5c]"; display: block; font-size: 12px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recurringIcon { color: "[theme:themePrimary, default:#0078d4]"; flex: 0 0 28px; font-size: 18px; text-align: center; }
.status { color: "[theme:bodyText, default:#323130]"; padding: 16px; }
.error { color: "[theme:errorText, default:#a80000]"; padding: 16px; }
@media (max-width: 900px) { .card { flex-basis: calc((100% - 16px) / 2); } }
@media (max-width: 560px) { .card { flex-basis: 88%; } .heading { font-size: 21px; margin-bottom: 14px; } }
@media (max-width: 560px) {
.card { flex-basis: 88%; }
.heading { font-size: 21px; }
}
@@ -7,8 +7,16 @@ export default class RecurringEvents extends React.Component<IRecurringEventsPro
public render(): React.ReactElement<IRecurringEventsProps> {
return (
<section className={styles.recurringEvents} aria-label={this.props.title}>
<div className={styles.commandBar}>
<a className={styles.commandLink} href={this.props.newEventUrl}>
<span className={styles.addIcon} aria-hidden='true'>+</span>{this.props.newEventLabel}
</a>
<a className={styles.commandLink} href={this.props.seeAllUrl}>{this.props.seeAllLabel}</a>
</div>
{this.props.title && <h2 className={styles.heading}>{this.props.title}</h2>}
{this.props.layout === 'compact' ? this.renderCompact() : this.renderFilmstrip()}
{!this.props.events.length
? <div className={styles.empty} role='status'>{this.props.emptyMessage}</div>
: (this.props.layout === 'compact' ? this.renderCompact() : this.renderFilmstrip())}
</section>
);
}
@@ -19,19 +27,14 @@ export default class RecurringEvents extends React.Component<IRecurringEventsPro
{this.props.events.map((event: IRecurringEvent): JSX.Element => (
<li className={styles.card} key={event.key}>
<a className={styles.cardLink} href={event.detailsUrl} aria-label={this.eventLabel(event)}>
<span className={event.imageUrl ? styles.image : styles.imageFallback}
style={event.imageUrl ? { backgroundImage: 'url("' + this.cssUrl(event.imageUrl) + '")' } : {}}>
{!event.imageUrl && <span className={styles.calendarIcon} aria-hidden='true'>&#9638;</span>}
</span>
<span className={styles.cardBody}>
{this.renderDateBadge(event)}
<span className={styles.cardContent}>
<span className={styles.title}>{event.title}</span>
<span className={styles.dateLine}>{this.formatDateLine(event)}</span>
{event.location && <span className={styles.location}>{event.location}</span>}
{event.recurring && <span className={styles.recurring}>{this.props.recurringLabel}</span>}
</span>
{this.renderDateBadge(event)}
<span className={styles.cardContent}>
<span className={styles.title}>{event.title}</span>
<span className={styles.dateLine}>{this.formatDateLine(event)}</span>
{event.location && <span className={styles.location}>{event.location}</span>}
</span>
{event.recurring && <span className={styles.recurringIcon}
title={this.props.recurringLabel} aria-hidden='true'>&#8635;</span>}
</a>
</li>
))}
@@ -83,5 +86,4 @@ export default class RecurringEvents extends React.Component<IRecurringEventsPro
return parts.join('. ');
}
private cssUrl(value: string): string { return String(value || '').replace(/["'()\\\r\n]/g, ''); }
}
+3 -1
View File
@@ -24,6 +24,8 @@ define([], function() {
LoadErrorMessage: 'Die Ereignisse konnten nicht geladen werden.',
DefaultTitle: 'Ereignisse',
AllDayLabel: 'Ganztägig',
RecurringLabel: 'Serientermin'
RecurringLabel: 'Serientermin',
NewEventLabel: 'Neues Ereignis',
SeeAllLabel: 'Alle anzeigen'
};
});
+3 -1
View File
@@ -24,6 +24,8 @@ define([], function() {
LoadErrorMessage: 'The events could not be loaded.',
DefaultTitle: 'Events',
AllDayLabel: 'All day',
RecurringLabel: 'Recurring event'
RecurringLabel: 'Recurring event',
NewEventLabel: 'New event',
SeeAllLabel: 'See all'
};
});
+2
View File
@@ -24,6 +24,8 @@ declare interface IRecurringEventsWebPartStrings {
DefaultTitle: string;
AllDayLabel: string;
RecurringLabel: string;
NewEventLabel: string;
SeeAllLabel: string;
}
declare module 'RecurringEventsWebPartStrings' {
const strings: IRecurringEventsWebPartStrings;
+4
View File
@@ -34,6 +34,10 @@ assert(webPart.indexOf("PropertyPaneDropdown('category'") >= 0 && webPart.indexO
'Event filtering controls are missing.');
assert(component.indexOf("role='list'") >= 0 && component.indexOf('aria-label={this.eventLabel(event)}') >= 0,
'Accessible event semantics are missing.');
assert(component.indexOf('newEventUrl') >= 0 && component.indexOf('seeAllUrl') >= 0,
'New Event and See All commands are missing.');
assert(webPart.indexOf('list.defaultNewFormUrl') >= 0 && webPart.indexOf('list.defaultViewUrl') >= 0,
'Event-list command URLs are missing.');
['bodyText', 'white', 'neutralLight', 'themePrimary', 'themeDark', 'focusBorder', 'errorText'].forEach(function(slot) {
assert(styles.indexOf('[theme:' + slot + ', default:') >= 0, 'SharePoint theme slot is missing: ' + slot);
});