parent
a5d19c91f0
commit
f0cff27e99
64 changed files with 2405 additions and 7905 deletions
|
@ -11,5 +11,5 @@
|
||||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||||
*/
|
*/
|
||||||
"isTypeScriptProject": false
|
"isTypeScriptProject": true
|
||||||
}
|
}
|
||||||
|
|
23
.eslintrc.js
23
.eslintrc.js
|
@ -2,18 +2,11 @@
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: '@babel/eslint-parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
|
||||||
requireConfigFile: false,
|
|
||||||
babelOptions: {
|
|
||||||
plugins: [
|
|
||||||
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plugins: ['ember'],
|
plugins: ['ember', '@typescript-eslint'],
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:ember/recommended',
|
'plugin:ember/recommended',
|
||||||
|
@ -32,6 +25,15 @@ module.exports = {
|
||||||
'no-prototype-builtins': 'warn',
|
'no-prototype-builtins': 'warn',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
// ts files
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
// node files
|
// node files
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
|
@ -46,9 +48,6 @@ module.exports = {
|
||||||
'./lib/*/index.js',
|
'./lib/*/index.js',
|
||||||
'./server/**/*.js',
|
'./server/**/*.js',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'script',
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
browser: false,
|
browser: false,
|
||||||
node: true,
|
node: true,
|
||||||
|
|
|
@ -4,6 +4,8 @@ import loadInitializers from 'ember-load-initializers';
|
||||||
import config from 'croodle/config/environment';
|
import config from 'croodle/config/environment';
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
|
LOG_TRANSITIONS = true;
|
||||||
|
|
||||||
modulePrefix = config.modulePrefix;
|
modulePrefix = config.modulePrefix;
|
||||||
podModulePrefix = config.podModulePrefix;
|
podModulePrefix = config.podModulePrefix;
|
||||||
Resolver = Resolver;
|
Resolver = Resolver;
|
|
@ -1,38 +0,0 @@
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { isArray } from '@ember/array';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
|
|
||||||
export default class CreateOptionsDates extends Component {
|
|
||||||
@tracked calendarCenter =
|
|
||||||
this.selectedDays.length >= 1 ? this.selectedDays[0] : DateTime.local();
|
|
||||||
|
|
||||||
get selectedDays() {
|
|
||||||
return Array.from(this.args.options).map(({ value }) =>
|
|
||||||
DateTime.fromISO(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get calendarCenterNext() {
|
|
||||||
return this.calendarCenter.plus({ months: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleSelectedDaysChange({ datetime: newDatesAsLuxonDateTime }) {
|
|
||||||
if (!isArray(newDatesAsLuxonDateTime)) {
|
|
||||||
// special case: all options are unselected
|
|
||||||
this.args.updateOptions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.args.updateOptions(
|
|
||||||
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
updateCalenderCenter(diff) {
|
|
||||||
this.calendarCenter = this.calendarCenter.add(diff, 'months');
|
|
||||||
}
|
|
||||||
}
|
|
48
app/components/create-options-dates.ts
Normal file
48
app/components/create-options-dates.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { isArray } from '@ember/array';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import type { TrackedSet } from 'tracked-built-ins';
|
||||||
|
import type { FormDataOption } from './create-options';
|
||||||
|
|
||||||
|
export interface CreateOptionsDatesSignature {
|
||||||
|
Args: {
|
||||||
|
options: TrackedSet<FormDataOption>;
|
||||||
|
updateOptions: (options: string[]) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CreateOptionsDates extends Component<CreateOptionsDatesSignature> {
|
||||||
|
@tracked calendarCenter =
|
||||||
|
this.selectedDays.length >= 1
|
||||||
|
? (this.selectedDays[0] as DateTime)
|
||||||
|
: DateTime.local();
|
||||||
|
|
||||||
|
get selectedDays(): DateTime[] {
|
||||||
|
return Array.from(this.args.options).map(
|
||||||
|
({ value }) => DateTime.fromISO(value) as DateTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get calendarCenterNext() {
|
||||||
|
return this.calendarCenter.plus({ months: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleSelectedDaysChange({
|
||||||
|
datetime: newDatesAsLuxonDateTime,
|
||||||
|
}: {
|
||||||
|
datetime: DateTime[];
|
||||||
|
}) {
|
||||||
|
if (!isArray(newDatesAsLuxonDateTime)) {
|
||||||
|
// special case: all options are unselected
|
||||||
|
this.args.updateOptions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.updateOptions(
|
||||||
|
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate() as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,15 +5,17 @@ import { tracked } from '@glimmer/tracking';
|
||||||
import { TrackedMap, TrackedSet } from 'tracked-built-ins';
|
import { TrackedMap, TrackedSet } from 'tracked-built-ins';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import IntlMessage from '../utils/intl-message';
|
import IntlMessage from '../utils/intl-message';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
|
||||||
class FormDataTimeOption {
|
class FormDataTimeOption {
|
||||||
formData;
|
formData;
|
||||||
|
|
||||||
// ISO 8601 date string: YYYY-MM-DD
|
// ISO 8601 date string: YYYY-MM-DD
|
||||||
date;
|
date: string;
|
||||||
|
|
||||||
// ISO 8601 time string without seconds: HH:mm
|
// ISO 8601 time string without seconds: HH:mm
|
||||||
@tracked time;
|
@tracked time: string | null;
|
||||||
|
|
||||||
// helper property set by modifiers to track if input element is invalid
|
// helper property set by modifiers to track if input element is invalid
|
||||||
// because user only entered the time partly (e.g. "10:--").
|
// because user only entered the time partly (e.g. "10:--").
|
||||||
|
@ -32,7 +34,7 @@ class FormDataTimeOption {
|
||||||
// the same day already before. Only the second input field containing the
|
// the same day already before. Only the second input field containing the
|
||||||
// duplicated time should show the validation error.
|
// duplicated time should show the validation error.
|
||||||
const { formData, date } = this;
|
const { formData, date } = this;
|
||||||
const timesForThisDate = Array.from(formData.datetimes.get(date));
|
const timesForThisDate = Array.from(formData.datetimes.get(date)!);
|
||||||
const isDuplicate = timesForThisDate
|
const isDuplicate = timesForThisDate
|
||||||
.slice(0, timesForThisDate.indexOf(this))
|
.slice(0, timesForThisDate.indexOf(this))
|
||||||
.some((timeOption) => timeOption.time == this.time);
|
.some((timeOption) => timeOption.time == this.time);
|
||||||
|
@ -64,11 +66,14 @@ class FormDataTimeOption {
|
||||||
const { datetimes } = formData;
|
const { datetimes } = formData;
|
||||||
return (
|
return (
|
||||||
Array.from(datetimes.keys())[0] === date &&
|
Array.from(datetimes.keys())[0] === date &&
|
||||||
Array.from(datetimes.get(date))[0] === this
|
Array.from(datetimes.get(date)!)[0] === this
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(formData, { date, time }) {
|
constructor(
|
||||||
|
formData: FormData,
|
||||||
|
{ date, time }: { date: string; time: string | null },
|
||||||
|
) {
|
||||||
this.formData = formData;
|
this.formData = formData;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.time = time;
|
this.time = time;
|
||||||
|
@ -76,7 +81,7 @@ class FormDataTimeOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormData {
|
class FormData {
|
||||||
@tracked datetimes;
|
@tracked datetimes: Map<string, Set<FormDataTimeOption>>;
|
||||||
|
|
||||||
get optionsValidation() {
|
get optionsValidation() {
|
||||||
const { datetimes } = this;
|
const { datetimes } = this;
|
||||||
|
@ -87,7 +92,7 @@ class FormData {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!allTimeOptionsAreValid) {
|
if (!allTimeOptionsAreValid) {
|
||||||
return IntlMessage('create.options-datetime.error.invalidTime');
|
return new IntlMessage('create.options-datetime.error.invalidTime');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -98,9 +103,9 @@ class FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addOption(date) {
|
addOption(date: string) {
|
||||||
this.datetimes
|
this.datetimes
|
||||||
.get(date)
|
.get(date)!
|
||||||
.add(new FormDataTimeOption(this, { date, time: null }));
|
.add(new FormDataTimeOption(this, { date, time: null }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,8 +114,8 @@ class FormData {
|
||||||
* otherwise it deletes time for this date
|
* otherwise it deletes time for this date
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
deleteOption(option) {
|
deleteOption(option: FormDataTimeOption) {
|
||||||
const timeOptionsForDate = this.datetimes.get(option.date);
|
const timeOptionsForDate = this.datetimes.get(option.date)!;
|
||||||
|
|
||||||
if (timeOptionsForDate.size > 1) {
|
if (timeOptionsForDate.size > 1) {
|
||||||
timeOptionsForDate.delete(option);
|
timeOptionsForDate.delete(option);
|
||||||
|
@ -122,8 +127,8 @@ class FormData {
|
||||||
@action
|
@action
|
||||||
adoptTimesOfFirstDay() {
|
adoptTimesOfFirstDay() {
|
||||||
const timeOptionsForFirstDay = Array.from(
|
const timeOptionsForFirstDay = Array.from(
|
||||||
Array.from(this.datetimes.values())[0],
|
Array.from(this.datetimes.values())[0]!,
|
||||||
);
|
) as FormDataTimeOption[];
|
||||||
const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
|
const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
|
||||||
(timeOption) => timeOption.isValid,
|
(timeOption) => timeOption.isValid,
|
||||||
);
|
);
|
||||||
|
@ -143,12 +148,18 @@ class FormData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ dates, times }) {
|
constructor({
|
||||||
|
dates,
|
||||||
|
times,
|
||||||
|
}: {
|
||||||
|
dates: Set<string>;
|
||||||
|
times: Map<string, Set<string>>;
|
||||||
|
}) {
|
||||||
const datetimes = new Map();
|
const datetimes = new Map();
|
||||||
|
|
||||||
for (const date of dates) {
|
for (const date of dates) {
|
||||||
const timesForDate = times.has(date)
|
const timesForDate = times.has(date)
|
||||||
? Array.from(times.get(date))
|
? Array.from(times.get(date) as Set<string>)
|
||||||
: [null];
|
: [null];
|
||||||
datetimes.set(
|
datetimes.set(
|
||||||
date,
|
date,
|
||||||
|
@ -164,12 +175,22 @@ class FormData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CreateOptionsDatetime extends Component {
|
export interface CreateOptoinsDatetimeSignature {
|
||||||
@service router;
|
Args: {
|
||||||
|
dates: TrackedSet<string>;
|
||||||
|
onNextPage: () => void;
|
||||||
|
onPrevPage: () => void;
|
||||||
|
times: Map<string, Set<string>>;
|
||||||
|
updateOptions: (datetimes: Map<string, Set<string>>) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CreateOptionsDatetime extends Component<CreateOptoinsDatetimeSignature> {
|
||||||
|
@service declare router: RouterService;
|
||||||
|
|
||||||
formData = new FormData({ dates: this.args.dates, times: this.args.times });
|
formData = new FormData({ dates: this.args.dates, times: this.args.times });
|
||||||
|
|
||||||
@tracked errorMesage = null;
|
@tracked errorMessage: string | null = null;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
adoptTimesOfFirstDay() {
|
adoptTimesOfFirstDay() {
|
||||||
|
@ -177,7 +198,7 @@ export default class CreateOptionsDatetime extends Component {
|
||||||
const successful = formData.adoptTimesOfFirstDay();
|
const successful = formData.adoptTimesOfFirstDay();
|
||||||
|
|
||||||
if (!successful) {
|
if (!successful) {
|
||||||
this.errorMesage =
|
this.errorMessage =
|
||||||
'create.options-datetime.fix-validation-errors-first-day';
|
'create.options-datetime.fix-validation-errors-first-day';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,8 +215,8 @@ export default class CreateOptionsDatetime extends Component {
|
||||||
|
|
||||||
// validate input field for being partially filled
|
// validate input field for being partially filled
|
||||||
@action
|
@action
|
||||||
validateInput(option, event) {
|
validateInput(option: FormDataTimeOption, event: InputEvent) {
|
||||||
const element = event.target;
|
const element = event.target as HTMLInputElement;
|
||||||
|
|
||||||
// update partially filled time validation error
|
// update partially filled time validation error
|
||||||
option.isPartiallyFilled = !element.checkValidity();
|
option.isPartiallyFilled = !element.checkValidity();
|
||||||
|
@ -203,8 +224,8 @@ export default class CreateOptionsDatetime extends Component {
|
||||||
|
|
||||||
// remove partially filled validation error if user fixed it
|
// remove partially filled validation error if user fixed it
|
||||||
@action
|
@action
|
||||||
updateInputValidation(option, event) {
|
updateInputValidation(option: FormDataTimeOption, event: InputEvent) {
|
||||||
const element = event.target;
|
const element = event.target as HTMLInputElement;
|
||||||
|
|
||||||
if (element.checkValidity() && option.isPartiallyFilled) {
|
if (element.checkValidity() && option.isPartiallyFilled) {
|
||||||
option.isPartiallyFilled = false;
|
option.isPartiallyFilled = false;
|
||||||
|
@ -212,23 +233,25 @@ export default class CreateOptionsDatetime extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTransition(transition) {
|
handleTransition(transition: Transition) {
|
||||||
if (transition.from?.name === 'create.options-datetime') {
|
if (transition.from?.name === 'create.options-datetime') {
|
||||||
this.args.updateOptions(
|
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(
|
new Map(
|
||||||
|
// FormData.datetimes Map has a Set of FormDataTime object as values
|
||||||
|
// We need to transform it to a Set of plain time strings
|
||||||
Array.from(this.formData.datetimes.entries())
|
Array.from(this.formData.datetimes.entries())
|
||||||
.map(([key, timeOptions]) => [
|
.map(([key, timeOptions]): [string, Set<string>] => {
|
||||||
key,
|
return [
|
||||||
new Set(
|
key,
|
||||||
Array.from(timeOptions)
|
new Set(
|
||||||
.map(({ time }) => time)
|
Array.from(timeOptions)
|
||||||
// There might be FormDataTime objects without a time, which
|
.map(({ time }: FormDataTimeOption) => time)
|
||||||
// we need to filter out
|
// There might be FormDataTime objects without a time, which
|
||||||
.filter((time) => time !== null),
|
// we need to filter out
|
||||||
),
|
.filter((time) => time !== null),
|
||||||
])
|
) as Set<string>,
|
||||||
|
];
|
||||||
|
})
|
||||||
// There might be dates without any time, which we need to filter out
|
// There might be dates without any time, which we need to filter out
|
||||||
.filter(([, times]) => times.size > 0),
|
.filter(([, times]) => times.size > 0),
|
||||||
),
|
),
|
||||||
|
@ -237,8 +260,8 @@ export default class CreateOptionsDatetime extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor(owner: unknown, args: CreateOptoinsDatetimeSignature['Args']) {
|
||||||
super(...arguments);
|
super(owner, args);
|
||||||
|
|
||||||
this.router.on('routeWillChange', this.handleTransition);
|
this.router.on('routeWillChange', this.handleTransition);
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { TrackedArray } from 'tracked-built-ins';
|
import { TrackedArray, TrackedSet } from 'tracked-built-ins';
|
||||||
import IntlMessage from '../utils/intl-message';
|
import IntlMessage from '../utils/intl-message';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
|
||||||
class FormDataOption {
|
export class FormDataOption {
|
||||||
@tracked value;
|
@tracked value;
|
||||||
formData;
|
formData;
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ class FormDataOption {
|
||||||
return this.valueValidation === null;
|
return this.valueValidation === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(formData, value) {
|
constructor(formData: FormData, value: string) {
|
||||||
this.formData = formData;
|
this.formData = formData;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
@ -59,25 +61,28 @@ class FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateOptions(values) {
|
updateOptions(values: string[]) {
|
||||||
this.options = new TrackedArray(
|
this.options = new TrackedArray(
|
||||||
values.map((value) => new FormDataOption(this, value)),
|
values.map((value) => new FormDataOption(this, value)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addOption(value, afterPosition = this.options.length - 1) {
|
addOption(value: string, afterPosition = this.options.length - 1) {
|
||||||
const option = new FormDataOption(this, value);
|
const option = new FormDataOption(this, value);
|
||||||
|
|
||||||
this.options.splice(afterPosition + 1, 0, option);
|
this.options.splice(afterPosition + 1, 0, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
deleteOption(option) {
|
deleteOption(option: FormDataOption) {
|
||||||
this.options.splice(this.options.indexOf(option), 1);
|
this.options.splice(this.options.indexOf(option), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ options }, { defaultOptionCount }) {
|
constructor(
|
||||||
|
{ options }: { options: Set<string> },
|
||||||
|
{ defaultOptionCount }: { defaultOptionCount: number },
|
||||||
|
) {
|
||||||
const normalizedOptions =
|
const normalizedOptions =
|
||||||
options.size === 0 && defaultOptionCount > 0
|
options.size === 0 && defaultOptionCount > 0
|
||||||
? ['', '']
|
? ['', '']
|
||||||
|
@ -89,8 +94,18 @@ class FormData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CreateOptionsComponent extends Component {
|
export interface CreateOptionsSignature {
|
||||||
@service router;
|
Args: {
|
||||||
|
isMakeAPoll: boolean;
|
||||||
|
options: TrackedSet<string>;
|
||||||
|
onNextPage: () => void;
|
||||||
|
onPrevPage: () => void;
|
||||||
|
updateOptions: (options: { value: string }[]) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CreateOptionsComponent extends Component<CreateOptionsSignature> {
|
||||||
|
@service declare router: RouterService;
|
||||||
|
|
||||||
formData = new FormData(
|
formData = new FormData(
|
||||||
{ options: this.args.options },
|
{ options: this.args.options },
|
||||||
|
@ -108,15 +123,15 @@ export default class CreateOptionsComponent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTransition(transition) {
|
handleTransition(transition: Transition) {
|
||||||
if (transition.from?.name === 'create.options') {
|
if (transition.from?.name === 'create.options') {
|
||||||
this.args.updateOptions(this.formData.options);
|
this.args.updateOptions(this.formData.options);
|
||||||
this.router.off('routeWillChange', this.handleTransition);
|
this.router.off('routeWillChange', this.handleTransition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor(owner: unknown, args: CreateOptionsSignature['Args']) {
|
||||||
super(...arguments);
|
super(owner, args);
|
||||||
|
|
||||||
this.router.on('routeWillChange', this.handleTransition);
|
this.router.on('routeWillChange', this.handleTransition);
|
||||||
}
|
}
|
|
@ -2,25 +2,26 @@ import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import localesMeta from 'croodle/locales/meta';
|
import localesMeta from 'croodle/locales/meta';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
import type IntlService from 'ember-intl/services/intl';
|
||||||
|
import type PowerCalendarService from 'ember-power-calendar/services/power-calendar';
|
||||||
|
|
||||||
export default class LanguageSelect extends Component {
|
export default class LanguageSelect extends Component {
|
||||||
@service intl;
|
@service declare intl: IntlService;
|
||||||
@service powerCalendar;
|
@service declare powerCalendar: PowerCalendarService;
|
||||||
|
|
||||||
get currentLocale() {
|
get currentLocale() {
|
||||||
return this.intl.primaryLocale;
|
return this.intl.primaryLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
get locales() {
|
locales = localesMeta;
|
||||||
return localesMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleChange(event) {
|
handleChange(event: Event) {
|
||||||
const locale = event.target.value;
|
const selectElement = event.target as HTMLSelectElement;
|
||||||
|
const locale = selectElement.value as keyof typeof this.locales;
|
||||||
|
|
||||||
this.intl.locale = locale.includes('-')
|
this.intl.locale = locale.includes('-')
|
||||||
? [locale, locale.split('-')[0]]
|
? [locale, locale.split('-')[0] as string]
|
||||||
: [locale];
|
: [locale];
|
||||||
this.powerCalendar.locale = locale;
|
this.powerCalendar.locale = locale;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
|
import type Poll from 'croodle/models/poll';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
export default class PollEvaluationParticipantsTable extends Component {
|
export interface PollEvaluationParticipantsTableSignature {
|
||||||
|
Args: {
|
||||||
|
poll: Poll;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PollEvaluationParticipantsTable extends Component<PollEvaluationParticipantsTableSignature> {
|
||||||
get optionsPerDay() {
|
get optionsPerDay() {
|
||||||
const { poll } = this.args;
|
const { poll } = this.args;
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import type IntlService from 'ember-intl/services/intl';
|
||||||
|
import type Option from 'croodle/models/option';
|
||||||
|
import type User from 'croodle/models/user';
|
||||||
|
import type { Answer } from 'croodle/utils/answers-for-answer-type';
|
||||||
|
import type Poll from 'croodle/models/poll';
|
||||||
|
|
||||||
export default class PollEvaluationSummary extends Component {
|
export interface PollEvaluationSummarySignature {
|
||||||
@service intl;
|
Args: {
|
||||||
|
poll: Poll;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PollEvaluationSummary extends Component<PollEvaluationSummarySignature> {
|
||||||
|
@service declare intl: IntlService;
|
||||||
|
|
||||||
get bestOptions() {
|
get bestOptions() {
|
||||||
const { poll } = this.args;
|
const { poll } = this.args;
|
||||||
|
@ -18,34 +29,41 @@ export default class PollEvaluationSummary extends Component {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let answers = poll.answers.reduce((answers, answer) => {
|
const answers = poll.answers.reduce(
|
||||||
answers[answer.type] = 0;
|
(answers: Record<string, number>, answer: Answer) => {
|
||||||
return answers;
|
answers[answer.type] = 0;
|
||||||
}, {});
|
return answers;
|
||||||
let evaluation = options.map((option) => {
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const evaluation: {
|
||||||
|
answers: Record<string, number>;
|
||||||
|
option: Option;
|
||||||
|
score: number;
|
||||||
|
}[] = options.map((option: Option) => {
|
||||||
return {
|
return {
|
||||||
answers: { ...answers },
|
answers: { ...answers },
|
||||||
option,
|
option,
|
||||||
score: 0,
|
score: 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let bestOptions = [];
|
const bestOptions = [];
|
||||||
|
|
||||||
users.forEach((user) => {
|
users.forEach((user: User) => {
|
||||||
user.selections.forEach(({ type }, i) => {
|
user.selections.forEach(({ type }, i) => {
|
||||||
evaluation[i].answers[type]++;
|
evaluation[i]!.answers[type]++;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'yes':
|
case 'yes':
|
||||||
evaluation[i].score += 2;
|
evaluation[i]!.score += 2;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maybe':
|
case 'maybe':
|
||||||
evaluation[i].score += 1;
|
evaluation[i]!.score += 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'no':
|
case 'no':
|
||||||
evaluation[i].score -= 2;
|
evaluation[i]!.score -= 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -53,9 +71,9 @@ export default class PollEvaluationSummary extends Component {
|
||||||
|
|
||||||
evaluation.sort((a, b) => b.score - a.score);
|
evaluation.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
let bestScore = evaluation[0].score;
|
const bestScore = evaluation[0]!.score;
|
||||||
for (let i = 0; i < evaluation.length; i++) {
|
for (let i = 0; i < evaluation.length; i++) {
|
||||||
if (bestScore === evaluation[i].score) {
|
if (bestScore === evaluation[i]!.score) {
|
||||||
bestOptions.push(evaluation[i]);
|
bestOptions.push(evaluation[i]);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
16
app/config/environment.d.ts
vendored
Normal file
16
app/config/environment.d.ts
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Type declarations for
|
||||||
|
* import config from 'croodle/config/environment'
|
||||||
|
*/
|
||||||
|
declare const config: {
|
||||||
|
environment: string;
|
||||||
|
modulePrefix: string;
|
||||||
|
podModulePrefix: string;
|
||||||
|
locationType: 'history' | 'hash' | 'none';
|
||||||
|
rootURL: string;
|
||||||
|
APP: {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -1,6 +1,7 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
|
||||||
|
|
||||||
export default class ApplicationController extends Controller {
|
export default class ApplicationController extends Controller {
|
||||||
@service flashMessages;
|
@service declare flashMessages: FlashMessagesService;
|
||||||
}
|
}
|
|
@ -2,16 +2,22 @@ import { inject as service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { TrackedSet } from 'tracked-built-ins';
|
import { TrackedSet } from 'tracked-built-ins';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type { CreateRouteModel } from 'croodle/routes/create';
|
||||||
|
|
||||||
export default class CreateController extends Controller {
|
export default class CreateController extends Controller {
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateRouteModel;
|
||||||
|
|
||||||
|
visitedSteps = new TrackedSet();
|
||||||
|
|
||||||
get canEnterMetaStep() {
|
get canEnterMetaStep() {
|
||||||
return this.visitedSteps.has('meta') && this.model.pollType;
|
return this.visitedSteps.has('meta') && this.model.pollType;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canEnterOptionsStep() {
|
get canEnterOptionsStep() {
|
||||||
let { title } = this.model;
|
const { title } = this.model;
|
||||||
return (
|
return (
|
||||||
this.visitedSteps.has('options') &&
|
this.visitedSteps.has('options') &&
|
||||||
typeof title === 'string' &&
|
typeof title === 'string' &&
|
||||||
|
@ -40,18 +46,18 @@ export default class CreateController extends Controller {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateVisitedSteps() {
|
updateVisitedSteps() {
|
||||||
let { currentRouteName } = this.router;
|
const { currentRouteName } = this.router;
|
||||||
|
|
||||||
// currentRouteName might not be defined in some edge cases
|
// currentRouteName might not be defined in some edge cases
|
||||||
if (!currentRouteName) {
|
if (!currentRouteName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let step = currentRouteName.split('.').pop();
|
const step = currentRouteName.split('.').pop();
|
||||||
this.visitedSteps.add(step);
|
this.visitedSteps.add(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action transitionTo(route) {
|
@action transitionTo(route: string) {
|
||||||
this.router.transitionTo(route);
|
this.router.transitionTo(route);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
import type { CreateRouteIndexModel } from 'croodle/routes/create/index';
|
||||||
|
|
||||||
export default class CreateIndex extends Controller {
|
export default class CreateIndex extends Controller {
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateRouteIndexModel;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
submit() {
|
submit() {
|
||||||
|
@ -11,7 +16,7 @@ export default class CreateIndex extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTransition(transition) {
|
handleTransition(transition: Transition) {
|
||||||
if (transition.from?.name === 'create.index') {
|
if (transition.from?.name === 'create.index') {
|
||||||
const { poll, formData } = this.model;
|
const { poll, formData } = this.model;
|
||||||
|
|
||||||
|
@ -20,6 +25,7 @@ export default class CreateIndex extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
this.router.on('routeWillChange', this.handleTransition);
|
this.router.on('routeWillChange', this.handleTransition);
|
|
@ -1,9 +1,14 @@
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
import type { CreateMetaRouteModel } from 'croodle/routes/create/meta';
|
||||||
|
|
||||||
export default class CreateMetaController extends Controller {
|
export default class CreateMetaController extends Controller {
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateMetaRouteModel;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
previousPage() {
|
previousPage() {
|
||||||
|
@ -16,7 +21,7 @@ export default class CreateMetaController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTransition(transition) {
|
handleTransition(transition: Transition) {
|
||||||
if (transition.from?.name === 'create.meta') {
|
if (transition.from?.name === 'create.meta') {
|
||||||
const { poll, formData } = this.model;
|
const { poll, formData } = this.model;
|
||||||
|
|
||||||
|
@ -26,6 +31,7 @@ export default class CreateMetaController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
this.router.on('routeWillChange', this.handleTransition);
|
this.router.on('routeWillChange', this.handleTransition);
|
|
@ -1,9 +1,13 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import type { CreateOptionsDatetimeRouteModel } from 'croodle/routes/create/options-datetime';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
|
||||||
export default class CreateOptionsDatetimeController extends Controller {
|
export default class CreateOptionsDatetimeController extends Controller {
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateOptionsDatetimeRouteModel;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
nextPage() {
|
nextPage() {
|
||||||
|
@ -16,7 +20,7 @@ export default class CreateOptionsDatetimeController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateOptions(datetimes) {
|
updateOptions(datetimes: Map<string, Set<string>>) {
|
||||||
this.model.timesForDateOptions = new Map(datetimes.entries());
|
this.model.timesForDateOptions = new Map(datetimes.entries());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,14 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { TrackedSet } from 'tracked-built-ins/.';
|
import { TrackedSet } from 'tracked-built-ins';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type { CreateOptionsRouteModel } from 'croodle/routes/create/options';
|
||||||
|
|
||||||
export default class CreateOptionsController extends Controller {
|
export default class CreateOptionsController extends Controller {
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateOptionsRouteModel;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
nextPage() {
|
nextPage() {
|
||||||
|
@ -23,7 +27,7 @@ export default class CreateOptionsController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateOptions(newOptions) {
|
updateOptions(newOptions: { value: string }[]) {
|
||||||
const { pollType } = this.model;
|
const { pollType } = this.model;
|
||||||
const options = newOptions.map(({ value }) => value);
|
const options = newOptions.map(({ value }) => value);
|
||||||
|
|
|
@ -5,11 +5,17 @@ import { action } from '@ember/object';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import Poll from '../../models/poll';
|
import Poll from '../../models/poll';
|
||||||
import { generatePassphrase } from '../../utils/encryption';
|
import { generatePassphrase } from '../../utils/encryption';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings';
|
||||||
|
import type IntlService from 'ember-intl/services/intl';
|
||||||
|
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
|
||||||
|
|
||||||
export default class CreateSettings extends Controller {
|
export default class CreateSettings extends Controller {
|
||||||
@service flashMessages;
|
@service declare flashMessages: FlashMessagesService;
|
||||||
@service intl;
|
@service declare intl: IntlService;
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: CreateSettingsRouteModel;
|
||||||
|
|
||||||
get anonymousUser() {
|
get anonymousUser() {
|
||||||
return this.model.anonymousUser;
|
return this.model.anonymousUser;
|
||||||
|
@ -39,7 +45,7 @@ export default class CreateSettings extends Controller {
|
||||||
}
|
}
|
||||||
set expirationDuration(value) {
|
set expirationDuration(value) {
|
||||||
this.model.expirationDate = isPresent(value)
|
this.model.expirationDate = isPresent(value)
|
||||||
? DateTime.local().plus(Duration.fromISO(value)).toISO()
|
? (DateTime.local().plus(Duration.fromISO(value)).toISO() as string)
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +84,7 @@ export default class CreateSettings extends Controller {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
previousPage() {
|
previousPage() {
|
||||||
let { pollType } = this.model;
|
const { pollType } = this.model;
|
||||||
|
|
||||||
if (pollType === 'FindADate') {
|
if (pollType === 'FindADate') {
|
||||||
this.router.transitionTo('create.options-datetime');
|
this.router.transitionTo('create.options-datetime');
|
||||||
|
@ -104,22 +110,22 @@ export default class CreateSettings extends Controller {
|
||||||
} = model;
|
} = model;
|
||||||
|
|
||||||
// calculate options
|
// calculate options
|
||||||
let options = [];
|
const options: string[] = [];
|
||||||
if (pollType === 'FindADate') {
|
if (pollType === 'FindADate') {
|
||||||
// merge date with times
|
// merge date with times
|
||||||
for (const date of dateOptions) {
|
for (const date of dateOptions) {
|
||||||
if (timesForDateOptions.has(date)) {
|
if (timesForDateOptions.has(date)) {
|
||||||
for (const time of timesForDateOptions.get(date)) {
|
for (const time of timesForDateOptions.get(date)!) {
|
||||||
const [hour, minute] = time.split(':');
|
const [hour, minute] = time.split(':') as [string, string];
|
||||||
options.push(
|
options.push(
|
||||||
DateTime.fromISO(date)
|
DateTime.fromISO(date)
|
||||||
.set({
|
.set({
|
||||||
hour,
|
hour: parseInt(hour),
|
||||||
minute,
|
minute: parseInt(minute),
|
||||||
second: 0,
|
second: 0,
|
||||||
millisecond: 0,
|
millisecond: 0,
|
||||||
})
|
})
|
||||||
.toISO(),
|
.toISO() as string,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -139,7 +145,6 @@ export default class CreateSettings extends Controller {
|
||||||
{
|
{
|
||||||
anonymousUser,
|
anonymousUser,
|
||||||
answerType,
|
answerType,
|
||||||
creationDate: new Date().toISOString(),
|
|
||||||
description,
|
description,
|
||||||
expirationDate,
|
expirationDate,
|
||||||
forceAnswer,
|
forceAnswer,
|
|
@ -4,11 +4,17 @@ import { isPresent, isEmpty } from '@ember/utils';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
|
||||||
|
import type IntlService from 'ember-intl/services/intl';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type { PollRouteModel } from 'croodle/routes/poll';
|
||||||
|
|
||||||
export default class PollController extends Controller {
|
export default class PollController extends Controller {
|
||||||
@service flashMessages;
|
@service declare flashMessages: FlashMessagesService;
|
||||||
@service intl;
|
@service declare intl: IntlService;
|
||||||
@service router;
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
declare model: PollRouteModel;
|
||||||
|
|
||||||
queryParams = ['encryptionKey'];
|
queryParams = ['encryptionKey'];
|
||||||
encryptionKey = '';
|
encryptionKey = '';
|
||||||
|
@ -61,8 +67,8 @@ export default class PollController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
linkAction(type) {
|
linkAction(type: 'copied' | 'selected') {
|
||||||
let flashMessages = this.flashMessages;
|
const flashMessages = this.flashMessages;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'copied':
|
case 'copied':
|
||||||
flashMessages.success(`poll.link.copied`);
|
flashMessages.success(`poll.link.copied`);
|
|
@ -1,11 +1,20 @@
|
||||||
import Helper from '@ember/component/helper';
|
import Helper from '@ember/component/helper';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import type IntlService from 'ember-intl/services/intl';
|
||||||
|
|
||||||
|
type Positional = [date: Date | string];
|
||||||
|
|
||||||
|
export interface FormatDateRelativeHelperSignature {
|
||||||
|
Args: {
|
||||||
|
Positional: Positional;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class FormatDateRelativeHelper extends Helper {
|
export default class FormatDateRelativeHelper extends Helper {
|
||||||
@service intl;
|
@service declare intl: IntlService;
|
||||||
|
|
||||||
compute([date]) {
|
compute([date]: Positional) {
|
||||||
if (date instanceof Date) {
|
if (date instanceof Date) {
|
||||||
date = date.toISOString();
|
date = date.toISOString();
|
||||||
}
|
}
|
16
app/helpers/mark-as-safe-html.ts
Normal file
16
app/helpers/mark-as-safe-html.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
import { htmlSafe } from '@ember/template';
|
||||||
|
|
||||||
|
type Positional = [html: string];
|
||||||
|
|
||||||
|
export interface MarkAsSafeHtmlHelperSignature {
|
||||||
|
Args: {
|
||||||
|
Positional: Positional;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default helper<MarkAsSafeHtmlHelperSignature>(function markAsSafeHtml([
|
||||||
|
html,
|
||||||
|
]) {
|
||||||
|
return htmlSafe(html);
|
||||||
|
});
|
|
@ -2,23 +2,33 @@ import { helper } from '@ember/component/helper';
|
||||||
import { next } from '@ember/runloop';
|
import { next } from '@ember/runloop';
|
||||||
import { assert } from '@ember/debug';
|
import { assert } from '@ember/debug';
|
||||||
|
|
||||||
function elementIsNotVisible(element) {
|
function elementIsNotVisible(element: Element) {
|
||||||
let elementPosition = element.getBoundingClientRect();
|
const elementPosition = element.getBoundingClientRect();
|
||||||
let windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
// an element is not visible if
|
// check if the element is within current view port
|
||||||
return (
|
if (
|
||||||
false ||
|
// above current view port
|
||||||
// it's above the current view port
|
|
||||||
elementPosition.top <= 0 ||
|
elementPosition.top <= 0 ||
|
||||||
// it's below the current view port
|
// below current view port
|
||||||
elementPosition.bottom >= windowHeight ||
|
elementPosition.bottom >= windowHeight
|
||||||
// it's in current view port but hidden by fixed navigation
|
) {
|
||||||
(getComputedStyle(document.querySelector('.cr-steps-bottom-nav'))
|
return true;
|
||||||
.position === 'fixed' &&
|
}
|
||||||
elementPosition.bottom >=
|
|
||||||
windowHeight -
|
// check if element is within current view port button hidden behind
|
||||||
document.querySelector('.cr-steps-bottom-nav').offsetHeight)
|
// fixed bottom navigation bar
|
||||||
|
const bottomNavEl = document.querySelector(
|
||||||
|
'.cr-steps-bottom-nav',
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!bottomNavEl) {
|
||||||
|
// bottom navigation bar can not overlay element if it does not exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
getComputedStyle(bottomNavEl).position === 'fixed' &&
|
||||||
|
elementPosition.bottom >= windowHeight - bottomNavEl.offsetHeight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +37,9 @@ export function scrollFirstInvalidElementIntoViewPort() {
|
||||||
// timing issue in Firefox causing the Browser not scrolling up far enough if doing so
|
// timing issue in Firefox causing the Browser not scrolling up far enough if doing so
|
||||||
// delaying to next runloop therefore
|
// delaying to next runloop therefore
|
||||||
next(function () {
|
next(function () {
|
||||||
let invalidInput = document.querySelector(
|
const invalidInput = document.querySelector(
|
||||||
'.form-control.is-invalid, .custom-control-input.is-invalid',
|
'.form-control.is-invalid, .custom-control-input.is-invalid',
|
||||||
);
|
) as HTMLInputElement;
|
||||||
assert(
|
assert(
|
||||||
'Atleast one form control must be marked as invalid if form submission was rejected as invalid',
|
'Atleast one form control must be marked as invalid if form submission was rejected as invalid',
|
||||||
invalidInput,
|
invalidInput,
|
||||||
|
@ -46,7 +56,7 @@ export function scrollFirstInvalidElementIntoViewPort() {
|
||||||
// https://github.com/kaliber5/ember-bootstrap/issues/931
|
// https://github.com/kaliber5/ember-bootstrap/issues/931
|
||||||
// As a work-a-round we look the correct label up by a custom convention for the `id` of the
|
// As a work-a-round we look the correct label up by a custom convention for the `id` of the
|
||||||
// inputs and the `for` of the input group `<label>` (which should be a `<legend>`).
|
// inputs and the `for` of the input group `<label>` (which should be a `<legend>`).
|
||||||
let scrollTarget =
|
const scrollTarget =
|
||||||
document.querySelector(
|
document.querySelector(
|
||||||
`label[for="${invalidInput.id.substr(
|
`label[for="${invalidInput.id.substr(
|
||||||
0,
|
0,
|
|
@ -7,7 +7,7 @@ export default class Option {
|
||||||
// 1) ISO 8601 date string: `YYYY-MM-DD`
|
// 1) ISO 8601 date string: `YYYY-MM-DD`
|
||||||
// 2) ISO 8601 datetime string: `YYYY-MM-DDTHH:mm:ss.0000+01:00`
|
// 2) ISO 8601 datetime string: `YYYY-MM-DDTHH:mm:ss.0000+01:00`
|
||||||
// 3) Free text if poll type is MakeAPoll
|
// 3) Free text if poll type is MakeAPoll
|
||||||
title;
|
title: string;
|
||||||
|
|
||||||
get datetime() {
|
get datetime() {
|
||||||
const { title } = this;
|
const { title } = this;
|
||||||
|
@ -16,16 +16,18 @@ export default class Option {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DateTime.fromISO(title);
|
const datetime = DateTime.fromISO(title);
|
||||||
|
|
||||||
|
return datetime.isValid ? datetime : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDate() {
|
get isDate() {
|
||||||
const { datetime } = this;
|
const { datetime } = this;
|
||||||
return datetime !== null && datetime.isValid;
|
return datetime !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get day() {
|
get day() {
|
||||||
if (!this.isDate) {
|
if (!this.datetime) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +35,10 @@ export default class Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
get jsDate() {
|
get jsDate() {
|
||||||
|
if (!this.datetime) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.datetime.toJSDate();
|
return this.datetime.toJSDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,14 +47,14 @@ export default class Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
get time() {
|
get time() {
|
||||||
if (!this.isDate || !this.hasTime) {
|
if (!this.datetime || !this.hasTime) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.datetime.toISOTime().substring(0, 5);
|
return this.datetime.toISOTime()?.substring(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ title }) {
|
constructor({ title }: { title: string }) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,48 +6,68 @@ import { decrypt, encrypt } from '../utils/encryption';
|
||||||
import answersForAnswerType from '../utils/answers-for-answer-type';
|
import answersForAnswerType from '../utils/answers-for-answer-type';
|
||||||
import fetch from 'fetch';
|
import fetch from 'fetch';
|
||||||
import config from 'croodle/config/environment';
|
import config from 'croodle/config/environment';
|
||||||
|
import type { SelectionInput } from './selection';
|
||||||
|
|
||||||
const DAY_STRING_LENGTH = 10; // 'YYYY-MM-DD'.length
|
const DAY_STRING_LENGTH = 10; // 'YYYY-MM-DD'.length
|
||||||
|
|
||||||
|
export type AnswerType = 'YesNo' | 'YesNoMaybe' | 'FreeText';
|
||||||
|
type OptionInput = { title: string }[];
|
||||||
|
export type PollType = 'FindADate' | 'MakeAPoll';
|
||||||
|
type PollInput = {
|
||||||
|
anonymousUser: boolean;
|
||||||
|
answerType: AnswerType;
|
||||||
|
creationDate: string;
|
||||||
|
description: string;
|
||||||
|
expirationDate: string;
|
||||||
|
forceAnswer: boolean;
|
||||||
|
id: string;
|
||||||
|
options: OptionInput;
|
||||||
|
pollType: PollType;
|
||||||
|
timezone: string | null;
|
||||||
|
title: string;
|
||||||
|
users: User[];
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default class Poll {
|
export default class Poll {
|
||||||
// Is participation without user name possibile?
|
// Is participation without user name possibile?
|
||||||
anonymousUser;
|
anonymousUser: boolean;
|
||||||
|
|
||||||
// YesNo, YesNoMaybe or Freetext
|
// YesNo, YesNoMaybe or Freetext
|
||||||
answerType;
|
answerType: AnswerType;
|
||||||
|
|
||||||
// ISO-8601 combined date and time string in UTC
|
// ISO-8601 combined date and time string in UTC
|
||||||
creationDate;
|
creationDate: string;
|
||||||
|
|
||||||
// poll's description
|
// poll's description
|
||||||
description;
|
description: string;
|
||||||
|
|
||||||
// ISO 8601 date + time string in UTC
|
// ISO 8601 date + time string in UTC
|
||||||
expirationDate;
|
expirationDate: string;
|
||||||
|
|
||||||
// Must all options been answered?
|
// Must all options been answered?
|
||||||
forceAnswer;
|
forceAnswer: boolean;
|
||||||
|
|
||||||
// ID of the poll
|
// ID of the poll
|
||||||
id;
|
id: string;
|
||||||
|
|
||||||
// array of poll's options
|
// array of poll's options
|
||||||
options;
|
options: Option[];
|
||||||
|
|
||||||
// FindADate or MakeAPoll
|
// FindADate or MakeAPoll
|
||||||
pollType;
|
pollType: 'FindADate' | 'MakeAPoll';
|
||||||
|
|
||||||
// timezone poll got created in (like "Europe/Berlin")
|
// timezone poll got created in (like "Europe/Berlin")
|
||||||
timezone;
|
timezone: string | null;
|
||||||
|
|
||||||
// polls title
|
// polls title
|
||||||
title;
|
title: string;
|
||||||
|
|
||||||
// participants of the poll
|
// participants of the poll
|
||||||
users;
|
users: TrackedArray<User>;
|
||||||
|
|
||||||
// Croodle version poll got created with
|
// Croodle version poll got created with
|
||||||
version;
|
version: string;
|
||||||
|
|
||||||
get answers() {
|
get answers() {
|
||||||
const { answerType } = this;
|
const { answerType } = this;
|
||||||
|
@ -90,7 +110,7 @@ export default class Poll {
|
||||||
title,
|
title,
|
||||||
users,
|
users,
|
||||||
version,
|
version,
|
||||||
}) {
|
}: PollInput) {
|
||||||
this.anonymousUser = anonymousUser;
|
this.anonymousUser = anonymousUser;
|
||||||
this.answerType = answerType;
|
this.answerType = answerType;
|
||||||
this.creationDate = creationDate;
|
this.creationDate = creationDate;
|
||||||
|
@ -106,7 +126,7 @@ export default class Poll {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async load(id, passphrase) {
|
static async load(id: string, passphrase: string) {
|
||||||
const url = apiUrl(`polls/${id}`);
|
const url = apiUrl(`polls/${id}`);
|
||||||
|
|
||||||
// TODO: Handle network connectivity error
|
// TODO: Handle network connectivity error
|
||||||
|
@ -125,26 +145,51 @@ export default class Poll {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle malformed server response
|
// TODO: Handle malformed server response
|
||||||
const payload = await response.json();
|
const payload = (await response.json()) as {
|
||||||
|
poll: {
|
||||||
|
anonymousUser: string;
|
||||||
|
answerType: string;
|
||||||
|
creationDate: string;
|
||||||
|
description: string;
|
||||||
|
expirationDate: string;
|
||||||
|
forceAnswer: string;
|
||||||
|
id: string;
|
||||||
|
options: string;
|
||||||
|
pollType: string;
|
||||||
|
timezone: string;
|
||||||
|
title: string;
|
||||||
|
users: {
|
||||||
|
creationDate: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
selections: string;
|
||||||
|
version: string;
|
||||||
|
}[];
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return new Poll({
|
return new Poll({
|
||||||
anonymousUser: decrypt(payload.poll.anonymousUser, passphrase),
|
anonymousUser: decrypt(payload.poll.anonymousUser, passphrase) as boolean,
|
||||||
answerType: decrypt(payload.poll.answerType, passphrase),
|
answerType: decrypt(payload.poll.answerType, passphrase) as AnswerType,
|
||||||
creationDate: decrypt(payload.poll.creationDate, passphrase),
|
creationDate: decrypt(payload.poll.creationDate, passphrase) as string,
|
||||||
description: decrypt(payload.poll.description, passphrase),
|
description: decrypt(payload.poll.description, passphrase) as string,
|
||||||
expirationDate: decrypt(payload.poll.expirationDate, passphrase),
|
expirationDate: decrypt(
|
||||||
forceAnswer: decrypt(payload.poll.forceAnswer, passphrase),
|
payload.poll.expirationDate,
|
||||||
|
passphrase,
|
||||||
|
) as string,
|
||||||
|
forceAnswer: decrypt(payload.poll.forceAnswer, passphrase) as boolean,
|
||||||
id: payload.poll.id,
|
id: payload.poll.id,
|
||||||
options: decrypt(payload.poll.options, passphrase),
|
options: decrypt(payload.poll.options, passphrase) as OptionInput,
|
||||||
pollType: decrypt(payload.poll.pollType, passphrase),
|
pollType: decrypt(payload.poll.pollType, passphrase) as PollType,
|
||||||
timezone: decrypt(payload.poll.timezone, passphrase),
|
timezone: decrypt(payload.poll.timezone, passphrase) as string,
|
||||||
title: decrypt(payload.poll.title, passphrase),
|
title: decrypt(payload.poll.title, passphrase) as string,
|
||||||
users: payload.poll.users.map((user) => {
|
users: payload.poll.users.map((user) => {
|
||||||
return new User({
|
return new User({
|
||||||
creationDate: decrypt(user.creationDate, passphrase),
|
creationDate: decrypt(user.creationDate, passphrase) as string,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: decrypt(user.name, passphrase),
|
name: decrypt(user.name, passphrase) as string,
|
||||||
selections: decrypt(user.selections, passphrase),
|
selections: decrypt(user.selections, passphrase) as SelectionInput[],
|
||||||
version: user.version,
|
version: user.version,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -162,15 +207,24 @@ export default class Poll {
|
||||||
options,
|
options,
|
||||||
pollType,
|
pollType,
|
||||||
title,
|
title,
|
||||||
|
}: {
|
||||||
|
anonymousUser: boolean;
|
||||||
|
answerType: AnswerType;
|
||||||
|
description: string;
|
||||||
|
expirationDate: string;
|
||||||
|
forceAnswer: boolean;
|
||||||
|
options: OptionInput;
|
||||||
|
pollType: PollType;
|
||||||
|
title: string;
|
||||||
},
|
},
|
||||||
passphrase,
|
passphrase: string,
|
||||||
) {
|
) {
|
||||||
const creationDate = new Date().toISOString();
|
const creationDate = new Date().toISOString();
|
||||||
const version = config.APP.version;
|
const version = config.APP.version;
|
||||||
const timezone =
|
const timezone =
|
||||||
pollType === 'FindADate' &&
|
pollType === 'FindADate' &&
|
||||||
options.some(({ title }) => {
|
options.some(({ title }) => {
|
||||||
return title >= 'YYYY-MM-DDTHH:mm'.length;
|
return title.length >= 'YYYY-MM-DDTHH:mm'.length;
|
||||||
})
|
})
|
||||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
: null;
|
: null;
|
|
@ -1,13 +0,0 @@
|
||||||
export default class Answer {
|
|
||||||
icon;
|
|
||||||
label;
|
|
||||||
labelTranslation;
|
|
||||||
type;
|
|
||||||
|
|
||||||
constructor({ icon, label, labelTranslation, type }) {
|
|
||||||
this.icon = icon;
|
|
||||||
this.label = label;
|
|
||||||
this.labelTranslation = labelTranslation;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
}
|
|
20
app/models/selection.ts
Normal file
20
app/models/selection.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export type SelectionInput = {
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
labelTranslation: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Selection {
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
labelTranslation: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
constructor({ icon, label, labelTranslation, type }: SelectionInput) {
|
||||||
|
this.icon = icon;
|
||||||
|
this.label = label;
|
||||||
|
this.labelTranslation = labelTranslation;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,35 @@
|
||||||
import Selection from './selection';
|
import Selection, { type SelectionInput } from './selection';
|
||||||
import config from 'croodle/config/environment';
|
import config from 'croodle/config/environment';
|
||||||
import { encrypt } from '../utils/encryption';
|
import { encrypt } from '../utils/encryption';
|
||||||
import { apiUrl } from '../utils/api';
|
import { apiUrl } from '../utils/api';
|
||||||
import fetch from 'fetch';
|
import fetch from 'fetch';
|
||||||
|
import type Poll from './poll';
|
||||||
|
|
||||||
|
type UserInput = {
|
||||||
|
creationDate: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
selections: SelectionInput[];
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default class User {
|
export default class User {
|
||||||
// ISO 8601 date + time string
|
// ISO 8601 date + time string
|
||||||
creationDate;
|
creationDate: string;
|
||||||
|
|
||||||
id;
|
id: string;
|
||||||
|
|
||||||
// user name
|
// user name
|
||||||
name;
|
name: string;
|
||||||
|
|
||||||
// array of users selections
|
// array of users selections
|
||||||
// must be in same order as options property of poll
|
// must be in same order as options property of poll
|
||||||
selections;
|
selections: Selection[];
|
||||||
|
|
||||||
// Croodle version user got created with
|
// Croodle version user got created with
|
||||||
version;
|
version: string;
|
||||||
|
|
||||||
constructor({ creationDate, id, name, selections, version }) {
|
constructor({ creationDate, id, name, selections, version }: UserInput) {
|
||||||
this.creationDate = creationDate;
|
this.creationDate = creationDate;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -28,7 +37,14 @@ export default class User {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async create({ name, poll, selections }, passphrase) {
|
static async create(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
poll,
|
||||||
|
selections,
|
||||||
|
}: { name: string; poll: Poll; selections: SelectionInput[] },
|
||||||
|
passphrase: string,
|
||||||
|
) {
|
||||||
const creationDate = new Date().toISOString();
|
const creationDate = new Date().toISOString();
|
||||||
const version = config.APP.version;
|
const version = config.APP.version;
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
import Modifier from 'ember-modifier';
|
import Modifier from 'ember-modifier';
|
||||||
|
|
||||||
export default class AutofocusModifier extends Modifier {
|
type Named = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AutofocusModifierSignature {
|
||||||
|
Element: HTMLInputElement;
|
||||||
|
Args: {
|
||||||
|
Named: Named;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AutofocusModifier extends Modifier<AutofocusModifierSignature> {
|
||||||
isInstalled = false;
|
isInstalled = false;
|
||||||
|
|
||||||
modify(element, positional, { enabled = true }) {
|
modify(element: HTMLInputElement, _: [], { enabled = true }: Named) {
|
||||||
// element should be only autofocused on initial render
|
// element should be only autofocused on initial render
|
||||||
// not when `enabled` option is invalidated
|
// not when `enabled` option is invalidated
|
||||||
if (this.isInstalled) {
|
if (this.isInstalled) {
|
|
@ -17,5 +17,4 @@ Router.map(function () {
|
||||||
this.route('options-datetime');
|
this.route('options-datetime');
|
||||||
this.route('settings');
|
this.route('settings');
|
||||||
});
|
});
|
||||||
this.route('404');
|
|
||||||
});
|
});
|
|
@ -1,43 +0,0 @@
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CreateRoute extends Route {
|
|
||||||
@service router;
|
|
||||||
|
|
||||||
beforeModel(transition) {
|
|
||||||
// enforce that wizzard is started at create.index
|
|
||||||
if (transition.targetName !== 'create.index') {
|
|
||||||
this.router.transitionTo('create.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model() {
|
|
||||||
return new PollData();
|
|
||||||
}
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
let controller = this.controllerFor(this.routeName);
|
|
||||||
controller.listenForStepChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate() {
|
|
||||||
let controller = this.controllerFor(this.routeName);
|
|
||||||
controller.clearListenerForStepChanges();
|
|
||||||
}
|
|
||||||
}
|
|
52
app/routes/create.ts
Normal file
52
app/routes/create.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import RouterService from '@ember/routing/router-service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { TrackedSet } from 'tracked-built-ins';
|
||||||
|
import type CreateController from 'croodle/controllers/create';
|
||||||
|
import type { AnswerType, PollType } from 'croodle/models/poll';
|
||||||
|
|
||||||
|
class PollData {
|
||||||
|
@tracked anonymousUser: boolean = false;
|
||||||
|
@tracked answerType: AnswerType = 'YesNo';
|
||||||
|
@tracked description: string = '';
|
||||||
|
@tracked expirationDate: string = DateTime.local()
|
||||||
|
.plus({ months: 3 })
|
||||||
|
.toISO() as string;
|
||||||
|
@tracked forceAnswer: boolean = true;
|
||||||
|
@tracked freetextOptions: TrackedSet<string> = new TrackedSet();
|
||||||
|
@tracked dateOptions: TrackedSet<string> = new TrackedSet();
|
||||||
|
@tracked timesForDateOptions: Map<string, Set<string>> = new Map();
|
||||||
|
@tracked pollType: PollType = 'FindADate';
|
||||||
|
@tracked title: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CreateRoute extends Route {
|
||||||
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
beforeModel(transition: Transition) {
|
||||||
|
// enforce that wizzard is started at create.index
|
||||||
|
if (transition.to.name !== 'create.index') {
|
||||||
|
this.router.transitionTo('create.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return new PollData();
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
const controller = this.controllerFor(this.routeName) as CreateController;
|
||||||
|
controller.listenForStepChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
const controller = this.controllerFor(this.routeName) as CreateController;
|
||||||
|
controller.clearListenerForStepChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateRouteModel = Resolved<ReturnType<CreateRoute['model']>>;
|
|
@ -1,21 +0,0 @@
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
|
|
||||||
class FormData {
|
|
||||||
@tracked pollType;
|
|
||||||
|
|
||||||
constructor({ pollType }) {
|
|
||||||
this.pollType = pollType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class IndexRoute extends Route {
|
|
||||||
model() {
|
|
||||||
const { pollType } = this.modelFor('create');
|
|
||||||
|
|
||||||
return {
|
|
||||||
formData: new FormData({ pollType }),
|
|
||||||
poll: this.modelFor('create'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
28
app/routes/create/index.ts
Normal file
28
app/routes/create/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import type { PollType } from 'croodle/models/poll';
|
||||||
|
import type { CreateRouteModel } from '../create';
|
||||||
|
|
||||||
|
class FormData {
|
||||||
|
@tracked declare pollType;
|
||||||
|
|
||||||
|
constructor({ pollType }: { pollType: PollType }) {
|
||||||
|
this.pollType = pollType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CreateIndexRoute extends Route {
|
||||||
|
model() {
|
||||||
|
const { pollType } = this.modelFor('create') as CreateRouteModel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
formData: new FormData({ pollType }),
|
||||||
|
poll: this.modelFor('create') as CreateRouteModel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateRouteIndexModel = Resolved<
|
||||||
|
ReturnType<CreateIndexRoute['model']>
|
||||||
|
>;
|
|
@ -1,10 +1,11 @@
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import IntlMessage from '../../utils/intl-message';
|
import IntlMessage from '../../utils/intl-message';
|
||||||
|
import type { CreateRouteModel } from '../create';
|
||||||
|
|
||||||
class FormData {
|
class FormData {
|
||||||
@tracked title;
|
@tracked title: string;
|
||||||
@tracked description;
|
@tracked description: string;
|
||||||
|
|
||||||
get titleValidation() {
|
get titleValidation() {
|
||||||
const { title } = this;
|
const { title } = this;
|
||||||
|
@ -22,19 +23,24 @@ class FormData {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ title, description }) {
|
constructor({ title, description }: { title: string; description: string }) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MetaRoute extends Route {
|
export default class CreateMetaRoute extends Route {
|
||||||
model() {
|
model() {
|
||||||
const { title, description } = this.modelFor('create');
|
const { title, description } = this.modelFor('create') as CreateRouteModel;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
formData: new FormData({ title, description }),
|
formData: new FormData({ title, description }),
|
||||||
poll: this.modelFor('create'),
|
poll: this.modelFor('create') as CreateRouteModel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateMetaRouteModel = Resolved<
|
||||||
|
ReturnType<CreateMetaRoute['model']>
|
||||||
|
>;
|
|
@ -1,7 +0,0 @@
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default class OptionsDatetimeRoute extends Route {
|
|
||||||
model() {
|
|
||||||
return this.modelFor('create');
|
|
||||||
}
|
|
||||||
}
|
|
13
app/routes/create/options-datetime.ts
Normal file
13
app/routes/create/options-datetime.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import type { CreateRouteModel } from '../create';
|
||||||
|
|
||||||
|
export default class CreateOptionsDatetimeRoute extends Route {
|
||||||
|
model() {
|
||||||
|
return this.modelFor('create') as CreateRouteModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateOptionsDatetimeRouteModel = Resolved<
|
||||||
|
ReturnType<CreateOptionsDatetimeRoute['model']>
|
||||||
|
>;
|
|
@ -1,7 +0,0 @@
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default class OptionsRoute extends Route {
|
|
||||||
model() {
|
|
||||||
return this.modelFor('create');
|
|
||||||
}
|
|
||||||
}
|
|
13
app/routes/create/options.ts
Normal file
13
app/routes/create/options.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import type { CreateRouteModel } from '../create';
|
||||||
|
|
||||||
|
export default class CreateOptionsRoute extends Route {
|
||||||
|
model() {
|
||||||
|
return this.modelFor('create') as CreateRouteModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateOptionsRouteModel = Resolved<
|
||||||
|
ReturnType<CreateOptionsRoute['model']>
|
||||||
|
>;
|
|
@ -1,7 +0,0 @@
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default class SettingsRoute extends Route {
|
|
||||||
model() {
|
|
||||||
return this.modelFor('create');
|
|
||||||
}
|
|
||||||
}
|
|
13
app/routes/create/settings.ts
Normal file
13
app/routes/create/settings.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import type { CreateRouteModel } from '../create';
|
||||||
|
|
||||||
|
export default class CreateSettingsRoute extends Route {
|
||||||
|
model() {
|
||||||
|
return this.modelFor('create') as CreateRouteModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type CreateSettingsRouteModel = Resolved<
|
||||||
|
ReturnType<CreateSettingsRoute['model']>
|
||||||
|
>;
|
|
@ -1,33 +0,0 @@
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import Poll from '../models/poll';
|
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default class PollRoute extends Route {
|
|
||||||
@service router;
|
|
||||||
|
|
||||||
@action
|
|
||||||
error(error) {
|
|
||||||
if (error && error.status === 404) {
|
|
||||||
return this.router.transitionTo('404');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
model({ encryptionKey, poll_id: id }) {
|
|
||||||
return Poll.load(id, encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(poll, transition) {
|
|
||||||
if (transition.targetName === 'poll.index') {
|
|
||||||
const { encryptionKey } = this.paramsFor(this.routeName);
|
|
||||||
|
|
||||||
this.router.transitionTo('poll.participation', poll, {
|
|
||||||
queryParams: {
|
|
||||||
encryptionKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
36
app/routes/poll.ts
Normal file
36
app/routes/poll.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import Poll from '../models/poll';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import type Transition from '@ember/routing/transition';
|
||||||
|
import RouterService from '@ember/routing/router-service';
|
||||||
|
|
||||||
|
export default class PollRoute extends Route {
|
||||||
|
@service declare router: RouterService;
|
||||||
|
|
||||||
|
model({
|
||||||
|
encryptionKey,
|
||||||
|
poll_id: id,
|
||||||
|
}: {
|
||||||
|
encryptionKey: string;
|
||||||
|
poll_id: string;
|
||||||
|
}) {
|
||||||
|
return Poll.load(id, encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(poll: Poll, transition: Transition) {
|
||||||
|
if (transition.to.name === 'poll.index') {
|
||||||
|
const { encryptionKey } = this.paramsFor(this.routeName) as {
|
||||||
|
encryptionKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.router.transitionTo('poll.participation', poll, {
|
||||||
|
queryParams: {
|
||||||
|
encryptionKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolved<P> = P extends Promise<infer T> ? T : P;
|
||||||
|
export type PollRouteModel = Resolved<ReturnType<PollRoute['model']>>;
|
|
@ -1,7 +1,9 @@
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { TrackedArray } from 'tracked-built-ins';
|
import { TrackedArray } from 'tracked-built-ins';
|
||||||
import IntlMessage from '../../utils/intl-message';
|
import IntlMessage from 'croodle/utils/intl-message';
|
||||||
|
import type Poll from 'croodle/models/poll';
|
||||||
|
import type Option from 'croodle/models/option';
|
||||||
|
|
||||||
class FormDataSelections {
|
class FormDataSelections {
|
||||||
@tracked value = null;
|
@tracked value = null;
|
||||||
|
@ -21,13 +23,13 @@ class FormDataSelections {
|
||||||
return this.valueValidation === null;
|
return this.valueValidation === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(valueIsRequired) {
|
constructor(valueIsRequired: boolean) {
|
||||||
this.valueIsRequired = valueIsRequired;
|
this.valueIsRequired = valueIsRequired;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormData {
|
class FormData {
|
||||||
@tracked name = null;
|
@tracked name: null | string = null;
|
||||||
nameIsRequired;
|
nameIsRequired;
|
||||||
namesTaken;
|
namesTaken;
|
||||||
selections;
|
selections;
|
||||||
|
@ -39,8 +41,7 @@ class FormData {
|
||||||
return new IntlMessage('poll.error.name.valueMissing');
|
return new IntlMessage('poll.error.name.valueMissing');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Validate that name is unique for this poll
|
if (name && namesTaken.includes(name)) {
|
||||||
if (namesTaken.includes(name)) {
|
|
||||||
return new IntlMessage('poll.error.name.duplicate');
|
return new IntlMessage('poll.error.name.duplicate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +58,18 @@ class FormData {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(options, { nameIsRequired, namesTaken, selectionIsRequired }) {
|
constructor(
|
||||||
|
options: Option[],
|
||||||
|
{
|
||||||
|
nameIsRequired,
|
||||||
|
namesTaken,
|
||||||
|
selectionIsRequired,
|
||||||
|
}: {
|
||||||
|
nameIsRequired: boolean;
|
||||||
|
namesTaken: string[];
|
||||||
|
selectionIsRequired: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
this.nameIsRequired = nameIsRequired;
|
this.nameIsRequired = nameIsRequired;
|
||||||
this.namesTaken = namesTaken;
|
this.namesTaken = namesTaken;
|
||||||
this.selections = new TrackedArray(
|
this.selections = new TrackedArray(
|
||||||
|
@ -68,7 +80,7 @@ class FormData {
|
||||||
|
|
||||||
export default class ParticipationRoute extends Route {
|
export default class ParticipationRoute extends Route {
|
||||||
model() {
|
model() {
|
||||||
const poll = this.modelFor('poll');
|
const poll = this.modelFor('poll') as Poll;
|
||||||
const { anonymousUser, forceAnswer, options, users } = poll;
|
const { anonymousUser, forceAnswer, options, users } = poll;
|
||||||
const formData = new FormData(options, {
|
const formData = new FormData(options, {
|
||||||
nameIsRequired: !anonymousUser,
|
nameIsRequired: !anonymousUser,
|
|
@ -1,6 +0,0 @@
|
||||||
<div class="box">
|
|
||||||
<h2>poll could not be found</h2>
|
|
||||||
<p>
|
|
||||||
The poll with you url could not be found. Perhaps it got deleted?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { assert } from '@ember/debug';
|
import { assert } from '@ember/debug';
|
||||||
|
|
||||||
export default function (answerType) {
|
export type Answer = {
|
||||||
|
labelTranslation: string;
|
||||||
|
icon: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (
|
||||||
|
answerType: 'YesNo' | 'YesNoMaybe' | 'FreeText',
|
||||||
|
): Answer[] {
|
||||||
switch (answerType) {
|
switch (answerType) {
|
||||||
case 'YesNo':
|
case 'YesNo':
|
||||||
return [
|
return [
|
|
@ -10,7 +10,7 @@ const baseUrl = window.location.pathname
|
||||||
// add api/index.php
|
// add api/index.php
|
||||||
.concat('/api/index.php');
|
.concat('/api/index.php');
|
||||||
|
|
||||||
function apiUrl(path) {
|
function apiUrl(path: string) {
|
||||||
return `${baseUrl}/${path}`;
|
return `${baseUrl}/${path}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { decrypt as sjclDecrypt, encrypt as sjclEncrypt } from 'sjcl';
|
import { decrypt as sjclDecrypt, encrypt as sjclEncrypt } from 'sjcl';
|
||||||
|
|
||||||
function decrypt(encryptedValue, passphrase) {
|
function decrypt(encryptedValue: string, passphrase: string): unknown {
|
||||||
return JSON.parse(sjclDecrypt(passphrase, encryptedValue));
|
return JSON.parse(sjclDecrypt(passphrase, encryptedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
function encrypt(plainValue, passphrase) {
|
function encrypt(plainValue: unknown, passphrase: string) {
|
||||||
return sjclEncrypt(passphrase, JSON.stringify(plainValue));
|
return sjclEncrypt(passphrase, JSON.stringify(plainValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePassphrase() {
|
function generatePassphrase(): string {
|
||||||
const length = 40;
|
const length = 40;
|
||||||
const possible =
|
const possible =
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
@ -18,7 +18,9 @@ function generatePassphrase() {
|
||||||
|
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
for (let j = length; j--; ) {
|
for (let j = length; j--; ) {
|
||||||
passphrase += possible.charAt(Math.floor(randomArray[j] % possible.length));
|
passphrase += possible.charAt(
|
||||||
|
Math.floor(randomArray[j]! % possible.length),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return passphrase;
|
return passphrase;
|
|
@ -2,7 +2,7 @@ export default class IntlMessage {
|
||||||
key;
|
key;
|
||||||
options;
|
options;
|
||||||
|
|
||||||
constructor(key, options) {
|
constructor(key: string, options?: Record<string, string>) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
|
@ -11,8 +11,8 @@
|
||||||
"codemodsSource": "ember-app-codemods-manifest@1",
|
"codemodsSource": "ember-app-codemods-manifest@1",
|
||||||
"isBaseBlueprint": true,
|
"isBaseBlueprint": true,
|
||||||
"options": [
|
"options": [
|
||||||
"--yarn",
|
"--ci-provider=github",
|
||||||
"--no-welcome"
|
"--typescript"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"application-template-wrapper": false,
|
"application-template-wrapper": false,
|
||||||
|
"default-async-observers": true,
|
||||||
"jquery-integration": false,
|
"jquery-integration": false,
|
||||||
"template-only-glimmer-components": true
|
"template-only-glimmer-components": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ module.exports = function (defaults) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'ember-cli-babel': {
|
'ember-cli-babel': {
|
||||||
includePolyfill: true,
|
enableTypeScriptTransform: true,
|
||||||
},
|
},
|
||||||
'ember-composable-helpers': {
|
'ember-composable-helpers': {
|
||||||
only: ['array', 'object-at', 'pick'],
|
only: ['array', 'object-at', 'pick'],
|
||||||
|
|
9109
package-lock.json
generated
9109
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -18,8 +18,9 @@
|
||||||
"lint:hbs": "ember-template-lint .",
|
"lint:hbs": "ember-template-lint .",
|
||||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||||
"lint:js": "eslint . --cache",
|
"lint:js": "eslint . --cache",
|
||||||
"release": "release-it",
|
|
||||||
"lint:js:fix": "eslint . --fix",
|
"lint:js:fix": "eslint . --fix",
|
||||||
|
"lint:types": "tsc --noEmit",
|
||||||
|
"release": "release-it",
|
||||||
"start": "ember serve",
|
"start": "ember serve",
|
||||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||||
"test:ember": "ember test",
|
"test:ember": "ember test",
|
||||||
|
@ -28,14 +29,21 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.20",
|
"@babel/core": "^7.22.20",
|
||||||
"@babel/eslint-parser": "^7.22.15",
|
|
||||||
"@babel/plugin-proposal-decorators": "^7.22.15",
|
|
||||||
"@ember/optional-features": "^2.0.0",
|
"@ember/optional-features": "^2.0.0",
|
||||||
"@ember/string": "^3.1.1",
|
"@ember/string": "^3.1.1",
|
||||||
"@ember/test-helpers": "^3.2.0",
|
"@ember/test-helpers": "^3.2.0",
|
||||||
"@glimmer/component": "^1.1.2",
|
"@glimmer/component": "^1.1.2",
|
||||||
"@glimmer/tracking": "^1.1.2",
|
"@glimmer/tracking": "^1.1.2",
|
||||||
|
"@glint/environment-ember-loose": "^1.1.0",
|
||||||
|
"@glint/template": "^1.1.0",
|
||||||
"@release-it-plugins/lerna-changelog": "^6.0.0",
|
"@release-it-plugins/lerna-changelog": "^6.0.0",
|
||||||
|
"@tsconfig/ember": "^3.0.1",
|
||||||
|
"@types/luxon": "^3.3.3",
|
||||||
|
"@types/qunit": "^2.19.6",
|
||||||
|
"@types/rsvp": "^4.0.4",
|
||||||
|
"@types/sjcl": "^1.0.32",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||||
|
"@typescript-eslint/parser": "^6.7.2",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"concurrently": "^8.2.1",
|
"concurrently": "^8.2.1",
|
||||||
|
@ -98,6 +106,7 @@
|
||||||
"stylelint-config-standard-scss": "^11.0.0",
|
"stylelint-config-standard-scss": "^11.0.0",
|
||||||
"stylelint-prettier": "^4.0.2",
|
"stylelint-prettier": "^4.0.2",
|
||||||
"tracked-built-ins": "^3.3.0",
|
"tracked-built-ins": "^3.3.0",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
"webpack": "^5.88.2"
|
"webpack": "^5.88.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -2,13 +2,14 @@ import {
|
||||||
setupApplicationTest as upstreamSetupApplicationTest,
|
setupApplicationTest as upstreamSetupApplicationTest,
|
||||||
setupRenderingTest as upstreamSetupRenderingTest,
|
setupRenderingTest as upstreamSetupRenderingTest,
|
||||||
setupTest as upstreamSetupTest,
|
setupTest as upstreamSetupTest,
|
||||||
|
type SetupTestOptions,
|
||||||
} from 'ember-qunit';
|
} from 'ember-qunit';
|
||||||
|
|
||||||
// This file exists to provide wrappers around ember-qunit's
|
// This file exists to provide wrappers around ember-qunit's
|
||||||
// test setup functions. This way, you can easily extend the setup that is
|
// test setup functions. This way, you can easily extend the setup that is
|
||||||
// needed per test type.
|
// needed per test type.
|
||||||
|
|
||||||
function setupApplicationTest(hooks, options) {
|
function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||||
upstreamSetupApplicationTest(hooks, options);
|
upstreamSetupApplicationTest(hooks, options);
|
||||||
|
|
||||||
// Additional setup for application tests can be done here.
|
// Additional setup for application tests can be done here.
|
||||||
|
@ -27,13 +28,13 @@ function setupApplicationTest(hooks, options) {
|
||||||
// setupMirage(hooks); // ember-cli-mirage
|
// setupMirage(hooks); // ember-cli-mirage
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRenderingTest(hooks, options) {
|
function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||||
upstreamSetupRenderingTest(hooks, options);
|
upstreamSetupRenderingTest(hooks, options);
|
||||||
|
|
||||||
// Additional setup for rendering tests can be done here.
|
// Additional setup for rendering tests can be done here.
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTest(hooks, options) {
|
function setupTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||||
upstreamSetupTest(hooks, options);
|
upstreamSetupTest(hooks, options);
|
||||||
|
|
||||||
// Additional setup for unit tests can be done here.
|
// Additional setup for unit tests can be done here.
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/ember/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// The combination of `baseUrl` with `paths` allows Ember's classic package
|
||||||
|
// layout, which is not resolvable with the Node resolution algorithm, to
|
||||||
|
// work with TypeScript.
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"croodle/tests/*": ["tests/*"],
|
||||||
|
"croodle/*": ["app/*"],
|
||||||
|
"fetch": [
|
||||||
|
"node_modules/ember-fetch"
|
||||||
|
],
|
||||||
|
"*": ["types/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
types/ember-cli-flash/flash/object.d.ts
vendored
Normal file
19
types/ember-cli-flash/flash/object.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Source: https://github.com/adopted-ember-addons/ember-cli-flash/blob/5e9ca769ce30b168eef1a4a8e4cdf5ad0d538a8d/ember-cli-flash/declarations/flash/object.d.ts
|
||||||
|
declare module 'ember-cli-flash/flash/object' {
|
||||||
|
import EmberObject from '@ember/object';
|
||||||
|
import Evented from '@ember/object/evented';
|
||||||
|
|
||||||
|
class FlashObject extends EmberObject.extend(Evented) {
|
||||||
|
exiting: boolean;
|
||||||
|
exitTimer: number;
|
||||||
|
isExitable: boolean;
|
||||||
|
initializedTime: number;
|
||||||
|
destroyMessage(): void;
|
||||||
|
exitMessage(): void;
|
||||||
|
preventExit(): void;
|
||||||
|
allowExit(): void;
|
||||||
|
timerTask(): void;
|
||||||
|
exitTimerTask(): void;
|
||||||
|
}
|
||||||
|
export default FlashObject;
|
||||||
|
}
|
46
types/ember-cli-flash/services/intl.d.ts
vendored
Normal file
46
types/ember-cli-flash/services/intl.d.ts
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Source: https://github.com/adopted-ember-addons/ember-cli-flash/blob/5e9ca769ce30b168eef1a4a8e4cdf5ad0d538a8d/ember-cli-flash/declarations/services/flash-messages.d.ts
|
||||||
|
declare module 'ember-cli-flash/services/flash-messages' {
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import FlashObject from 'ember-cli-flash/flash/object';
|
||||||
|
|
||||||
|
export interface MessageOptions {
|
||||||
|
type: string;
|
||||||
|
priority: number;
|
||||||
|
timeout: number;
|
||||||
|
sticky: boolean;
|
||||||
|
showProgress: boolean;
|
||||||
|
extendedTimeout: number;
|
||||||
|
destroyOnClick: boolean;
|
||||||
|
onDestroy: () => void;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomMessageInfo extends Partial<MessageOptions> {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlashFunction {
|
||||||
|
(message: string, options?: Partial<MessageOptions>): FlashMessageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlashMessageService extends Service {
|
||||||
|
queue: FlashObject[];
|
||||||
|
readonly arrangedQueue: FlashObject[];
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
success: FlashFunction;
|
||||||
|
warning: FlashFunction;
|
||||||
|
info: FlashFunction;
|
||||||
|
danger: FlashFunction;
|
||||||
|
alert: FlashFunction;
|
||||||
|
secondary: FlashFunction;
|
||||||
|
add(messageInfo: CustomMessageInfo): this;
|
||||||
|
clearMessages(): this;
|
||||||
|
registerTypes(types: string[]): this;
|
||||||
|
getFlashObject(): FlashObject;
|
||||||
|
peekFirst(): FlashObject | undefined;
|
||||||
|
peekLast(): FlashObject | undefined;
|
||||||
|
readonly flashMessageDefaults: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashMessageService;
|
||||||
|
}
|
7
types/ember-power-calendar/services/power-calendar.d.ts
vendored
Normal file
7
types/ember-power-calendar/services/power-calendar.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type Service from '@ember/service';
|
||||||
|
|
||||||
|
declare module 'ember-power-calendar/services/power-calendar' {
|
||||||
|
export default class PowerCalendarService extends Service {
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
}
|
1
types/global.d.ts
vendored
Normal file
1
types/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import '@glint/environment-ember-loose';
|
Loading…
Reference in a new issue