Add Dynamic Poll Web Part with core functionality and styling

- Initialize Gulp build process with gulpfile.js
- Create package.json for project dependencies and scripts
- Add TypeScript declarations for SCSS modules
- Implement DynamicPollWebPart with property pane configuration
- Create PollDataService for SharePoint list interactions
- Develop core polling logic in PollCore.ts
- Define types for polls and answers in PollTypes.ts
- Create React components for DynamicPoll UI
- Add localization support for English and German
- Implement unit tests for core polling functionality
- Set up TypeScript and TSLint configurations
This commit is contained in:
Torsten Brendgen
2026-08-23 22:51:10 +02:00
commit ca41674188
30 changed files with 955 additions and 0 deletions
@@ -0,0 +1,25 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "a786d2cc-2f81-488e-849a-312c75892a0f",
"alias": "DynamicPollWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other", "de-de": "Weitere" },
"title": { "default": "Dynamic Poll", "de-de": "Umfrage" },
"description": {
"default": "Displays an active poll and its results.",
"de-de": "Zeigt eine aktive Umfrage und deren Ergebnisse an."
},
"officeFabricIconFontName": "PollResults",
"properties": {
"pollListId": "",
"answerListId": "",
"showResultsBeforeVote": false,
"refreshSeconds": 60
}
}]
}
@@ -0,0 +1,164 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart, IPropertyPaneConfiguration, IPropertyPaneDropdownOption,
PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'DynamicPollWebPartStrings';
import DynamicPoll from './components/DynamicPoll';
import { IDynamicPollProps } from './components/IDynamicPollProps';
import styles from './components/DynamicPoll.module.scss';
import { PollDataService } from './PollDataService';
import { IListOption, IPollSnapshot } from './PollTypes';
export interface IDynamicPollWebPartProps {
pollListId: string;
answerListId: string;
showResultsBeforeVote: boolean;
refreshSeconds: number;
}
export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPollWebPartProps> {
private service: PollDataService;
private lists: IListOption[] = [];
private loadingLists: boolean = false;
private requestVersion: number = 0;
private refreshHandle: number | undefined;
private currentRefreshSeconds: number = 0;
private disposed: boolean = false;
protected onInit(): Promise<void> {
this.service = new PollDataService(this.context);
return super.onInit().then((): Promise<void> => this.loadLists());
}
public render(): void {
this.ensureRefreshTimer();
this.loadSnapshot(true);
}
protected onDispose(): void {
this.disposed = true;
this.requestVersion++;
this.clearRefreshTimer();
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version { return Version.parse('1.0'); }
protected onPropertyPaneConfigurationStart(): void {
if (!this.lists.length && !this.loadingLists) { this.loadLists(); }
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
const options: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectOption }];
for (let i: number = 0; i < this.lists.length; i++) {
options.push({ key: this.lists[i].id, text: this.lists[i].title });
}
return { pages: [{
header: { description: strings.PropertyPaneDescription },
groups: [{
groupName: strings.DataGroupName,
groupFields: [
PropertyPaneDropdown('pollListId', { label: strings.PollListLabel, options: options, disabled: this.loadingLists }),
PropertyPaneDropdown('answerListId', { label: strings.AnswerListLabel, options: options, disabled: this.loadingLists })
]
}, {
groupName: strings.BehaviorGroupName,
groupFields: [
PropertyPaneToggle('showResultsBeforeVote', { label: strings.ShowResultsBeforeVoteLabel }),
PropertyPaneSlider('refreshSeconds', {
label: strings.RefreshSecondsLabel, min: 0, max: 300, step: 15, showValue: true
})
]
}]
}] };
}
private loadSnapshot(showLoading: boolean): Promise<void> {
const version: number = ++this.requestVersion;
if (!this.isConfigured()) {
this.renderStatus(strings.ConfigureMessage, false);
return Promise.resolve();
}
if (showLoading) { this.renderStatus(strings.LoadingMessage, false); }
return this.service.getSnapshot(this.properties.pollListId, this.properties.answerListId, this.userKey)
.then((snapshot: IPollSnapshot): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (!snapshot) { this.renderStatus(strings.NoPollMessage, false); return; }
const element: React.ReactElement<IDynamicPollProps> = React.createElement(DynamicPoll, {
snapshot: snapshot,
showResultsBeforeVote: this.properties.showResultsBeforeVote === true,
strings: {
voteButton: strings.VoteButton, votingButton: strings.VotingButton,
thanksMessage: strings.ThanksMessage, resultsHeading: strings.ResultsHeading,
votesLabel: strings.VotesLabel, oneVoteLabel: strings.OneVoteLabel,
selectAnswerMessage: strings.SelectAnswerMessage, voteErrorMessage: strings.VoteErrorMessage,
alreadyVotedMessage: strings.AlreadyVotedMessage
},
onVote: (answer: string): Promise<void> => this.vote(snapshot.poll.id, answer)
});
ReactDom.render(element, this.domElement);
}).catch((error: Error): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (window.console) { console.error('[DynamicPoll] ' + strings.LoadErrorMessage, error); }
this.renderStatus(strings.LoadErrorMessage, true);
});
}
private vote(pollId: number, answer: string): Promise<void> {
return this.service.vote(this.properties.pollListId, this.properties.answerListId, pollId, answer, this.userKey)
.then((): Promise<void> => this.loadSnapshot(false));
}
private loadLists(): Promise<void> {
this.loadingLists = true;
this.refreshPropertyPane();
return this.service.getLists().then((lists: IListOption[]): void => { this.lists = lists; })
.catch((error: Error): void => {
this.lists = [];
if (window.console) { console.error('[DynamicPoll] Lists could not be loaded.', error); }
}).then((): void => {
this.loadingLists = false;
this.refreshPropertyPane();
});
}
private ensureRefreshTimer(): void {
const seconds: number = this.normalizedRefreshSeconds();
if (seconds === this.currentRefreshSeconds && (seconds === 0 || this.refreshHandle !== undefined)) { return; }
this.clearRefreshTimer();
this.currentRefreshSeconds = seconds;
if (seconds > 0) {
this.refreshHandle = window.setInterval((): void => { this.loadSnapshot(false); }, seconds * 1000);
}
}
private clearRefreshTimer(): void {
if (this.refreshHandle !== undefined) { window.clearInterval(this.refreshHandle); this.refreshHandle = undefined; }
this.currentRefreshSeconds = 0;
}
private normalizedRefreshSeconds(): number {
const value: number = Number(this.properties.refreshSeconds);
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 0), 300) : 60;
}
private get userKey(): string {
const user: any = this.context.pageContext.user as any; // tslint:disable-line:no-any
return String(user.email || user.loginName || user.displayName || '').trim();
}
private isConfigured(): boolean {
return !!(this.service && this.properties.pollListId && this.properties.answerListId && this.userKey);
}
private refreshPropertyPane(): void {
if (this.context && this.context.propertyPane) { this.context.propertyPane.refresh(); }
}
private renderStatus(message: string, error: boolean): void {
ReactDom.unmountComponentAtNode(this.domElement);
this.domElement.innerHTML = '';
const status: HTMLDivElement = document.createElement('div');
status.className = error ? styles.errorStatus : styles.status;
status.setAttribute('role', error ? 'alert' : 'status');
status.textContent = message;
this.domElement.appendChild(status);
}
}
+37
View File
@@ -0,0 +1,37 @@
import { IPoll, IPollAnswer, IPollResult } from './PollTypes';
export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined {
const timestamp: number = now.getTime();
const active: IPoll[] = polls.filter((poll: IPoll): boolean =>
poll.isActive && poll.options.length > 0 &&
(!poll.startDate || poll.startDate.getTime() <= timestamp) &&
(!poll.endDate || poll.endDate.getTime() >= timestamp));
active.sort((left: IPoll, right: IPoll): number =>
dateValue(right.startDate) - dateValue(left.startDate) || right.id - left.id);
return active[0];
}
export function calculatePollResults(options: string[], answers: IPollAnswer[]): IPollResult[] {
const counts: { [option: string]: number } = {};
for (let i: number = 0; i < options.length; i++) { counts[options[i]] = 0; }
for (let i: number = 0; i < answers.length; i++) {
if (counts[answers[i].answer] !== undefined) { counts[answers[i].answer]++; }
}
const total: number = answers.filter((answer: IPollAnswer): boolean => counts[answer.answer] !== undefined).length;
return options.map((option: string): IPollResult => ({
option: option,
votes: counts[option],
percentage: total > 0 ? Math.round((counts[option] / total) * 100) : 0
}));
}
export function findUserAnswer(answers: IPollAnswer[], userKey: string): string {
const expected: string = String(userKey || '').trim().toLowerCase();
for (let i: number = 0; i < answers.length; i++) {
if (String(answers[i].userKey || '').trim().toLowerCase() === expected) { return answers[i].answer; }
}
return '';
}
function dateValue(value?: Date): number { return value ? value.getTime() : 0; }
+130
View File
@@ -0,0 +1,130 @@
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { calculatePollResults, findUserAnswer, selectActivePoll } from './PollCore';
import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes';
export class PollDataService {
public constructor(private context: WebPartContext) { }
public getLists(): Promise<IListOption[]> {
const url: string = this.webUrl + '/_api/web/lists?$select=Id,Title&$filter=Hidden eq false&$orderby=Title';
return this.getJson(url).then((payload: any): IListOption[] => this.results(payload).map((list: any): IListOption => ({ // tslint:disable-line:no-any
id: this.cleanGuid(list.Id), title: String(list.Title || list.Id)
})));
}
public getSnapshot(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot | undefined> {
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<IPollSnapshot | undefined> => {
if (!poll) { return Promise.resolve(undefined); }
return this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => {
const results = calculatePollResults(poll.options, answers);
return {
poll: poll,
results: results,
totalVotes: results.reduce((total, result): number => total + result.votes, 0),
userAnswer: findUserAnswer(answers, userKey)
};
});
});
}
public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> {
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<void> => {
if (!poll || poll.id !== pollId || poll.options.indexOf(answer) < 0) {
return Promise.reject(new Error('POLL_NOT_ACTIVE'));
}
return this.saveVote(answerListId, pollId, answer, userKey);
});
}
private saveVote(answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> {
return this.getAnswers(answerListId, pollId).then((answers: IPollAnswer[]): Promise<void> => {
if (findUserAnswer(answers, userKey)) { return Promise.reject(new Error('ALREADY_VOTED')); }
return this.getListEntityType(answerListId).then((entityType: string): Promise<void> => {
const body: any = { // tslint:disable-line:no-any
'__metadata': { 'type': entityType },
'Title': userKey,
'PollId': pollId,
'Answer': answer
};
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(answerListId) + "')/items";
return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
headers: this.writeHeaders, body: JSON.stringify(body)
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response));
});
});
}
private getActivePoll(listId: string): Promise<IPoll | undefined> {
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
'?$select=Id,Title,Question,Options,StartDate,EndDate,IsActive&$filter=IsActive eq 1&$orderby=StartDate desc&$top=100';
return this.getJson(url).then((payload: any): IPoll | undefined => { // tslint:disable-line:no-any
const polls: IPoll[] = this.results(payload).map((item: any): IPoll => ({ // tslint:disable-line:no-any
id: Number(item.Id),
title: String(item.Title || ''),
question: String(item.Question || item.Title || ''),
options: this.toStringArray(item.Options),
startDate: this.toDate(item.StartDate),
endDate: this.toDate(item.EndDate),
isActive: item.IsActive === true || item.IsActive === 1
}));
return selectActivePoll(polls, new Date());
});
}
private getAnswers(listId: string, pollId: number): Promise<IPollAnswer[]> {
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
'?$select=Id,Title,Answer,PollId&$filter=PollId eq ' + Number(pollId) + '&$top=5000';
return this.getJson(url).then((payload: any): IPollAnswer[] => this.results(payload).map((item: any): IPollAnswer => ({ // tslint:disable-line:no-any
id: Number(item.Id), userKey: String(item.Title || ''), answer: String(item.Answer || '')
})));
}
private getListEntityType(listId: string): Promise<string> {
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')?$select=ListItemEntityTypeFullName";
return this.getJson(url).then((payload: any): string => { // tslint:disable-line:no-any
const source: any = payload.d || payload; // tslint:disable-line:no-any
if (!source.ListItemEntityTypeFullName) { throw new Error('List item entity type is missing.'); }
return String(source.ListItemEntityTypeFullName);
});
}
private getJson(url: string): Promise<any> { // tslint:disable-line:no-any
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.readHeaders })
.then((response: SPHttpClientResponse): Promise<any> => { // tslint:disable-line:no-any
if (response.ok) { return response.json(); }
return this.error(response).then((error: Error): Promise<any> => Promise.reject(error)); // tslint:disable-line:no-any
});
}
private ensureOk(response: SPHttpClientResponse): Promise<void> {
return response.ok ? Promise.resolve() : this.error(response).then((error: Error): Promise<void> => Promise.reject(error));
}
private error(response: SPHttpClientResponse): Promise<Error> {
return response.text().then((text: string): Error => new Error('SharePoint request failed (' + response.status + '): ' + text));
}
private results(payload: any): any[] { // tslint:disable-line:no-any
const source: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any
return source && Array.isArray(source.results) ? source.results : [];
}
private toStringArray(value: any): string[] { // tslint:disable-line:no-any
if (typeof value === 'string' && value.trim()) { return [value.trim()]; }
const source: any[] = value && Array.isArray(value.results) ? value.results : (Array.isArray(value) ? value : []); // tslint:disable-line:no-any
return source.map((entry: any): string => String(entry || '').trim()).filter((entry: string): boolean => !!entry); // tslint:disable-line:no-any
}
private toDate(value: any): Date | undefined { // tslint:disable-line:no-any
if (!value) { return undefined; }
const date: Date = new Date(String(value));
return isNaN(date.getTime()) ? undefined : date;
}
private validateGuid(value: string): string {
const guid: string = this.cleanGuid(value);
if (!/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(guid)) { throw new Error('Invalid list ID.'); }
return guid;
}
private cleanGuid(value: string): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
private get readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } // tslint:disable-line:no-any
private get writeHeaders(): any { return { // tslint:disable-line:no-any
'Accept': 'application/json;odata=verbose', 'Content-Type': 'application/json;odata=verbose', 'OData-Version': '3.0'
}; }
}
+19
View File
@@ -0,0 +1,19 @@
export interface IListOption { id: string; title: string; }
export interface IPoll {
id: number;
title: string;
question: string;
options: string[];
startDate?: Date;
endDate?: Date;
isActive: boolean;
}
export interface IPollAnswer { id: number; userKey: string; answer: string; }
export interface IPollResult { option: string; votes: number; percentage: number; }
export interface IPollSnapshot {
poll: IPoll;
results: IPollResult[];
totalVotes: number;
userAnswer: string;
}
@@ -0,0 +1,70 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.poll {
background: "[theme: white, default: #ffffff]";
border: 1px solid "[theme: neutralLight, default: #edebe9]";
box-sizing: border-box;
color: "[theme: neutralPrimary, default: #323130]";
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
font-size: 14px;
padding: 20px;
width: 100%;
}
.question { font-size: 22px; font-weight: 600; line-height: 1.3; margin: 0 0 16px; }
.options { border: 0; margin: 0; padding: 0; }
.option {
align-items: center;
border-bottom: 1px solid "[theme: neutralLight, default: #edebe9]";
cursor: pointer;
display: flex;
gap: 10px;
min-height: 40px;
padding: 4px 8px;
}
.option:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; }
.option input { height: 16px; margin: 0; width: 16px; }
.voteButton {
background: "[theme: themePrimary, default: #0078d4]";
border: 1px solid "[theme: themePrimary, default: #0078d4]";
color: "[theme: white, default: #ffffff]";
cursor: pointer;
font: inherit;
font-weight: 600;
margin-top: 16px;
min-height: 32px;
padding: 6px 20px;
}
.voteButton:hover { background: "[theme: themeDark, default: #005a9e]"; }
.voteButton:focus { outline: 2px solid "[theme: neutralPrimary, default: #323130]"; outline-offset: 2px; }
.voteButton:disabled { cursor: default; opacity: .6; }
.thanks { background: "[theme: themeLighterAlt, default: #eff6fc]"; margin-bottom: 16px; padding: 10px 12px; }
.error { color: "[theme: errorText, default: #a80000]"; margin-top: 10px; }
.results { margin-top: 20px; }
.resultsHeading { font-size: 18px; font-weight: 600; margin: 0 0 12px; }
.result { margin-bottom: 12px; }
.resultHeader { display: flex; gap: 12px; justify-content: space-between; margin-bottom: 4px; }
.track { background: "[theme: neutralLighter, default: #f3f2f1]"; height: 10px; overflow: hidden; width: 100%; }
.bar { background: "[theme: themePrimary, default: #0078d4]"; height: 100%; min-width: 0; transition: width 180ms ease; }
.bar1 { background: #498205; }
.bar2 { background: #8764b8; }
.bar3 { background: #ca5010; }
.bar4 { background: #038387; }
.bar0 { background: "[theme: themePrimary, default: #0078d4]"; }
.status,
.errorStatus {
border: 1px solid "[theme: neutralLight, default: #edebe9]";
color: "[theme: neutralPrimary, default: #323130]";
font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif;
padding: 16px;
}
.errorStatus { border-color: "[theme: errorText, default: #a80000]"; color: "[theme: errorText, default: #a80000]"; }
.visuallyHidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
@@ -0,0 +1,92 @@
import * as React from 'react';
import { IPollResult } from '../PollTypes';
import { IDynamicPollProps } from './IDynamicPollProps';
import styles from './DynamicPoll.module.scss';
export interface IDynamicPollState {
selected: string;
submitting: boolean;
error: string;
voted: boolean;
}
export default class DynamicPoll extends React.Component<IDynamicPollProps, IDynamicPollState> {
private groupName: string = 'poll-' + Math.random().toString(36).substring(2, 10);
public constructor(props: IDynamicPollProps) {
super(props);
this.state = { selected: props.snapshot.userAnswer || '', submitting: false, error: '', voted: false };
}
public componentWillReceiveProps(nextProps: IDynamicPollProps): void {
if (nextProps.snapshot.poll.id !== this.props.snapshot.poll.id) {
this.setState({ selected: nextProps.snapshot.userAnswer || '', submitting: false, error: '', voted: false });
}
}
public render(): React.ReactElement<any> { // tslint:disable-line:no-any
const hasVoted: boolean = !!this.props.snapshot.userAnswer || this.state.voted;
const showResults: boolean = hasVoted || this.props.showResultsBeforeVote;
return <section className={styles.poll} aria-labelledby={this.groupName + '-question'}>
<h2 id={this.groupName + '-question'} className={styles.question}>{this.props.snapshot.poll.question}</h2>
{!hasVoted ? this.renderForm() : <div className={styles.thanks} role='status'>{this.props.strings.thanksMessage}</div>}
{showResults ? this.renderResults() : undefined}
</section>;
}
private renderForm(): React.ReactElement<any> { // tslint:disable-line:no-any
return <div>
<fieldset className={styles.options} disabled={this.state.submitting}>
<legend className={styles.visuallyHidden}>{this.props.snapshot.poll.question}</legend>
{this.props.snapshot.poll.options.map((option: string, index: number) => {
const id: string = this.groupName + '-option-' + index;
return <label className={styles.option} htmlFor={id} key={option}>
<input id={id} type='radio' name={this.groupName} value={option}
checked={this.state.selected === option}
onChange={() => this.setState({ selected: option, error: '' })} />
<span>{option}</span>
</label>;
})}
</fieldset>
{this.state.error ? <div className={styles.error} role='alert'>{this.state.error}</div> : undefined}
<button type='button' className={styles.voteButton} disabled={this.state.submitting} onClick={this.submitVote}>
{this.state.submitting ? this.props.strings.votingButton : this.props.strings.voteButton}
</button>
</div>;
}
private renderResults(): React.ReactElement<any> { // tslint:disable-line:no-any
return <div className={styles.results} aria-live='polite'>
<h3 className={styles.resultsHeading}>{this.props.strings.resultsHeading}</h3>
{this.props.snapshot.results.map((result: IPollResult, index: number) =>
<div className={styles.result} key={result.option}>
<div className={styles.resultHeader}>
<span>{result.option}</span>
<span>{result.percentage}% · {result.votes} {result.votes === 1
? this.props.strings.oneVoteLabel : this.props.strings.votesLabel}</span>
</div>
<div className={styles.track} role='progressbar' aria-label={result.option}
aria-valuemin={0} aria-valuemax={100} aria-valuenow={result.percentage}>
<div className={styles.bar + ' ' + styles['bar' + (index % 5)]} style={{ width: result.percentage + '%' }} />
</div>
</div>
)}
</div>;
}
private submitVote = (): void => {
if (!this.state.selected) {
this.setState({ error: this.props.strings.selectAnswerMessage });
return;
}
this.setState({ submitting: true, error: '' });
this.props.onVote(this.state.selected).then((): void => {
this.setState({ submitting: false, voted: true });
}).catch((error: Error): void => {
const message: string = error && error.message === 'ALREADY_VOTED'
? this.props.strings.alreadyVotedMessage : this.props.strings.voteErrorMessage;
this.setState({ submitting: false, error: message });
});
}
}
@@ -0,0 +1,14 @@
import { IPollSnapshot } from '../PollTypes';
export interface IDynamicPollStrings {
voteButton: string; votingButton: string; thanksMessage: string; resultsHeading: string;
votesLabel: string; oneVoteLabel: string; selectAnswerMessage: string;
voteErrorMessage: string; alreadyVotedMessage: string;
}
export interface IDynamicPollProps {
snapshot: IPollSnapshot;
showResultsBeforeVote: boolean;
strings: IDynamicPollStrings;
onVote: (answer: string) => Promise<void>;
}
+24
View File
@@ -0,0 +1,24 @@
define([], function() { return {
"PropertyPaneDescription": "Wählen Sie die Umfrage- und Antwortliste aus.",
"DataGroupName": "Datenquelle",
"BehaviorGroupName": "Verhalten",
"PollListLabel": "Umfrageliste",
"AnswerListLabel": "Antwortliste",
"ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen",
"RefreshSecondsLabel": "Automatische Aktualisierung (Sekunden)",
"SelectOption": "Auswählen...",
"ConfigureMessage": "Wählen Sie in den WebPart-Einstellungen die Umfrage- und Antwortliste aus.",
"LoadingMessage": "Umfrage wird geladen...",
"NoPollMessage": "Derzeit ist keine aktive Umfrage vorhanden.",
"LoadErrorMessage": "Die Umfrage konnte nicht geladen werden. Prüfen Sie die Listen und deren Spalten.",
"VoteErrorMessage": "Ihre Stimme konnte nicht gespeichert werden.",
"AlreadyVotedMessage": "Sie haben an dieser Umfrage bereits teilgenommen.",
"VoteButton": "Abstimmen",
"VotingButton": "Wird gespeichert...",
"ThanksMessage": "Vielen Dank für Ihre Teilnahme!",
"ResultsHeading": "Ergebnis",
"VotesLabel": "Stimmen",
"OneVoteLabel": "Stimme",
"SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus."
}; });
+24
View File
@@ -0,0 +1,24 @@
define([], function() { return {
"PropertyPaneDescription": "Select the poll and answer lists.",
"DataGroupName": "Data source",
"BehaviorGroupName": "Behavior",
"PollListLabel": "Poll list",
"AnswerListLabel": "Answer list",
"ShowResultsBeforeVoteLabel": "Show results before voting",
"RefreshSecondsLabel": "Automatic refresh (seconds)",
"SelectOption": "Select...",
"ConfigureMessage": "Select the poll and answer lists in the web part settings.",
"LoadingMessage": "Loading poll...",
"NoPollMessage": "There is currently no active poll.",
"LoadErrorMessage": "The poll could not be loaded. Check the lists and their columns.",
"VoteErrorMessage": "Your vote could not be saved.",
"AlreadyVotedMessage": "You have already voted in this poll.",
"VoteButton": "Vote",
"VotingButton": "Saving...",
"ThanksMessage": "Thank you for voting!",
"ResultsHeading": "Results",
"VotesLabel": "votes",
"OneVoteLabel": "vote",
"SelectAnswerMessage": "Select an answer before voting."
}; });
+13
View File
@@ -0,0 +1,13 @@
declare interface IDynamicPollWebPartStrings {
PropertyPaneDescription: string; DataGroupName: string; BehaviorGroupName: string;
PollListLabel: string; AnswerListLabel: string; ShowResultsBeforeVoteLabel: string;
RefreshSecondsLabel: string; SelectOption: string; ConfigureMessage: string;
LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string;
AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string;
ResultsHeading: string; VotesLabel: string; OneVoteLabel: string; SelectAnswerMessage: string;
}
declare module 'DynamicPollWebPartStrings' {
const strings: IDynamicPollWebPartStrings;
export = strings;
}