refactor poll creation to not rely on Ember Data model (#710)
This commit is contained in:
parent
8a4954f4e8
commit
1072953cd4
16 changed files with 366 additions and 401 deletions
|
@ -9,17 +9,9 @@ export default class CreateOptionsDates extends Component {
|
|||
this.selectedDays.length >= 1 ? this.selectedDays[0] : DateTime.local();
|
||||
|
||||
get selectedDays() {
|
||||
// Options may contain the same date multiple times with different ime
|
||||
// Must filter out those duplicates as otherwise unselect would only
|
||||
// remove one entry but not all duplicates.
|
||||
return Array.from(
|
||||
// using Set to remove duplicate values
|
||||
new Set(
|
||||
this.args.options.map(({ value }) =>
|
||||
DateTime.fromISO(value).toISODate(),
|
||||
),
|
||||
),
|
||||
).map((isoDate) => DateTime.fromISO(isoDate));
|
||||
return Array.from(this.args.options).map(({ value }) =>
|
||||
DateTime.fromISO(value),
|
||||
);
|
||||
}
|
||||
|
||||
get calendarCenterNext() {
|
||||
|
@ -34,53 +26,8 @@ export default class CreateOptionsDates extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
// A date has either been added or removed. If it has been removed, we must
|
||||
// remove all options for that date. It may be multiple options with
|
||||
// different times at that date.
|
||||
|
||||
// If any date received as an input argument is _not_ yet in the list of
|
||||
// dates, it has been added.
|
||||
const dateAdded = newDatesAsLuxonDateTime.find((newDateAsLuxonDateTime) => {
|
||||
return !this.selectedDays.some(
|
||||
(selectedDay) =>
|
||||
selectedDay.toISODate() === newDateAsLuxonDateTime.toISODate(),
|
||||
);
|
||||
});
|
||||
|
||||
if (dateAdded) {
|
||||
this.args.updateOptions(
|
||||
[
|
||||
...this.args.options.map(({ value }) => value),
|
||||
dateAdded.toISODate(),
|
||||
].sort(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no date has been added, one date must have been removed. It has been
|
||||
// removed if there is one date in current selectedDays but not in the new
|
||||
// dates received as input argument to the function.
|
||||
const dateRemoved = this.selectedDays.find((selectedDay) => {
|
||||
return !newDatesAsLuxonDateTime.some(
|
||||
(newDateAsLuxonDateTime) =>
|
||||
newDateAsLuxonDateTime.toISODate() === selectedDay.toISODate(),
|
||||
);
|
||||
});
|
||||
|
||||
if (dateRemoved) {
|
||||
this.args.updateOptions(
|
||||
this.args.options
|
||||
.filter(
|
||||
({ value }) =>
|
||||
DateTime.fromISO(value).toISODate() !== dateRemoved.toISODate(),
|
||||
)
|
||||
.map(({ value }) => value),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'No date has been added or removed. This cannot be the case. Something spooky is going on.',
|
||||
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,47 +14,39 @@
|
|||
as |form|
|
||||
>
|
||||
<div class="days">
|
||||
{{#each this.formData.options as |option index|}}
|
||||
{{#each-in this.formData.datetimes as |date timeOptions|}}
|
||||
{{#each timeOptions as |timeOption indexInTimeOptions|}}
|
||||
{{!
|
||||
show summarized validation state for all times in a day
|
||||
}}
|
||||
|
||||
<div
|
||||
{{!
|
||||
TODO: daysValidationState is not defined!
|
||||
}}
|
||||
class={{if
|
||||
(get this.daysValidationState option.day)
|
||||
(concat "label-has-" (get this.daysValidationState option.day))
|
||||
(get this.daysValidationState date)
|
||||
(concat "label-has-" (get this.daysValidationState date))
|
||||
"label-has-no-validation"
|
||||
}}
|
||||
data-test-day={{option.day}}
|
||||
data-test-day={{date}}
|
||||
>
|
||||
<form.element
|
||||
{{!
|
||||
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
|
||||
}}
|
||||
@label={{format-date
|
||||
option.jsDate
|
||||
timeOption.jsDate
|
||||
weekday="long"
|
||||
day="numeric"
|
||||
month="long"
|
||||
year="numeric"
|
||||
}}
|
||||
{{!
|
||||
show label only if it differ from label before
|
||||
Nested-helpers are called first and object-at requires a positive integer
|
||||
but returns undefined if an element with the passed in index does not exist.
|
||||
Therefore we pass in array length if index is null. Cause index starting
|
||||
by zero there can't be any element with an index === array.length.
|
||||
show label only for the first time of this date
|
||||
}}
|
||||
@invisibleLabel={{eq
|
||||
option.day
|
||||
(get
|
||||
(object-at
|
||||
(if index (sub index 1) this.formData.options.length)
|
||||
this.formData.options
|
||||
)
|
||||
"day"
|
||||
)
|
||||
}}
|
||||
@model={{option}}
|
||||
@invisibleLabel={{gt indexInTimeOptions 0}}
|
||||
@model={{timeOption}}
|
||||
@property="time"
|
||||
class="option"
|
||||
as |el|
|
||||
|
@ -65,10 +57,10 @@
|
|||
@type="time"
|
||||
@value={{el.value}}
|
||||
{{! focus input if it's the first one }}
|
||||
{{autofocus enabled=(eq index 0)}}
|
||||
{{autofocus enabled=timeOption.isFirstTimeOnFirstDate}}
|
||||
{{! run validation for partially filled input on focusout event }}
|
||||
{{on "focusout" (fn this.validateInput option)}}
|
||||
{{on "change" (fn this.validateInput option)}}
|
||||
{{on "focusout" (fn this.validateInput timeOption)}}
|
||||
{{on "change" (fn this.validateInput timeOption)}}
|
||||
{{!
|
||||
Validation for partially input field must be reset if input is cleared.
|
||||
But `@onChange` is not called and `focusout` event not triggered in that
|
||||
|
@ -83,13 +75,13 @@
|
|||
partially filling in first place and Desktop Safari as well as IE 11
|
||||
do not support `<input type="time">`.
|
||||
}}
|
||||
{{on "focusin" (fn this.updateInputValidation option)}}
|
||||
{{on "keyup" (fn this.updateInputValidation option)}}
|
||||
{{on "focusin" (fn this.updateInputValidation timeOption)}}
|
||||
{{on "keyup" (fn this.updateInputValidation timeOption)}}
|
||||
id={{el.id}}
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<BsButton
|
||||
@onClick={{fn this.formData.deleteOption option}}
|
||||
@onClick={{fn this.formData.deleteOption timeOption}}
|
||||
@type="link"
|
||||
class="delete"
|
||||
data-test-action="delete"
|
||||
|
@ -107,7 +99,7 @@
|
|||
</div>
|
||||
|
||||
<BsButton
|
||||
@onClick={{fn this.formData.addOption index option.day}}
|
||||
@onClick={{fn this.formData.addOption date}}
|
||||
@type="link"
|
||||
@size="sm"
|
||||
class="add cr-option-menu__button cr-option-menu__add-button float-left"
|
||||
|
@ -125,6 +117,7 @@
|
|||
</form.element>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
</div>
|
||||
|
||||
{{#if this.formData.hasMultipleDays}}
|
||||
|
@ -145,7 +138,7 @@
|
|||
<NextButton />
|
||||
</div>
|
||||
<div class="col-6 col-md-4 order-1 text-right">
|
||||
<BackButton @onClick={{action "previousPage"}} />
|
||||
<BackButton @onClick={{this.previousPage}} />
|
||||
</div>
|
||||
</div>
|
||||
</BsForm>
|
||||
|
|
|
@ -2,15 +2,15 @@ import Component from '@glimmer/component';
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { TrackedArray } from 'tracked-built-ins';
|
||||
import { TrackedMap, TrackedSet } from 'tracked-built-ins';
|
||||
import { DateTime } from 'luxon';
|
||||
import IntlMessage from '../utils/intl-message';
|
||||
|
||||
class FormDataOption {
|
||||
class FormDataTimeOption {
|
||||
formData;
|
||||
|
||||
// ISO 8601 date string: YYYY-MM-DD
|
||||
day;
|
||||
date;
|
||||
|
||||
// ISO 8601 time string without seconds: HH:mm
|
||||
@tracked time;
|
||||
|
@ -31,11 +31,11 @@ class FormDataOption {
|
|||
// It should show a validation error if the same time has been entered for
|
||||
// the same day already before. Only the second input field containing the
|
||||
// duplicated time should show the validation error.
|
||||
const { formData, day } = this;
|
||||
const optionsForThisDay = formData.optionsGroupedByDay[day];
|
||||
const isDuplicate = optionsForThisDay
|
||||
.slice(0, optionsForThisDay.indexOf(this))
|
||||
.some((option) => option.time == this.time);
|
||||
const { formData, date } = this;
|
||||
const timesForThisDate = Array.from(formData.datetimes.get(date));
|
||||
const isDuplicate = timesForThisDate
|
||||
.slice(0, timesForThisDate.indexOf(this))
|
||||
.some((timeOption) => timeOption.time == this.time);
|
||||
if (isDuplicate) {
|
||||
return new IntlMessage('create.options-datetime.error.duplicatedDate');
|
||||
}
|
||||
|
@ -44,8 +44,8 @@ class FormDataOption {
|
|||
}
|
||||
|
||||
get datetime() {
|
||||
const { day, time } = this;
|
||||
const isoString = time === null ? day : `${day}T${time}`;
|
||||
const { date, time } = this;
|
||||
const isoString = time === null ? date : `${date}T${time}`;
|
||||
return DateTime.fromISO(isoString);
|
||||
}
|
||||
|
||||
|
@ -59,66 +59,61 @@ class FormDataOption {
|
|||
return timeValidation === null;
|
||||
}
|
||||
|
||||
constructor(formData, { day, time }) {
|
||||
get isFirstTimeOnFirstDate() {
|
||||
const { formData, date } = this;
|
||||
const { datetimes } = formData;
|
||||
return (
|
||||
Array.from(datetimes.keys())[0] === date &&
|
||||
Array.from(datetimes.get(date))[0] === this
|
||||
);
|
||||
}
|
||||
|
||||
constructor(formData, { date, time }) {
|
||||
this.formData = formData;
|
||||
this.day = day;
|
||||
this.date = date;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
class FormData {
|
||||
@tracked options;
|
||||
@tracked datetimes;
|
||||
|
||||
get optionsValidation() {
|
||||
const { options } = this;
|
||||
const allOptionsAreValid = options.every((option) => option.isValid);
|
||||
if (!allOptionsAreValid) {
|
||||
const { datetimes } = this;
|
||||
const allTimeOptionsAreValid = Array.from(datetimes.values()).every(
|
||||
(timeOptionsForDate) =>
|
||||
Array.from(timeOptionsForDate).every(
|
||||
(timeOption) => timeOption.isValid,
|
||||
),
|
||||
);
|
||||
if (!allTimeOptionsAreValid) {
|
||||
return IntlMessage('create.options-datetime.error.invalidTime');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get optionsGroupedByDay() {
|
||||
const { options } = this;
|
||||
const groupedOptions = {};
|
||||
|
||||
for (const option of options) {
|
||||
const { day } = option;
|
||||
|
||||
if (!groupedOptions[day]) {
|
||||
groupedOptions[day] = [];
|
||||
}
|
||||
|
||||
groupedOptions[day].push(option);
|
||||
}
|
||||
|
||||
return groupedOptions;
|
||||
}
|
||||
|
||||
get hasMultipleDays() {
|
||||
return Object.keys(this.optionsGroupedByDay).length > 1;
|
||||
return this.datetimes.size > 1;
|
||||
}
|
||||
|
||||
@action
|
||||
addOption(position, day) {
|
||||
this.options.splice(
|
||||
position + 1,
|
||||
0,
|
||||
new FormDataOption(this, { day, time: null }),
|
||||
);
|
||||
addOption(date) {
|
||||
this.datetimes
|
||||
.get(date)
|
||||
.add(new FormDataTimeOption(this, { date, time: null }));
|
||||
}
|
||||
|
||||
/*
|
||||
* removes target option if it's not the only date for this day
|
||||
* removes target option if it's not the only time for this date
|
||||
* otherwise it deletes time for this date
|
||||
*/
|
||||
@action
|
||||
deleteOption(option) {
|
||||
const optionsForThisDay = this.optionsGroupedByDay[option.day];
|
||||
const timeOptionsForDate = this.datetimes.get(option.date);
|
||||
|
||||
if (optionsForThisDay.length > 1) {
|
||||
this.options.splice(this.options.indexOf(option), 1);
|
||||
if (timeOptionsForDate.size > 1) {
|
||||
timeOptionsForDate.delete(option);
|
||||
} else {
|
||||
option.time = null;
|
||||
}
|
||||
|
@ -126,42 +121,53 @@ class FormData {
|
|||
|
||||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
const { optionsGroupedByDay } = this;
|
||||
const days = Object.keys(optionsGroupedByDay).sort();
|
||||
const firstDay = days[0];
|
||||
const optionsForFirstDay = optionsGroupedByDay[firstDay];
|
||||
|
||||
const timesForFirstDayAreValid = optionsForFirstDay.every(
|
||||
(option) => option.isValid,
|
||||
const timeOptionsForFirstDay = Array.from(
|
||||
Array.from(this.datetimes.values())[0],
|
||||
);
|
||||
const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
|
||||
(timeOption) => timeOption.isValid,
|
||||
);
|
||||
if (!timesForFirstDayAreValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timesForFirstDay = optionsForFirstDay.map((option) => option.time);
|
||||
|
||||
this.options = new TrackedArray(
|
||||
days
|
||||
.map((day) =>
|
||||
timesForFirstDay.map(
|
||||
(time) => new FormDataOption(this, { day, time }),
|
||||
for (const date of Array.from(this.datetimes.keys()).slice(1)) {
|
||||
this.datetimes.set(
|
||||
date,
|
||||
new TrackedSet(
|
||||
timeOptionsForFirstDay.map(
|
||||
({ time }) => new FormDataTimeOption(this, { date, time }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
constructor({ dates, times }) {
|
||||
const datetimes = new Map();
|
||||
|
||||
for (const date of dates) {
|
||||
const timesForDate = times.has(date)
|
||||
? Array.from(times.get(date))
|
||||
: [null];
|
||||
datetimes.set(
|
||||
date,
|
||||
new TrackedSet(
|
||||
timesForDate.map(
|
||||
(time) => new FormDataTimeOption(this, { date, time }),
|
||||
),
|
||||
),
|
||||
)
|
||||
.flat(),
|
||||
);
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
this.options = new TrackedArray(
|
||||
options.map(({ day, time }) => new FormDataOption(this, { day, time })),
|
||||
);
|
||||
this.datetimes = new TrackedMap(datetimes);
|
||||
}
|
||||
}
|
||||
|
||||
export default class CreateOptionsDatetime extends Component {
|
||||
@service router;
|
||||
|
||||
formData = new FormData(this.args.dates);
|
||||
formData = new FormData({ dates: this.args.dates, times: this.args.times });
|
||||
|
||||
@tracked errorMesage = null;
|
||||
|
||||
|
@ -208,7 +214,25 @@ export default class CreateOptionsDatetime extends Component {
|
|||
@action
|
||||
handleTransition(transition) {
|
||||
if (transition.from?.name === 'create.options-datetime') {
|
||||
this.args.updateOptions(this.formData.options);
|
||||
this.args.updateOptions(
|
||||
// FormData.datetimes Map has a Set of FormDataTime object as values
|
||||
// We need to transform it to a Set of plain time strings
|
||||
new Map(
|
||||
Array.from(this.formData.datetimes.entries())
|
||||
.map(([key, timeOptions]) => [
|
||||
key,
|
||||
new Set(
|
||||
Array.from(timeOptions)
|
||||
.map(({ time }) => time)
|
||||
// There might be FormDataTime objects without a time, which
|
||||
// we need to filter out
|
||||
.filter((time) => time !== null),
|
||||
),
|
||||
])
|
||||
// There might be dates without any time, which we need to filter out
|
||||
.filter(([, times]) => times.size > 0),
|
||||
),
|
||||
);
|
||||
this.router.off('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,10 +79,12 @@ class FormData {
|
|||
|
||||
constructor({ options }, { defaultOptionCount }) {
|
||||
const normalizedOptions =
|
||||
options.length === 0 && defaultOptionCount > 0 ? ['', ''] : options;
|
||||
options.size === 0 && defaultOptionCount > 0
|
||||
? ['', '']
|
||||
: Array.from(options);
|
||||
|
||||
this.options = new TrackedArray(
|
||||
normalizedOptions.map(({ title }) => new FormDataOption(this, title)),
|
||||
normalizedOptions.map((value) => new FormDataOption(this, value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,16 @@ export default class CreateController extends Controller {
|
|||
get canEnterOptionsDatetimeStep() {
|
||||
return (
|
||||
this.visitedSteps.has('options-datetime') &&
|
||||
this.model.options.length >= 1
|
||||
this.model.dateOptions.size >= 1
|
||||
);
|
||||
}
|
||||
|
||||
get canEnterSettingsStep() {
|
||||
return this.visitedSteps.has('settings') && this.model.options.length >= 1;
|
||||
const { model, visitedSteps } = this;
|
||||
const { dateOptions, freetextOptions, pollType } = model;
|
||||
const options = pollType === 'FindADate' ? dateOptions : freetextOptions;
|
||||
|
||||
return visitedSteps.has('settings') && options.size >= 1;
|
||||
}
|
||||
|
||||
get isFindADate() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { DateTime } from 'luxon';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class CreateOptionsDatetimeController extends Controller {
|
||||
|
@ -18,14 +17,7 @@ export default class CreateOptionsDatetimeController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
updateOptions(options) {
|
||||
this.model.options = options
|
||||
.map(({ day, time }) =>
|
||||
time ? DateTime.fromISO(`${day}T${time}`).toISO() : day,
|
||||
)
|
||||
.sort()
|
||||
.map((isoString) => {
|
||||
return this.store.createFragment('option', { title: isoString });
|
||||
});
|
||||
updateOptions(datetimes) {
|
||||
this.model.timesForDateOptions = new Map(datetimes.entries());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { TrackedSet } from 'tracked-built-ins/.';
|
||||
|
||||
export default class CreateOptionsController extends Controller {
|
||||
@service router;
|
||||
|
@ -8,9 +9,9 @@ export default class CreateOptionsController extends Controller {
|
|||
|
||||
@action
|
||||
nextPage() {
|
||||
const { isFindADate } = this.model;
|
||||
const { pollType } = this.model;
|
||||
|
||||
if (isFindADate) {
|
||||
if (pollType === 'FindADate') {
|
||||
this.router.transitionTo('create.options-datetime');
|
||||
} else {
|
||||
this.router.transitionTo('create.settings');
|
||||
|
@ -24,8 +25,13 @@ export default class CreateOptionsController extends Controller {
|
|||
|
||||
@action
|
||||
updateOptions(newOptions) {
|
||||
this.model.options = newOptions.map(({ value }) =>
|
||||
this.store.createFragment('option', { title: value }),
|
||||
);
|
||||
const { pollType } = this.model;
|
||||
const options = newOptions.map(({ value }) => value);
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
this.model.dateOptions = new TrackedSet(options.sort());
|
||||
} else {
|
||||
this.model.freetextOptions = new TrackedSet(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class CreateSettings extends Controller {
|
|||
@service flashMessages;
|
||||
@service intl;
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
get anonymousUser() {
|
||||
return this.model.anonymousUser;
|
||||
|
@ -77,9 +78,9 @@ export default class CreateSettings extends Controller {
|
|||
|
||||
@action
|
||||
previousPage() {
|
||||
let { isFindADate } = this.model;
|
||||
let { pollType } = this.model;
|
||||
|
||||
if (isFindADate) {
|
||||
if (pollType === 'FindADate') {
|
||||
this.router.transitionTo('create.options-datetime');
|
||||
} else {
|
||||
this.router.transitionTo('create.options');
|
||||
|
@ -88,19 +89,68 @@ export default class CreateSettings extends Controller {
|
|||
|
||||
@action
|
||||
async submit() {
|
||||
const { model: poll } = this;
|
||||
const { model } = this;
|
||||
const {
|
||||
anonymousUser,
|
||||
answerType,
|
||||
description,
|
||||
expirationDate,
|
||||
forceAnswer,
|
||||
freetextOptions,
|
||||
dateOptions,
|
||||
timesForDateOptions,
|
||||
pollType,
|
||||
title,
|
||||
} = model;
|
||||
let options = [];
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
// merge date with times
|
||||
for (const date of dateOptions) {
|
||||
if (timesForDateOptions.has(date)) {
|
||||
for (const time of timesForDateOptions.get(date)) {
|
||||
const [hour, minute] = time.split(':');
|
||||
options.push(
|
||||
DateTime.fromISO(date)
|
||||
.set({
|
||||
hour,
|
||||
minute,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
})
|
||||
.toISO(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
options.push(date);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
options.push(...freetextOptions);
|
||||
}
|
||||
|
||||
const poll = this.store.createRecord('poll', {
|
||||
anonymousUser,
|
||||
answerType,
|
||||
creationDate: new Date().toISOString(),
|
||||
description,
|
||||
expirationDate,
|
||||
forceAnswer,
|
||||
options: options.map((option) =>
|
||||
this.store.createFragment('option', { title: option }),
|
||||
),
|
||||
pollType,
|
||||
title,
|
||||
});
|
||||
|
||||
// set timezone if there is atleast one option with time
|
||||
if (
|
||||
poll.isFindADate &&
|
||||
poll.options.toArray().some((option) => {
|
||||
return option.hasTime;
|
||||
pollType === 'FindADate' &&
|
||||
Array.from(timesForDateOptions.values()).some((timesForDateOptions) => {
|
||||
return timesForDateOptions.size > 0;
|
||||
})
|
||||
) {
|
||||
this.set(
|
||||
'model.timezone',
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
);
|
||||
poll.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
// save poll
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import { modifier } from 'ember-modifier';
|
||||
import Modifier from 'ember-modifier';
|
||||
|
||||
export default class AutofocusModifier extends Modifier {
|
||||
isInstalled = false;
|
||||
|
||||
modify(element, positional, { enabled = true }) {
|
||||
// element should be only autofocused on initial render
|
||||
// not when `enabled` option is invalidated
|
||||
if (this.isInstalled) {
|
||||
return;
|
||||
}
|
||||
this.isInstalled = true;
|
||||
|
||||
export default modifier(function autofocus(
|
||||
element,
|
||||
params,
|
||||
{ enabled = true },
|
||||
) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
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 { DateTime } from 'luxon';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { TrackedSet } from 'tracked-built-ins';
|
||||
|
||||
class PollData {
|
||||
@tracked anonymousUser = false;
|
||||
@tracked answerType = 'YesNo';
|
||||
@tracked description = '';
|
||||
@tracked expirationDate = DateTime.local().plus({ months: 3 }).toISO();
|
||||
@tracked forceAnswer = true;
|
||||
@tracked freetextOptions = new TrackedSet();
|
||||
@tracked dateOptions = new TrackedSet();
|
||||
@tracked timesForDateOptions = new Map();
|
||||
@tracked pollType = 'FindADate';
|
||||
@tracked title = '';
|
||||
}
|
||||
|
||||
@classic
|
||||
export default class CreateRoute extends Route {
|
||||
@service encryption;
|
||||
@service router;
|
||||
|
@ -21,17 +33,7 @@ export default class CreateRoute extends Route {
|
|||
}
|
||||
|
||||
model() {
|
||||
// create empty poll
|
||||
return this.store.createRecord('poll', {
|
||||
answerType: 'YesNo',
|
||||
creationDate: new Date(),
|
||||
forceAnswer: true,
|
||||
anonymousUser: false,
|
||||
pollType: 'FindADate',
|
||||
timezone: null,
|
||||
expirationDate: DateTime.local().plus({ months: 3 }).toISO(),
|
||||
version: config.APP.version,
|
||||
});
|
||||
return new PollData();
|
||||
}
|
||||
|
||||
activate() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<CreateOptionsDatetime
|
||||
@dates={{@model.options}}
|
||||
@dates={{@model.dateOptions}}
|
||||
@times={{@model.timesForDateOptions}}
|
||||
@onNextPage={{this.nextPage}}
|
||||
@onPrevPage={{this.previousPage}}
|
||||
@updateOptions={{this.updateOptions}}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<CreateOptions
|
||||
@isFindADate={{@model.isFindADate}}
|
||||
@isMakeAPoll={{@model.isMakeAPoll}}
|
||||
@options={{@model.options}}
|
||||
@isFindADate={{eq @model.pollType "FindADate"}}
|
||||
@isMakeAPoll={{eq @model.pollType "MakeAPoll"}}
|
||||
@options={{if
|
||||
(eq @model.pollType "FindADate")
|
||||
@model.dateOptions
|
||||
@model.freetextOptions
|
||||
}}
|
||||
@onPrevPage={{this.previousPage}}
|
||||
@onNextPage={{this.nextPage}}
|
||||
@updateOptions={{this.updateOptions}}
|
||||
|
|
|
@ -923,7 +923,7 @@ module('Acceptance | create a poll', function (hooks) {
|
|||
await pageCreateMeta.next();
|
||||
assert.equal(currentRouteName(), 'create.options');
|
||||
|
||||
await pageCreateOptions.textOptions.objectAt(0).add();
|
||||
await pageCreateOptions.textOptions.objectAt(0).title('foo');
|
||||
await pageCreateOptions.textOptions.objectAt(1).title('bar');
|
||||
await pageCreateOptions.next();
|
||||
assert.equal(currentRouteName(), 'create.settings');
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { run } from '@ember/runloop';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, find, findAll } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
module('Integration | Component | create options datetime', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
@ -20,20 +18,11 @@ module('Integration | Component | create options datetime', function (hooks) {
|
|||
*/
|
||||
|
||||
test('it generates input field for options iso 8601 date string (without time)', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
run(() => {
|
||||
this.set(
|
||||
'poll',
|
||||
this.store.createRecord('poll', {
|
||||
pollType: 'FindADate',
|
||||
options: [{ title: '2015-01-01' }],
|
||||
}),
|
||||
this.set('dates', new Set(['2015-01-01']));
|
||||
this.set('times', new Map());
|
||||
await render(
|
||||
hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
|
||||
);
|
||||
});
|
||||
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
|
||||
|
||||
assert.equal(
|
||||
findAll('.days .form-group input').length,
|
||||
|
@ -48,20 +37,11 @@ module('Integration | Component | create options datetime', function (hooks) {
|
|||
});
|
||||
|
||||
test('it generates input field for options iso 8601 datetime string (with time)', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
run(() => {
|
||||
this.set(
|
||||
'poll',
|
||||
this.store.createRecord('poll', {
|
||||
pollType: 'FindADate',
|
||||
options: [{ title: '2015-01-01T11:11:00.000Z' }],
|
||||
}),
|
||||
this.set('dates', new Set(['2015-01-01']));
|
||||
this.set('times', new Map([['2015-01-01', new Set(['11:11'])]]));
|
||||
await render(
|
||||
hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
|
||||
);
|
||||
});
|
||||
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
|
||||
|
||||
assert.equal(
|
||||
findAll('.days .form-group input').length,
|
||||
|
@ -70,30 +50,17 @@ module('Integration | Component | create options datetime', function (hooks) {
|
|||
);
|
||||
assert.equal(
|
||||
find('.days .form-group input').value,
|
||||
DateTime.fromISO('2015-01-01T11:11:00.000Z').toFormat('HH:mm'),
|
||||
'11:11',
|
||||
'it has time in option as value',
|
||||
);
|
||||
});
|
||||
|
||||
test('it hides repeated labels', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
run(() => {
|
||||
this.set(
|
||||
'poll',
|
||||
this.store.createRecord('poll', {
|
||||
pollType: 'FindADate',
|
||||
options: [
|
||||
{ title: DateTime.fromISO('2015-01-01T10:11').toISO() },
|
||||
{ title: DateTime.fromISO('2015-01-01T22:22').toISO() },
|
||||
{ title: '2015-02-02' },
|
||||
],
|
||||
}),
|
||||
this.set('dates', new Set(['2015-01-01', '2015-02-02']));
|
||||
this.set('times', new Map([['2015-01-01', new Set(['11:11', '22:22'])]]));
|
||||
await render(
|
||||
hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
|
||||
);
|
||||
});
|
||||
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
|
||||
|
||||
assert.equal(
|
||||
findAll('.days label').length,
|
||||
|
@ -126,19 +93,11 @@ module('Integration | Component | create options datetime', function (hooks) {
|
|||
});
|
||||
|
||||
test('allows to add another option', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
run(() => {
|
||||
this.set(
|
||||
'poll',
|
||||
this.store.createRecord('poll', {
|
||||
options: [{ title: '2015-01-01' }, { title: '2015-02-02' }],
|
||||
}),
|
||||
this.set('dates', new Set(['2015-01-01', '2015-02-02']));
|
||||
this.set('times', new Map());
|
||||
await render(
|
||||
hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
|
||||
);
|
||||
});
|
||||
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
|
||||
|
||||
assert.equal(
|
||||
findAll('.days .form-group input').length,
|
||||
|
@ -166,23 +125,11 @@ module('Integration | Component | create options datetime', function (hooks) {
|
|||
});
|
||||
|
||||
test('allows to delete an option', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
run(() => {
|
||||
this.set(
|
||||
'poll',
|
||||
this.store.createRecord('poll', {
|
||||
pollType: 'FindADate',
|
||||
options: [
|
||||
{ title: DateTime.fromISO('2015-01-01T11:11').toISO() },
|
||||
{ title: DateTime.fromISO('2015-01-01T22:22').toISO() },
|
||||
],
|
||||
}),
|
||||
this.set('dates', new Set(['2015-01-01']));
|
||||
this.set('times', new Map([['2015-01-01', new Set(['11:11', '22:22'])]]));
|
||||
await render(
|
||||
hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
|
||||
);
|
||||
});
|
||||
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
|
||||
|
||||
assert.equal(
|
||||
findAll('.days input').length,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { run } from '@ember/runloop';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, findAll, blur, fillIn, focus } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { TrackedSet } from 'tracked-built-ins';
|
||||
|
||||
module('Integration | Component | create options', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
@ -14,25 +14,14 @@ module('Integration | Component | create options', function (hooks) {
|
|||
test('shows validation errors if options are not unique (makeAPoll)', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
let poll;
|
||||
run(() => {
|
||||
poll = this.store.createRecord('poll', {
|
||||
pollType: 'MakeAPoll',
|
||||
});
|
||||
});
|
||||
this.set('poll', poll);
|
||||
this.set('options', poll.get('options'));
|
||||
this.set('options', new TrackedSet());
|
||||
|
||||
await render(hbs`
|
||||
<CreateOptions
|
||||
@options={{this.options}}
|
||||
@isDateTime={{false}}
|
||||
@isFindADate={{this.poll.isFindADate}}
|
||||
@isMakeAPoll={{this.poll.isMakeAPoll}}
|
||||
@isFindADate={{false}}
|
||||
@isMakeAPoll={{true}}
|
||||
/>
|
||||
`);
|
||||
|
||||
|
@ -64,25 +53,14 @@ module('Integration | Component | create options', function (hooks) {
|
|||
});
|
||||
|
||||
test('shows validation errors if option is empty (makeAPoll)', async function (assert) {
|
||||
// validation is based on validation of every option fragment
|
||||
// which validates according to poll model it belongs to
|
||||
// therefore each option needs to be pushed to poll model to have it as
|
||||
// it's owner
|
||||
let poll;
|
||||
run(() => {
|
||||
poll = this.store.createRecord('poll', {
|
||||
pollType: 'MakeAPoll',
|
||||
});
|
||||
});
|
||||
this.set('poll', poll);
|
||||
this.set('options', poll.get('options'));
|
||||
this.set('options', new TrackedSet());
|
||||
|
||||
await render(hbs`
|
||||
<CreateOptions
|
||||
@options={{this.options}}
|
||||
@isDateTime={{false}}
|
||||
@isFindADate={{this.poll.isFindADate}}
|
||||
@isMakeAPoll={{this.poll.isMakeAPoll}}
|
||||
@isFindADate={{false}}
|
||||
@isMakeAPoll={{true}}
|
||||
/>
|
||||
`);
|
||||
|
||||
|
|
|
@ -25,4 +25,12 @@ module('Integration | Modifier | autofocus', function (hooks) {
|
|||
|
||||
assert.dom('input').isNotFocused();
|
||||
});
|
||||
|
||||
test('it does not focus the element if `enabled` changes', async function (assert) {
|
||||
this.set('enabled', false);
|
||||
|
||||
await render(hbs`<input {{autofocus enabled=this.enabled}} />`);
|
||||
this.set('enabled', true);
|
||||
assert.dom('input').isNotFocused();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue