refactor to native ECMAScript classes (#344)

Replaces Ember's old object model by native ECMAScript classes. Mostly automated with ember-native-class-codemod.
This commit is contained in:
Jeldrik Hanschke 2020-01-18 10:13:50 +01:00 committed by GitHub
parent 9983f76189
commit c9482786c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1755 additions and 1260 deletions

View file

@ -18,6 +18,7 @@ sudo: false
addons:
chrome: stable
firefox: latest-esr
cache:
yarn: true

View file

@ -2,11 +2,12 @@ import RESTAdapter from '@ember-data/adapter/rest';
import { inject as service } from '@ember/service';
import AdapterFetch from 'ember-fetch/mixins/adapter-fetch';
export default RESTAdapter.extend(AdapterFetch, {
encryption: service(),
export default class ApplicationAdapter extends RESTAdapter.extend(AdapterFetch) {
@service
encryption;
// set namespace to api.php in same subdirectory
namespace:
namespace =
window.location.pathname
// remove index.html if it's there
.replace(/index.html$/, '')
@ -20,4 +21,4 @@ export default RESTAdapter.extend(AdapterFetch, {
.concat('/api/index.php')
// remove leading slash
.replace(/^\//g, '')
});
}

View file

@ -1,13 +1,15 @@
import classic from 'ember-classic-decorator';
import Component from '@ember/component';
export default Component.extend({
autofocus: true,
@classic
export default class AutofocusableElement extends Component {
autofocus = true;
didInsertElement() {
this._super(...arguments);
super.didInsertElement(...arguments);
if (this.autofocus) {
this.element.focus();
}
},
});
}
}

View file

@ -1,11 +1,13 @@
import BsInput from 'ember-bootstrap/components/bs-form/element/control/input';
import classic from 'ember-classic-decorator';
import BaseBsInput from 'ember-bootstrap/components/bs-form/element/control/input';
export default BsInput.extend({
@classic
export default class CustomizedBsInput extends BaseBsInput {
didInsertElement() {
this._super(...arguments);
super.didInsertElement(...arguments);
if (this.autofocus) {
this.element.focus();
}
},
});
}
}

View file

@ -1,15 +1,21 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import Component from '@ember/component';
import { isArray } from '@ember/array';
import { isPresent } from '@ember/utils';
import moment from 'moment';
export default Component.extend({
i18n: service(),
store: service('store'),
@classic
export default class CreateOptionsDates extends Component {
@service
i18n;
selectedDays: computed('options.[]', function() {
@service('store')
store;
@computed('options.[]')
get selectedDays() {
return this.options
// should be unique
.uniqBy('day')
@ -18,64 +24,67 @@ export default Component.extend({
// filter out invalid
.filter(moment.isMoment)
.toArray();
}),
calendarCenterNext: computed('calendarCenter', function() {
}
@computed('calendarCenter')
get calendarCenterNext() {
return moment(this.calendarCenter).add(1, 'months');
}),
}
actions: {
daysSelected({ moment: newMoments }) {
let { options } = this;
@action
daysSelected({ moment: newMoments }) {
let { options } = this;
if (!isArray(newMoments)) {
// special case: all options are unselected
options.clear();
return;
}
if (!isArray(newMoments)) {
// special case: all options are unselected
options.clear();
return;
}
// array of options that represent days missing in updated selection
let removedOptions = options.filter((option) => {
return !newMoments.find((newMoment) => newMoment.format('YYYY-MM-DD') === option.day);
// array of options that represent days missing in updated selection
let removedOptions = options.filter((option) => {
return !newMoments.find((newMoment) => newMoment.format('YYYY-MM-DD') === option.day);
});
// array of moments that aren't represented yet by an option
let addedMoments = newMoments.filter((moment) => {
return !options.find((option) => moment.format('YYYY-MM-DD') === option.day);
});
// remove options that represent deselected days
options.removeObjects(removedOptions);
// add options for newly selected days
let newOptions = addedMoments.map((moment) => {
return this.store.createFragment('option', {
title: moment.format('YYYY-MM-DD'),
})
});
newOptions.forEach((newOption) => {
// options must be insert into options array at correct position
let insertBefore = options.find(({ date }) => {
if (!moment.isMoment(date)) {
// ignore options that do not represent a valid date
return false;
}
return date.isAfter(newOption.date);
});
let position = isPresent(insertBefore) ? options.indexOf(insertBefore) : options.length;
options.insertAt(position, newOption);
});
}
// array of moments that aren't represented yet by an option
let addedMoments = newMoments.filter((moment) => {
return !options.find((option) => moment.format('YYYY-MM-DD') === option.day);
});
// remove options that represent deselected days
options.removeObjects(removedOptions);
// add options for newly selected days
let newOptions = addedMoments.map((moment) => {
return this.store.createFragment('option', {
title: moment.format('YYYY-MM-DD'),
})
});
newOptions.forEach((newOption) => {
// options must be insert into options array at correct position
let insertBefore = options.find(({ date }) => {
if (!moment.isMoment(date)) {
// ignore options that do not represent a valid date
return false;
}
return date.isAfter(newOption.date);
});
let position = isPresent(insertBefore) ? options.indexOf(insertBefore) : options.length;
options.insertAt(position, newOption);
});
},
updateCalenderCenter(diff) {
this.calendarCenter.add(diff, 'months');
this.notifyPropertyChange('calenderCenter');
},
},
@action
updateCalenderCenter(diff) {
this.calendarCenter.add(diff, 'months');
this.notifyPropertyChange('calenderCenter');
}
init() {
this._super(arguments);
super.init(arguments);
let { selectedDays } = this;
this.set('calendarCenter', selectedDays.length >= 1 ? selectedDays[0] : moment());
},
});
}
}

View file

@ -1,8 +1,7 @@
import { inject as service } from '@ember/service';
import { readOnly, mapBy, filter } from '@ember/object/computed';
import Component from '@ember/component';
import { isPresent, isEmpty } from '@ember/utils';
import { action, observer, get } from '@ember/object';
import { action, get } from '@ember/object';
import {
validator, buildValidations
}
@ -24,141 +23,151 @@ let modelValidations = buildValidations({
]
});
export default Component.extend(modelValidations, {
actions: {
addOption(afterOption) {
let options = this.dates;
let dayString = afterOption.get('day');
let fragment = this.store.createFragment('option', {
title: dayString
});
let position = options.indexOf(afterOption) + 1;
options.insertAt(
position,
fragment
);
export default class CreateOptionsDatetime extends Component.extend(modelValidations) {
@service
store;
next(() => {
this.notifyPropertyChange('_nestedChildViews');
});
},
adoptTimesOfFirstDay() {
const dates = this.dates;
const datesForFirstDay = this.datesForFirstDay;
const timesForFirstDay = this.timesForFirstDay;
const datesWithoutFirstDay = this.groupedDates.slice(1);
errorMesage = null;
/* validate if times on firstDay are valid */
const datesForFirstDayAreValid = datesForFirstDay.every((date) => {
// ignore dates where time is null
return isEmpty(date.get('time')) || date.get('validations.isValid');
});
// group dates by day
@groupBy('dates', raw('day'))
groupedDates;
if (!datesForFirstDayAreValid) {
this.set('errorMessage', 'create.options-datetime.fix-validation-errors-first-day');
return;
}
get datesForFirstDay() {
// dates are sorted
let firstDay = this.groupedDates[0];
return firstDay.items;
}
datesWithoutFirstDay.forEach(({ items }) => {
if (isEmpty(timesForFirstDay)) {
// there aren't any times on first day
const remainingOption = items[0];
// remove all times but the first one
get timesForFirstDay() {
return this.datesForFirstDay.map((date) => date.time).filter((time) => isPresent(time));
}
@action
addOption(afterOption) {
let options = this.dates;
let dayString = afterOption.get('day');
let fragment = this.store.createFragment('option', {
title: dayString
});
let position = options.indexOf(afterOption) + 1;
options.insertAt(
position,
fragment
);
next(() => {
this.notifyPropertyChange('_nestedChildViews');
});
}
@action
adoptTimesOfFirstDay() {
const dates = this.dates;
const datesForFirstDay = this.datesForFirstDay;
const timesForFirstDay = this.timesForFirstDay;
const datesWithoutFirstDay = this.groupedDates.slice(1);
/* validate if times on firstDay are valid */
const datesForFirstDayAreValid = datesForFirstDay.every((date) => {
// ignore dates where time is null
return isEmpty(date.get('time')) || date.get('validations.isValid');
});
if (!datesForFirstDayAreValid) {
this.set('errorMessage', 'create.options-datetime.fix-validation-errors-first-day');
return;
}
datesWithoutFirstDay.forEach(({ items }) => {
if (isEmpty(timesForFirstDay)) {
// there aren't any times on first day
const remainingOption = items[0];
// remove all times but the first one
dates.removeObjects(
items.slice(1)
);
// set title as date without time
remainingOption.set('title', remainingOption.get('date').format('YYYY-MM-DD'));
} else {
// adopt times of first day
if (timesForFirstDay.get('length') < items.length) {
// remove excess options
dates.removeObjects(
items.slice(1)
items.slice(timesForFirstDay.get('length'))
);
// set title as date without time
remainingOption.set('title', remainingOption.get('date').format('YYYY-MM-DD'));
} else {
// adopt times of first day
if (timesForFirstDay.get('length') < items.length) {
// remove excess options
dates.removeObjects(
items.slice(timesForFirstDay.get('length'))
);
}
// set times according to first day
let targetPosition;
timesForFirstDay.forEach((timeOfFirstDate, index) => {
const target = items[index];
if (target === undefined) {
const basisDate = get(items[0], 'date').clone();
let [hour, minute] = timeOfFirstDate.split(':');
let dateString = basisDate.hour(hour).minute(minute).toISOString();
let fragment = this.store.createFragment('option', {
title: dateString
});
dates.insertAt(
targetPosition,
fragment
);
targetPosition++;
} else {
target.set('time', timeOfFirstDate);
targetPosition = dates.indexOf(target) + 1;
}
});
}
});
},
/*
* removes target option if it's not the only date for this day
* otherwise it deletes time for this date
*/
deleteOption(target) {
let position = this.dates.indexOf(target);
let datesForThisDay = this.groupedDates.find((group) => {
return group.value === target.get('day');
}).items;
if (datesForThisDay.length > 1) {
this.dates.removeAt(position);
} else {
target.set('time', null);
// set times according to first day
let targetPosition;
timesForFirstDay.forEach((timeOfFirstDate, index) => {
const target = items[index];
if (target === undefined) {
const basisDate = get(items[0], 'date').clone();
let [hour, minute] = timeOfFirstDate.split(':');
let dateString = basisDate.hour(hour).minute(minute).toISOString();
let fragment = this.store.createFragment('option', {
title: dateString
});
dates.insertAt(
targetPosition,
fragment
);
targetPosition++;
} else {
target.set('time', timeOfFirstDate);
targetPosition = dates.indexOf(target) + 1;
}
});
}
},
});
}
previousPage() {
this.onPrevPage();
},
/*
* removes target option if it's not the only date for this day
* otherwise it deletes time for this date
*/
@action
deleteOption(target) {
let position = this.dates.indexOf(target);
let datesForThisDay = this.groupedDates.find((group) => {
return group.value === target.get('day');
}).items;
if (datesForThisDay.length > 1) {
this.dates.removeAt(position);
} else {
target.set('time', null);
}
}
submit() {
if (this.get('validations.isValid')) {
this.onNextPage();
} else {
this.set('shouldShowErrors', true);
}
},
},
// dates are sorted
datesForFirstDay: readOnly('groupedDates.firstObject.items'),
@action
previousPage() {
this.onPrevPage();
}
// errorMessage should be reset to null on all user interactions
errorMesage: null,
resetErrorMessage: observer('dates.@each.time', function() {
this.set('errorMessage', null);
}),
@action
submit() {
if (this.get('validations.isValid')) {
this.onNextPage();
} else {
this.set('shouldShowErrors', true);
}
}
// can't use multiple computed macros at once
_timesForFirstDay: mapBy('datesForFirstDay', 'time'),
timesForFirstDay: filter('_timesForFirstDay', function(time) {
return isPresent(time);
}),
groupedDates: groupBy('dates', raw('day')),
store: service(),
inputChanged: action(function(date, value) {
@action
inputChanged(date, value) {
// update property, which is normally done by default
date.set('time', value);
// reset partially filled state
date.set('isPartiallyFilled', false);
}),
// reset error message
this.set('errorMessage', null);
}
// validate input field for being partially filled
validateInput: action(function(date, event) {
@action
validateInput(date, event) {
let element = event.target;
// update partially filled time validation error
@ -167,14 +176,15 @@ export default Component.extend(modelValidations, {
} else {
date.set('isPartiallyFilled', false);
}
}),
}
// remove partially filled validation error if user fixed it
updateInputValidation: action(function(date, event) {
@action
updateInputValidation(date, event) {
let element = event.target;
if (element.checkValidity() && date.isPartiallyFilled) {
date.set('isPartiallyFilled', false);
}
}),
});
}
}

View file

@ -1,23 +1,27 @@
import classic from 'ember-classic-decorator';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { next } from '@ember/runloop';
export default Component.extend({
actions: {
addOption(element) {
let fragment = this.store.createFragment('option');
let options = this.options;
let position = this.options.indexOf(element) + 1;
options.insertAt(
position,
fragment
);
},
deleteOption(element) {
let position = this.options.indexOf(element);
this.options.removeAt(position);
}
},
@classic
export default class CreateOptionsText extends Component {
@action
addOption(element) {
let fragment = this.store.createFragment('option');
let options = this.options;
let position = this.options.indexOf(element) + 1;
options.insertAt(
position,
fragment
);
}
@action
deleteOption(element) {
let position = this.options.indexOf(element);
this.options.removeAt(position);
}
enforceMinimalOptionsAmount() {
let options = this.options;
@ -27,12 +31,13 @@ export default Component.extend({
this.store.createFragment('option')
);
}
},
}
store: service('store'),
@service('store')
store;
init() {
this._super(...arguments);
super.init(...arguments);
// need to delay pushing fragments into options array to prevent
// > You modified "disabled" twice on <(unknown):ember330> in a single render.
@ -41,4 +46,4 @@ export default Component.extend({
this.enforceMinimalOptionsAmount();
});
}
});
}

View file

@ -1,4 +1,5 @@
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import Component from '@ember/component';
import {
validator, buildValidations
@ -21,20 +22,23 @@ let Validations = buildValidations({
]
});
export default Component.extend(Validations, {
actions: {
previousPage() {
this.onPrevPage();
},
submit() {
if (this.get('validations.isValid')) {
this.onNextPage();
} else {
this.set('shouldShowErrors', true);
}
}
},
export default class CreateOptionsComponent extends Component.extend(Validations) {
shouldShowErrors = false;
// consumed by validator
i18n: service(),
shouldShowErrors: false
});
@service i18n;
@action
previousPage() {
this.onPrevPage();
}
@action
submit() {
if (this.get('validations.isValid')) {
this.onNextPage();
} else {
this.set('shouldShowErrors', true);
}
}
}

View file

@ -1,5 +1,7 @@
import classic from 'ember-classic-decorator';
import { tagName } from '@ember-decorators/component';
import Component from '@ember/component';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class InlineDatepicker extends Component {}

View file

@ -1,20 +1,29 @@
import classic from 'ember-classic-decorator';
import { classNames, tagName } from '@ember-decorators/component';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import { readOnly } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import localesMeta from 'croodle/locales/meta';
export default Component.extend({
tagName: 'select',
classNames: [ 'language-select' ],
@classic
@tagName('select')
@classNames('language-select')
export default class LanguageSelect extends Component {
@service
i18n;
i18n: service(),
moment: service(),
powerCalendar: service(),
@service
moment;
current: readOnly('i18n.locale'),
@service
powerCalendar;
locales: computed('i18n.locales', function() {
@readOnly('i18n.locale')
current;
@computed('i18n.locales')
get locales() {
let currentLocale = this.get('i18n.locale');
return this.get('i18n.locales').map(function(locale) {
@ -24,7 +33,7 @@ export default Component.extend({
text: localesMeta[locale]
};
});
}),
}
change() {
let locale = this.element.options[this.element.selectedIndex].value;
@ -37,4 +46,4 @@ export default Component.extend({
window.localStorage.setItem('locale', locale);
}
}
});
}

View file

@ -1,7 +1,8 @@
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import { readOnly } from '@ember/object/computed';
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';
@ -24,10 +25,13 @@ const addArrays = function() {
return basis;
};
export default Component.extend({
i18n: service(),
@classic
export default class PollEvaluationChart extends Component {
@service
i18n;
chartOptions: computed(function () {
@computed
get chartOptions() {
return {
legend: {
display: false
@ -60,9 +64,10 @@ export default Component.extend({
}
}
}
}),
}
data: computed('users.[]', 'options.{[],each.title}', 'currentLocale', function() {
@computed('users.[]', 'options.{[],each.title}', 'currentLocale')
get data() {
let labels = this.options.map((option) => {
let value = get(option, 'title');
if (!this.isFindADate) {
@ -110,11 +115,20 @@ export default Component.extend({
datasets,
labels
};
}),
}
answerType: readOnly('poll.answerType'),
currentLocale: readOnly('i18n.locale'),
isFindADate: readOnly('poll.isFindADate'),
options: readOnly('poll.options'),
users: readOnly('poll.users'),
});
@readOnly('poll.answerType')
answerType;
@readOnly('i18n.locale')
currentLocale;
@readOnly('poll.isFindADate')
isFindADate;
@readOnly('poll.options')
options;
@readOnly('poll.users')
users;
}

View file

@ -1,17 +1,29 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { readOnly } from '@ember/object/computed';
import Component from '@ember/component';
import { raw } from 'ember-awesome-macros';
import { groupBy, sort } from 'ember-awesome-macros/array';
export default Component.extend({
hasTimes: readOnly('poll.hasTimes'),
@classic
export default class PollEvaluationParticipantsTable extends Component {
@readOnly('poll.hasTimes')
hasTimes;
isFindADate: readOnly('poll.isFindADate'),
isFreeText: readOnly('poll.isFreeText'),
@readOnly('poll.isFindADate')
isFindADate;
options: readOnly('poll.options'),
optionsGroupedByDays: groupBy('options', raw('day')),
@readOnly('poll.isFreeText')
isFreeText;
users: readOnly('poll.users'),
usersSorted: sort('users', ['creationDate']),
});
@readOnly('poll.options')
options;
@groupBy('options', raw('day'))
optionsGroupedByDays;
@readOnly('poll.users')
users;
@sort('users', ['creationDate'])
usersSorted;
}

View file

@ -1,4 +1,5 @@
import classic from 'ember-classic-decorator';
import Component from '@ember/component';
export default Component.extend({
});
@classic
export default class PollEvaluationSummaryOption extends Component {}

View file

@ -1,16 +1,20 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { classNames } from '@ember-decorators/component';
import { computed } from '@ember/object';
import { gt, mapBy, max, readOnly } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { readOnly, max, mapBy, gt } from '@ember/object/computed';
import Component from '@ember/component';
import { copy } from '@ember/object/internals';
import { isEmpty } from '@ember/utils';
import { inject as service } from '@ember/service';
export default Component.extend({
i18n: service(),
@classic
@classNames('evaluation-summary')
export default class PollEvaluationSummary extends Component {
@service
i18n;
classNames: ['evaluation-summary'],
bestOptions: computed('users.[]', function() {
@computed('users.[]')
get bestOptions() {
// can not evaluate answer type free text
if (this.get('poll.isFreeText')) {
return undefined;
@ -70,16 +74,23 @@ export default Component.extend({
}
return bestOptions;
}),
}
currentLocale: readOnly('i18n.locale'),
@readOnly('i18n.locale')
currentLocale;
multipleBestOptions: gt('bestOptions.length', 1),
@gt('bestOptions.length', 1)
multipleBestOptions;
lastParticipationAt: max('participationDates'),
participationDates: mapBy('users', 'creationDate'),
@max('participationDates')
lastParticipationAt;
participantsCount: readOnly('users.length'),
@mapBy('users', 'creationDate')
participationDates;
users: readOnly('poll.users'),
});
@readOnly('users.length')
participantsCount;
@readOnly('poll.users')
users;
}

View file

@ -2,32 +2,39 @@ import { inject as service } from '@ember/service';
import { action, computed } from '@ember/object';
import Controller from '@ember/controller';
export default Controller.extend({
router: service(),
export default class CreateController extends Controller {
@service
router;
canEnterMetaStep: computed('model.pollType', 'visitedSteps', function() {
@computed('model.pollType', 'visitedSteps')
get canEnterMetaStep() {
return this.visitedSteps.has('meta') && this.model.pollType;
}),
}
canEnterOptionsStep: computed('model.title', 'visitedSteps', function() {
@computed('model.title', 'visitedSteps')
get canEnterOptionsStep() {
let { title } = this.model;
return this.visitedSteps.has('options') &&
typeof title === 'string' && title.length >= 2;
}),
}
canEnterOptionsDatetimeStep: computed('model.options.[]', 'visitedSteps', function() {
@computed('model.options.[]', 'visitedSteps')
get canEnterOptionsDatetimeStep() {
return this.visitedSteps.has('options-datetime') && this.model.options.length >= 1;
}),
}
canEnterSettingsStep: computed('model.options.[]', 'visitedSteps', function() {
@computed('model.options.[]', 'visitedSteps')
get canEnterSettingsStep() {
return this.visitedSteps.has('settings') && this.model.options.length >= 1;
}),
}
isFindADate: computed('model.pollType', function() {
@computed('model.pollType')
get isFindADate() {
return this.model.pollType === 'FindADate';
}),
}
updateVisitedSteps: action(function() {
@action
updateVisitedSteps() {
let { currentRouteName } = this.router;
// currentRouteName might not be defined in some edge cases
@ -40,15 +47,15 @@ export default Controller.extend({
// as visitedSteps is a Set must notify about changes manually
this.notifyPropertyChange('visitedSteps');
}),
}
listenForStepChanges() {
this.set('visitedSteps', new Set());
this.router.on('routeDidChange', this.updateVisitedSteps);
},
}
clearListenerForStepChanges() {
this.router.off('routeDidChange', this.updateVisitedSteps);
},
});
}
}

View file

@ -1,4 +1,5 @@
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import {
@ -19,22 +20,23 @@ const Validations = buildValidations({
]
});
export default Controller.extend(Validations, {
actions: {
submit() {
if (this.get('validations.isValid')) {
this.transitionToRoute('create.meta');
}
}
},
export default class CreateIndex extends Controller.extend(Validations) {
@service
i18n;
i18n: service(),
@alias('model.pollType')
pollType;
@action
submit() {
if (this.get('validations.isValid')) {
this.transitionToRoute('create.meta');
}
}
init() {
this._super(...arguments);
super.init(...arguments);
this.get('i18n.locale');
},
pollType: alias('model.pollType')
});
this.i18n.locale;
}
}

View file

@ -1,4 +1,5 @@
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import {
@ -19,27 +20,31 @@ const Validations = buildValidations({
]
});
export default Controller.extend(Validations, {
actions: {
previousPage() {
this.transitionToRoute('create.index');
},
submit() {
if (this.get('validations.isValid')) {
this.transitionToRoute('create.options');
}
}
},
export default class CreateMetaController extends Controller.extend(Validations) {
@service
i18n;
description: alias('model.description'),
@alias('model.description')
description;
@alias('model.title')
title;
init() {
this._super(...arguments);
super.init(...arguments);
this.get('i18n.locale');
},
}
i18n: service(),
@action
previousPage() {
this.transitionToRoute('create.index');
}
title: alias('model.title')
});
@action
submit() {
if (this.get('validations.isValid')) {
this.transitionToRoute('create.options');
}
}
}

View file

@ -1,18 +1,22 @@
import classic from 'ember-classic-decorator';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import moment from 'moment';
export default Controller.extend({
actions: {
nextPage() {
this.normalizeOptions();
@classic
export default class CreateOptionsDatetimeController extends Controller {
@action
nextPage() {
this.normalizeOptions();
this.transitionToRoute('create.settings');
},
previousPage() {
this.transitionToRoute('create.options');
},
},
this.transitionToRoute('create.settings');
}
@action
previousPage() {
this.transitionToRoute('create.options');
}
normalizeOptions() {
const options = this.options;
@ -36,6 +40,8 @@ export default Controller.extend({
// sort options
// ToDo: Find a better way without reseting the options
this.set('options', options.sortBy('title'));
},
options: alias('model.options')
});
}
@alias('model.options')
options;
}

View file

@ -1,19 +1,24 @@
import classic from 'ember-classic-decorator';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
export default Controller.extend({
actions: {
nextPage() {
if (this.isFindADate) {
this.transitionToRoute('create.options-datetime');
} else {
this.transitionToRoute('create.settings');
}
},
previousPage() {
this.transitionToRoute('create.meta');
},
},
@classic
export default class CreateOptionsController extends Controller {
@action
nextPage() {
if (this.isFindADate) {
this.transitionToRoute('create.options-datetime');
} else {
this.transitionToRoute('create.settings');
}
}
isFindADate: alias('model.isFindADate')
});
@action
previousPage() {
this.transitionToRoute('create.meta');
}
@alias('model.isFindADate')
isFindADate;
}

View file

@ -2,7 +2,7 @@ import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { isPresent } from '@ember/utils';
import { computed } from '@ember/object';
import { action, computed } from '@ember/object';
import answersForAnswerType from 'croodle/utils/answers-for-answer-type';
import {
validator, buildValidations
@ -28,92 +28,43 @@ const Validations = buildValidations({
forceAnswer: validator('presence', true)
});
export default Controller.extend(Validations, {
actions: {
previousPage() {
let { isFindADate } = this.model;
export default class CreateSettings extends Controller.extend(Validations) {
@service
encryption;
if (isFindADate) {
this.transitionToRoute('create.options-datetime');
} else {
this.transitionToRoute('create.options');
}
},
async submit() {
if (!this.validations.isValid) {
return;
}
@service
i18n;
let poll = this.model;
@alias('model.anonymousUser')
anonymousUser;
// set timezone if there is atleast one option with time
if (
poll.isFindADate &&
poll.options.any(({ title }) => {
return !moment(title, 'YYYY-MM-DD', true).isValid();
})
) {
this.set('model.timezone', moment.tz.guess());
}
@alias('model.answerType')
answerType;
// save poll
try {
await poll.save();
} catch(err) {
this.flashMessages.danger('error.poll.savingFailed');
throw err;
}
try {
// reload as workaround for bug: duplicated records after save
await poll.reload();
// redirect to new poll
await this.transitionToRoute('poll', poll, {
queryParams: {
encryptionKey: this.encryption.key,
},
});
} catch(err) {
// TODO: show feedback to user
throw err;
}
},
updateAnswerType(answerType) {
this.set('model.answerType', answerType);
this.set('model.answers', answersForAnswerType(answerType));
}
},
anonymousUser: alias('model.anonymousUser'),
answerType: alias('model.answerType'),
answerTypes: computed(function() {
@computed
get answerTypes() {
return [
{ id: 'YesNo', labelTranslation: 'answerTypes.yesNo.label' },
{ id: 'YesNoMaybe', labelTranslation: 'answerTypes.yesNoMaybe.label' },
{ id: 'FreeText', labelTranslation: 'answerTypes.freeText.label' },
];
}),
}
encryption: service(),
@computed('model.expirationDate')
get expirationDuration() {
// TODO: must be calculated based on model.expirationDate
return 'P3M';
}
set expirationDuration(value) {
this.set(
'model.expirationDate',
isPresent(value) ? moment().add(moment.duration(value)).toISOString(): ''
);
return value;
}
expirationDuration: computed('model.expirationDate', {
get() {
// TODO: must be calculated based on model.expirationDate
return 'P3M';
},
set(key, value) {
this.set(
'model.expirationDate',
isPresent(value) ? moment().add(moment.duration(value)).toISOString(): ''
);
return value;
}
}),
expirationDurations: computed('', function() {
@computed
get expirationDurations() {
return [
{ id: 'P7D', labelTranslation: 'create.settings.expirationDurations.P7D' },
{ id: 'P1M', labelTranslation: 'create.settings.expirationDurations.P1M' },
@ -122,15 +73,74 @@ export default Controller.extend(Validations, {
{ id: 'P1Y', labelTranslation: 'create.settings.expirationDurations.P1Y' },
{ id: '', labelTranslation: 'create.settings.expirationDurations.never' },
];
}),
}
forceAnswer: alias('model.forceAnswer'),
@alias('model.forceAnswer')
forceAnswer;
i18n: service(),
@action
previousPage() {
let { isFindADate } = this.model;
if (isFindADate) {
this.transitionToRoute('create.options-datetime');
} else {
this.transitionToRoute('create.options');
}
}
@action
async submit() {
if (!this.validations.isValid) {
return;
}
let poll = this.model;
// set timezone if there is atleast one option with time
if (
poll.isFindADate &&
poll.options.any(({ title }) => {
return !moment(title, 'YYYY-MM-DD', true).isValid();
})
) {
this.set('model.timezone', moment.tz.guess());
}
// save poll
try {
await poll.save();
} catch(err) {
this.flashMessages.danger('error.poll.savingFailed');
throw err;
}
try {
// reload as workaround for bug: duplicated records after save
await poll.reload();
// redirect to new poll
await this.transitionToRoute('poll', poll, {
queryParams: {
encryptionKey: this.encryption.key,
},
});
} catch(err) {
// TODO: show feedback to user
throw err;
}
}
@action
updateAnswerType(answerType) {
this.set('model.answerType', answerType);
this.set('model.answers', answersForAnswerType(answerType));
}
init() {
this._super(...arguments);
super.init(...arguments);
this.get('i18n.locale');
this.i18n.locale;
}
});
}

View file

@ -1,3 +1,5 @@
import classic from 'ember-classic-decorator';
import Controller from '@ember/controller';
export default Controller.extend({});
@classic
export default class ErrorController extends Controller {}

View file

@ -1,11 +1,16 @@
import Controller from '@ember/controller';
import classic from 'ember-classic-decorator';
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
import Controller from '@ember/controller';
import sjcl from 'sjcl';
export default Controller.extend({
decryptionFailed: computed('model', function() {
@classic
export default class PollErrorController extends Controller {
@computed('model')
get decryptionFailed() {
return this.model instanceof sjcl.exception.corrupt;
}),
notFound: equal('model.errors.firstObject.status', '404')
});
}
@equal('model.errors.firstObject.status', '404')
notFound;
}

View file

@ -2,55 +2,101 @@ import { inject as service } from '@ember/service';
import { readOnly } from '@ember/object/computed';
import Controller from '@ember/controller';
import { isPresent, isEmpty } from '@ember/utils';
import { observer, computed } from '@ember/object';
import { action, computed } from '@ember/object';
import { observes } from '@ember-decorators/object';
import moment from 'moment';
export default Controller.extend({
encryption: service(),
flashMessages: service(),
i18n: service(),
router: service(),
export default class PollController extends Controller {
@service
encryption;
actions: {
linkAction(type) {
let flashMessages = this.flashMessages;
switch (type) {
case 'copied':
flashMessages.success(`poll.link.copied`);
break;
@service
flashMessages;
case 'selected':
flashMessages.info(`poll.link.selected`);
break;
}
},
useLocalTimezone() {
this.set('useLocalTimezone', true);
this.set('timezoneChoosen', true);
}
},
@service
i18n;
currentLocale: readOnly('i18n.locale'),
@service
router;
encryptionKey: '',
queryParams: ['encryptionKey'],
queryParams = ['encryptionKey'];
momentLongDayFormat: computed('currentLocale', function() {
encryptionKey = '';
timezoneChoosen = false;
useLocalTimezone = false;
@readOnly('i18n.locale')
currentLocale;
@computed('currentLocale')
get momentLongDayFormat() {
let currentLocale = this.currentLocale;
return moment.localeData(currentLocale)
.longDateFormat('LLLL')
.replace(
moment.localeData(currentLocale).longDateFormat('LT'), '')
.trim();
}),
}
poll: readOnly('model'),
pollUrl: computed('router.currentURL', 'encryptionKey', function() {
@readOnly('model')
poll;
@computed('router.currentURL', 'encryptionKey')
get pollUrl() {
return window.location.href;
}),
}
@computed('poll.expirationDate')
get showExpirationWarning() {
let expirationDate = this.poll.expirationDate;
if (isEmpty(expirationDate)) {
return false;
}
return moment().add(2, 'weeks').isAfter(moment(expirationDate));
}
/*
* return true if current timezone differs from timezone poll got created with
*/
@computed('poll.timezone')
get timezoneDiffers() {
let modelTimezone = this.poll.timezone;
return isPresent(modelTimezone) && moment.tz.guess() !== modelTimezone;
}
@computed('timezoneDiffers', 'timezoneChoosen')
get mustChooseTimezone() {
return this.timezoneDiffers && !this.timezoneChoosen;
}
@computed('useLocalTimezone')
get timezone() {
return this.useLocalTimezone ? undefined : this.poll.timezone;
}
@action
linkAction(type) {
let flashMessages = this.flashMessages;
switch (type) {
case 'copied':
flashMessages.success(`poll.link.copied`);
break;
case 'selected':
flashMessages.info(`poll.link.selected`);
break;
}
}
@action
useLocalTimezone() {
this.set('useLocalTimezone', true);
this.set('timezoneChoosen', true);
}
// TODO: Remove this code. It's spooky.
preventEncryptionKeyChanges: observer('encryptionKey', function() {
@observes('encryptionKey')
preventEncryptionKeyChanges() {
if (
!isEmpty(this.encryption.key) &&
this.encryptionKey !== this.encryption.key
@ -60,33 +106,5 @@ export default Controller.extend({
this.set('encryptionKey', this.encryption.key);
}
}),
showExpirationWarning: computed('poll.expirationDate', function() {
let expirationDate = this.poll.expirationDate;
if (isEmpty(expirationDate)) {
return false;
}
return moment().add(2, 'weeks').isAfter(moment(expirationDate));
}),
timezoneChoosen: false,
/*
* return true if current timezone differs from timezone poll got created with
*/
timezoneDiffers: computed('poll.timezone', function() {
let modelTimezone = this.poll.timezone;
return isPresent(modelTimezone) && moment.tz.guess() !== modelTimezone;
}),
useLocalTimezone: false,
mustChooseTimezone: computed('timezoneDiffers', 'timezoneChoosen', function() {
return this.timezoneDiffers && !this.timezoneChoosen;
}),
timezone: computed('useLocalTimezone', function() {
return this.useLocalTimezone ? undefined : this.poll.timezone;
})
});
}
}

View file

@ -1,29 +1,41 @@
import { inject as service } from '@ember/service';
import { and, gt, not, readOnly } from '@ember/object/computed';
import classic from 'ember-classic-decorator';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import { readOnly, not, gt, and } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
export default Controller.extend({
currentLocale: readOnly('i18n.locale'),
@classic
export default class PollEvaluationController extends Controller {
@readOnly('i18n.locale')
currentLocale;
hasTimes: readOnly('poll.hasTimes'),
@readOnly('poll.hasTimes')
hasTimes;
i18n: service(),
@service
i18n;
momentLongDayFormat: readOnly('pollController.momentLongDayFormat'),
@readOnly('pollController.momentLongDayFormat')
momentLongDayFormat;
poll: readOnly('model'),
pollController: controller('poll'),
@readOnly('model')
poll;
timezone: readOnly('pollController.timezone'),
@controller('poll')
pollController;
users: readOnly('poll.users'),
@readOnly('pollController.timezone')
timezone;
@readOnly('poll.users')
users;
/*
* evaluates poll data
* if free text answers are allowed evaluation is disabled
*/
evaluation: computed('users.[]', function() {
@computed('users.[]')
get evaluation() {
if (!this.isEvaluable) {
return [];
}
@ -83,9 +95,14 @@ export default Controller.extend({
});
return evaluation;
}),
}
hasUsers: gt('poll.users.length', 0),
isNotFreeText: not('poll.isFreeText'),
isEvaluable: and('hasUsers', 'isNotFreeText'),
});
@gt('poll.users.length', 0)
hasUsers;
@not('poll.isFreeText')
isNotFreeText;
@and('hasUsers', 'isNotFreeText')
isEvaluable;
}

View file

@ -1,5 +1,6 @@
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import { readOnly, not } from '@ember/object/computed';
import { not, readOnly } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
import { getOwner } from '@ember/application';
import { isPresent, isEmpty } from '@ember/utils';
@ -56,6 +57,11 @@ const SelectionValidations = buildValidations({
})
});
@classic
class SelectionObject extends EmberObject.extend(SelectionValidations) {
value = null;
}
export default Controller.extend(Validations, {
actions: {
async submit() {
@ -179,14 +185,6 @@ export default Controller.extend(Validations, {
let isFindADate = this.isFindADate;
let lastDate;
let SelectionObject = EmberObject.extend(SelectionValidations, {
// forceAnswer and isFreeText must be included in model
// cause otherwise validations can't depend on it
forceAnswer: this.forceAnswer,
isFreeText: this.isFreeText,
value: null
});
return options.map((option) => {
let labelValue;
let momentFormat;
@ -216,7 +214,12 @@ export default Controller.extend(Validations, {
let owner = getOwner(this);
return SelectionObject.create(owner.ownerInjection(), {
labelValue,
momentFormat
momentFormat,
// forceAnswer and isFreeText must be included in model
// cause otherwise validations can't depend on it
forceAnswer: this.forceAnswer,
isFreeText: this.isFreeText,
});
});
}),

View file

@ -1,9 +1,18 @@
import classic from 'ember-classic-decorator';
import { attr } from '@ember-data/model';
import Fragment from 'ember-data-model-fragments/fragment';
export default Fragment.extend({
type: attr('string'),
label: attr('string'),
labelTranslation: attr('string'),
icon: attr('string')
});
@classic
export default class Answer extends Fragment {
@attr('string')
type;
@attr('string')
label;
@attr('string')
labelTranslation;
@attr('string')
icon;
}

View file

@ -1,8 +1,9 @@
import { attr } from '@ember-data/model';
import classic from 'ember-classic-decorator';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import { readOnly } from '@ember/object/computed';
import { attr } from '@ember-data/model';
import { assert } from '@ember/debug';
import { computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import moment from 'moment';
import Fragment from 'ember-data-model-fragments/fragment';
@ -58,18 +59,32 @@ const Validations = buildValidations({
]
});
export default Fragment.extend(Validations, {
poll: fragmentOwner(),
title: attr('string'),
@classic
export default class Option extends Fragment.extend(Validations) {
@service
i18n;
date: computed('title', function() {
@fragmentOwner()
poll;
@attr('string')
title;
// isPartiallyFilled should be set only for times on creation if input is filled
// partially (e.g. "11:--"). It's required cause ember-cp-validations does not
// provide any method to push a validation error into validations. It's only
// working based on a property of the model.
isPartiallyFilled = false;
@computed('title')
get date() {
const allowedFormats = [
'YYYY-MM-DD',
'YYYY-MM-DDTHH:mm:ss.SSSZ'
];
const value = this.title;
if (isEmpty(value)) {
return;
return null;
}
const format = allowedFormats.find((f) => {
@ -78,24 +93,26 @@ export default Fragment.extend(Validations, {
return f.length === value.length && moment(value, f, true).isValid();
});
if (isEmpty(format)) {
return;
return null;
}
return moment(value, format, true);
}),
}
day: computed('date', function() {
@computed('date')
get day() {
const date = this.date;
if (!moment.isMoment(date)) {
return;
return null;
}
return date.format('YYYY-MM-DD');
}),
}
dayFormatted: computed('date', 'i18n.locale', function() {
@computed('date', 'i18n.locale')
get dayFormatted() {
let date = this.date;
if (!moment.isMoment(date)) {
return;
return null;
}
const locale = this.get('i18n.locale');
@ -113,59 +130,54 @@ export default Fragment.extend(Validations, {
}
return date.format(format);
}),
}
hasTime: computed('title', function() {
@computed('title')
get hasTime() {
return moment.isMoment(this.date) &&
this.title.length === 'YYYY-MM-DDTHH:mm:ss.SSSZ'.length;
}),
}
// isPartiallyFilled should be set only for times on creation if input is filled
// partially (e.g. "11:--"). It's required cause ember-cp-validations does not
// provide any method to push a validation error into validations. It's only
// working based on a property of the model.
isPartiallyFilled: false,
@computed('date')
get time() {
const date = this.date;
if (!moment.isMoment(date)) {
return null;
}
// verify that value is an ISO 8601 date string containg time
// testing length is faster than parsing with moment
const value = this.title;
if (value.length !== 'YYYY-MM-DDTHH:mm:ss.SSSZ'.length) {
return null;
}
time: computed('date', {
get() {
const date = this.date;
if (!moment.isMoment(date)) {
return;
}
// verify that value is an ISO 8601 date string containg time
// testing length is faster than parsing with moment
const value = this.title;
if (value.length !== 'YYYY-MM-DDTHH:mm:ss.SSSZ'.length) {
return;
}
return date.format('HH:mm');
}
set time(value) {
let date = this.date;
assert(
'can not set a time if current value is not a valid date',
moment.isMoment(date)
);
return date.format('HH:mm');
},
set(key, value) {
let date = this.date;
assert(
'can not set a time if current value is not a valid date',
moment.isMoment(date)
);
// set time to undefined if value is false
if (isEmpty(value)) {
this.set('title', date.format('YYYY-MM-DD'));
return value;
}
if (!moment(value, 'HH:mm', true).isValid()) {
return value;
}
const [ hour, minute ] = value.split(':');
this.set('title', date.hour(hour).minute(minute).toISOString());
// set time to undefined if value is false
if (isEmpty(value)) {
this.set('title', date.format('YYYY-MM-DD'));
return value;
}
}),
i18n: service(),
if (!moment(value, 'HH:mm', true).isValid()) {
return value;
}
const [ hour, minute ] = value.split(':');
this.set('title', date.hour(hour).minute(minute).toISOString());
return value;
}
init() {
super.init(...arguments);
this.get('i18n.locale');
}
});
}

View file

@ -1,63 +1,79 @@
import Model, { hasMany, attr } from '@ember-data/model';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import classic from 'ember-classic-decorator';
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
import Model, { hasMany, attr } from '@ember-data/model';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
export default Model.extend({
@classic
export default class Poll extends Model {
/*
* relationships
*/
users: hasMany('user', { async: false }),
@hasMany('user', { async: false })
users;
/*
* properties
*/
// Is participation without user name possibile?
anonymousUser: attr('boolean'),
@attr('boolean')
anonymousUser;
// array of possible answers
answers: fragmentArray('answer'),
@fragmentArray('answer')
answers;
// YesNo, YesNoMaybe or Freetext
answerType: attr('string'),
@attr('string')
answerType;
// ISO-8601 combined date and time string in UTC
creationDate: attr('date'),
@attr('date')
creationDate;
// polls description
description: attr('string', {
@attr('string', {
defaultValue: ''
}),
})
description;
// ISO 8601 date + time string in UTC
expirationDate: attr('string', {
@attr('string', {
includePlainOnCreate: 'serverExpirationDate'
}),
})
expirationDate;
// Must all options been answered?
forceAnswer: attr('boolean'),
@attr('boolean')
forceAnswer;
// array of polls options
options: fragmentArray('option'),
@fragmentArray('option')
options;
// FindADate or MakeAPoll
pollType: attr('string'),
@attr('string')
pollType;
// timezone poll got created in (like "Europe/Berlin")
timezone: attr('string'),
@attr('string')
timezone;
// polls title
title: attr('string'),
@attr('string')
title;
// Croodle version poll got created with
version: attr('string', {
@attr('string', {
encrypted: false
}),
})
version;
/*
* computed properties
*/
hasTimes: computed('options.[]', function() {
@computed('options.[]')
get hasTimes() {
if (this.isMakeAPoll) {
return false;
}
@ -66,9 +82,14 @@ export default Model.extend({
let dayStringLength = 10; // 'YYYY-MM-DD'.length
return option.title.length > dayStringLength;
});
}),
}
isFindADate: equal('pollType', 'FindADate'),
isFreeText: equal('answerType', 'FreeText'),
isMakeAPoll: equal('pollType', 'MakeAPoll'),
});
@equal('pollType', 'FindADate')
isFindADate;
@equal('answerType', 'FreeText')
isFreeText;
@equal('pollType', 'MakeAPoll')
isMakeAPoll;
}

View file

@ -1,29 +1,36 @@
import classic from 'ember-classic-decorator';
import Model, { belongsTo, attr } from '@ember-data/model';
import {
fragmentArray
} from 'ember-data-model-fragments/attributes';
export default Model.extend({
@classic
export default class User extends Model {
/*
* relationship
*/
poll: belongsTo('poll'),
@belongsTo('poll')
poll;
/*
* properties
*/
// ISO 8601 date + time string
creationDate: attr('date'),
@attr('date')
creationDate;
// user name
name: attr('string'),
@attr('string')
name;
// array of users selections
// must be in same order as options property of poll
selections: fragmentArray('selection'),
@fragmentArray('selection')
selections;
// Croodle version user got created with
version: attr('string', {
@attr('string', {
encrypted: false
})
});
version;
}

View file

@ -1,10 +1,12 @@
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import config from 'croodle/config/environment';
import answersForAnswerType from 'croodle/utils/answers-for-answer-type';
/* global moment */
export default Route.extend({
@classic
export default class CreateRoute extends Route {
beforeModel(transition) {
// enforce that wizzard is started at create.index
if (transition.targetName !== 'create.index') {
@ -13,9 +15,10 @@ export default Route.extend({
// set encryption key
this.encryption.generateKey();
},
}
encryption: service(),
@service
encryption;
model() {
// create empty poll
@ -30,15 +33,15 @@ export default Route.extend({
expirationDate: moment().add(3, 'month').toISOString(),
version: config.APP.version,
});
},
}
activate() {
let controller = this.controllerFor(this.routeName);
controller.listenForStepChanges();
},
}
deactivate() {
let controller = this.controllerFor(this.routeName);
controller.clearListenerForStepChanges();
},
});
}
}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Route from '@ember/routing/route';
export default Route.extend({
@classic
export default class IndexRoute extends Route {
model() {
return this.modelFor('create');
}
});
}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Route from '@ember/routing/route';
export default Route.extend({
@classic
export default class MetaRoute extends Route {
model() {
return this.modelFor('create');
}
});
}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Route from '@ember/routing/route';
export default Route.extend({
@classic
export default class OptionsDatetimeRoute extends Route {
model() {
return this.modelFor('create');
}
});
}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Route from '@ember/routing/route';
export default Route.extend({
@classic
export default class OptionsRoute extends Route {
model() {
return this.modelFor('create');
}
});
}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Route from '@ember/routing/route';
export default Route.extend({
@classic
export default class SettingsRoute extends Route {
model() {
return this.modelFor('create');
}
});
}

View file

@ -1,18 +1,21 @@
import classic from 'ember-classic-decorator';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
export default Route.extend({
actions: {
error(error) {
if (error && error.status === 404) {
return this.transitionTo('404');
}
return true;
@classic
export default class PollRoute extends Route {
@action
error(error) {
if (error && error.status === 404) {
return this.transitionTo('404');
}
},
encryption: service(),
return true;
}
@service
encryption;
model(params) {
// get encryption key from query parameter in singleton
@ -20,7 +23,7 @@ export default Route.extend({
this.set('encryption.key', params.encryptionKey);
return this.store.find('poll', params.poll_id);
},
}
redirect(poll, transition) {
if (transition.targetName === 'poll.index') {
@ -31,4 +34,4 @@ export default Route.extend({
});
}
}
});
}

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import classic from 'ember-classic-decorator';
import RESTSerializer from '@ember-data/serializer/rest';
export default RESTSerializer.extend({
});
@classic
export default class AnswerSerializer extends RESTSerializer {}

View file

@ -1,6 +1,7 @@
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import RESTSerializer from '@ember-data/serializer/rest';
import { isEmpty } from '@ember/utils';
import { inject as service } from '@ember/service';
/*
* extends DS.RESTSerializer to implement encryption
@ -15,10 +16,12 @@ import { inject as service } from '@ember/service';
* If set the attribute will be included plain (not encrypted) when
* recorde is created. Value is the attributes name used.
*/
export default RESTSerializer.extend({
isNewSerializerAPI: true,
@classic
export default class ApplicationSerializer extends RESTSerializer {
isNewSerializerAPI = true;
encryption: service(),
@service
encryption;
/*
* implement decryption
@ -41,14 +44,14 @@ export default RESTSerializer.extend({
resourceHash = this.legacySupport(resourceHash);
}
return this._super(modelClass, resourceHash, prop);
},
return super.normalize(modelClass, resourceHash, prop);
}
/*
* implement encryption
*/
serializeAttribute(snapshot, json, key, attribute) {
this._super(snapshot, json, key, attribute);
super.serializeAttribute(snapshot, json, key, attribute);
// map includePlainOnCreate after serialization of attribute hash
// but before encryption so we can just use the serialized hash
@ -66,4 +69,4 @@ export default RESTSerializer.extend({
json[key] = this.encryption.encrypt(json[key]);
}
}
});
}

View file

@ -1,4 +1,5 @@
import classic from 'ember-classic-decorator';
import RESTSerializer from '@ember-data/serializer/rest';
export default RESTSerializer.extend({
});
@classic
export default class OptionSerializer extends RESTSerializer {}

View file

@ -1,13 +1,13 @@
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application';
import ApplicationSerializer from './application';
export default ApplicationAdapter.extend(EmbeddedRecordsMixin, {
attrs: {
export default class PollSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
attrs = {
users: {
deserialize: 'records'
}
},
};
legacySupport(resourceHash) {
// croodle <= 0.3.0
@ -24,4 +24,4 @@ export default ApplicationAdapter.extend(EmbeddedRecordsMixin, {
return resourceHash;
}
});
}

View file

@ -1,4 +1,5 @@
import classic from 'ember-classic-decorator';
import RESTSerializer from '@ember-data/serializer/rest';
export default RESTSerializer.extend({
});
@classic
export default class SelectionSerializer extends RESTSerializer {}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application';
import ApplicationSerializer from './application';
export default ApplicationAdapter.extend({
@classic
export default class UserSerializer extends ApplicationSerializer {
legacySupport(resourceHash) {
/*
* Croodle <= 0.3.0:
@ -30,4 +32,4 @@ export default ApplicationAdapter.extend({
return resourceHash;
}
});
}

View file

@ -1,9 +1,11 @@
import classic from 'ember-classic-decorator';
import Service from '@ember/service';
import generatePassphrase from '../utils/generate-passphrase';
import sjcl from 'sjcl';
export default Service.extend({
key: null,
@classic
export default class EncryptionService extends Service {
key = null;
decrypt(value) {
return JSON.parse(
@ -12,21 +14,17 @@ export default Service.extend({
value
)
);
},
}
encrypt(value) {
return sjcl.encrypt(
this.key,
JSON.stringify(value)
);
},
}
generateKey() {
const passphraseLength = 40;
this.set('key', generatePassphrase(passphraseLength));
},
init() {
this._super(...arguments);
}
});
}

View file

@ -86,7 +86,7 @@
</div>
<BsButton
@onClick={{action "addOption" date}}
@onClick={{fn this.addOption date}}
@type="link"
@size="sm"
class="add cr-option-menu__button cr-option-menu__add-button float-left"

View file

@ -31,7 +31,7 @@
</div>
<BsButton
@onClick={{action "addOption" option}}
@onClick={{fn this.addOption option}}
@type="link"
@size="sm"
class="add float-left"

View file

@ -8,7 +8,7 @@
as |form|
>
{{#if isMakeAPoll}}
<CreateOptionsText @options={{options}} @addOption="addOption" @deleteOption="deleteOption" @form={{form}} />
<CreateOptionsText @options={{options}} @form={{form}} />
{{else}}
<CreateOptionsDates @options={{options}} @form={{form}} />
{{/if}}

View file

@ -1,7 +1,9 @@
import classic from 'ember-classic-decorator';
import Alias from 'ember-cp-validations/validators/alias';
export default Alias.extend({
@classic
export default class AliasValidator extends Alias {
validate(value, options, model, attribute) {
return this._super(value, options, model, attribute) || true;
return super.validate(value, options, model, attribute) || true;
}
});
}

View file

@ -1,9 +1,11 @@
import classic from 'ember-classic-decorator';
import BaseValidator from 'ember-cp-validations/validators/base';
const Truthy = BaseValidator.extend({
@classic
class FalsyValidator extends BaseValidator {
validate(value, options) {
return value ? this.createErrorMessage('iso8601', value, options) : true;
}
});
}
export default Truthy;
export default FalsyValidator;

View file

@ -1,10 +1,12 @@
import classic from 'ember-classic-decorator';
import { isArray } from '@ember/array';
import { isEmpty } from '@ember/utils';
import { assert } from '@ember/debug';
import BaseValidator from 'ember-cp-validations/validators/base';
import moment from 'moment';
export default BaseValidator.extend({
@classic
export default class Iso8601Validator extends BaseValidator {
validate(value, options = {}) {
assert(
'options.validFormats must not be set or an array of momentJS format strings',
@ -33,4 +35,4 @@ export default BaseValidator.extend({
return this.createErrorMessage('iso8601', value, options);
}
}
});
}

View file

@ -1,6 +1,8 @@
import Messages from 'ember-i18n-cp-validations/validators/messages';
import classic from 'ember-classic-decorator';
import BaseMessages from 'ember-i18n-cp-validations/validators/messages';
export default Messages.extend({
validCollection: 'This collection is not valid.',
time: '{{value}} is not a vaild time.'
});
@classic
export default class ValidationMessages extends BaseMessages {
validCollection = 'This collection is not valid.';
time = '{{value}} is not a vaild time.';
}

View file

@ -1,8 +1,10 @@
import classic from 'ember-classic-decorator';
import { isEmpty } from '@ember/utils';
import BaseValidator from 'ember-cp-validations/validators/base';
import moment from 'moment';
export default BaseValidator.extend({
@classic
export default class TimeValidator extends BaseValidator {
validate(value, options) {
let valid;
@ -28,4 +30,4 @@ export default BaseValidator.extend({
return this.createErrorMessage('time', value, options);
}
}
});
}

View file

@ -1,9 +1,11 @@
import classic from 'ember-classic-decorator';
import { isArray } from '@ember/array';
import { isPresent, isEmpty } from '@ember/utils';
import { assert } from '@ember/debug';
import BaseValidator from 'ember-cp-validations/validators/base';
export default BaseValidator.extend({
@classic
export default class UniqueValidator extends BaseValidator {
validate(value, options, model, attribute) {
assert(
'options.parent is required',
@ -46,4 +48,4 @@ export default BaseValidator.extend({
return true;
}
}
});
}

View file

@ -1,6 +1,8 @@
import classic from 'ember-classic-decorator';
import BaseValidator from 'ember-cp-validations/validators/base';
export default BaseValidator.extend({
@classic
export default class ValidCollectionValidator extends BaseValidator {
validate(value, options) {
if (options.active === false) {
return true;
@ -16,4 +18,4 @@ export default BaseValidator.extend({
return this.createErrorMessage('validCollection', options, value);
}
}
});
}

View file

@ -4,7 +4,7 @@ module.exports = {
app: {
javascript: {
pattern: 'assets/*.js',
limit: '410KB',
limit: '420KB',
compression: 'gzip'
},
css: {

View file

@ -21,13 +21,14 @@
"devDependencies": {
"@ember/optional-features": "^1.1.0",
"@glimmer/component": "^1.0.0",
"babel-eslint": "^10.0.3",
"babel-eslint": "^8.0.0",
"bootstrap": "^4.3.1",
"broccoli-asset-rev": "^3.0.0",
"ember-auto-import": "^1.5.3",
"ember-awesome-macros": "^5.0.0",
"ember-bootstrap": "^3.0.0",
"ember-bootstrap-cp-validations": "^1.0.0",
"ember-classic-decorator": "^1.0.5",
"ember-cli": "~3.15.1",
"ember-cli-acceptance-test-helpers": "^1.0.0",
"ember-cli-app-version": "^3.2.0",
@ -55,6 +56,7 @@
"ember-cp-validations": "^4.0.0-beta.8",
"ember-data": "~3.12.0",
"ember-data-model-fragments": "^4.0.0",
"ember-decorators": "^6.1.1",
"ember-export-application-global": "^2.0.1",
"ember-fetch": "^7.0.0",
"ember-i18n": "^5.0.2",

1317
yarn.lock

File diff suppressed because it is too large Load diff