feat: Update dynamic poll web part to version 1.3.0 with enhanced field mapping and configurable title
- Bump version in package.json to 1.3.0 - Add new properties for field mapping in DynamicPollWebPart.manifest.json - Enhance DynamicPollWebPart.ts to support dynamic field mapping for polls and answers - Implement field loading and default application logic in PollDataService.ts - Update PollTypes.ts to include new interfaces for field mapping - Modify DynamicPollCollection.tsx to display configurable web part title - Update styles in DynamicPoll.module.scss for improved UI - Add localization strings for new properties in en-us.js and de-de.js - Update tests to reflect changes in data structure and functionality - Validate project structure and ensure all new features are accounted for
This commit is contained in:
@@ -6,6 +6,9 @@ Ein eigenstaendiges, mit SharePoint Server Subscription Edition kompatibles SPFx
|
||||
|
||||
- alle aktiven Umfragen anhand von Aktiv-Flag sowie optionalem Start- und Enddatum
|
||||
- umschaltbare Darstellung mehrerer aktiver Umfragen als Accordion oder Slider mit Punktenavigation
|
||||
- konfigurierbarer WebPart-Titel
|
||||
- vollstaendige Spaltenzuordnung im Property Pane
|
||||
- Content-Type-Hub-taugliche Verbindung zwischen Umfragen und Antworten ueber einen stabilen Textschluessel
|
||||
- genau eine Antwort je angemeldetem Benutzer und Umfrage
|
||||
- unmittelbare Ergebnisanzeige mit Stimmen und Prozentwerten
|
||||
- optional sichtbare Ergebnisse vor der eigenen Abstimmung
|
||||
@@ -30,8 +33,8 @@ In der SharePoint Management Shell auf einem SharePoint-Server ausfuehren:
|
||||
|
||||
Das Skript ist idempotent und legt standardmaessig diese Listen an:
|
||||
|
||||
- `Polls`: `Title`, `Question`, `Options` (mehrzeiliger Nur-Text), `StartDate`, `EndDate`, `IsActive`
|
||||
- `Poll Answers`: `Title` (Benutzerschluessel), `Poll` (Lookup), `Answer`
|
||||
- `Polls`: `PollKey`, `Title`, `Question`, `Options` (mehrzeiliger Nur-Text), `StartDate`, `EndDate`, `IsActive`
|
||||
- `Poll Answers`: `PollKey` (Text), `UserKey` (Text), `Answer`
|
||||
|
||||
Die Benutzer des WebParts benoetigen Leserechte auf `Polls` sowie Lese- und Hinzufuegerechte auf `Poll Answers`. Fuer die Ergebnisberechnung muessen sie die vorhandenen Antworten lesen koennen.
|
||||
|
||||
@@ -47,7 +50,27 @@ Anderer Termin
|
||||
|
||||
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, im Property Pane die Listen `Polls` und `Poll Answers`
|
||||
auswaehlen und die Spalten zuordnen. Bekannte Standardnamen werden automatisch vorbelegt.
|
||||
|
||||
## Verwendung mit dem Inhaltstyp-Katalog
|
||||
|
||||
Die beiden Listen duerfen aus zentral verteilten Inhaltstypen und Websitespalten aufgebaut werden. Fuer die
|
||||
Beziehung wird absichtlich kein Lookup verwendet: Der Poll-Schluessel enthaelt einen eindeutigen, dauerhaften
|
||||
Textwert und derselbe Wert wird beim Abstimmen in die zugeordnete Textspalte der Antwortliste geschrieben. Ein Lookup verweist
|
||||
auf die ID einer konkreten Liste und kann deshalb nicht verlaesslich ueber den Content Type Hub auf andere
|
||||
Websitesammlungen verteilt werden.
|
||||
|
||||
Im Inhaltstyp-Katalog sollten dafuer zwei getrennte Websitespalten angelegt werden: eine eindeutige/indexierte
|
||||
Schluesselspalte fuer die Poll-Liste und eine nicht eindeutige Verweisspalte fuer die Antwortliste. Die
|
||||
Verweisspalte darf nicht eindeutig sein, weil mehrere Benutzer dieselbe Umfrage beantworten. Interne Namen
|
||||
wie `PollDefinitionKey` und `PollReferenceKey` sind daher ebenso moeglich und werden im Property Pane zugeordnet.
|
||||
|
||||
Die internen Spaltennamen duerfen abweichen. Im Property Pane lassen sich Umfrageschluessel, Titel, Frage,
|
||||
Antwortoptionen, Zeitfenster, Aktiv-Flag sowie Umfrageschluessel, Benutzer und Antwort der Antwortliste frei
|
||||
zuordnen. Fuer vorhandene Installationen bleibt ein bestehendes `Poll`-Lookup lesbar: In diesem Fall muss als
|
||||
Umfrageschluessel `ID` und als Antwortschluessel das Lookup `Poll` ausgewaehlt werden. Empfohlen ist die
|
||||
Migration auf die beiden Textspalten `PollKey`.
|
||||
|
||||
## Paket bauen
|
||||
|
||||
@@ -60,6 +83,8 @@ Das fertige Paket liegt unter `sharepoint/solution/dynamic-poll-web-part.sppkg`.
|
||||
|
||||
## Hinweise
|
||||
|
||||
Der Benutzerschluessel wird in der `Title`-Spalte der Antwortliste gespeichert. Die doppelte Stimmabgabe wird vor dem Schreiben geprueft. Zwei exakt gleichzeitige Requests desselben Benutzers lassen sich ohne serverseitige Eindeutigkeitsregel dennoch nicht vollstaendig ausschliessen.
|
||||
Der Benutzerschluessel wird in der zugeordneten Spalte (standardmaessig `UserKey`) gespeichert. Die doppelte Stimmabgabe wird vor dem Schreiben geprueft. Zwei exakt gleichzeitige Requests desselben Benutzers lassen sich ohne serverseitige Eindeutigkeitsregel dennoch nicht vollstaendig ausschliessen.
|
||||
|
||||
Nach einem Upgrade von der bisherigen Choice-Spalte muss das Provisionierungsskript erneut ausgefuehrt werden. Es legt das neue Feld `OptionsText` mit dem Anzeigenamen `Options` an, uebernimmt vorhandene Choice-Werte und blendet die alte Spalte als `Options (legacy)` in den Formularen aus. Das WebPart kann waehrend der Umstellung weiterhin das alte Choice-Feld lesen.
|
||||
Nach einem Upgrade muss das Provisionierungsskript erneut ausgefuehrt werden. Es legt `PollKey`, `UserKey` und
|
||||
das mehrzeilige `OptionsText` an, uebernimmt vorhandene Werte und blendet das alte Poll-Lookup sowie die alte
|
||||
Choice-Spalte in den Formularen aus. Das Skript legt bei neuen Installationen kein Lookup mehr an.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "dynamic-poll-web-part",
|
||||
"title": "dynamic-poll-web-part",
|
||||
"id": "900239c9-b6c7-495d-a386-2d08a81a66f3",
|
||||
"version": "1.1.0.0",
|
||||
"version": "1.3.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ try {
|
||||
}
|
||||
|
||||
Ensure-Field -List $polls -InternalName 'Question' -SchemaXml '<Field Type="Text" DisplayName="Question" Name="Question" StaticName="Question" Required="TRUE" />'
|
||||
Ensure-Field -List $polls -InternalName 'PollKey' -SchemaXml '<Field Type="Text" DisplayName="Poll key" Name="PollKey" StaticName="PollKey" Required="FALSE" MaxLength="255" EnforceUniqueValues="TRUE" Indexed="TRUE" />'
|
||||
|
||||
$legacyOptionsField = $null
|
||||
try { $legacyOptionsField = $polls.Fields.GetFieldByInternalName('Options') } catch { }
|
||||
@@ -71,6 +72,13 @@ try {
|
||||
Ensure-Field -List $polls -InternalName 'EndDate' -SchemaXml '<Field Type="DateTime" DisplayName="End date" Name="EndDate" StaticName="EndDate" Format="DateTime" />'
|
||||
Ensure-Field -List $polls -InternalName 'IsActive' -SchemaXml '<Field Type="Boolean" DisplayName="Active" Name="IsActive" StaticName="IsActive"><Default>0</Default></Field>'
|
||||
|
||||
foreach ($item in $polls.Items) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$item['PollKey'])) {
|
||||
$item['PollKey'] = ('Poll-{0}' -f $item.ID)
|
||||
$item.SystemUpdate($false)
|
||||
}
|
||||
}
|
||||
|
||||
$answers = $web.Lists.TryGetList($AnswerListTitle)
|
||||
if (-not $answers) {
|
||||
$listId = $web.Lists.Add($AnswerListTitle, 'Votes submitted through the Dynamic Poll web part.', [Microsoft.SharePoint.SPListTemplateType]::GenericList)
|
||||
@@ -79,10 +87,41 @@ try {
|
||||
$answers.Update()
|
||||
}
|
||||
|
||||
$pollListId = $polls.ID.ToString('B')
|
||||
Ensure-Field -List $answers -InternalName 'Poll' -SchemaXml (('<Field Type="Lookup" DisplayName="Poll" Name="Poll" StaticName="Poll" List="{0}" ShowField="Title" Required="TRUE" />' -f $pollListId))
|
||||
Ensure-Field -List $answers -InternalName 'PollKey' -SchemaXml '<Field Type="Text" DisplayName="Poll key" Name="PollKey" StaticName="PollKey" Required="FALSE" MaxLength="255" Indexed="TRUE" />'
|
||||
Ensure-Field -List $answers -InternalName 'UserKey' -SchemaXml '<Field Type="Text" DisplayName="User key" Name="UserKey" StaticName="UserKey" Required="FALSE" MaxLength="255" Indexed="TRUE" />'
|
||||
Ensure-Field -List $answers -InternalName 'Answer' -SchemaXml '<Field Type="Text" DisplayName="Answer" Name="Answer" StaticName="Answer" Required="TRUE" MaxLength="255" />'
|
||||
|
||||
# A lookup cannot be distributed safely through the Content Type Hub because its target list ID
|
||||
# belongs to one site collection. Keep an existing legacy lookup readable, but stop requiring it.
|
||||
$legacyPollField = $null
|
||||
try { $legacyPollField = $answers.Fields.GetFieldByInternalName('Poll') } catch { }
|
||||
if ($legacyPollField -and $legacyPollField.Type -eq [Microsoft.SharePoint.SPFieldType]::Lookup) {
|
||||
$legacyPollField.Required = $false
|
||||
$legacyPollField.ShowInNewForm = $false
|
||||
$legacyPollField.ShowInEditForm = $false
|
||||
$legacyPollField.ShowInDisplayForm = $false
|
||||
$legacyPollField.Update()
|
||||
|
||||
}
|
||||
|
||||
foreach ($answerItem in $answers.Items) {
|
||||
$changed = $false
|
||||
if ($legacyPollField -and $legacyPollField.Type -eq [Microsoft.SharePoint.SPFieldType]::Lookup) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$answerItem['PollKey']) -and $null -ne $answerItem[$legacyPollField.Id]) {
|
||||
$lookup = New-Object Microsoft.SharePoint.SPFieldLookupValue([string]$answerItem[$legacyPollField.Id])
|
||||
$pollItem = $polls.GetItemById($lookup.LookupId)
|
||||
$answerItem['PollKey'] = [string]$pollItem['PollKey']
|
||||
$changed = $true
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace([string]$answerItem['UserKey']) -and
|
||||
-not [string]::IsNullOrWhiteSpace([string]$answerItem['Title'])) {
|
||||
$answerItem['UserKey'] = [string]$answerItem['Title']
|
||||
$changed = $true
|
||||
}
|
||||
if ($changed) { $answerItem.SystemUpdate($false) }
|
||||
}
|
||||
|
||||
Write-Host ('Dynamic Poll lists are ready at {0}' -f $web.Url) -ForegroundColor Green
|
||||
Write-Host ('Poll list: {0} ({1})' -f $polls.Title, $polls.ID)
|
||||
Write-Host ('Answer list: {0} ({1})' -f $answers.Title, $answers.ID)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dynamic-poll-web-part",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": { "node": ">=6.9.0 <9.0.0" },
|
||||
|
||||
@@ -16,8 +16,19 @@
|
||||
},
|
||||
"officeFabricIconFontName": "PollResults",
|
||||
"properties": {
|
||||
"title": "Dynamic Poll",
|
||||
"pollListId": "",
|
||||
"answerListId": "",
|
||||
"pollKeyField": "",
|
||||
"pollTitleField": "",
|
||||
"pollQuestionField": "",
|
||||
"pollOptionsField": "",
|
||||
"pollStartField": "",
|
||||
"pollEndField": "",
|
||||
"pollActiveField": "",
|
||||
"answerPollKeyField": "",
|
||||
"answerUserKeyField": "",
|
||||
"answerValueField": "",
|
||||
"showResultsBeforeVote": false,
|
||||
"displayMode": "slider",
|
||||
"refreshSeconds": 60
|
||||
|
||||
@@ -3,17 +3,28 @@ import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart, IPropertyPaneConfiguration, IPropertyPaneDropdownOption,
|
||||
PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle
|
||||
PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneTextField, PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'DynamicPollWebPartStrings';
|
||||
import DynamicPollCollection, { IDynamicPollCollectionProps } from './components/DynamicPollCollection';
|
||||
import styles from './components/DynamicPoll.module.scss';
|
||||
import { PollDataService } from './PollDataService';
|
||||
import { IListOption, IPollSnapshot, PollDisplayMode } from './PollTypes';
|
||||
import { IListFieldOption, IListOption, IPollFieldMapping, IPollSnapshot, PollDisplayMode } from './PollTypes';
|
||||
|
||||
export interface IDynamicPollWebPartProps {
|
||||
title: string;
|
||||
pollListId: string;
|
||||
answerListId: string;
|
||||
pollKeyField: string;
|
||||
pollTitleField: string;
|
||||
pollQuestionField: string;
|
||||
pollOptionsField: string;
|
||||
pollStartField: string;
|
||||
pollEndField: string;
|
||||
pollActiveField: string;
|
||||
answerPollKeyField: string;
|
||||
answerUserKeyField: string;
|
||||
answerValueField: string;
|
||||
showResultsBeforeVote: boolean;
|
||||
refreshSeconds: number;
|
||||
displayMode: PollDisplayMode;
|
||||
@@ -22,7 +33,11 @@ export interface IDynamicPollWebPartProps {
|
||||
export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPollWebPartProps> {
|
||||
private service: PollDataService;
|
||||
private lists: IListOption[] = [];
|
||||
private pollFields: IListFieldOption[] = [];
|
||||
private answerFields: IListFieldOption[] = [];
|
||||
private loadingLists: boolean = false;
|
||||
private loadingPollFields: boolean = false;
|
||||
private loadingAnswerFields: boolean = false;
|
||||
private requestVersion: number = 0;
|
||||
private refreshHandle: number | undefined;
|
||||
private currentRefreshSeconds: number = 0;
|
||||
@@ -30,7 +45,13 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
this.service = new PollDataService(this.context);
|
||||
return super.onInit().then((): Promise<void> => this.loadLists());
|
||||
return super.onInit().then((): Promise<void> => this.loadLists())
|
||||
.then((): Promise<void> => {
|
||||
const requests: Array<Promise<void>> = [];
|
||||
if (this.properties.pollListId) { requests.push(this.loadFields(this.properties.pollListId, true)); }
|
||||
if (this.properties.answerListId) { requests.push(this.loadFields(this.properties.answerListId, false)); }
|
||||
return Promise.all(requests).then((): void => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
@@ -49,6 +70,24 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
|
||||
protected onPropertyPaneConfigurationStart(): void {
|
||||
if (!this.lists.length && !this.loadingLists) { this.loadLists(); }
|
||||
if (this.properties.pollListId && !this.pollFields.length && !this.loadingPollFields) {
|
||||
this.loadFields(this.properties.pollListId, true);
|
||||
}
|
||||
if (this.properties.answerListId && !this.answerFields.length && !this.loadingAnswerFields) {
|
||||
this.loadFields(this.properties.answerListId, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { // tslint:disable-line:no-any
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
if (newValue === oldValue) { return; }
|
||||
if (propertyPath === 'pollListId') {
|
||||
this.resetPollMappings();
|
||||
this.loadFields(String(newValue || ''), true);
|
||||
} else if (propertyPath === 'answerListId') {
|
||||
this.resetAnswerMappings();
|
||||
this.loadFields(String(newValue || ''), false);
|
||||
}
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
@@ -56,14 +95,43 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
for (let i: number = 0; i < this.lists.length; i++) {
|
||||
options.push({ key: this.lists[i].id, text: this.lists[i].title });
|
||||
}
|
||||
const pollKeyOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.pollFields,
|
||||
['Text', 'Number', 'Integer', 'Counter', 'Computed'], false);
|
||||
const pollTextOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.pollFields,
|
||||
['Text', 'Note', 'Choice', 'Computed'], false);
|
||||
const pollOptionsOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.pollFields,
|
||||
['Text', 'Note', 'Choice', 'MultiChoice'], false);
|
||||
const pollDateOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.pollFields, ['DateTime'], false);
|
||||
const pollBooleanOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.pollFields, ['Boolean'], false);
|
||||
const answerPollKeyOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.answerFields,
|
||||
['Text', 'Note', 'Lookup', 'Number', 'Integer'], true);
|
||||
const answerUserOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.answerFields,
|
||||
['Text', 'Note'], true);
|
||||
const answerValueOptions: IPropertyPaneDropdownOption[] = this.typedFieldOptions(this.answerFields,
|
||||
['Text', 'Note', 'Choice'], true);
|
||||
return { pages: [{
|
||||
header: { description: strings.PropertyPaneDescription },
|
||||
groups: [{
|
||||
groupName: strings.DataGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('title', { label: strings.TitleFieldLabel }),
|
||||
PropertyPaneDropdown('pollListId', { label: strings.PollListLabel, options: options, disabled: this.loadingLists }),
|
||||
PropertyPaneDropdown('answerListId', { label: strings.AnswerListLabel, options: options, disabled: this.loadingLists })
|
||||
]
|
||||
}, {
|
||||
groupName: strings.FieldMappingGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('pollKeyField', { label: strings.PollKeyFieldLabel, options: pollKeyOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollTitleField', { label: strings.PollTitleFieldLabel, options: pollTextOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollQuestionField', { label: strings.PollQuestionFieldLabel, options: pollTextOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollOptionsField', { label: strings.PollOptionsFieldLabel, options: pollOptionsOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollStartField', { label: strings.PollStartFieldLabel, options: pollDateOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollEndField', { label: strings.PollEndFieldLabel, options: pollDateOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('pollActiveField', { label: strings.PollActiveFieldLabel, options: pollBooleanOptions, disabled: this.loadingPollFields }),
|
||||
PropertyPaneDropdown('answerPollKeyField', { label: strings.AnswerPollKeyFieldLabel, options: answerPollKeyOptions, disabled: this.loadingAnswerFields }),
|
||||
PropertyPaneDropdown('answerUserKeyField', { label: strings.AnswerUserKeyFieldLabel, options: answerUserOptions, disabled: this.loadingAnswerFields }),
|
||||
PropertyPaneDropdown('answerValueField', { label: strings.AnswerValueFieldLabel, options: answerValueOptions, disabled: this.loadingAnswerFields })
|
||||
]
|
||||
}, {
|
||||
groupName: strings.BehaviorGroupName,
|
||||
groupFields: [
|
||||
@@ -90,7 +158,7 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (showLoading) { this.renderStatus(strings.LoadingMessage, false); }
|
||||
return this.service.getSnapshots(this.properties.pollListId, this.properties.answerListId, this.userKey)
|
||||
return this.service.getSnapshots(this.properties.pollListId, this.properties.answerListId, this.userKey, this.fieldMapping)
|
||||
.then((snapshots: IPollSnapshot[]): void => {
|
||||
if (this.disposed || version !== this.requestVersion) { return; }
|
||||
if (!snapshots.length) { this.renderStatus(strings.NoPollMessage, false); return; }
|
||||
@@ -104,6 +172,7 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
pollNavigationLabel: strings.PollNavigationLabel
|
||||
};
|
||||
const element: React.ReactElement<IDynamicPollCollectionProps> = React.createElement(DynamicPollCollection, {
|
||||
title: this.properties.title || strings.DefaultTitle,
|
||||
snapshots: snapshots,
|
||||
displayMode: this.properties.displayMode === 'accordion' ? 'accordion' : 'slider',
|
||||
showResultsBeforeVote: this.properties.showResultsBeforeVote === true,
|
||||
@@ -119,7 +188,8 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
}
|
||||
|
||||
private vote(pollId: number, answer: string): Promise<void> {
|
||||
return this.service.vote(this.properties.pollListId, this.properties.answerListId, pollId, answer, this.userKey)
|
||||
return this.service.vote(this.properties.pollListId, this.properties.answerListId, pollId, answer,
|
||||
this.userKey, this.fieldMapping)
|
||||
.then((): Promise<void> => this.loadSnapshot(false));
|
||||
}
|
||||
|
||||
@@ -136,6 +206,80 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
});
|
||||
}
|
||||
|
||||
private loadFields(listId: string, pollList: boolean): Promise<void> {
|
||||
if (!listId) {
|
||||
if (pollList) { this.pollFields = []; } else { this.answerFields = []; }
|
||||
this.refreshPropertyPane();
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (pollList) { this.loadingPollFields = true; } else { this.loadingAnswerFields = true; }
|
||||
this.refreshPropertyPane();
|
||||
return this.service.getFields(listId).then((fields: IListFieldOption[]): void => {
|
||||
if (pollList) { this.pollFields = fields; this.applyPollDefaults(); }
|
||||
else { this.answerFields = fields; this.applyAnswerDefaults(); }
|
||||
}).catch((error: Error): void => {
|
||||
if (pollList) { this.pollFields = []; } else { this.answerFields = []; }
|
||||
if (window.console) { console.error('[DynamicPoll] Fields could not be loaded.', error); }
|
||||
}).then((): void => {
|
||||
if (pollList) { this.loadingPollFields = false; } else { this.loadingAnswerFields = false; }
|
||||
this.refreshPropertyPane();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private applyPollDefaults(): void {
|
||||
this.properties.pollKeyField = this.validOrDefault(this.properties.pollKeyField, this.pollFields, ['PollKey', 'ID']);
|
||||
this.properties.pollTitleField = this.validOrDefault(this.properties.pollTitleField, this.pollFields, ['Title'], true);
|
||||
this.properties.pollQuestionField = this.validOrDefault(this.properties.pollQuestionField, this.pollFields, ['Question', 'Title']);
|
||||
this.properties.pollOptionsField = this.validOrDefault(this.properties.pollOptionsField, this.pollFields, ['OptionsText', 'Options']);
|
||||
this.properties.pollStartField = this.validOrDefault(this.properties.pollStartField, this.pollFields, ['StartDate'], true);
|
||||
this.properties.pollEndField = this.validOrDefault(this.properties.pollEndField, this.pollFields, ['EndDate'], true);
|
||||
this.properties.pollActiveField = this.validOrDefault(this.properties.pollActiveField, this.pollFields, ['IsActive']);
|
||||
}
|
||||
|
||||
private applyAnswerDefaults(): void {
|
||||
this.properties.answerPollKeyField = this.validOrDefault(this.properties.answerPollKeyField,
|
||||
this.answerFields, ['PollKey', 'Poll']);
|
||||
this.properties.answerUserKeyField = this.validOrDefault(this.properties.answerUserKeyField,
|
||||
this.answerFields, ['UserKey', 'Title']);
|
||||
this.properties.answerValueField = this.validOrDefault(this.properties.answerValueField, this.answerFields, ['Answer']);
|
||||
}
|
||||
|
||||
private validOrDefault(current: string, fields: IListFieldOption[], candidates: string[], optional?: boolean): string {
|
||||
const names: string[] = fields.map((field: IListFieldOption): string => field.internalName);
|
||||
if (current && names.indexOf(current) >= 0) { return current; }
|
||||
for (let i: number = 0; i < candidates.length; i++) {
|
||||
if (names.indexOf(candidates[i]) >= 0) { return candidates[i]; }
|
||||
}
|
||||
return optional ? '' : '';
|
||||
}
|
||||
|
||||
private fieldOptions(fields: IListFieldOption[], required: boolean): IPropertyPaneDropdownOption[] {
|
||||
const options: IPropertyPaneDropdownOption[] = required ? [] : [{ key: '', text: strings.NoFieldOption }];
|
||||
for (let i: number = 0; i < fields.length; i++) {
|
||||
options.push({ key: fields[i].internalName, text: fields[i].title + ' (' + fields[i].internalName + ')' });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private typedFieldOptions(fields: IListFieldOption[], types: string[], writable: boolean): IPropertyPaneDropdownOption[] {
|
||||
return this.fieldOptions(fields.filter((field: IListFieldOption): boolean =>
|
||||
types.indexOf(field.type) >= 0 && (!writable || !field.readOnly)), false);
|
||||
}
|
||||
|
||||
private resetPollMappings(): void {
|
||||
this.pollFields = [];
|
||||
this.properties.pollKeyField = ''; this.properties.pollTitleField = '';
|
||||
this.properties.pollQuestionField = ''; this.properties.pollOptionsField = '';
|
||||
this.properties.pollStartField = ''; this.properties.pollEndField = ''; this.properties.pollActiveField = '';
|
||||
}
|
||||
|
||||
private resetAnswerMappings(): void {
|
||||
this.answerFields = [];
|
||||
this.properties.answerPollKeyField = ''; this.properties.answerUserKeyField = '';
|
||||
this.properties.answerValueField = '';
|
||||
}
|
||||
|
||||
private ensureRefreshTimer(): void {
|
||||
const seconds: number = this.normalizedRefreshSeconds();
|
||||
if (seconds === this.currentRefreshSeconds && (seconds === 0 || this.refreshHandle !== undefined)) { return; }
|
||||
@@ -158,7 +302,22 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart<IDynamicPo
|
||||
return String(user.email || user.loginName || user.displayName || '').trim();
|
||||
}
|
||||
private isConfigured(): boolean {
|
||||
return !!(this.service && this.properties.pollListId && this.properties.answerListId && this.userKey);
|
||||
return !!(this.service && this.properties.pollListId && this.properties.answerListId && this.userKey &&
|
||||
this.properties.pollKeyField && this.properties.pollQuestionField && this.properties.pollOptionsField &&
|
||||
this.properties.pollActiveField && this.properties.answerPollKeyField &&
|
||||
this.properties.answerUserKeyField && this.properties.answerValueField);
|
||||
}
|
||||
private get fieldMapping(): IPollFieldMapping {
|
||||
const answerKey: IListFieldOption = this.answerFields.filter((field: IListFieldOption): boolean =>
|
||||
field.internalName === this.properties.answerPollKeyField)[0];
|
||||
return {
|
||||
pollKeyField: this.properties.pollKeyField || '', pollTitleField: this.properties.pollTitleField || '',
|
||||
pollQuestionField: this.properties.pollQuestionField || '', pollOptionsField: this.properties.pollOptionsField || '',
|
||||
pollStartField: this.properties.pollStartField || '', pollEndField: this.properties.pollEndField || '',
|
||||
pollActiveField: this.properties.pollActiveField || '', answerPollKeyField: this.properties.answerPollKeyField || '',
|
||||
answerPollKeyFieldType: answerKey ? answerKey.type : 'Text',
|
||||
answerUserKeyField: this.properties.answerUserKeyField || '', answerValueField: this.properties.answerValueField || ''
|
||||
};
|
||||
}
|
||||
private refreshPropertyPane(): void {
|
||||
if (this.context && this.context.propertyPane) { this.context.propertyPane.refresh(); }
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePolls } from './PollCore';
|
||||
import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes';
|
||||
import { IListFieldOption, IListOption, IPoll, IPollAnswer, IPollFieldMapping, IPollSnapshot } from './PollTypes';
|
||||
|
||||
export class PollDataService {
|
||||
private optionsFieldNames: { [listId: string]: string } = {};
|
||||
|
||||
public constructor(private context: WebPartContext) { }
|
||||
|
||||
public getLists(): Promise<IListOption[]> {
|
||||
@@ -15,42 +13,58 @@ export class PollDataService {
|
||||
})));
|
||||
}
|
||||
|
||||
public getSnapshots(pollListId: string, answerListId: string, userKey: string): Promise<IPollSnapshot[]> {
|
||||
return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<IPollSnapshot[]> => {
|
||||
const requests: Array<Promise<IPollSnapshot>> = polls.map((poll: IPoll): Promise<IPollSnapshot> =>
|
||||
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)
|
||||
};
|
||||
}));
|
||||
return Promise.all(requests);
|
||||
});
|
||||
public getFields(listId: string): Promise<IListFieldOption[]> {
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/fields" +
|
||||
'?$select=InternalName,Title,TypeAsString,ReadOnlyField,Hidden&$filter=Hidden eq false&$orderby=Title';
|
||||
return this.getJson(url).then((payload: any): IListFieldOption[] => this.results(payload) // tslint:disable-line:no-any
|
||||
.map((field: any): IListFieldOption => ({ // tslint:disable-line:no-any
|
||||
internalName: String(field.InternalName || ''),
|
||||
title: String(field.Title || field.InternalName || ''),
|
||||
type: String(field.TypeAsString || ''),
|
||||
readOnly: field.ReadOnlyField === true
|
||||
})).filter((field: IListFieldOption): boolean => !!field.internalName));
|
||||
}
|
||||
|
||||
public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise<void> {
|
||||
return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise<void> => {
|
||||
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 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
|
||||
public getSnapshots(pollListId: string, answerListId: string, userKey: string,
|
||||
mapping: IPollFieldMapping): Promise<IPollSnapshot[]> {
|
||||
return Promise.all([this.getActivePolls(pollListId, mapping), this.getAnswers(answerListId, mapping)])
|
||||
.then((values: [IPoll[], IPollAnswer[]]): IPollSnapshot[] => values[0].map((poll: IPoll): IPollSnapshot => {
|
||||
const answers: IPollAnswer[] = values[1].filter((candidate: IPollAnswer): boolean =>
|
||||
candidate.pollKey.toLocaleLowerCase() === poll.key.toLocaleLowerCase());
|
||||
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, mapping: IPollFieldMapping): Promise<void> {
|
||||
return this.getActivePolls(pollListId, mapping).then((polls: IPoll[]): Promise<void> => {
|
||||
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 this.saveVote(answerListId, poll, answer, userKey, mapping);
|
||||
});
|
||||
}
|
||||
|
||||
private saveVote(answerListId: string, poll: IPoll, answer: string, userKey: string,
|
||||
mapping: IPollFieldMapping): Promise<void> {
|
||||
return this.getAnswers(answerListId, mapping).then((answers: IPollAnswer[]): Promise<void> => {
|
||||
const pollAnswers: IPollAnswer[] = answers.filter((candidate: IPollAnswer): boolean =>
|
||||
candidate.pollKey.toLocaleLowerCase() === poll.key.toLocaleLowerCase());
|
||||
if (findUserAnswer(pollAnswers, userKey)) { return Promise.reject(new Error('ALREADY_VOTED')); }
|
||||
return this.getListEntityType(answerListId).then((entityType: string): Promise<void> => {
|
||||
const body: any = { '__metadata': { 'type': entityType } }; // tslint:disable-line:no-any
|
||||
body[mapping.answerUserKeyField] = userKey;
|
||||
body[mapping.answerValueField] = answer;
|
||||
if (mapping.answerPollKeyFieldType === 'Lookup') {
|
||||
body[mapping.answerPollKeyField + 'Id'] = poll.id;
|
||||
} else {
|
||||
body[mapping.answerPollKeyField] = poll.key;
|
||||
}
|
||||
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)
|
||||
@@ -59,47 +73,40 @@ export class PollDataService {
|
||||
});
|
||||
}
|
||||
|
||||
private getActivePolls(listId: string): Promise<IPoll[]> {
|
||||
return this.getOptionsFieldName(listId).then((optionsField: string): Promise<IPoll[]> => {
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
|
||||
'?$select=Id,Title,Question,' + optionsField +
|
||||
',StartDate,EndDate,IsActive&$filter=IsActive eq 1&$orderby=StartDate desc&$top=100';
|
||||
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
|
||||
id: Number(item.Id),
|
||||
title: String(item.Title || ''),
|
||||
question: String(item.Question || item.Title || ''),
|
||||
options: this.toStringArray(item[optionsField]),
|
||||
startDate: this.toDate(item.StartDate),
|
||||
endDate: this.toDate(item.EndDate),
|
||||
isActive: item.IsActive === true || item.IsActive === 1
|
||||
}));
|
||||
return selectActivePolls(polls, new Date());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getOptionsFieldName(listId: string): Promise<string> {
|
||||
const cleanListId: string = this.validateGuid(listId);
|
||||
if (this.optionsFieldNames[cleanListId]) { return Promise.resolve(this.optionsFieldNames[cleanListId]); }
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + cleanListId + "')/fields" +
|
||||
'?$select=InternalName';
|
||||
return this.getJson(url).then((payload: any): string => { // tslint:disable-line:no-any
|
||||
const names: string[] = this.results(payload).map((field: any): string => String(field.InternalName || '')); // tslint:disable-line:no-any
|
||||
const fieldName: string = names.indexOf('OptionsText') >= 0 ? 'OptionsText' :
|
||||
(names.indexOf('Options') >= 0 ? 'Options' : '');
|
||||
if (!fieldName) { throw new Error('The poll options field is missing.'); }
|
||||
this.optionsFieldNames[cleanListId] = fieldName;
|
||||
return fieldName;
|
||||
});
|
||||
}
|
||||
|
||||
private getAnswers(listId: string, pollId: number): Promise<IPollAnswer[]> {
|
||||
private getActivePolls(listId: string, mapping: IPollFieldMapping): Promise<IPoll[]> {
|
||||
const fields: string[] = this.uniqueFields(['Id', mapping.pollKeyField, mapping.pollTitleField,
|
||||
mapping.pollQuestionField, mapping.pollOptionsField, mapping.pollStartField,
|
||||
mapping.pollEndField, mapping.pollActiveField]);
|
||||
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';
|
||||
'?$select=' + fields.join(',') + '&$top=5000';
|
||||
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
|
||||
id: Number(item.Id),
|
||||
key: this.readKey(item, mapping.pollKeyField),
|
||||
title: this.readText(item, mapping.pollTitleField),
|
||||
question: this.readText(item, mapping.pollQuestionField) || this.readText(item, mapping.pollTitleField),
|
||||
options: this.toStringArray(item[mapping.pollOptionsField]),
|
||||
startDate: this.toDate(item[mapping.pollStartField]),
|
||||
endDate: this.toDate(item[mapping.pollEndField]),
|
||||
isActive: this.toBoolean(item[mapping.pollActiveField])
|
||||
})).filter((poll: IPoll): boolean => !!poll.key && !!poll.question && poll.options.length > 0);
|
||||
return selectActivePolls(polls, new Date());
|
||||
});
|
||||
}
|
||||
|
||||
private getAnswers(listId: string, mapping: IPollFieldMapping): Promise<IPollAnswer[]> {
|
||||
const keySelect: string = mapping.answerPollKeyFieldType === 'Lookup' ? mapping.answerPollKeyField + 'Id' :
|
||||
mapping.answerPollKeyField;
|
||||
const fields: string[] = this.uniqueFields(['Id', keySelect, mapping.answerUserKeyField, mapping.answerValueField]);
|
||||
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" +
|
||||
'?$select=' + fields.join(',') + '&$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 || '')
|
||||
})));
|
||||
id: Number(item.Id),
|
||||
pollKey: mapping.answerPollKeyFieldType === 'Lookup' ? String(item[keySelect] || '') :
|
||||
this.readKey(item, mapping.answerPollKeyField),
|
||||
userKey: this.readText(item, mapping.answerUserKeyField),
|
||||
answer: this.readText(item, mapping.answerValueField)
|
||||
})).filter((item: IPollAnswer): boolean => !!item.pollKey && !!item.userKey));
|
||||
}
|
||||
|
||||
private getListEntityType(listId: string): Promise<string> {
|
||||
@@ -128,16 +135,32 @@ export class PollDataService {
|
||||
const source: any = payload && payload.d !== undefined ? payload.d : payload; // tslint:disable-line:no-any
|
||||
return source && Array.isArray(source.results) ? source.results : [];
|
||||
}
|
||||
private readKey(item: any, fieldName: string): string { // tslint:disable-line:no-any
|
||||
if (!fieldName) { return ''; }
|
||||
const value: any = fieldName.toLocaleUpperCase() === 'ID' ? item.Id : item[fieldName]; // tslint:disable-line:no-any
|
||||
return String(value && (value.Name || value.LoginName || value.EMail || value.LookupValue || value.Title || value.Id) || value || '').trim();
|
||||
}
|
||||
private readText(item: any, fieldName: string): string { // tslint:disable-line:no-any
|
||||
if (!fieldName) { return ''; }
|
||||
const value: any = item[fieldName]; // tslint:disable-line:no-any
|
||||
return String(value && (value.Title || value.LookupValue || value.Description || value.Url || value.Name) || value || '').trim();
|
||||
}
|
||||
private toStringArray(value: any): string[] { // tslint:disable-line:no-any
|
||||
const source: any[] = value && Array.isArray(value.results) ? value.results : // tslint:disable-line:no-any
|
||||
(Array.isArray(value) ? value : [value]);
|
||||
return parsePollOptionLines(source.map((entry: any): string => String(entry || ''))); // tslint:disable-line:no-any
|
||||
}
|
||||
private toBoolean(value: any): boolean { // tslint:disable-line:no-any
|
||||
return value === true || value === 1 || String(value || '').toLowerCase() === 'true' || String(value) === '1';
|
||||
}
|
||||
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 uniqueFields(fields: string[]): string[] {
|
||||
return fields.filter((field: string, index: number): boolean => !!field && fields.indexOf(field) === index);
|
||||
}
|
||||
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.'); }
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
export interface IListOption { id: string; title: string; }
|
||||
export interface IListFieldOption { internalName: string; title: string; type: string; readOnly: boolean; }
|
||||
export interface IPollFieldMapping {
|
||||
pollKeyField: string;
|
||||
pollTitleField: string;
|
||||
pollQuestionField: string;
|
||||
pollOptionsField: string;
|
||||
pollStartField: string;
|
||||
pollEndField: string;
|
||||
pollActiveField: string;
|
||||
answerPollKeyField: string;
|
||||
answerPollKeyFieldType: string;
|
||||
answerUserKeyField: string;
|
||||
answerValueField: string;
|
||||
}
|
||||
export interface IPoll {
|
||||
id: number;
|
||||
key: string;
|
||||
title: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
@@ -8,7 +23,7 @@ export interface IPoll {
|
||||
endDate?: Date;
|
||||
isActive: boolean;
|
||||
}
|
||||
export interface IPollAnswer { id: number; userKey: string; answer: string; }
|
||||
export interface IPollAnswer { id: number; pollKey: string; userKey: string; answer: string; }
|
||||
export interface IPollResult { option: string; votes: number; percentage: number; }
|
||||
export interface IPollSnapshot {
|
||||
poll: IPoll;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.webPart { color: "[theme: neutralPrimary, default: #323130]"; font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif; }
|
||||
.webPartHeading { font-size: 24px; font-weight: 300; line-height: 32px; margin: 0 0 14px; }
|
||||
|
||||
.poll {
|
||||
background: "[theme: white, default: #ffffff]";
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IDynamicPollStrings } from './IDynamicPollProps';
|
||||
import styles from './DynamicPoll.module.scss';
|
||||
|
||||
export interface IDynamicPollCollectionProps {
|
||||
title: string;
|
||||
displayMode: PollDisplayMode;
|
||||
snapshots: IPollSnapshot[];
|
||||
showResultsBeforeVote: boolean;
|
||||
@@ -29,7 +30,10 @@ export default class DynamicPollCollection
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return this.props.displayMode === 'accordion' ? this.renderAccordion() : this.renderSlider();
|
||||
return <section className={styles.webPart} aria-label={this.props.title}>
|
||||
{this.props.title && <h2 className={styles.webPartHeading}>{this.props.title}</h2>}
|
||||
{this.props.displayMode === 'accordion' ? this.renderAccordion() : this.renderSlider()}
|
||||
</section>;
|
||||
}
|
||||
|
||||
private renderAccordion(): JSX.Element {
|
||||
|
||||
@@ -2,8 +2,22 @@ define([], function() { return {
|
||||
"PropertyPaneDescription": "Wählen Sie die Umfrage- und Antwortliste aus.",
|
||||
"DataGroupName": "Datenquelle",
|
||||
"BehaviorGroupName": "Verhalten",
|
||||
"TitleFieldLabel": "WebPart-Titel",
|
||||
"DefaultTitle": "Umfrage",
|
||||
"PollListLabel": "Umfrageliste",
|
||||
"AnswerListLabel": "Antwortliste",
|
||||
"FieldMappingGroupName": "Spaltenzuordnung",
|
||||
"NoFieldOption": "Nicht verwendet",
|
||||
"PollKeyFieldLabel": "Umfrage: stabiler Schlüssel",
|
||||
"PollTitleFieldLabel": "Umfrage: Titel (optional)",
|
||||
"PollQuestionFieldLabel": "Umfrage: Frage",
|
||||
"PollOptionsFieldLabel": "Umfrage: Antwortoptionen",
|
||||
"PollStartFieldLabel": "Umfrage: Startdatum (optional)",
|
||||
"PollEndFieldLabel": "Umfrage: Enddatum (optional)",
|
||||
"PollActiveFieldLabel": "Umfrage: Aktiv",
|
||||
"AnswerPollKeyFieldLabel": "Antwort: Umfrageschlüssel",
|
||||
"AnswerUserKeyFieldLabel": "Antwort: Benutzerschlüssel",
|
||||
"AnswerValueFieldLabel": "Antwort: ausgewählte Option",
|
||||
"ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen",
|
||||
"DisplayModeLabel": "Mehrere aktive Umfragen anzeigen als",
|
||||
"AccordionModeLabel": "Accordion",
|
||||
|
||||
@@ -2,8 +2,22 @@ define([], function() { return {
|
||||
"PropertyPaneDescription": "Select the poll and answer lists.",
|
||||
"DataGroupName": "Data source",
|
||||
"BehaviorGroupName": "Behavior",
|
||||
"TitleFieldLabel": "Web part title",
|
||||
"DefaultTitle": "Dynamic Poll",
|
||||
"PollListLabel": "Poll list",
|
||||
"AnswerListLabel": "Answer list",
|
||||
"FieldMappingGroupName": "Column mapping",
|
||||
"NoFieldOption": "Not used",
|
||||
"PollKeyFieldLabel": "Poll: stable key",
|
||||
"PollTitleFieldLabel": "Poll: title (optional)",
|
||||
"PollQuestionFieldLabel": "Poll: question",
|
||||
"PollOptionsFieldLabel": "Poll: answer options",
|
||||
"PollStartFieldLabel": "Poll: start date (optional)",
|
||||
"PollEndFieldLabel": "Poll: end date (optional)",
|
||||
"PollActiveFieldLabel": "Poll: active",
|
||||
"AnswerPollKeyFieldLabel": "Answer: poll key",
|
||||
"AnswerUserKeyFieldLabel": "Answer: user key",
|
||||
"AnswerValueFieldLabel": "Answer: selected option",
|
||||
"ShowResultsBeforeVoteLabel": "Show results before voting",
|
||||
"DisplayModeLabel": "Show multiple active polls as",
|
||||
"AccordionModeLabel": "Accordion",
|
||||
|
||||
+6
@@ -1,6 +1,12 @@
|
||||
declare interface IDynamicPollWebPartStrings {
|
||||
PropertyPaneDescription: string; DataGroupName: string; BehaviorGroupName: string;
|
||||
TitleFieldLabel: string; DefaultTitle: string;
|
||||
PollListLabel: string; AnswerListLabel: string; ShowResultsBeforeVoteLabel: string;
|
||||
FieldMappingGroupName: string; NoFieldOption: string;
|
||||
PollKeyFieldLabel: string; PollTitleFieldLabel: string; PollQuestionFieldLabel: string;
|
||||
PollOptionsFieldLabel: string; PollStartFieldLabel: string; PollEndFieldLabel: string;
|
||||
PollActiveFieldLabel: string; AnswerPollKeyFieldLabel: string;
|
||||
AnswerUserKeyFieldLabel: string; AnswerValueFieldLabel: string;
|
||||
RefreshSecondsLabel: string; SelectOption: string; ConfigureMessage: string;
|
||||
LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string;
|
||||
AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string;
|
||||
|
||||
@@ -34,10 +34,10 @@ assert('all active polls are returned in activation order',
|
||||
activePolls.length === 2 && activePolls[0].id === 2 && activePolls[1].id === 4);
|
||||
|
||||
const answers: IPollAnswer[] = [
|
||||
{ id: 1, userKey: 'one@contoso.local', answer: 'A' },
|
||||
{ id: 2, userKey: 'two@contoso.local', answer: 'B' },
|
||||
{ id: 3, userKey: 'three@contoso.local', answer: 'A' },
|
||||
{ id: 4, userKey: 'legacy@contoso.local', answer: 'Removed option' }
|
||||
{ id: 1, pollKey: 'poll-1', userKey: 'one@contoso.local', answer: 'A' },
|
||||
{ id: 2, pollKey: 'poll-1', userKey: 'two@contoso.local', answer: 'B' },
|
||||
{ id: 3, pollKey: 'poll-1', userKey: 'three@contoso.local', answer: 'A' },
|
||||
{ id: 4, pollKey: 'poll-1', userKey: 'legacy@contoso.local', answer: 'Removed option' }
|
||||
];
|
||||
const results = calculatePollResults(['A', 'B'], answers);
|
||||
assert('votes are counted per configured option', results[0].votes === 2 && results[1].votes === 1);
|
||||
|
||||
@@ -13,9 +13,10 @@ var manifest = json('src/webparts/dynamicPoll/DynamicPollWebPart.manifest.json')
|
||||
var webPart = read('src/webparts/dynamicPoll/DynamicPollWebPart.ts');
|
||||
var service = read('src/webparts/dynamicPoll/PollDataService.ts');
|
||||
var component = read('src/webparts/dynamicPoll/components/DynamicPoll.tsx');
|
||||
var collection = read('src/webparts/dynamicPoll/components/DynamicPollCollection.tsx');
|
||||
var styles = read('src/webparts/dynamicPoll/components/DynamicPoll.module.scss');
|
||||
|
||||
assert(packageJson.version === '1.1.0' && solution.version === '1.1.0.0', 'Version mismatch.');
|
||||
assert(packageJson.version === '1.3.0' && solution.version === '1.3.0.0', 'Version mismatch.');
|
||||
assert(solution.includeClientSideAssets === true && solution.skipFeatureDeployment === true, 'Central deployment missing.');
|
||||
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.');
|
||||
@@ -24,17 +25,27 @@ assert(json('config/package-solution.json').paths.zippedPackage === 'solution/dy
|
||||
assert(manifest.componentType === 'WebPart', 'Component is not a web part.');
|
||||
assert(webPart.indexOf("PropertyPaneDropdown('pollListId'") >= 0 && webPart.indexOf("PropertyPaneDropdown('answerListId'") >= 0,
|
||||
'List selection is missing.');
|
||||
assert(webPart.indexOf("PropertyPaneDropdown('pollKeyField'") >= 0 &&
|
||||
webPart.indexOf("PropertyPaneDropdown('answerPollKeyField'") >= 0 &&
|
||||
webPart.indexOf("PropertyPaneDropdown('answerValueField'") >= 0, 'Column mapping is missing.');
|
||||
assert(webPart.indexOf("PropertyPaneTextField('title'") >= 0 &&
|
||||
collection.indexOf('styles.webPartHeading') >= 0,
|
||||
'Configurable web part title 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("body[mapping.answerPollKeyField]") >= 0 &&
|
||||
service.indexOf("body[mapping.answerValueField]") >= 0, 'Mapped vote persistence 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("'OptionsText'") >= 0 && service.indexOf('parsePollOptionLines') >= 0,
|
||||
assert(service.indexOf('mapping.pollOptionsField') >= 0 && service.indexOf('parsePollOptionLines') >= 0,
|
||||
'Multiline poll options are missing.');
|
||||
assert(read('deployment/Initialize-DynamicPollLists.ps1').indexOf('Type="Note" DisplayName="Options"') >= 0,
|
||||
'Multiline options field provisioning is missing.');
|
||||
assert(read('deployment/Initialize-DynamicPollLists.ps1').indexOf("InternalName 'PollKey'") >= 0 &&
|
||||
read('deployment/Initialize-DynamicPollLists.ps1').indexOf('Type="Lookup" DisplayName="Poll"') < 0,
|
||||
'Content Type Hub compatible text key provisioning is missing.');
|
||||
assert(component.indexOf("type='radio'") >= 0 && component.indexOf("role='progressbar'") >= 0,
|
||||
'Accessible voting or results UI is missing.');
|
||||
assert(fs.existsSync(path.join(root, 'deployment/Initialize-DynamicPollLists.ps1')), 'Provisioning script is missing.');
|
||||
|
||||
Reference in New Issue
Block a user