From bb160cc503e89bf756401f0b28cf3ef53c749e44 Mon Sep 17 00:00:00 2001 From: jelhan Date: Sat, 20 Apr 2019 23:29:59 +0200 Subject: [PATCH] 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 ``. Haven't tested Safari. --- app/components/poll-evaluation-chart.js | 92 ++++----- .../poll-evaluation-participants-table.js | 176 +----------------- app/components/poll-evaluation-summary.js | 45 +++-- app/controllers/poll.js | 43 ++--- app/controllers/poll/evaluation.js | 62 +++--- app/controllers/poll/participation.js | 118 ++++++------ app/models/poll.js | 28 +-- app/routes/poll/evaluation.js | 7 + app/routes/poll/participation.js | 7 + app/styles/_participants-table.scss | 47 +++++ app/styles/app.scss | 21 +-- .../components/poll-evaluation-chart.hbs | 2 +- .../poll-evaluation-participants-table.hbs | 94 +++++----- .../components/poll-evaluation-summary.hbs | 14 +- app/templates/create/settings.hbs | 6 +- app/templates/poll/evaluation.hbs | 17 +- ember-cli-build.js | 7 +- package.json | 1 - tests/acceptance/legacy-support-test.js | 79 ++++---- .../acceptance/participate-in-a-poll-test.js | 72 +++++-- tests/acceptance/view-evaluation-test.js | 114 ++++++------ tests/helpers/poll-participate.js | 4 +- .../components/poll-evaluation-chart-test.js | 72 ++----- tests/pages/poll/evaluation.js | 27 ++- .../components/poll-evaluation-chart-test.js | 126 +++++++------ tests/unit/models/poll-test.js | 90 +++++++++ tests/unit/routes/poll/evaluation-test.js | 11 ++ tests/unit/routes/poll/participation-test.js | 11 ++ yarn.lock | 5 - 29 files changed, 686 insertions(+), 712 deletions(-) create mode 100644 app/routes/poll/evaluation.js create mode 100644 app/routes/poll/participation.js create mode 100644 app/styles/_participants-table.scss create mode 100644 tests/unit/models/poll-test.js create mode 100644 tests/unit/routes/poll/evaluation-test.js create mode 100644 tests/unit/routes/poll/participation-test.js diff --git a/app/components/poll-evaluation-chart.js b/app/components/poll-evaluation-chart.js index c68bc52eb..7054600fc 100644 --- a/app/components/poll-evaluation-chart.js +++ b/app/components/poll-evaluation-chart.js @@ -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,7 +26,42 @@ const addArrays = function() { export default Component.extend({ i18n: service(), - type: 'bar', + + chartOptions: computed(function () { + return { + legend: { + display: false + }, + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true, + ticks: { + callback(value) { + return `${value} %`; + }, + max: 100, + min: 0 + } + }] + }, + tooltips: { + mode: 'label', + callbacks: { + label(tooltipItem, data) { + let { datasets } = data; + let { datasetIndex } = tooltipItem; + let { label } = datasets[datasetIndex]; + let value = tooltipItem.yLabel; + return `${label}: ${value} %`; + } + } + } + } + }), + data: computed('users.[]', 'options.{[],each.title}', 'currentLocale', function() { let labels = this.options.map((option) => { let value = get(option, 'title'); @@ -42,12 +78,10 @@ export default Component.extend({ }); let datasets = []; - let participants = this.get('users.length'); + let participants = this.users.length; - let yes = this.users.map((user) => { - return user.get('selections').map((selection) => { - return selection.get('type') === 'yes' ? 1 : 0; - }); + let yes = this.users.map(({ selections }) => { + return selections.map(({ type }) => type === 'yes' ? 1 : 0); }); datasets.push({ label: this.i18n.t('answerTypes.yes.label').toString(), @@ -59,10 +93,8 @@ export default Component.extend({ }); if (this.answerType === 'YesNoMaybe') { - let maybe = this.users.map((user) => { - return user.get('selections').map((selection) => { - return selection.get('type') === 'maybe' ? 1 : 0; - }); + let maybe = this.users.map(({ selections }) => { + return selections.map(({ type }) => type === 'maybe' ? 1 : 0); }); datasets.push({ label: this.i18n.t('answerTypes.maybe.label').toString(), @@ -79,38 +111,10 @@ export default Component.extend({ labels }; }), - chartOptions: computed(function () { - return { - legend: { - display: false - }, - scales: { - xAxes: [{ - stacked: true - }], - yAxes: [{ - stacked: true, - ticks: { - callback(value) { - return `${value} %`; - }, - max: 100, - min: 0 - } - }] - }, - tooltips: { - mode: 'label', - callbacks: { - label(tooltipItem, data) { - let { datasets } = data; - let { datasetIndex } = tooltipItem; - let { label } = datasets[datasetIndex]; - let value = tooltipItem.yLabel; - return `${label}: ${value} %`; - } - } - } - } - }), + + answerType: readOnly('poll.answerType'), + currentLocale: readOnly('i18n.locale'), + isFindADate: readOnly('poll.isFindADate'), + options: readOnly('poll.options'), + users: readOnly('poll.users'), }); diff --git a/app/components/poll-evaluation-participants-table.js b/app/components/poll-evaluation-participants-table.js index 674dcf876..90deab209 100644 --- a/app/components/poll-evaluation-participants-table.js +++ b/app/components/poll-evaluation-participants-table.js @@ -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 = $('
') - .css('width', $('.user-selections-table').width()) - .css('height', '1px'); - const topScrollbarOuter = $('
') - .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 = $('
') - .css('width', $('.user-selections-table').width()) - .css('height', '1px'); - const topScrollbarOuterThead = $('
') - .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 = $('
').attr('id', 'wide_scroll_div_one').css({ - 'width': 50, - 'height': 50, - 'overflow-y': 'scroll', - 'position': 'absolute', - 'top': -200, - 'left': -200 - }).append( - $('
').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']), }); diff --git a/app/components/poll-evaluation-summary.js b/app/components/poll-evaluation-summary.js index 9a003069f..7068add9d 100644 --- a/app/components/poll-evaluation-summary.js +++ b/app/components/poll-evaluation-summary.js @@ -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), + + lastParticipationAt: max('participationDates'), + participationDates: mapBy('users', 'creationDate'), + + participantsCount: readOnly('users.length'), - evaluationParticipants: computed('poll.users.[]', function() { - return this.get('poll.users.length'); - }) + users: readOnly('poll.users'), }); diff --git a/app/controllers/poll.js b/app/controllers/poll.js index 781ae381e..31291f1be 100644 --- a/app/controllers/poll.js +++ b/app/controllers/poll.js @@ -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; }) }); diff --git a/app/controllers/poll/evaluation.js b/app/controllers/poll/evaluation.js index 79c95c7ff..4aa362874 100644 --- a/app/controllers/poll/evaluation.js +++ b/app/controllers/poll/evaluation.js @@ -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'), }); diff --git a/app/controllers/poll/participation.js b/app/controllers/poll/participation.js index 967435838..90a1a43fa 100644 --- a/app/controllers/poll/participation.js +++ b/app/controllers/poll/participation.js @@ -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, - }); + if (!this.get('validations.isValid')) { + return; + } - 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(); - } - }); + let poll = this.poll; + let selections = this.selections.map(({ value }) => { + if (value === null) { + return {}; + } - this.set('newUserRecord', user); - this.send('save'); - } + 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(() => { - this.set('savingFailed', false); + async save() { + let user = this.newUserRecord; - // reset form - this.set('name', ''); - this.selections.forEach((selection) => { - selection.set('value', null); - }); + try { + await user.save(); - this.transitionToRoute('poll.evaluation', this.model, { - queryParams: { encryptionKey: this.get('encryption.key') } - }); - }, () => { + this.set('savingFailed', false); + } 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({ diff --git a/app/models/poll.js b/app/models/poll.js index a23303765..6a765af4c 100644 --- a/app/models/poll.js +++ b/app/models/poll.js @@ -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'; - }), - - isFreeText: computed('answerType', function() { - return this.answerType === 'FreeText'; + 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; + }); }), - isMakeAPoll: computed('pollType', function() { - return this.pollType === 'MakeAPoll'; - }) + isFindADate: equal('pollType', 'FindADate'), + isFreeText: equal('answerType', 'FreeText'), + isMakeAPoll: equal('pollType', 'MakeAPoll'), }); diff --git a/app/routes/poll/evaluation.js b/app/routes/poll/evaluation.js new file mode 100644 index 000000000..a97272f75 --- /dev/null +++ b/app/routes/poll/evaluation.js @@ -0,0 +1,7 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.modelFor('poll'); + } +}); diff --git a/app/routes/poll/participation.js b/app/routes/poll/participation.js new file mode 100644 index 000000000..a97272f75 --- /dev/null +++ b/app/routes/poll/participation.js @@ -0,0 +1,7 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ + model() { + return this.modelFor('poll'); + } +}); diff --git a/app/styles/_participants-table.scss b/app/styles/_participants-table.scss new file mode 100644 index 000000000..dbb09095e --- /dev/null +++ b/app/styles/_participants-table.scss @@ -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; + } + } + } + } + } + } +} diff --git a/app/styles/app.scss b/app/styles/app.scss index 0008320a8..ade5d9ba0 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -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; } diff --git a/app/templates/components/poll-evaluation-chart.hbs b/app/templates/components/poll-evaluation-chart.hbs index 392e2875d..cdeb29b93 100644 --- a/app/templates/components/poll-evaluation-chart.hbs +++ b/app/templates/components/poll-evaluation-chart.hbs @@ -1,5 +1,5 @@ {{ember-chart - type=type + type="bar" data=data options=chartOptions }} diff --git a/app/templates/components/poll-evaluation-participants-table.hbs b/app/templates/components/poll-evaluation-participants-table.hbs index 4734f270b..6af7de9d1 100644 --- a/app/templates/components/poll-evaluation-participants-table.hbs +++ b/app/templates/components/poll-evaluation-participants-table.hbs @@ -1,44 +1,34 @@ -
- +
+
- {{#if hasTimes}} - - - {{#each optionsGroupedByDates as |optionGroup|}} + {{#if this.hasTimes}} + + + {{#each this.optionsGroupedByDays as |optionGroup|}} {{/each}} - {{/if}} + - - {{#each options as |option|}} + + {{#each this.options as |option|}} - {{#each sortedUsers as |user|}} - - - {{#each user.selections as |selection|}} - + + {{#each this.options as |option index|}} + {{/each}} {{/each}}
 
+ {{!-- column for name --}} + - {{moment-format - optionGroup.value - momentLongDayFormat - locale=currentLocale - timeZone=timezone - }} + {{moment-format optionGroup.value this.momentLongDayFormat}}  
  + {{!-- column for name --}} + - {{#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 @@
{{user.name}} - {{#if selection.label}} - {{#if isFreeText}} + {{#each this.usersSorted as |user|}} +
+ {{user.name}} + + {{#let (object-at index user.selections) as |selection|}} + {{#if this.isFreeText}} {{selection.label}} + {{else}} + {{#if selection.type}} + + + {{t selection.labelTranslation}} + + {{/if}} {{/if}} - {{/if}} - {{#if selection.labelTranslation}} - {{#unless isFreeText}} - - - {{t selection.labelTranslation}} - - {{/unless}} - {{/if}} + {{/let}}
-
+
\ No newline at end of file diff --git a/app/templates/components/poll-evaluation-summary.hbs b/app/templates/components/poll-evaluation-summary.hbs index 57393c94b..6f6a3a1b0 100644 --- a/app/templates/components/poll-evaluation-summary.hbs +++ b/app/templates/components/poll-evaluation-summary.hbs @@ -3,25 +3,25 @@

- {{t "poll.evaluation.participants" count=evaluationParticipants}} + {{t "poll.evaluation.participants" count=participantsCount}}

{{#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}}