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:
parent
9983f76189
commit
c9482786c1
58 changed files with 1755 additions and 1260 deletions
|
@ -18,6 +18,7 @@ sudo: false
|
|||
|
||||
addons:
|
||||
chrome: stable
|
||||
firefox: latest-esr
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
|
|
@ -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, '')
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
|
||||
<BsButton
|
||||
@onClick={{action "addOption" option}}
|
||||
@onClick={{fn this.addOption option}}
|
||||
@type="link"
|
||||
@size="sm"
|
||||
class="add float-left"
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ module.exports = {
|
|||
app: {
|
||||
javascript: {
|
||||
pattern: 'assets/*.js',
|
||||
limit: '410KB',
|
||||
limit: '420KB',
|
||||
compression: 'gzip'
|
||||
},
|
||||
css: {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue