Füge Unterstützung für die Anzeige mehrerer aktiver Umfragen als Accordion oder Slider hinzu und aktualisiere die Version auf 1.1.0

This commit is contained in:
Torsten Brendgen
2026-08-24 19:38:12 +02:00
parent fcd8e026ee
commit f87c49a91a
16 changed files with 214 additions and 51 deletions
+3 -2
View File
@@ -4,7 +4,8 @@ Ein eigenstaendiges, mit SharePoint Server Subscription Edition kompatibles SPFx
## Funktionen ## Funktionen
- aktive Umfrage anhand von Aktiv-Flag sowie optionalem Start- und Enddatum - alle aktiven Umfragen anhand von Aktiv-Flag sowie optionalem Start- und Enddatum
- umschaltbare Darstellung mehrerer aktiver Umfragen als Accordion oder Slider mit Punktenavigation
- genau eine Antwort je angemeldetem Benutzer und Umfrage - genau eine Antwort je angemeldetem Benutzer und Umfrage
- unmittelbare Ergebnisanzeige mit Stimmen und Prozentwerten - unmittelbare Ergebnisanzeige mit Stimmen und Prozentwerten
- optional sichtbare Ergebnisse vor der eigenen Abstimmung - optional sichtbare Ergebnisse vor der eigenen Abstimmung
@@ -44,7 +45,7 @@ Nein
Anderer Termin Anderer Termin
``` ```
Leerzeilen und doppelte Eintraege werden ignoriert. `IsActive` aktivieren; Start- und Enddatum koennen leer bleiben. Sind mehrere Umfragen gleichzeitig aktiv, wird die mit dem neuesten Startdatum verwendet. Leerzeilen und doppelte Eintraege werden ignoriert. `IsActive` aktivieren; Start- und Enddatum koennen leer bleiben. Sind mehrere Umfragen gleichzeitig aktiv, werden sie nach Startdatum sortiert und entsprechend dem im Property Pane gewählten Accordion- oder Slider-Modus angezeigt.
Danach das WebPart auf einer modernen Seite einfuegen und im Property Pane die Listen `Polls` und `Poll Answers` auswaehlen. Danach das WebPart auf einer modernen Seite einfuegen und im Property Pane die Listen `Polls` und `Poll Answers` auswaehlen.
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "dynamic-poll-web-part", "name": "dynamic-poll-web-part",
"title": "dynamic-poll-web-part", "title": "dynamic-poll-web-part",
"id": "900239c9-b6c7-495d-a386-2d08a81a66f3", "id": "900239c9-b6c7-495d-a386-2d08a81a66f3",
"version": "1.0.0.0", "version": "1.1.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dynamic-poll-web-part", "name": "dynamic-poll-web-part",
"version": "1.0.0", "version": "1.1.0",
"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" },
@@ -19,6 +19,7 @@
"pollListId": "", "pollListId": "",
"answerListId": "", "answerListId": "",
"showResultsBeforeVote": false, "showResultsBeforeVote": false,
"displayMode": "slider",
"refreshSeconds": 60 "refreshSeconds": 60
} }
}] }]
+27 -16
View File
@@ -6,17 +6,17 @@ import {
PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle
} from '@microsoft/sp-webpart-base'; } from '@microsoft/sp-webpart-base';
import * as strings from 'DynamicPollWebPartStrings'; import * as strings from 'DynamicPollWebPartStrings';
import DynamicPoll from './components/DynamicPoll'; import DynamicPollCollection, { IDynamicPollCollectionProps } from './components/DynamicPollCollection';
import { IDynamicPollProps } from './components/IDynamicPollProps';
import styles from './components/DynamicPoll.module.scss'; import styles from './components/DynamicPoll.module.scss';
import { PollDataService } from './PollDataService'; import { PollDataService } from './PollDataService';
import { IListOption, IPollSnapshot } from './PollTypes'; import { IListOption, IPollSnapshot, PollDisplayMode } from './PollTypes';
export interface IDynamicPollWebPartProps { export interface IDynamicPollWebPartProps {
pollListId: string; pollListId: string;
answerListId: string; answerListId: string;
showResultsBeforeVote: boolean; showResultsBeforeVote: boolean;
refreshSeconds: number; refreshSeconds: number;
displayMode: PollDisplayMode;
} }
export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPollWebPartProps> { export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPollWebPartProps> {
@@ -68,6 +68,13 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
groupName: strings.BehaviorGroupName, groupName: strings.BehaviorGroupName,
groupFields: [ groupFields: [
PropertyPaneToggle('showResultsBeforeVote', { label: strings.ShowResultsBeforeVoteLabel }), PropertyPaneToggle('showResultsBeforeVote', { label: strings.ShowResultsBeforeVoteLabel }),
PropertyPaneDropdown('displayMode', {
label: strings.DisplayModeLabel,
options: [
{ key: 'accordion', text: strings.AccordionModeLabel },
{ key: 'slider', text: strings.SliderModeLabel }
]
}),
PropertyPaneSlider('refreshSeconds', { PropertyPaneSlider('refreshSeconds', {
label: strings.RefreshSecondsLabel, min: 0, max: 300, step: 15, showValue: true label: strings.RefreshSecondsLabel, min: 0, max: 300, step: 15, showValue: true
}) })
@@ -83,21 +90,25 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
return Promise.resolve(); return Promise.resolve();
} }
if (showLoading) { this.renderStatus(strings.LoadingMessage, false); } if (showLoading) { this.renderStatus(strings.LoadingMessage, false); }
return this.service.getSnapshot(this.properties.pollListId, this.properties.answerListId, this.userKey) return this.service.getSnapshots(this.properties.pollListId, this.properties.answerListId, this.userKey)
.then((snapshot: IPollSnapshot): void => { .then((snapshots: IPollSnapshot[]): void => {
if (this.disposed || version !== this.requestVersion) { return; } if (this.disposed || version !== this.requestVersion) { return; }
if (!snapshot) { this.renderStatus(strings.NoPollMessage, false); return; } if (!snapshots.length) { this.renderStatus(strings.NoPollMessage, false); return; }
const element: React.ReactElement<IDynamicPollProps> = React.createElement(DynamicPoll, { const pollStrings = {
snapshot: snapshot, 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,
previousPollLabel: strings.PreviousPollLabel, nextPollLabel: strings.NextPollLabel,
pollNavigationLabel: strings.PollNavigationLabel
};
const element: React.ReactElement<IDynamicPollCollectionProps> = React.createElement(DynamicPollCollection, {
snapshots: snapshots,
displayMode: this.properties.displayMode === 'accordion' ? 'accordion' : 'slider',
showResultsBeforeVote: this.properties.showResultsBeforeVote === true, showResultsBeforeVote: this.properties.showResultsBeforeVote === true,
strings: { strings: pollStrings,
voteButton: strings.VoteButton, votingButton: strings.VotingButton, onVote: (pollId: number, answer: string): Promise<void> => this.vote(pollId, answer)
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); ReactDom.render(element, this.domElement);
}).catch((error: Error): void => { }).catch((error: Error): void => {
+5 -1
View File
@@ -15,6 +15,10 @@ export function parsePollOptionLines(values: string[]): string[] {
} }
export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined { export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined {
return selectActivePolls(polls, now)[0];
}
export function selectActivePolls(polls: IPoll[], now: Date): IPoll[] {
const timestamp: number = now.getTime(); const timestamp: number = now.getTime();
const active: IPoll[] = polls.filter((poll: IPoll): boolean => const active: IPoll[] = polls.filter((poll: IPoll): boolean =>
poll.isActive && poll.options.length > 0 && poll.isActive && poll.options.length > 0 &&
@@ -22,7 +26,7 @@ export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined {
(!poll.endDate || poll.endDate.getTime() >= timestamp)); (!poll.endDate || poll.endDate.getTime() >= timestamp));
active.sort((left: IPoll, right: IPoll): number => active.sort((left: IPoll, right: IPoll): number =>
dateValue(right.startDate) - dateValue(left.startDate) || right.id - left.id); dateValue(right.startDate) - dateValue(left.startDate) || right.id - left.id);
return active[0]; return active;
} }
export function calculatePollResults(options: string[], answers: IPollAnswer[]): IPollResult[] { export function calculatePollResults(options: string[], answers: IPollAnswer[]): IPollResult[] {
+21 -19
View File
@@ -1,6 +1,6 @@
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base'; import { WebPartContext } from '@microsoft/sp-webpart-base';
import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePoll } from './PollCore'; import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePolls } from './PollCore';
import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes'; import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes';
export class PollDataService { export class PollDataService {
@@ -15,24 +15,26 @@ export class PollDataService {
}))); })));
} }
public getSnapshot(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot | undefined> { public getSnapshots(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot[]> {
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<IPollSnapshot | undefined> => { return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<IPollSnapshot[]> => {
if (!poll) { return Promise.resolve(undefined); } const requests: Array<Promise<IPollSnapshot>> = polls.map((poll: IPoll): Promise<IPollSnapshot> =>
return this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => { this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => {
const results = calculatePollResults(poll.options, answers); const results = calculatePollResults(poll.options, answers);
return { return {
poll: poll, poll: poll,
results: results, results: results,
totalVotes: results.reduce((total, result): number => total + result.votes, 0), totalVotes: results.reduce((total, result): number => total + result.votes, 0),
userAnswer: findUserAnswer(answers, userKey) userAnswer: findUserAnswer(answers, userKey)
}; };
}); }));
return Promise.all(requests);
}); });
} }
public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> { public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> {
return this.getActivePoll(pollListId).then((poll: IPoll): Promise<void> => { return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<void> => {
if (!poll || poll.id !== pollId || poll.options.indexOf(answer) < 0) { const poll: IPoll = polls.filter((candidate: IPoll): boolean => candidate.id === pollId)[0];
if (!poll || poll.options.indexOf(answer) < 0) {
return Promise.reject(new Error('POLL_NOT_ACTIVE')); return Promise.reject(new Error('POLL_NOT_ACTIVE'));
} }
return this.saveVote(answerListId, pollId, answer, userKey); return this.saveVote(answerListId, pollId, answer, userKey);
@@ -57,12 +59,12 @@ export class PollDataService {
}); });
} }
private getActivePoll(listId: string): Promise<IPoll | undefined> { private getActivePolls(listId: string): Promise<IPoll[]> {
return this.getOptionsFieldName(listId).then((optionsField: string): Promise<IPoll | undefined> => { return this.getOptionsFieldName(listId).then((optionsField: string): Promise<IPoll[]> => {
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" + const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
'?$select=Id,Title,Question,' + optionsField + '?$select=Id,Title,Question,' + optionsField +
',StartDate,EndDate,IsActive&$filter=IsActive eq 1&$orderby=StartDate desc&$top=100'; ',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 return this.getJson(url).then((payload: any): IPoll[] => { // tslint:disable-line:no-any
const polls: IPoll[] = this.results(payload).map((item: any): IPoll => ({ // tslint:disable-line:no-any const polls: IPoll[] = this.results(payload).map((item: any): IPoll => ({ // tslint:disable-line:no-any
id: Number(item.Id), id: Number(item.Id),
title: String(item.Title || ''), title: String(item.Title || ''),
@@ -72,7 +74,7 @@ export class PollDataService {
endDate: this.toDate(item.EndDate), endDate: this.toDate(item.EndDate),
isActive: item.IsActive === true || item.IsActive === 1 isActive: item.IsActive === true || item.IsActive === 1
})); }));
return selectActivePoll(polls, new Date()); return selectActivePolls(polls, new Date());
}); });
}); });
} }
+1 -1
View File
@@ -16,4 +16,4 @@ export interface IPollSnapshot {
totalVotes: number; totalVotes: number;
userAnswer: string; userAnswer: string;
} }
export type PollDisplayMode = 'accordion' | 'slider';
@@ -2,7 +2,6 @@
.poll { .poll {
background: "[theme: white, default: #ffffff]"; background: "[theme: white, default: #ffffff]";
border: 1px solid #edebe9;
box-sizing: border-box; box-sizing: border-box;
color: "[theme: neutralPrimary, default: #323130]"; color: "[theme: neutralPrimary, default: #323130]";
font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@@ -67,4 +66,32 @@
white-space: nowrap; white-space: nowrap;
width: 1px; width: 1px;
} }
.collection { width: 100%; }
.accordionItem { border-bottom: 1px solid "[theme: neutralLight, default: #edebe9]"; }
.accordionHeader {
align-items: center;
background: "[theme: white, default: #ffffff]";
border: 0;
color: "[theme: neutralPrimary, default: #323130]";
cursor: pointer;
display: flex;
font: inherit;
font-weight: 600;
justify-content: space-between;
min-height: 44px;
padding: 8px 12px;
text-align: left;
width: 100%;
}
.accordionHeader:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; }
.accordionHeader:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: -2px; }
.accordionChevron { font-family: 'FabricMDL2Icons'; font-size: 12px; margin-left: 12px; }
.accordionPanel .poll { padding-top: 14px; }
.sliderNavigation { align-items: center; display: flex; justify-content: center; margin-top: 8px; }
.sliderArrow { background: transparent; border: 0; color: "[theme: themePrimary, default: #0078d4]"; cursor: pointer; font-size: 28px; height: 32px; line-height: 24px; padding: 0; width: 36px; }
.sliderArrow:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; }
.sliderArrow:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; }
.pagination { align-items: center; display: flex; gap: 8px; padding: 0 8px; }
.paginationDot, .paginationDotActive { background: "[theme: neutralTertiary, default: #a19f9d]"; border: 0; border-radius: 50%; cursor: pointer; height: 8px; padding: 0; width: 8px; }
.paginationDotActive { background: "[theme: themePrimary, default: #0078d4]"; height: 10px; width: 10px; }
.paginationDot:focus, .paginationDotActive:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: 2px; }
@@ -0,0 +1,88 @@
import * as React from 'react';
import { IPollSnapshot, PollDisplayMode } from '../PollTypes';
import DynamicPoll from './DynamicPoll';
import { IDynamicPollStrings } from './IDynamicPollProps';
import styles from './DynamicPoll.module.scss';
export interface IDynamicPollCollectionProps {
displayMode: PollDisplayMode;
snapshots: IPollSnapshot[];
showResultsBeforeVote: boolean;
strings: IDynamicPollStrings;
onVote: (pollId: number, answer: string) => Promise<void>;
}
export interface IDynamicPollCollectionState {
activeIndex: number;
}
export default class DynamicPollCollection
extends React.Component<IDynamicPollCollectionProps, IDynamicPollCollectionState> {
public constructor(props: IDynamicPollCollectionProps) {
super(props);
this.state = { activeIndex: 0 };
}
public componentWillReceiveProps(nextProps: IDynamicPollCollectionProps): void {
if (this.state.activeIndex >= nextProps.snapshots.length) { this.setState({ activeIndex: 0 }); }
}
public render(): JSX.Element {
return this.props.displayMode === 'accordion' ? this.renderAccordion() : this.renderSlider();
}
private renderAccordion(): JSX.Element {
return <div className={styles.collection}>
{this.props.snapshots.map((snapshot: IPollSnapshot, index: number): JSX.Element => {
const expanded: boolean = index === this.state.activeIndex;
const panelId: string = 'poll-panel-' + snapshot.poll.id;
return <section className={styles.accordionItem} key={snapshot.poll.id}>
<button type='button' className={styles.accordionHeader}
aria-expanded={expanded} aria-controls={panelId}
onClick={(): void => this.setState({ activeIndex: index })}>
<span>{snapshot.poll.title || snapshot.poll.question}</span>
<span className={styles.accordionChevron} aria-hidden='true'>{expanded ? '\uE70E' : '\uE70D'}</span>
</button>
{expanded && <div id={panelId} className={styles.accordionPanel}>
{this.renderPoll(snapshot)}
</div>}
</section>;
})}
</div>;
}
private renderSlider(): JSX.Element {
const count: number = this.props.snapshots.length;
const active: number = Math.min(this.state.activeIndex, Math.max(count - 1, 0));
const snapshot: IPollSnapshot = this.props.snapshots[active];
return <div className={styles.collection}>
{snapshot && this.renderPoll(snapshot)}
{count > 1 && <div className={styles.sliderNavigation}>
<button type='button' className={styles.sliderArrow} aria-label={this.props.strings.previousPollLabel}
onClick={(): void => this.move(-1)}>&#8249;</button>
<div className={styles.pagination} role='tablist' aria-label={this.props.strings.pollNavigationLabel}>
{this.props.snapshots.map((item: IPollSnapshot, index: number): JSX.Element =>
<button type='button' role='tab' key={item.poll.id}
className={index === active ? styles.paginationDotActive : styles.paginationDot}
aria-selected={index === active} aria-label={(index + 1) + ' / ' + count}
onClick={(): void => this.setState({ activeIndex: index })} />)}
</div>
<button type='button' className={styles.sliderArrow} aria-label={this.props.strings.nextPollLabel}
onClick={(): void => this.move(1)}>&#8250;</button>
</div>}
</div>;
}
private renderPoll(snapshot: IPollSnapshot): JSX.Element {
return <DynamicPoll snapshot={snapshot}
showResultsBeforeVote={this.props.showResultsBeforeVote}
strings={this.props.strings}
onVote={(answer: string): Promise<void> => this.props.onVote(snapshot.poll.id, answer)} />;
}
private move(offset: number): void {
const count: number = this.props.snapshots.length;
if (count) { this.setState({ activeIndex: (this.state.activeIndex + offset + count) % count }); }
}
}
@@ -4,6 +4,7 @@ export interface IDynamicPollStrings {
voteButton: string; votingButton: string; thanksMessage: string; resultsHeading: string; voteButton: string; votingButton: string; thanksMessage: string; resultsHeading: string;
votesLabel: string; oneVoteLabel: string; selectAnswerMessage: string; votesLabel: string; oneVoteLabel: string; selectAnswerMessage: string;
voteErrorMessage: string; alreadyVotedMessage: string; voteErrorMessage: string; alreadyVotedMessage: string;
previousPollLabel: string; nextPollLabel: string; pollNavigationLabel: string;
} }
export interface IDynamicPollProps { export interface IDynamicPollProps {
snapshot: IPollSnapshot; snapshot: IPollSnapshot;
@@ -11,4 +12,3 @@ export interface IDynamicPollProps {
strings: IDynamicPollStrings; strings: IDynamicPollStrings;
onVote: (answer: string) => Promise<void>; onVote: (answer: string) => Promise<void>;
} }
+7 -2
View File
@@ -5,6 +5,9 @@ define([], function() { return {
"PollListLabel": "Umfrageliste", "PollListLabel": "Umfrageliste",
"AnswerListLabel": "Antwortliste", "AnswerListLabel": "Antwortliste",
"ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen", "ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen",
"DisplayModeLabel": "Mehrere aktive Umfragen anzeigen als",
"AccordionModeLabel": "Accordion",
"SliderModeLabel": "Slider mit Seitennavigation",
"RefreshSecondsLabel": "Automatische Aktualisierung (Sekunden)", "RefreshSecondsLabel": "Automatische Aktualisierung (Sekunden)",
"SelectOption": "Auswählen...", "SelectOption": "Auswählen...",
"ConfigureMessage": "Wählen Sie in den WebPart-Einstellungen die Umfrage- und Antwortliste aus.", "ConfigureMessage": "Wählen Sie in den WebPart-Einstellungen die Umfrage- und Antwortliste aus.",
@@ -19,6 +22,8 @@ define([], function() { return {
"ResultsHeading": "Ergebnis", "ResultsHeading": "Ergebnis",
"VotesLabel": "Stimmen", "VotesLabel": "Stimmen",
"OneVoteLabel": "Stimme", "OneVoteLabel": "Stimme",
"SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus." "SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus.",
"PreviousPollLabel": "Vorherige Umfrage",
"NextPollLabel": "Nächste Umfrage",
"PollNavigationLabel": "Zwischen aktiven Umfragen wechseln"
}; }); }; });
+7 -2
View File
@@ -5,6 +5,9 @@ define([], function() { return {
"PollListLabel": "Poll list", "PollListLabel": "Poll list",
"AnswerListLabel": "Answer list", "AnswerListLabel": "Answer list",
"ShowResultsBeforeVoteLabel": "Show results before voting", "ShowResultsBeforeVoteLabel": "Show results before voting",
"DisplayModeLabel": "Show multiple active polls as",
"AccordionModeLabel": "Accordion",
"SliderModeLabel": "Slider with pagination",
"RefreshSecondsLabel": "Automatic refresh (seconds)", "RefreshSecondsLabel": "Automatic refresh (seconds)",
"SelectOption": "Select...", "SelectOption": "Select...",
"ConfigureMessage": "Select the poll and answer lists in the web part settings.", "ConfigureMessage": "Select the poll and answer lists in the web part settings.",
@@ -19,6 +22,8 @@ define([], function() { return {
"ResultsHeading": "Results", "ResultsHeading": "Results",
"VotesLabel": "votes", "VotesLabel": "votes",
"OneVoteLabel": "vote", "OneVoteLabel": "vote",
"SelectAnswerMessage": "Select an answer before voting." "SelectAnswerMessage": "Select an answer before voting.",
"PreviousPollLabel": "Previous poll",
"NextPollLabel": "Next poll",
"PollNavigationLabel": "Switch between active polls"
}; }); }; });
+2 -1
View File
@@ -5,9 +5,10 @@ declare interface IDynamicPollWebPartStrings {
LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string; LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string;
AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string; AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string;
ResultsHeading: string; VotesLabel: string; OneVoteLabel: string; SelectAnswerMessage: string; ResultsHeading: string; VotesLabel: string; OneVoteLabel: string; SelectAnswerMessage: string;
DisplayModeLabel: string; AccordionModeLabel: string; SliderModeLabel: string;
PreviousPollLabel: string; NextPollLabel: string; PollNavigationLabel: string;
} }
declare module 'DynamicPollWebPartStrings' { declare module 'DynamicPollWebPartStrings' {
const strings: IDynamicPollWebPartStrings; const strings: IDynamicPollWebPartStrings;
export = strings; export = strings;
} }
+15 -1
View File
@@ -1,4 +1,10 @@
import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePoll } from '../src/webparts/dynamicPoll/PollCore'; import {
calculatePollResults,
findUserAnswer,
parsePollOptionLines,
selectActivePoll,
selectActivePolls
} from '../src/webparts/dynamicPoll/PollCore';
import { IPoll, IPollAnswer } from '../src/webparts/dynamicPoll/PollTypes'; import { IPoll, IPollAnswer } from '../src/webparts/dynamicPoll/PollTypes';
let passed: number = 0; let passed: number = 0;
@@ -18,6 +24,14 @@ const selected: IPoll = selectActivePoll([
startDate: new Date('2026-08-24T00:00:00Z') }) startDate: new Date('2026-08-24T00:00:00Z') })
], now); ], now);
assert('current poll is selected by activation window', !!selected && selected.id === 2); assert('current poll is selected by activation window', !!selected && selected.id === 2);
const activePolls: IPoll[] = selectActivePolls([
poll({ id: 2, question: 'newer', options: ['A'], isActive: true,
startDate: new Date('2026-08-23T00:00:00Z') }),
poll({ id: 4, question: 'older', options: ['A'], isActive: true,
startDate: new Date('2026-08-21T00:00:00Z') })
], now);
assert('all active polls are returned in activation order',
activePolls.length === 2 && activePolls[0].id === 2 && activePolls[1].id === 4);
const answers: IPollAnswer[] = [ const answers: IPollAnswer[] = [
{ id: 1, userKey: 'one@contoso.local', answer: 'A' }, { id: 1, userKey: 'one@contoso.local', answer: 'A' },
+5 -1
View File
@@ -15,7 +15,7 @@ var service = read('src/webparts/dynamicPoll/PollDataService.ts');
var component = read('src/webparts/dynamicPoll/components/DynamicPoll.tsx'); var component = read('src/webparts/dynamicPoll/components/DynamicPoll.tsx');
var styles = read('src/webparts/dynamicPoll/components/DynamicPoll.module.scss'); var styles = read('src/webparts/dynamicPoll/components/DynamicPoll.module.scss');
assert(packageJson.version === '1.0.0' && solution.version === '1.0.0.0', 'Version mismatch.'); assert(packageJson.version === '1.1.0' && solution.version === '1.1.0.0', 'Version mismatch.');
assert(solution.includeClientSideAssets === true && solution.skipFeatureDeployment === true, 'Central deployment missing.'); assert(solution.includeClientSideAssets === true && solution.skipFeatureDeployment === true, 'Central deployment missing.');
assert(packageJson.name === 'dynamic-poll-web-part' && solution.name === 'dynamic-poll-web-part' && assert(packageJson.name === 'dynamic-poll-web-part' && solution.name === 'dynamic-poll-web-part' &&
solution.title === 'dynamic-poll-web-part', 'Dynamic Poll package naming is inconsistent.'); solution.title === 'dynamic-poll-web-part', 'Dynamic Poll package naming is inconsistent.');
@@ -24,6 +24,10 @@ assert(json('config/package-solution.json').paths.zippedPackage === 'solution/dy
assert(manifest.componentType === 'WebPart', 'Component is not a web part.'); assert(manifest.componentType === 'WebPart', 'Component is not a web part.');
assert(webPart.indexOf("PropertyPaneDropdown('pollListId'") >= 0 && webPart.indexOf("PropertyPaneDropdown('answerListId'") >= 0, assert(webPart.indexOf("PropertyPaneDropdown('pollListId'") >= 0 && webPart.indexOf("PropertyPaneDropdown('answerListId'") >= 0,
'List selection is missing.'); 'List selection is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('displayMode'") >= 0 &&
webPart.indexOf('DynamicPollCollection') >= 0, 'Multiple-poll display mode is missing.');
assert(service.indexOf('getSnapshots') >= 0 && service.indexOf('getActivePolls') >= 0,
'All active polls must be loaded and remain voteable.');
assert(service.indexOf("'PollId': pollId") >= 0 && service.indexOf("'Answer': answer") >= 0, 'Vote persistence is missing.'); assert(service.indexOf("'PollId': pollId") >= 0 && service.indexOf("'Answer': answer") >= 0, 'Vote persistence is missing.');
assert(service.indexOf("new Error('ALREADY_VOTED')") >= 0, 'Duplicate vote protection is missing.'); assert(service.indexOf("new Error('ALREADY_VOTED')") >= 0, 'Duplicate vote protection is missing.');
assert(service.indexOf("new Error('POLL_NOT_ACTIVE')") >= 0, 'Active poll and answer validation is missing.'); assert(service.indexOf("new Error('POLL_NOT_ACTIVE')") >= 0, 'Active poll and answer validation is missing.');