refactor participants table (#164)

- Drops floatthead and additional scrollbar
- Makes header and first column sticky
- Refactors code for readability

Sticky header is only working in Firefox. Chrome and Edge does not support `position: sticky` for `<thead>`. Haven't tested Safari.
This commit is contained in:
jelhan 2019-04-20 23:29:59 +02:00 committed by GitHub
parent 6c122148c3
commit bb160cc503
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 697 additions and 723 deletions

View file

@ -1,6 +1,7 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { get, computed } from '@ember/object';
import { readOnly } from '@ember/object/computed';
import { isArray } from '@ember/array';
import { isPresent } from '@ember/utils';
import moment from 'moment';
@ -25,60 +26,7 @@ const addArrays = function() {
export default Component.extend({
i18n: service(),
type: 'bar',
data: computed('users.[]', 'options.{[],each.title}', 'currentLocale', function() {
let labels = this.options.map((option) => {
let value = get(option, 'title');
if (!this.isFindADate) {
return value;
}
let hasTime = value.length > 10; // 'YYYY-MM-DD'.length === 10
let momentFormat = hasTime ? 'LLLL' : this.momentLongDayFormat;
let timezone = this.timezone;
let date = hasTime && isPresent(timezone) ? moment.tz(value, timezone) : moment(value);
date.locale(this.currentLocale);
return date.format(momentFormat);
});
let datasets = [];
let participants = this.get('users.length');
let yes = this.users.map((user) => {
return user.get('selections').map((selection) => {
return selection.get('type') === 'yes' ? 1 : 0;
});
});
datasets.push({
label: this.i18n.t('answerTypes.yes.label').toString(),
backgroundColor: 'rgba(151,187,205,0.5)',
borderColor: 'rgba(151,187,205,0.8)',
hoverBackgroundColor: 'rgba(151,187,205,0.75)',
hoverBorderColor: 'rgba(151,187,205,1)',
data: addArrays.apply(this, yes).map((value) => Math.round(value / participants * 100))
});
if (this.answerType === 'YesNoMaybe') {
let maybe = this.users.map((user) => {
return user.get('selections').map((selection) => {
return selection.get('type') === 'maybe' ? 1 : 0;
});
});
datasets.push({
label: this.i18n.t('answerTypes.maybe.label').toString(),
backgroundColor: 'rgba(220,220,220,0.5)',
borderColor: 'rgba(220,220,220,0.8)',
hoverBackgroundColor: 'rgba(220,220,220,0.75)',
hoverBorderColor: 'rgba(220,220,220,1)',
data: addArrays.apply(this, maybe).map((value) => Math.round(value / participants * 100))
});
}
return {
datasets,
labels
};
}),
chartOptions: computed(function () {
return {
legend: {
@ -113,4 +61,60 @@ export default Component.extend({
}
}
}),
data: computed('users.[]', 'options.{[],each.title}', 'currentLocale', function() {
let labels = this.options.map((option) => {
let value = get(option, 'title');
if (!this.isFindADate) {
return value;
}
let hasTime = value.length > 10; // 'YYYY-MM-DD'.length === 10
let momentFormat = hasTime ? 'LLLL' : this.momentLongDayFormat;
let timezone = this.timezone;
let date = hasTime && isPresent(timezone) ? moment.tz(value, timezone) : moment(value);
date.locale(this.currentLocale);
return date.format(momentFormat);
});
let datasets = [];
let participants = this.users.length;
let yes = this.users.map(({ selections }) => {
return selections.map(({ type }) => type === 'yes' ? 1 : 0);
});
datasets.push({
label: this.i18n.t('answerTypes.yes.label').toString(),
backgroundColor: 'rgba(151,187,205,0.5)',
borderColor: 'rgba(151,187,205,0.8)',
hoverBackgroundColor: 'rgba(151,187,205,0.75)',
hoverBorderColor: 'rgba(151,187,205,1)',
data: addArrays.apply(this, yes).map((value) => Math.round(value / participants * 100))
});
if (this.answerType === 'YesNoMaybe') {
let maybe = this.users.map(({ selections }) => {
return selections.map(({ type }) => type === 'maybe' ? 1 : 0);
});
datasets.push({
label: this.i18n.t('answerTypes.maybe.label').toString(),
backgroundColor: 'rgba(220,220,220,0.5)',
borderColor: 'rgba(220,220,220,0.8)',
hoverBackgroundColor: 'rgba(220,220,220,0.75)',
hoverBorderColor: 'rgba(220,220,220,1)',
data: addArrays.apply(this, maybe).map((value) => Math.round(value / participants * 100))
});
}
return {
datasets,
labels
};
}),
answerType: readOnly('poll.answerType'),
currentLocale: readOnly('i18n.locale'),
isFindADate: readOnly('poll.isFindADate'),
options: readOnly('poll.options'),
users: readOnly('poll.users'),
});

View file

@ -1,173 +1,17 @@
import { observer } from '@ember/object';
import $ from 'jquery';
import { scheduleOnce, next } from '@ember/runloop';
import Component from '@ember/component';
import moment from 'moment';
import { groupBy } from 'ember-awesome-macros/array';
import { readOnly } from '@ember/object/computed';
import { raw } from 'ember-awesome-macros';
import { groupBy, sort } from 'ember-awesome-macros/array';
export default Component.extend({
didInsertElement() {
this._super();
scheduleOnce('afterRender', this, function() {
/*
* adding floatThead jQuery plugin to poll table
* https://mkoryak.github.io/floatThead/
*
* top:
* Offset from the top of the `window` where the floating header will
* 'stick' when scrolling down
* Since we are adding a browser horizontal scrollbar on top, scrollingTop
* has to be set to height of horizontal scrollbar which depends on
* used browser
*/
$('.user-selections-table').floatThead({
position: 'absolute',
top: this.getScrollbarHeight
});
hasTimes: readOnly('poll.hasTimes'),
/*
* fix width calculation error caused by bootstrap glyphicon on webkit
*/
$('.glyphicon').css('width', '14px');
isFindADate: readOnly('poll.isFindADate'),
isFreeText: readOnly('poll.isFreeText'),
/*
* scrollbar on top of table
*/
const topScrollbarInner = $('<div></div>')
.css('width', $('.user-selections-table').width())
.css('height', '1px');
const topScrollbarOuter = $('<div></div>')
.addClass('top-scrollbar')
.css('width', '100%')
.css('overflow-x', 'scroll')
.css('overflow-y', 'hidden')
.css('position', 'relative')
.css('z-index', '1002');
$('.table-scroll').before(
topScrollbarOuter.append(topScrollbarInner)
);
options: readOnly('poll.options'),
optionsGroupedByDays: groupBy('options', raw('day')),
/*
* scrollbar on top of table for thead
*/
const topScrollbarInnerThead = $('<div></div>')
.css('width', $('.user-selections-table').width())
.css('height', '1px');
const topScrollbarOuterThead = $('<div></div>')
.addClass('top-scrollbar-floatThead')
.css('width', $('.table-scroll').outerWidth())
.css('overflow-x', 'scroll')
.css('overflow-y', 'hidden')
.css('position', 'fixed')
.css('top', '-1px')
.css('z-index', '1002')
.css('margin-left', `${($('.table-scroll').outerWidth() - $('.table-scroll').width()) / 2 * (-1)}px`)
.css('margin-right', `${($('.table-scroll').outerWidth() - $('.table-scroll').width()) / 2 * (-1)}px`);
$('.table-scroll').prepend(
topScrollbarOuterThead.append(topScrollbarInnerThead).hide()
);
// add listener to resize scrollbars if window get resized
$(window).resize(this.resizeScrollbars);
/*
* bind scroll event on all scrollbars
*/
$('.table-scroll').scroll(function() {
$('.top-scrollbar').scrollLeft($('.table-scroll').scrollLeft());
$('.top-scrollbar-floatThead').scrollLeft($('.table-scroll').scrollLeft());
});
$('.top-scrollbar').scroll(function() {
$('.table-scroll').scrollLeft($('.top-scrollbar').scrollLeft());
$('.top-scrollbar-floatThead').scrollLeft($('.top-scrollbar').scrollLeft());
});
$('.top-scrollbar-floatThead').scroll(function() {
$('.table-scroll').scrollLeft($('.top-scrollbar-floatThead').scrollLeft());
$('.top-scrollbar').scrollLeft($('.top-scrollbar-floatThead').scrollLeft());
});
/*
* show inner scrollbar only, if header is fixed
*/
$(window).scroll($.proxy(this.updateScrollbarTopVisibility, this));
});
},
/*
* calculates horizontal scrollbar height depending on current browser
*/
getScrollbarHeight() {
const wideScrollWtml = $('<div>').attr('id', 'wide_scroll_div_one').css({
'width': 50,
'height': 50,
'overflow-y': 'scroll',
'position': 'absolute',
'top': -200,
'left': -200
}).append(
$('<div>').attr('id', 'wide_scroll_div_two').css({
'height': '100%',
'width': 100
})
);
$('body').append(wideScrollWtml); // Append our div and add the hmtl to your document for calculations
const scrollW1 = $('#wide_scroll_div_one').height(); // Getting the width of the surrounding(parent) div - we already know it is 50px since we styled it but just to make sure.
const scrollW2 = $('#wide_scroll_div_two').innerHeight(); // Find the inner width of the inner(child) div.
const scrollBarWidth = scrollW1 - scrollW2; // subtract the difference
$('#wide_scroll_div_one').remove(); // remove the html from your document
return scrollBarWidth;
},
optionsGroupedByDates: groupBy('options', 'optionsGroupedBy', function(groupValue, currentValue) {
// have to parse the date cause due to timezone it may start with another day string but be at same day due to timezone
// e.g. '2015-01-01T23:00:00.000Z' and '2015-01-02T00:00:00.000Z' both are at '2015-01-02' for timezone offset '+01:00'
return moment(groupValue).format('YYYY-MM-DD') === moment(currentValue).format('YYYY-MM-DD');
}),
optionsGroupedBy: 'title',
/*
* resize scrollbars
* used as event callback when window is resized
*/
resizeScrollbars() {
$('.top-scrollbar div').css('width', $('.user-selections-table').width());
$('.top-scrollbar-floatThead').css('width', $('.table-scroll').outerWidth());
$('.top-scrollbar-floatThead div').css('width', $('.user-selections-table').width());
},
/*
* resize scrollbars if document height might be changed
* and therefore scrollbars might be added
*/
triggerResizeScrollbars: observer('controller.isEvaluable', 'controller.model.users.[]', function() {
next(() => {
this.resizeScrollbars();
});
}),
/*
* show / hide top scrollbar depending on window position
* used as event callback when window is scrolled
*/
updateScrollbarTopVisibility() {
const windowTop = $(window).scrollTop();
const tableTop = $('.table-scroll table').offset().top;
if (windowTop >= tableTop - this.getScrollbarHeight()) {
$('.top-scrollbar-floatThead').show();
// update scroll position
$('.top-scrollbar-floatThead').scrollLeft($('.table-scroll').scrollLeft());
} else {
$('.top-scrollbar-floatThead').hide();
}
},
/*
* clean up
* especially remove event listeners
*/
willDestroyElement() {
$(window).off('resize', this.resizeScrollbars);
$(window).off('scroll', this.updateScrollbarTopVisibility);
}
users: readOnly('poll.users'),
usersSorted: sort('users', ['creationDate']),
});

View file

@ -1,27 +1,31 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { gt, mapBy, max, readOnly } from '@ember/object/computed';
import { copy } from '@ember/object/internals';
import { isEmpty } from '@ember/utils';
import { inject as service } from '@ember/service';
export default Component.extend({
i18n: service(),
classNames: ['evaluation-summary'],
evaluationBestOptions: computed('poll.users.[]', function() {
bestOptions: computed('users.[]', function() {
// can not evaluate answer type free text
if (this.get('poll.isFreeText')) {
return undefined;
}
// can not evaluate a poll without users
if (isEmpty(this.get('poll.users'))) {
if (isEmpty(this.users)) {
return undefined;
}
let answers = this.get('poll.answers').reduce((answers, answer) => {
let answers = this.poll.answers.reduce((answers, answer) => {
answers[answer.get('type')] = 0;
return answers;
}, {});
let evaluation = this.get('poll.options').map((option) => {
let evaluation = this.poll.options.map((option) => {
return {
answers: copy(answers),
option,
@ -30,11 +34,11 @@ export default Component.extend({
});
let bestOptions = [];
this.get('poll.users').forEach(function(user) {
user.get('selections').forEach(function(selection, i) {
evaluation[i].answers[selection.get('type')]++;
this.users.forEach((user) => {
user.selections.forEach(({ type }, i) => {
evaluation[i].answers[type]++;
switch (selection.get('type')) {
switch (type) {
case 'yes':
evaluation[i].score += 2;
break;
@ -50,9 +54,7 @@ export default Component.extend({
});
});
evaluation.sort(function(a, b) {
return b.score - a.score;
});
evaluation.sort((a, b) => b.score - a.score);
let bestScore = evaluation[0].score;
for (let i = 0; i < evaluation.length; i++) {
@ -70,19 +72,14 @@ export default Component.extend({
return bestOptions;
}),
evaluationBestOptionsMultiple: computed('evaluationBestOptions', function() {
if (this.get('evaluationBestOptions.length') > 1) {
return true;
} else {
return false;
}
}),
currentLocale: readOnly('i18n.locale'),
evaluationLastParticipation: computed('sortedUsers.[]', function() {
return this.get('sortedUsers.lastObject.creationDate');
}),
multipleBestOptions: gt('bestOptions.length', 1),
evaluationParticipants: computed('poll.users.[]', function() {
return this.get('poll.users.length');
})
lastParticipationAt: max('participationDates'),
participationDates: mapBy('users', 'creationDate'),
participantsCount: readOnly('users.length'),
users: readOnly('poll.users'),
});

View file

@ -6,6 +6,11 @@ import { observer, computed } from '@ember/object';
import moment from 'moment';
export default Controller.extend({
encryption: service(),
flashMessages: service(),
i18n: service(),
router: service(),
actions: {
linkAction(type) {
let flashMessages = this.flashMessages;
@ -27,25 +32,9 @@ export default Controller.extend({
currentLocale: readOnly('i18n.locale'),
encryption: service(),
encryptionKey: '',
queryParams: ['encryptionKey'],
flashMessages: service(),
hasTimes: computed('model.options.[]', function() {
if (this.get('model.isMakeAPoll')) {
return false;
} else {
return this.get('model.options').any((option) => {
let dayStringLength = 10; // 'YYYY-MM-DD'.length
return option.get('title').length > dayStringLength;
});
}
}),
i18n: service(),
momentLongDayFormat: computed('currentLocale', function() {
let currentLocale = this.currentLocale;
return moment.localeData(currentLocale)
@ -55,24 +44,26 @@ export default Controller.extend({
.trim();
}),
pollUrl: computed('currentPath', 'encryptionKey', function() {
poll: readOnly('model'),
pollUrl: computed('router.currentURL', 'encryptionKey', function() {
return window.location.href;
}),
// TODO: Remove this code. It's spooky.
preventEncryptionKeyChanges: observer('encryptionKey', function() {
if (
!isEmpty(this.get('encryption.key')) &&
this.encryptionKey !== this.get('encryption.key')
!isEmpty(this.encryption.key) &&
this.encryptionKey !== this.encryption.key
) {
// work-a-round for url not being updated
window.location.hash = window.location.hash.replace(this.encryptionKey, this.get('encryption.key'));
window.location.hash = window.location.hash.replace(this.encryptionKey, this.encryption.key);
this.set('encryptionKey', this.get('encryption.key'));
this.set('encryptionKey', this.encryption.key);
}
}),
showExpirationWarning: computed('model.expirationDate', function() {
let expirationDate = this.get('model.expirationDate');
showExpirationWarning: computed('poll.expirationDate', function() {
let expirationDate = this.poll.expirationDate;
if (isEmpty(expirationDate)) {
return false;
}
@ -84,8 +75,8 @@ export default Controller.extend({
/*
* return true if current timezone differs from timezone poll got created with
*/
timezoneDiffers: computed('model.timezone', function() {
const modelTimezone = this.get('model.timezone');
timezoneDiffers: computed('poll.timezone', function() {
let modelTimezone = this.poll.timezone;
return isPresent(modelTimezone) && moment.tz.guess() !== modelTimezone;
}),
@ -96,6 +87,6 @@ export default Controller.extend({
}),
timezone: computed('useLocalTimezone', function() {
return this.useLocalTimezone ? undefined : this.get('model.timezone');
return this.useLocalTimezone ? undefined : this.poll.timezone;
})
});

View file

@ -1,32 +1,31 @@
import { inject as service } from '@ember/service';
import { reads, readOnly, sort } from '@ember/object/computed';
import { and, gt, not, readOnly } from '@ember/object/computed';
import $ from 'jquery';
import { computed } from '@ember/object';
import Controller, { inject as controller } from '@ember/controller';
export default Controller.extend({
currentLocale: reads('i18n.locale'),
currentLocale: readOnly('i18n.locale'),
hasTimes: reads('pollController.hasTimes'),
hasTimes: readOnly('poll.hasTimes'),
i18n: service(),
momentLongDayFormat: readOnly('pollController.momentLongDayFormat'),
poll: readOnly('model'),
pollController: controller('poll'),
sortedUsers: sort('pollController.model.users', 'usersSorting'),
usersSorting: computed(() => ['creationDate']),
timezone: readOnly('pollController.timezone'),
timezone: reads('pollController.timezone'),
users: readOnly('poll.users'),
/*
* evaluates poll data
* if free text answers are allowed evaluation is disabled
*/
evaluation: computed('model.users.[]', function() {
// disable evaluation if answer type is free text
if (this.get('model.answerType') === 'FreeText') {
evaluation: computed('users.[]', function() {
if (!this.isEvaluable) {
return [];
}
@ -35,13 +34,13 @@ export default Controller.extend({
let lookup = [];
// init options array
this.get('model.options').forEach(function(option, index) {
this.poll.options.forEach((option, index) => {
options[index] = 0;
});
// init array of evalutation objects
// create object for every possible answer
this.get('model.answers').forEach(function(answer) {
this.poll.answers.forEach((answer) => {
evaluation.push({
id: answer.label,
label: answer.label,
@ -49,7 +48,7 @@ export default Controller.extend({
});
});
// create object for no answer if answers are not forced
if (!this.get('model.forceAnswer')) {
if (!this.poll.forceAnswer) {
evaluation.push({
id: null,
label: 'no answer',
@ -63,21 +62,21 @@ export default Controller.extend({
});
// loop over all users
this.get('model.users').forEach(function(user) {
this.poll.users.forEach((user) => {
// loop over all selections of the user
user.get('selections').forEach(function(selection, optionindex) {
let answerindex;
user.selections.forEach(function(selection, optionIndex) {
let answerIndex;
// get answer index by lookup array
if (typeof lookup[selection.get('value.label')] === 'undefined') {
answerindex = lookup[null];
if (typeof lookup[selection.value.label] === 'undefined') {
answerIndex = lookup[null];
} else {
answerindex = lookup[selection.get('value.label')];
answerIndex = lookup[selection.get('value.label')];
}
// increment counter
try {
evaluation[answerindex].options[optionindex] = evaluation[answerindex].options[optionindex] + 1;
evaluation[answerIndex].options[optionIndex]++;
} catch (e) {
// ToDo: Throw an error
}
@ -87,26 +86,7 @@ export default Controller.extend({
return evaluation;
}),
/*
* calculate colspan for a row which should use all columns in table
* used by evaluation row
*/
fullRowColspan: computed('model.options.[]', function() {
return this.get('model.options.length') + 2;
}),
isEvaluable: computed('model.{users.[],isFreeText}', function() {
if (
!this.get('model.isFreeText') &&
this.get('model.users.length') > 0
) {
return true;
} else {
return false;
}
}),
optionCount: computed('model.options', function() {
return this.get('model.options.length');
})
hasUsers: gt('poll.users.length', 0),
isNotFreeText: not('poll.isFreeText'),
isEvaluable: and('hasUsers', 'isNotFreeText'),
});

View file

@ -25,7 +25,7 @@ const Validations = buildValidations({
dependentKeys: ['model.i18n.locale']
}),
validator('unique', {
parent: 'pollController.model',
parent: 'poll',
attributeInParent: 'users',
dependentKeys: ['model.poll.users.[]', 'model.poll.users.@each.name', 'model.i18n.locale'],
disable: readOnly('model.anonymousUser'),
@ -58,67 +58,74 @@ const SelectionValidations = buildValidations({
export default Controller.extend(Validations, {
actions: {
submit() {
if (this.get('validations.isValid')) {
const user = this.store.createRecord('user', {
creationDate: new Date(),
poll: this.get('pollController.model'),
version: config.APP.version,
});
user.set('name', this.name);
const selections = user.get('selections');
const possibleAnswers = this.get('pollController.model.answers');
this.selections.forEach((selection) => {
if (selection.get('value') !== null) {
if (this.isFreeText) {
selections.createFragment({
label: selection.get('value')
});
} else {
const answer = possibleAnswers.findBy('type', selection.get('value'));
selections.createFragment({
icon: answer.get('icon'),
label: answer.get('label'),
labelTranslation: answer.get('labelTranslation'),
type: answer.get('type')
});
}
} else {
selections.createFragment();
}
});
this.set('newUserRecord', user);
this.send('save');
if (!this.get('validations.isValid')) {
return;
}
let poll = this.poll;
let selections = this.selections.map(({ value }) => {
if (value === null) {
return {};
}
if (this.isFreeText) {
return {
label: value,
};
}
// map selection to answer if it's not freetext
let answer = poll.answers.findBy('type', value);
let { icon, label, labelTranslation, type } = answer;
return {
icon,
label,
labelTranslation,
type,
};
});
let user = this.store.createRecord('user', {
creationDate: new Date(),
name: this.name,
poll,
selections,
version: config.APP.version,
});
this.set('newUserRecord', user);
this.send('save');
},
save() {
const user = this.newUserRecord;
user.save()
.then(() => {
async save() {
let user = this.newUserRecord;
try {
await user.save();
this.set('savingFailed', false);
// reset form
this.set('name', '');
this.selections.forEach((selection) => {
selection.set('value', null);
});
this.transitionToRoute('poll.evaluation', this.model, {
queryParams: { encryptionKey: this.get('encryption.key') }
});
}, () => {
} catch (error) {
// couldn't save user model
this.set('savingFailed', true);
return;
}
// reset form
this.set('name', '');
this.selections.forEach((selection) => {
selection.set('value', null);
});
this.transitionToRoute('poll.evaluation', this.model, {
queryParams: { encryptionKey: this.encryption.key }
});
}
},
anonymousUser: readOnly('pollController.model.anonymousUser'),
anonymousUser: readOnly('poll.anonymousUser'),
currentLocale: readOnly('i18n.locale'),
encryption: service(),
forceAnswer: readOnly('pollController.model.forceAnswer'),
forceAnswer: readOnly('poll.forceAnswer'),
i18n: service(),
init() {
@ -127,19 +134,20 @@ export default Controller.extend(Validations, {
this.get('i18n.locale');
},
isFreeText: readOnly('pollController.model.isFreeText'),
isFindADate: readOnly('pollController.model.isFindADate'),
isFreeText: readOnly('poll.isFreeText'),
isFindADate: readOnly('poll.isFindADate'),
momentLongDayFormat: readOnly('pollController.momentLongDayFormat'),
name: '',
options: readOnly('pollController.model.options'),
options: readOnly('poll.options'),
poll: readOnly('model'),
pollController: controller('poll'),
possibleAnswers: computed('pollController.model.answers', function() {
return this.get('pollController.model.answers').map((answer) => {
possibleAnswers: computed('poll.answers', function() {
return this.get('poll.answers').map((answer) => {
const owner = getOwner(this);
const AnswerObject = EmberObject.extend({

View file

@ -1,8 +1,7 @@
import { computed } from '@ember/object';
import DS from 'ember-data';
import {
fragmentArray
} from 'ember-data-model-fragments/attributes';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
const {
attr,
@ -64,15 +63,18 @@ export default Model.extend({
/*
* computed properties
*/
isFindADate: computed('pollType', function() {
return this.pollType === 'FindADate';
hasTimes: computed('options.[]', function() {
if (this.isMakeAPoll) {
return false;
}
return this.options.any((option) => {
let dayStringLength = 10; // 'YYYY-MM-DD'.length
return option.title.length > dayStringLength;
});
}),
isFreeText: computed('answerType', function() {
return this.answerType === 'FreeText';
}),
isMakeAPoll: computed('pollType', function() {
return this.pollType === 'MakeAPoll';
})
isFindADate: equal('pollType', 'FindADate'),
isFreeText: equal('answerType', 'FreeText'),
isMakeAPoll: equal('pollType', 'MakeAPoll'),
});

View file

@ -0,0 +1,7 @@
import Route from '@ember/routing/route';
export default Route.extend({
model() {
return this.modelFor('poll');
}
});

View file

@ -0,0 +1,7 @@
import Route from '@ember/routing/route';
export default Route.extend({
model() {
return this.modelFor('poll');
}
});

View file

@ -0,0 +1,47 @@
#poll {
.participants-table {
max-height: 95vh;
width: 100%;
overflow: scroll;
table {
overflow: scroll;
th, td {
white-space: nowrap;
}
thead {
// position sticky on thead is not supported by Chrome and Edge
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 100;
background-color: #fff;
tr {
&:not(:last-child) th:not([rowspan="2"]) {
// do not show a border between rows of a multi-row header
border-bottom: 0;
}
}
}
thead,
tbody {
tr {
th,
td {
&:first-child {
position: -webkit-sticky;
position: sticky;
left: 0;
z-index: 200;
background-color: #fff;
}
}
}
}
}
}
}

View file

@ -1,5 +1,6 @@
@import "ember-bootstrap/bootstrap";
@import "calendar";
@import "participants-table";
table tr td .form-group {
margin-bottom:0;
@ -168,25 +169,7 @@ body {
left:100%;
animation-delay:1.43s;
}
/*
* using floatThead with Bootstrap 3 fix
* https://mkoryak.github.io/floatThead/examples/bootstrap3/
*/
table.floatThead-table {
border-top:none;
border-bottom:none;
background-color:#ffffff;
}
#poll table tr th,
#poll table tr td {
white-space: nowrap;
}
#poll table thead tr.dateGroups th {
border-bottom: none;
}
#poll table thead tr.dateGroups ~ tr th {
border-top: none;
}
ul.nav-tabs {
margin-bottom: 15px;
}

View file

@ -1,5 +1,5 @@
{{ember-chart
type=type
type="bar"
data=data
options=chartOptions
}}

View file

@ -1,44 +1,34 @@
<div class="table-scroll">
<table class="user-selections-table table table-striped table-condensed">
<div class="participants-table">
<table
class="table"
data-test-table-of="participants"
>
<thead>
{{#if hasTimes}}
<tr class="dateGroups">
<th>&nbsp;</th>
{{#each optionsGroupedByDates as |optionGroup|}}
{{#if this.hasTimes}}
<tr>
<th>
{{!-- column for name --}}
</th>
{{#each this.optionsGroupedByDays as |optionGroup|}}
<th colspan={{optionGroup.items.length}}>
{{moment-format
optionGroup.value
momentLongDayFormat
locale=currentLocale
timeZone=timezone
}}
{{moment-format optionGroup.value this.momentLongDayFormat}}
</th>
{{/each}}
<th>&nbsp;</th>
</tr>
{{/if}}
<tr>
<th>&nbsp;</th>
{{#each options as |option|}}
<th>
{{!-- column for name --}}
</th>
{{#each this.options as |option|}}
<th>
{{#if isFindADate}}
{{#if hasTimes}}
{{#if option.hasTime}}
{{moment-format
option.title
"LT"
locale=currentLocale
timeZone=timezone
}}
{{/if}}
{{else}}
{{moment-format
option.title
momentLongDayFormat
locale=currentLocale
timeZone=timezone
}}
{{#if (and this.isFindADate this.hasTimes)}}
{{#if option.hasTime}}
{{moment-format option.date "LT"}}
{{/if}}
{{else if this.isFindADate}}
{{moment-format option.date this.momentLongDayFormat}}
{{else}}
{{option.title}}
{{/if}}
@ -48,28 +38,34 @@
</thead>
<tbody>
{{#each sortedUsers as |user|}}
<tr class="user">
<td>{{user.name}}</td>
{{#each user.selections as |selection|}}
<td>
{{#if selection.label}}
{{#if isFreeText}}
{{#each this.usersSorted as |user|}}
<tr data-test-participant={{user.id}}>
<td
data-test-value-for="name"
>
{{user.name}}
</td>
{{#each this.options as |option index|}}
<td
data-test-is-selection-cell
data-test-value-for={{option.value}}
>
{{#let (object-at index user.selections) as |selection|}}
{{#if this.isFreeText}}
{{selection.label}}
{{else}}
{{#if selection.type}}
<span class={{selection.type}}>
<span class={{selection.icon}}></span>
{{t selection.labelTranslation}}
</span>
{{/if}}
{{/if}}
{{/if}}
{{#if selection.labelTranslation}}
{{#unless isFreeText}}
<span class={{selection.type}}>
<span class={{selection.icon}}></span>
{{t selection.labelTranslation}}
</span>
{{/unless}}
{{/if}}
{{/let}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>

View file

@ -3,25 +3,25 @@
</h2>
<p class="participants">
{{t "poll.evaluation.participants" count=evaluationParticipants}}
{{t "poll.evaluation.participants" count=participantsCount}}
</p>
<p class="best-options">
{{#if poll.isFindADate}}
{{t
"poll.evaluation.bestOption.label.findADate"
count=evaluationBestOptions.length
count=bestOptions.length
}}
{{else}}
{{t
"poll.evaluation.bestOption.label.makeAPoll"
count=evaluationBestOptions.length
count=bestOptions.length
}}
{{/if}}
{{#if evaluationBestOptionsMultiple}}
{{#if multipleBestOptions}}
<ul>
{{#each evaluationBestOptions as |evaluationBestOption|}}
{{#each bestOptions as |evaluationBestOption|}}
{{poll-evaluation-summary-option
currentLocale=currentLocale
evaluationBestOption=evaluationBestOption
@ -35,7 +35,7 @@
{{else}}
{{poll-evaluation-summary-option
currentLocale=currentLocale
evaluationBestOption=evaluationBestOptions.firstObject
evaluationBestOption=bestOptions.firstObject
isFindADate=poll.isFindADate
momentLongDayFormat=momentLongDayFormat
tagName="span"
@ -47,6 +47,6 @@
<p class="last-participation">
{{t
"poll.evaluation.lastParticipation"
ago=(moment-from-now evaluationLastParticipation locale=currentLocale timezone=timezone)
ago=(moment-from-now lastParticipationAt locale=currentLocale timezone=timezone)
}}
</p>

View file

@ -16,7 +16,7 @@
}}
{{#autofocusable-element
tagName="select"
onchange=(action "updateAnswerType" value="target.value")
change=(action "updateAnswerType" value="target.value")
id=el.id
class="form-control"
}}
@ -52,14 +52,14 @@
controlType="checkbox"
label=(t "create.settings.anonymousUser.label")
showValidationOn="change"
value=anonymousUser
property="anonymousUser"
}}
{{form.element
classNames="force-answer"
controlType="checkbox"
label=(t "create.settings.forceAnswer.label")
showValidationOn="change"
value=forceAnswer
property="forceAnswer"
}}
{{form-navigation-buttons
nextButtonText=(t "action.save")

View file

@ -1,32 +1,21 @@
{{#if isEvaluable}}
{{poll-evaluation-summary
currentLocale=currentLocale
momentLongDayFormat=momentLongDayFormat
poll=model
sortedUsers=sortedUsers
poll=poll
timezone=timezone
}}
<h3>{{t "poll.evaluation.overview"}}</h3>
{{poll-evaluation-chart
answerType=model.answerType
currentLocale=currentLocale
isFindADate=model.isFindADate
momentLongDayFormat=momentLongDayFormat
options=model.options
users=model.users
poll=poll
timezone=timezone
}}
{{/if}}
<h3>{{t "poll.evaluation.participantTable"}}</h3>
{{poll-evaluation-participants-table
currentLocale=currentLocale
hasTimes=hasTimes
isFindADate=model.isFindADate
isFreeText=model.isFreeText
momentLongDayFormat=momentLongDayFormat
options=model.options
sortedUsers=sortedUsers
poll=poll
timezone=timezone
}}

View file

@ -23,7 +23,7 @@ module.exports = function(defaults) {
only: ['array', 'object-at'],
},
'ember-math-helpers': {
only: ['gt', 'lte', 'sub'],
only: ['lte', 'sub'],
},
});
@ -40,11 +40,6 @@ module.exports = function(defaults) {
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
app.import({
development: 'node_modules/floatthead/dist/jquery.floatThead.js',
production: 'node_modules/floatthead/dist/jquery.floatThead.min.js'
});
app.import('node_modules/sjcl/sjcl.js', {
using: [
{ transformation: 'amd', as: 'sjcl' }

View file

@ -69,7 +69,6 @@
"ember-transition-helper": "^1.0.0",
"ember-truth-helpers": "^2.1.0",
"eslint-plugin-ember": "^5.2.0",
"floatthead": "^2.1.2",
"fs-extra": "^7.0.1",
"loader.js": "^4.7.0",
"qunit-dom": "^0.7.1",

View file

@ -4,12 +4,16 @@ import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { t } from 'ember-i18n/test-support';
import switchTab from 'croodle/tests/helpers/switch-tab';
import pollHasUser from 'croodle/tests/helpers/poll-has-user';
import pollParticipate from 'croodle/tests/helpers/poll-participate';
import moment from 'moment';
import pagePollParticipation from 'croodle/tests/pages/poll/participation';
import PollParticipationPage from 'croodle/tests/pages/poll/participation';
import PollEvaluationPage from 'croodle/tests/pages/poll/evaluation';
module('Acceptance | legacy support', function(hooks) {
let yesLabel;
let maybeLabel;
let noLabel;
hooks.beforeEach(function() {
window.localStorage.setItem('locale', 'en');
});
@ -17,6 +21,13 @@ module('Acceptance | legacy support', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function() {
yesLabel = t('answerTypes.yes.label').toString();
maybeLabel = t('answerTypes.maybe.label').toString();
noLabel = t('answerTypes.no.label').toString();
});
test('show a default poll created with v0.3.0', async function(assert) {
const encryptionKey = '5MKFuNTKILUXw6RuqkAw6ooZw4k3mWWx98ZQw8vH';
@ -43,7 +54,7 @@ module('Acceptance | legacy support', function(hooks) {
await visit(`/poll/${poll.id}?encryptionKey=${encryptionKey}`);
assert.equal(currentRouteName(), 'poll.participation');
assert.deepEqual(
pagePollParticipation.options().labels,
PollParticipationPage.options().labels,
[
moment('2015-12-24T17:00:00.000Z').format('LLLL'),
moment('2015-12-24T19:00:00.000Z').format('LT'),
@ -51,23 +62,18 @@ module('Acceptance | legacy support', function(hooks) {
]
);
assert.deepEqual(
pagePollParticipation.options().answers,
[
t('answerTypes.yes.label').toString(),
t('answerTypes.maybe.label').toString(),
t('answerTypes.no.label').toString()
]
PollParticipationPage.options().answers,
[yesLabel, maybeLabel, noLabel]
);
await switchTab('evaluation');
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUser(assert,
'Fritz Bauer',
[
t('answerTypes.yes.label'),
t('answerTypes.no.label'),
t('answerTypes.no.label')
]
let participant = PollEvaluationPage.participants.filterBy('name', 'Fritz Bauer')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, noLabel, noLabel],
'participants table shows correct answers for new participant'
);
await switchTab('participation');
@ -75,13 +81,12 @@ module('Acceptance | legacy support', function(hooks) {
await pollParticipate('Hermann Langbein', ['yes', 'maybe', 'yes']);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUser(assert,
'Hermann Langbein',
[
t('answerTypes.yes.label'),
t('answerTypes.maybe.label'),
t('answerTypes.yes.label')
]
participant = PollEvaluationPage.participants.filterBy('name', 'Hermann Langbein')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, maybeLabel, yesLabel],
'participants table shows correct answers for new participant'
);
});
@ -111,7 +116,7 @@ module('Acceptance | legacy support', function(hooks) {
await visit(`/poll/${poll.id}?encryptionKey=${encryptionKey}`);
assert.equal(currentRouteName(), 'poll.participation');
assert.deepEqual(
pagePollParticipation.options().labels,
PollParticipationPage.options().labels,
[
'apple pie',
'pecan pie',
@ -121,13 +126,12 @@ module('Acceptance | legacy support', function(hooks) {
await switchTab('evaluation');
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUser(assert,
'Paul Levi',
[
'would be great!',
'no way',
'if I had to'
]
let participant = PollEvaluationPage.participants.filterBy('name', 'Paul Levi')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), ['would be great!', 'no way', 'if I had to'],
'participants table shows correct answers for new participant'
);
await switchTab('participation');
@ -135,13 +139,12 @@ module('Acceptance | legacy support', function(hooks) {
await pollParticipate('Hermann Langbein', ["I don't care", 'would be awesome', "can't imagine anything better"]);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUser(assert,
'Hermann Langbein',
[
"I don't care",
'would be awesome',
"can't imagine anything better"
]
participant = PollEvaluationPage.participants.filterBy('name', 'Hermann Langbein')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), ['I don\'t care', 'would be awesome', 'can\'t imagine anything better'],
'participants table shows correct answers for new participant'
);
});
});

View file

@ -8,12 +8,15 @@ import {
} from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { t } from 'ember-i18n/test-support';
import pollHasUser, { pollHasUsersCount } from 'croodle/tests/helpers/poll-has-user';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import PollEvaluationPage from 'croodle/tests/pages/poll/evaluation';
import pollParticipate from 'croodle/tests/helpers/poll-participate';
module('Acceptance | participate in a poll', function(hooks) {
let yesLabel;
let noLabel;
hooks.beforeEach(function() {
window.localStorage.setItem('locale', 'en');
});
@ -21,6 +24,11 @@ module('Acceptance | participate in a poll', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function() {
yesLabel = t('answerTypes.yes.label').toString();
noLabel = t('answerTypes.no.label').toString();
});
test('participate in a default poll', async function(assert) {
let encryptionKey = 'abcdefghijklmnopqrstuvwxyz0123456789';
let poll = this.server.create('poll', {
@ -37,8 +45,13 @@ module('Acceptance | participate in a poll', function(hooks) {
`encryptionKey=${encryptionKey}`,
'encryption key is part of query params'
);
pollHasUsersCount(assert, 1, 'user is added to user selections table');
pollHasUser(assert, 'Max Meiner', [t('answerTypes.yes.label'), t('answerTypes.no.label')]);
assert.equal(PollEvaluationPage.participants.length, 1, 'user is added to participants table');
let participant = PollEvaluationPage.participants.filterBy('name', 'Max Meiner')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, noLabel],
'participants table shows correct answers for new participant'
);
await click('.nav .participation');
assert.equal(currentRouteName(), 'poll.participation');
@ -50,8 +63,13 @@ module('Acceptance | participate in a poll', function(hooks) {
await pollParticipate('Peter Müller', ['yes', 'yes']);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUsersCount(assert, 2, 'user is added to user selections table');
pollHasUser(assert, 'Peter Müller', [t('answerTypes.yes.label'), t('answerTypes.yes.label')]);
assert.equal(PollEvaluationPage.participants.length, 2, 'user is added to participants table');
participant = PollEvaluationPage.participants.filterBy('name', 'Peter Müller')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, yesLabel],
'participants table shows correct answers for new participant'
);
});
test('participate in a poll using freetext', async function(assert) {
@ -67,8 +85,14 @@ module('Acceptance | participate in a poll', function(hooks) {
await pollParticipate('Max Manus', ['answer 1', 'answer 2']);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUsersCount(assert, 1, 'user is added to user selections table');
pollHasUser(assert, 'Max Manus', ['answer 1', 'answer 2']);
assert.equal(PollEvaluationPage.participants.length, 1, 'user is added to participants table');
let participant = PollEvaluationPage.participants.filterBy('name', 'Max Manus')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), ['answer 1', 'answer 2'],
'participants table shows correct answers for new participant'
);
});
test('participate in a poll which does not force an answer to all options', async function(assert) {
@ -83,8 +107,14 @@ module('Acceptance | participate in a poll', function(hooks) {
await pollParticipate('Karl Käfer', ['yes', null]);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUsersCount(assert, 1, 'user is added to user selections table');
pollHasUser(assert, 'Karl Käfer', [t('answerTypes.yes.label'), '']);
assert.equal(PollEvaluationPage.participants.length, 1, 'user is added to participants table');
let participant = PollEvaluationPage.participants.filterBy('name', 'Karl Käfer')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, ''],
'participants table shows correct answers for new participant'
);
});
test('participate in a poll which allows anonymous participation', async function(assert) {
@ -99,8 +129,14 @@ module('Acceptance | participate in a poll', function(hooks) {
await pollParticipate(null, ['yes', 'no']);
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUsersCount(assert, 1, 'user is added to user selections table');
pollHasUser(assert, '', [t('answerTypes.yes.label'), t('answerTypes.no.label')]);
assert.equal(PollEvaluationPage.participants.length, 1, 'user is added to participants table');
let participant = PollEvaluationPage.participants.filterBy('name', '')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, noLabel],
'participants table shows correct answers for new participant'
);
});
test('network connectivity errors', async function(assert) {
@ -116,7 +152,7 @@ module('Acceptance | participate in a poll', function(hooks) {
assert.dom('modal-saving-failed-modal')
.doesNotExist('failed saving notification is not shown before attempt to save');
await pollParticipate('foo bar', ['yes', 'no']);
await pollParticipate('John Doe', ['yes', 'no']);
assert.dom('#modal-saving-failed-modal')
.exists('user gets notified that saving failed');
@ -126,7 +162,13 @@ module('Acceptance | participate in a poll', function(hooks) {
assert.dom('#modal-saving-failed-modal')
.doesNotExist('Notification is hidden after another save attempt was successful');
assert.equal(currentRouteName(), 'poll.evaluation');
pollHasUsersCount(assert, 1, 'user is added to user selections table');
pollHasUser(assert, 'foo bar', [t('answerTypes.yes.label'), t('answerTypes.no.label')]);
assert.equal(PollEvaluationPage.participants.length, 1, 'user is added to participants table');
let participant = PollEvaluationPage.participants.filterBy('name', 'John Doe')[0];
assert.ok(participant, 'user exists in participants table');
assert.deepEqual(
participant.selections.map((_) => _.answer), [yesLabel, noLabel],
'participants table shows correct answers for new participant'
);
});
});

View file

@ -5,6 +5,8 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { t } from 'ember-i18n/test-support';
import switchTab from 'croodle/tests/helpers/switch-tab';
import moment from 'moment';
import PollEvaluationPage from 'croodle/tests/pages/poll/evaluation';
import { assign } from '@ember/polyfills';
module('Acceptance | view evaluation', function(hooks) {
hooks.beforeEach(function() {
@ -114,45 +116,47 @@ module('Acceptance | view evaluation', function(hooks) {
test('evaluation is correct for MakeAPoll', async function(assert) {
let encryptionKey = 'abcdefghijklmnopqrstuvwxyz0123456789';
let user1 = this.server.create('user', {
creationDate: '2015-01-01T00:00:00.000Z',
encryptionKey,
name: 'Maximilian',
selections: [
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
},
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
}
]
});
let user2 = this.server.create('user', {
creationDate: '2015-08-01T00:00:00.000Z',
encryptionKey,
name: 'Peter',
selections: [
{
type: 'no',
labelTranslation: 'answerTypes.no.label',
icon: 'glyphicon glyphicon-thumbs-down',
label: 'No'
},
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
}
]
});
let poll = this.server.create('poll', {
let usersData = [
{
creationDate: '2015-01-01T00:00:00.000Z',
encryptionKey,
name: 'Maximilian',
selections: [
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
},
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
}
]
},
{
creationDate: '2015-08-01T00:00:00.000Z',
encryptionKey,
name: 'Peter',
selections: [
{
type: 'no',
labelTranslation: 'answerTypes.no.label',
icon: 'glyphicon glyphicon-thumbs-down',
label: 'No'
},
{
type: 'yes',
labelTranslation: 'answerTypes.yes.label',
icon: 'glyphicon glyphicon-thumbs-up',
label: 'Yes'
}
]
},
];
let pollData = {
answers: [
{
type: 'yes',
@ -173,8 +177,8 @@ module('Acceptance | view evaluation', function(hooks) {
{ title: 'second option' }
],
pollType: 'MakeAPoll',
users: [user1, user2]
});
};
let poll = this.server.create('poll', assign(pollData, { users: usersData.map((_) => this.server.create('user', _)) }));
await visit(`/poll/${poll.id}/evaluation?encryptionKey=${encryptionKey}`);
assert.equal(currentRouteName(), 'poll.evaluation');
@ -189,25 +193,25 @@ module('Acceptance | view evaluation', function(hooks) {
'second option',
'options are evaluated correctly'
);
assert.ok(
findAll('.user-selections-table').length,
'has a table showing user selections'
);
assert.deepEqual(
findAll('.user-selections-table thead th').toArray().map((el) => el.textContent.trim()),
['', 'first option', 'second option'],
PollEvaluationPage.options.map((_) => _.label),
['first option', 'second option'],
'dates are used as table headers'
);
assert.deepEqual(
findAll('.user-selections-table tbody tr:nth-child(1) td').toArray().map((el) => el.textContent.trim()),
['Maximilian', 'Yes', 'Yes'],
'answers shown in table are correct for first user'
);
assert.deepEqual(
findAll('.user-selections-table tbody tr:nth-child(2) td').toArray().map((el) => el.textContent.trim()),
['Peter', 'No', 'Yes'],
'answers shown in table are correct for second user'
PollEvaluationPage.participants.map((_) => _.name), usersData.map((_) => _.name),
'users are listed in participants table with their names'
);
usersData.forEach((user) => {
let participant = PollEvaluationPage.participants.filterBy('name', user.name)[0];
assert.deepEqual(
participant.selections.map((_) => _.answer),
user.selections.map((_) => t(_.labelTranslation).toString()),
`answers are shown for user ${user.name} in participants table`
);
});
assert.equal(
find('.last-participation').textContent.trim(),
t('poll.evaluation.lastParticipation', {

View file

@ -1,5 +1,5 @@
import { isEmpty } from '@ember/utils';
import { findAll, fillIn, click } from '@ember/test-helpers';
import { findAll, fillIn, click, settled } from '@ember/test-helpers';
export default async function(name, selections) {
if (!isEmpty(name)) {
@ -18,4 +18,6 @@ export default async function(name, selections) {
}
await click('.participation button[type="submit"]');
await settled();
}

View file

@ -1,4 +1,3 @@
import EmberObject from '@ember/object';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
@ -13,66 +12,17 @@ module('Integration | Component | poll evaluation chart', function(hooks) {
});
test('it renders', async function(assert) {
this.set('options', [
EmberObject.create({
formatted: 'Thursday, January 1, 2015',
title: moment('2015-01-01'),
hasTime: false
}),
EmberObject.create({
formatted: 'Monday, February 2, 2015',
title: moment('2015-02-02'),
hasTime: false
}),
EmberObject.create({
formatted: 'Tuesday, March 3, 2015 1:00 AM',
title: moment('2015-03-03T01:00'),
hasTime: true
}),
EmberObject.create({
formatted: 'Tuesday, March 3, 2015 11:00 AM',
title: moment('2015-03-03T11:00'),
hasTime: true
})
]);
this.set('answerType', 'YesNoMaybe');
this.set('users', [
EmberObject.create({
id: 1,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
})
]
}),
EmberObject.create({
id: 2,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
}),
EmberObject.create({
type: 'no'
})
]
})
]);
await render(hbs`{{poll-evaluation-chart options=options answerType=answerType users=users}}`);
this.set('poll', {
answerType: 'YesNoMaybe',
options: [
{ title: '2015-01-01' },
],
users: [
{ selections: [{ type: 'yes' }]},
],
});
await render(hbs`{{poll-evaluation-chart poll=poll}}`);
assert.dom('canvas').exists('it renders a canvas element');
});
});

View file

@ -1,12 +1,23 @@
import PageObject from 'ember-cli-page-object';
import {
attribute,
collection,
create,
text,
} from 'ember-cli-page-object';
import { definition as Poll } from 'croodle/tests/pages/poll';
import { defaultsForApplication } from 'croodle/tests/pages/defaults';
import { assign } from '@ember/polyfills';
const { assign } = Object;
const {
text
} = PageObject;
export default PageObject.create(assign({}, defaultsForApplication, Poll, {
preferedOptions: text('.best-options .best-option-value', { multiple: true })
export default create(assign({}, defaultsForApplication, Poll, {
options: collection('[data-test-table-of="participants"] thead tr:last-child th:not(:first-child)', {
label: text(''),
}),
preferedOptions: text('.best-options .best-option-value', { multiple: true }),
participants: collection('[data-test-table-of="participants"] [data-test-participant]', {
name: text('[data-test-value-for="name"]'),
selections: collection('[data-test-is-selection-cell]', {
answer: text(''),
option: attribute('data-test-value-for', ''),
}),
}),
}));

View file

@ -67,21 +67,22 @@ module('Unit | Component | poll evaluation chart', function(hooks) {
]
})
];
let currentLocale = 'en';
let momentLongDayFormat = moment.localeData(currentLocale)
let momentLongDayFormat = moment.localeData('en')
.longDateFormat('LLLL')
.replace(
moment.localeData(currentLocale).longDateFormat('LT'), '')
moment.localeData('en').longDateFormat('LT'), '')
.trim();
let component = this.owner.factoryFor('component:poll-evaluation-chart').create({
answerType: 'YesNoMaybe',
currentLocale,
isFindADate: true,
momentLongDayFormat,
options,
poll: {
answerType: 'YesNoMaybe',
isFindADate: true,
options,
users,
},
timezone: 'Asia/Hong_Kong',
users
});
const data = component.get('data');
assert.deepEqual(
data.labels,
@ -126,45 +127,48 @@ module('Unit | Component | poll evaluation chart', function(hooks) {
})
];
let component = this.owner.factoryFor('component:poll-evaluation-chart').create({
answerType: 'YesNoMaybe',
options,
users: [
EmberObject.create({
id: 1,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
})
]
}),
EmberObject.create({
id: 2,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
}),
EmberObject.create({
type: 'no'
})
]
})
]
poll: {
answerType: 'YesNoMaybe',
options,
users: [
EmberObject.create({
id: 1,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
})
]
}),
EmberObject.create({
id: 2,
selections: [
EmberObject.create({
type: 'yes'
}),
EmberObject.create({
type: 'maybe'
}),
EmberObject.create({
type: 'no'
}),
EmberObject.create({
type: 'no'
})
]
})
]
}
});
const data = component.get('data');
assert.deepEqual(
data.labels,
@ -244,21 +248,22 @@ module('Unit | Component | poll evaluation chart', function(hooks) {
]
})
];
let currentLocale = 'en';
let momentLongDayFormat = moment.localeData(currentLocale)
let momentLongDayFormat = moment.localeData('en')
.longDateFormat('LLLL')
.replace(
moment.localeData(currentLocale).longDateFormat('LT'), '')
moment.localeData('en').longDateFormat('LT'), '')
.trim();
let component = this.owner.factoryFor('component:poll-evaluation-chart').create({
answerType: 'YesNoMaybe',
currentLocale,
isFindADate: true,
momentLongDayFormat,
options,
poll: {
answerType: 'YesNoMaybe',
isFindADate: true,
options,
users,
},
timezone: 'Asia/Hong_Kong',
users
});
const data = component.get('data');
assert.deepEqual(
data.labels,
@ -294,14 +299,15 @@ module('Unit | Component | poll evaluation chart', function(hooks) {
})
];
let component = this.owner.factoryFor('component:poll-evaluation-chart').create({
answerType: 'YesNoMaybe',
currentLocale: 'en',
isFindADate: true,
momentLongDayFormat: '',
options,
timezone: undefined,
users: []
poll: {
answerType: 'YesNoMaybe',
isFindADate: true,
options,
users: [],
},
});
const data = component.get('data');
assert.deepEqual(
data.labels,

View file

@ -0,0 +1,90 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | poll', function(hooks) {
setupTest(hooks);
test('#hasTimes: true if all options have times', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
pollType: 'FindADate',
options: [
{ title: '2019-01-01T00:00:00.000Z' },
{ title: '2019-01-01T10:00:00.000Z' },
],
});
assert.ok(poll.hasTimes);
});
test('#hasTimes: true if at least one option has times', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
options: [
{ title: '2019-01-01T00:00:00.000Z' },
{ title: '2019-01-02' },
],
pollType: 'FindADate',
});
assert.ok(poll.hasTimes);
});
test('#hasTimes: false if no option has times', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
options: [
{ title: '2019-01-01' },
{ title: '2019-01-02' },
],
pollType: 'FindADate',
});
assert.notOk(poll.hasTimes);
});
test('#hasTimes: false if poll is not FindADate', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
options: [
{ title: 'abc' },
{ title: 'def' },
],
pollType: 'MakeAPoll',
});
assert.notOk(poll.hasTimes);
});
test('#isFindADate', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
pollType: 'FindADate',
});
assert.ok(poll.isFindADate);
assert.notOk(poll.isMakeAPoll);
});
test('#isFreeText', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
answerType: 'FreeText',
});
assert.ok(poll.isFreeText);
poll.set('answerType', 'YesNo');
assert.notOk(poll.isFreeText);
poll.set('answerType', 'YesNoMaybe');
assert.notOk(poll.isFreeText);
});
test('#isMakeAPoll', function(assert) {
let store = this.owner.lookup('service:store');
let poll = store.createRecord('poll', {
pollType: 'MakeAPoll',
});
assert.ok(poll.isMakeAPoll);
assert.notOk(poll.isFindADate);
});
});

View file

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | poll/evaluation', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:poll/evaluation');
assert.ok(route);
});
});

View file

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | poll/participation', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:poll/participation');
assert.ok(route);
});
});

View file

@ -5336,11 +5336,6 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2"
write "^0.2.1"
floatthead@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/floatthead/-/floatthead-2.1.3.tgz#3e959c51de19672bc75af41be35d538a27e9b7eb"
integrity sha512-PglU8Oy6XHyjjZ2g1mu2iZIPIkcf+IkKpB6g12hVY6bXn+2lQj9+BOGMpaMgDKPTfawTAUQKtp7/UMcVs8q/eg==
follow-redirects@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.0.tgz#d12452c031e8c67eb6637d861bfc7a8090167933"