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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user