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:
Torsten Brendgen
2026-08-27 00:13:58 +02:00
parent f87c49a91a
commit 6bcf7c5020
15 changed files with 423 additions and 99 deletions
@@ -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
+165 -6
View File
@@ -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(); }
+98 -75
View File
@@ -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.'); }
+16 -1
View File
@@ -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 {
+14
View File
@@ -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",
+14
View File
@@ -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
View File
@@ -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;