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
|
||||
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 = {
|
||||
root: true,
|
||||
parser: '@babel/eslint-parser',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
requireConfigFile: false,
|
||||
babelOptions: {
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: ['ember'],
|
||||
plugins: ['ember', '@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:ember/recommended',
|
||||
|
@ -32,6 +25,15 @@ module.exports = {
|
|||
'no-prototype-builtins': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
// ts files
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
rules: {},
|
||||
},
|
||||
// node files
|
||||
{
|
||||
files: [
|
||||
|
@ -46,9 +48,6 @@ module.exports = {
|
|||
'./lib/*/index.js',
|
||||
'./server/**/*.js',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
env: {
|
||||
browser: false,
|
||||
node: true,
|
||||
|
|
|
@ -4,6 +4,8 @@ import loadInitializers from 'ember-load-initializers';
|
|||
import config from 'croodle/config/environment';
|
||||
|
||||
export default class App extends Application {
|
||||
LOG_TRANSITIONS = true;
|
||||
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
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 { DateTime } from 'luxon';
|
||||
import IntlMessage from '../utils/intl-message';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
class FormDataTimeOption {
|
||||
formData;
|
||||
|
||||
// ISO 8601 date string: YYYY-MM-DD
|
||||
date;
|
||||
date: string;
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// duplicated time should show the validation error.
|
||||
const { formData, date } = this;
|
||||
const timesForThisDate = Array.from(formData.datetimes.get(date));
|
||||
const timesForThisDate = Array.from(formData.datetimes.get(date)!);
|
||||
const isDuplicate = timesForThisDate
|
||||
.slice(0, timesForThisDate.indexOf(this))
|
||||
.some((timeOption) => timeOption.time == this.time);
|
||||
|
@ -64,11 +66,14 @@ class FormDataTimeOption {
|
|||
const { datetimes } = formData;
|
||||
return (
|
||||
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.date = date;
|
||||
this.time = time;
|
||||
|
@ -76,7 +81,7 @@ class FormDataTimeOption {
|
|||
}
|
||||
|
||||
class FormData {
|
||||
@tracked datetimes;
|
||||
@tracked datetimes: Map<string, Set<FormDataTimeOption>>;
|
||||
|
||||
get optionsValidation() {
|
||||
const { datetimes } = this;
|
||||
|
@ -87,7 +92,7 @@ class FormData {
|
|||
),
|
||||
);
|
||||
if (!allTimeOptionsAreValid) {
|
||||
return IntlMessage('create.options-datetime.error.invalidTime');
|
||||
return new IntlMessage('create.options-datetime.error.invalidTime');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -98,9 +103,9 @@ class FormData {
|
|||
}
|
||||
|
||||
@action
|
||||
addOption(date) {
|
||||
addOption(date: string) {
|
||||
this.datetimes
|
||||
.get(date)
|
||||
.get(date)!
|
||||
.add(new FormDataTimeOption(this, { date, time: null }));
|
||||
}
|
||||
|
||||
|
@ -109,8 +114,8 @@ class FormData {
|
|||
* otherwise it deletes time for this date
|
||||
*/
|
||||
@action
|
||||
deleteOption(option) {
|
||||
const timeOptionsForDate = this.datetimes.get(option.date);
|
||||
deleteOption(option: FormDataTimeOption) {
|
||||
const timeOptionsForDate = this.datetimes.get(option.date)!;
|
||||
|
||||
if (timeOptionsForDate.size > 1) {
|
||||
timeOptionsForDate.delete(option);
|
||||
|
@ -122,8 +127,8 @@ class FormData {
|
|||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
const timeOptionsForFirstDay = Array.from(
|
||||
Array.from(this.datetimes.values())[0],
|
||||
);
|
||||
Array.from(this.datetimes.values())[0]!,
|
||||
) as FormDataTimeOption[];
|
||||
const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
|
||||
(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();
|
||||
|
||||
for (const date of dates) {
|
||||
const timesForDate = times.has(date)
|
||||
? Array.from(times.get(date))
|
||||
? Array.from(times.get(date) as Set<string>)
|
||||
: [null];
|
||||
datetimes.set(
|
||||
date,
|
||||
|
@ -164,12 +175,22 @@ class FormData {
|
|||
}
|
||||
}
|
||||
|
||||
export default class CreateOptionsDatetime extends Component {
|
||||
@service router;
|
||||
export interface CreateOptoinsDatetimeSignature {
|
||||
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 });
|
||||
|
||||
@tracked errorMesage = null;
|
||||
@tracked errorMessage: string | null = null;
|
||||
|
||||
@action
|
||||
adoptTimesOfFirstDay() {
|
||||
|
@ -177,7 +198,7 @@ export default class CreateOptionsDatetime extends Component {
|
|||
const successful = formData.adoptTimesOfFirstDay();
|
||||
|
||||
if (!successful) {
|
||||
this.errorMesage =
|
||||
this.errorMessage =
|
||||
'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
|
||||
@action
|
||||
validateInput(option, event) {
|
||||
const element = event.target;
|
||||
validateInput(option: FormDataTimeOption, event: InputEvent) {
|
||||
const element = event.target as HTMLInputElement;
|
||||
|
||||
// update partially filled time validation error
|
||||
option.isPartiallyFilled = !element.checkValidity();
|
||||
|
@ -203,8 +224,8 @@ export default class CreateOptionsDatetime extends Component {
|
|||
|
||||
// remove partially filled validation error if user fixed it
|
||||
@action
|
||||
updateInputValidation(option, event) {
|
||||
const element = event.target;
|
||||
updateInputValidation(option: FormDataTimeOption, event: InputEvent) {
|
||||
const element = event.target as HTMLInputElement;
|
||||
|
||||
if (element.checkValidity() && option.isPartiallyFilled) {
|
||||
option.isPartiallyFilled = false;
|
||||
|
@ -212,23 +233,25 @@ export default class CreateOptionsDatetime extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
handleTransition(transition) {
|
||||
handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.options-datetime') {
|
||||
this.args.updateOptions(
|
||||
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
|
||||
new Map(
|
||||
Array.from(this.formData.datetimes.entries())
|
||||
.map(([key, timeOptions]) => [
|
||||
.map(([key, timeOptions]): [string, Set<string>] => {
|
||||
return [
|
||||
key,
|
||||
new Set(
|
||||
Array.from(timeOptions)
|
||||
.map(({ time }) => time)
|
||||
.map(({ time }: FormDataTimeOption) => time)
|
||||
// There might be FormDataTime objects without a time, which
|
||||
// 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
|
||||
.filter(([, times]) => times.size > 0),
|
||||
),
|
||||
|
@ -237,8 +260,8 @@ export default class CreateOptionsDatetime extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
constructor(owner: unknown, args: CreateOptoinsDatetimeSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
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 { 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;
|
||||
formData;
|
||||
|
||||
|
@ -33,7 +35,7 @@ class FormDataOption {
|
|||
return this.valueValidation === null;
|
||||
}
|
||||
|
||||
constructor(formData, value) {
|
||||
constructor(formData: FormData, value: string) {
|
||||
this.formData = formData;
|
||||
this.value = value;
|
||||
}
|
||||
|
@ -59,25 +61,28 @@ class FormData {
|
|||
}
|
||||
|
||||
@action
|
||||
updateOptions(values) {
|
||||
updateOptions(values: string[]) {
|
||||
this.options = new TrackedArray(
|
||||
values.map((value) => new FormDataOption(this, value)),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
addOption(value, afterPosition = this.options.length - 1) {
|
||||
addOption(value: string, afterPosition = this.options.length - 1) {
|
||||
const option = new FormDataOption(this, value);
|
||||
|
||||
this.options.splice(afterPosition + 1, 0, option);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteOption(option) {
|
||||
deleteOption(option: FormDataOption) {
|
||||
this.options.splice(this.options.indexOf(option), 1);
|
||||
}
|
||||
|
||||
constructor({ options }, { defaultOptionCount }) {
|
||||
constructor(
|
||||
{ options }: { options: Set<string> },
|
||||
{ defaultOptionCount }: { defaultOptionCount: number },
|
||||
) {
|
||||
const normalizedOptions =
|
||||
options.size === 0 && defaultOptionCount > 0
|
||||
? ['', '']
|
||||
|
@ -89,8 +94,18 @@ class FormData {
|
|||
}
|
||||
}
|
||||
|
||||
export default class CreateOptionsComponent extends Component {
|
||||
@service router;
|
||||
export interface CreateOptionsSignature {
|
||||
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(
|
||||
{ options: this.args.options },
|
||||
|
@ -108,15 +123,15 @@ export default class CreateOptionsComponent extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
handleTransition(transition) {
|
||||
handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.options') {
|
||||
this.args.updateOptions(this.formData.options);
|
||||
this.router.off('routeWillChange', this.handleTransition);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
constructor(owner: unknown, args: CreateOptionsSignature['Args']) {
|
||||
super(owner, args);
|
||||
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
||||
}
|
|
@ -2,25 +2,26 @@ import Component from '@glimmer/component';
|
|||
import { inject as service } from '@ember/service';
|
||||
import localesMeta from 'croodle/locales/meta';
|
||||
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 {
|
||||
@service intl;
|
||||
@service powerCalendar;
|
||||
@service declare intl: IntlService;
|
||||
@service declare powerCalendar: PowerCalendarService;
|
||||
|
||||
get currentLocale() {
|
||||
return this.intl.primaryLocale;
|
||||
}
|
||||
|
||||
get locales() {
|
||||
return localesMeta;
|
||||
}
|
||||
locales = localesMeta;
|
||||
|
||||
@action
|
||||
handleChange(event) {
|
||||
const locale = event.target.value;
|
||||
handleChange(event: Event) {
|
||||
const selectElement = event.target as HTMLSelectElement;
|
||||
const locale = selectElement.value as keyof typeof this.locales;
|
||||
|
||||
this.intl.locale = locale.includes('-')
|
||||
? [locale, locale.split('-')[0]]
|
||||
? [locale, locale.split('-')[0] as string]
|
||||
: [locale];
|
||||
this.powerCalendar.locale = locale;
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
import Component from '@glimmer/component';
|
||||
import type Poll from 'croodle/models/poll';
|
||||
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() {
|
||||
const { poll } = this.args;
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
import Component from '@glimmer/component';
|
||||
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 {
|
||||
@service intl;
|
||||
export interface PollEvaluationSummarySignature {
|
||||
Args: {
|
||||
poll: Poll;
|
||||
};
|
||||
}
|
||||
|
||||
export default class PollEvaluationSummary extends Component<PollEvaluationSummarySignature> {
|
||||
@service declare intl: IntlService;
|
||||
|
||||
get bestOptions() {
|
||||
const { poll } = this.args;
|
||||
|
@ -18,34 +29,41 @@ export default class PollEvaluationSummary extends Component {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
let answers = poll.answers.reduce((answers, answer) => {
|
||||
const answers = poll.answers.reduce(
|
||||
(answers: Record<string, number>, answer: Answer) => {
|
||||
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 {
|
||||
answers: { ...answers },
|
||||
option,
|
||||
score: 0,
|
||||
};
|
||||
});
|
||||
let bestOptions = [];
|
||||
const bestOptions = [];
|
||||
|
||||
users.forEach((user) => {
|
||||
users.forEach((user: User) => {
|
||||
user.selections.forEach(({ type }, i) => {
|
||||
evaluation[i].answers[type]++;
|
||||
evaluation[i]!.answers[type]++;
|
||||
|
||||
switch (type) {
|
||||
case 'yes':
|
||||
evaluation[i].score += 2;
|
||||
evaluation[i]!.score += 2;
|
||||
break;
|
||||
|
||||
case 'maybe':
|
||||
evaluation[i].score += 1;
|
||||
evaluation[i]!.score += 1;
|
||||
break;
|
||||
|
||||
case 'no':
|
||||
evaluation[i].score -= 2;
|
||||
evaluation[i]!.score -= 2;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -53,9 +71,9 @@ export default class PollEvaluationSummary extends Component {
|
|||
|
||||
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++) {
|
||||
if (bestScore === evaluation[i].score) {
|
||||
if (bestScore === evaluation[i]!.score) {
|
||||
bestOptions.push(evaluation[i]);
|
||||
} else {
|
||||
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 { inject as service } from '@ember/service';
|
||||
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
|
||||
|
||||
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 Controller from '@ember/controller';
|
||||
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 {
|
||||
@service router;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateRouteModel;
|
||||
|
||||
visitedSteps = new TrackedSet();
|
||||
|
||||
get canEnterMetaStep() {
|
||||
return this.visitedSteps.has('meta') && this.model.pollType;
|
||||
}
|
||||
|
||||
get canEnterOptionsStep() {
|
||||
let { title } = this.model;
|
||||
const { title } = this.model;
|
||||
return (
|
||||
this.visitedSteps.has('options') &&
|
||||
typeof title === 'string' &&
|
||||
|
@ -40,18 +46,18 @@ export default class CreateController extends Controller {
|
|||
|
||||
@action
|
||||
updateVisitedSteps() {
|
||||
let { currentRouteName } = this.router;
|
||||
const { currentRouteName } = this.router;
|
||||
|
||||
// currentRouteName might not be defined in some edge cases
|
||||
if (!currentRouteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let step = currentRouteName.split('.').pop();
|
||||
const step = currentRouteName.split('.').pop();
|
||||
this.visitedSteps.add(step);
|
||||
}
|
||||
|
||||
@action transitionTo(route) {
|
||||
@action transitionTo(route: string) {
|
||||
this.router.transitionTo(route);
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
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 {
|
||||
@service router;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateRouteIndexModel;
|
||||
|
||||
@action
|
||||
submit() {
|
||||
|
@ -11,7 +16,7 @@ export default class CreateIndex extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
handleTransition(transition) {
|
||||
handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.index') {
|
||||
const { poll, formData } = this.model;
|
||||
|
||||
|
@ -20,6 +25,7 @@ export default class CreateIndex extends Controller {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments);
|
||||
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
|
@ -1,9 +1,14 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
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 {
|
||||
@service router;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateMetaRouteModel;
|
||||
|
||||
@action
|
||||
previousPage() {
|
||||
|
@ -16,7 +21,7 @@ export default class CreateMetaController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
handleTransition(transition) {
|
||||
handleTransition(transition: Transition) {
|
||||
if (transition.from?.name === 'create.meta') {
|
||||
const { poll, formData } = this.model;
|
||||
|
||||
|
@ -26,6 +31,7 @@ export default class CreateMetaController extends Controller {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments);
|
||||
|
||||
this.router.on('routeWillChange', this.handleTransition);
|
|
@ -1,9 +1,13 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
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 {
|
||||
@service router;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateOptionsDatetimeRouteModel;
|
||||
|
||||
@action
|
||||
nextPage() {
|
||||
|
@ -16,7 +20,7 @@ export default class CreateOptionsDatetimeController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
updateOptions(datetimes) {
|
||||
updateOptions(datetimes: Map<string, Set<string>>) {
|
||||
this.model.timesForDateOptions = new Map(datetimes.entries());
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
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 {
|
||||
@service router;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateOptionsRouteModel;
|
||||
|
||||
@action
|
||||
nextPage() {
|
||||
|
@ -23,7 +27,7 @@ export default class CreateOptionsController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
updateOptions(newOptions) {
|
||||
updateOptions(newOptions: { value: string }[]) {
|
||||
const { pollType } = this.model;
|
||||
const options = newOptions.map(({ value }) => value);
|
||||
|
|
@ -5,11 +5,17 @@ import { action } from '@ember/object';
|
|||
import { DateTime, Duration } from 'luxon';
|
||||
import Poll from '../../models/poll';
|
||||
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 {
|
||||
@service flashMessages;
|
||||
@service intl;
|
||||
@service router;
|
||||
@service declare flashMessages: FlashMessagesService;
|
||||
@service declare intl: IntlService;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: CreateSettingsRouteModel;
|
||||
|
||||
get anonymousUser() {
|
||||
return this.model.anonymousUser;
|
||||
|
@ -39,7 +45,7 @@ export default class CreateSettings extends Controller {
|
|||
}
|
||||
set expirationDuration(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
|
||||
previousPage() {
|
||||
let { pollType } = this.model;
|
||||
const { pollType } = this.model;
|
||||
|
||||
if (pollType === 'FindADate') {
|
||||
this.router.transitionTo('create.options-datetime');
|
||||
|
@ -104,22 +110,22 @@ export default class CreateSettings extends Controller {
|
|||
} = model;
|
||||
|
||||
// calculate options
|
||||
let options = [];
|
||||
const options: string[] = [];
|
||||
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(':');
|
||||
for (const time of timesForDateOptions.get(date)!) {
|
||||
const [hour, minute] = time.split(':') as [string, string];
|
||||
options.push(
|
||||
DateTime.fromISO(date)
|
||||
.set({
|
||||
hour,
|
||||
minute,
|
||||
hour: parseInt(hour),
|
||||
minute: parseInt(minute),
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
})
|
||||
.toISO(),
|
||||
.toISO() as string,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -139,7 +145,6 @@ export default class CreateSettings extends Controller {
|
|||
{
|
||||
anonymousUser,
|
||||
answerType,
|
||||
creationDate: new Date().toISOString(),
|
||||
description,
|
||||
expirationDate,
|
||||
forceAnswer,
|
|
@ -4,11 +4,17 @@ import { isPresent, isEmpty } from '@ember/utils';
|
|||
import { action } from '@ember/object';
|
||||
import { DateTime } from 'luxon';
|
||||
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 {
|
||||
@service flashMessages;
|
||||
@service intl;
|
||||
@service router;
|
||||
@service declare flashMessages: FlashMessagesService;
|
||||
@service declare intl: IntlService;
|
||||
@service declare router: RouterService;
|
||||
|
||||
declare model: PollRouteModel;
|
||||
|
||||
queryParams = ['encryptionKey'];
|
||||
encryptionKey = '';
|
||||
|
@ -61,8 +67,8 @@ export default class PollController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
linkAction(type) {
|
||||
let flashMessages = this.flashMessages;
|
||||
linkAction(type: 'copied' | 'selected') {
|
||||
const flashMessages = this.flashMessages;
|
||||
switch (type) {
|
||||
case 'copied':
|
||||
flashMessages.success(`poll.link.copied`);
|
|
@ -1,11 +1,20 @@
|
|||
import Helper from '@ember/component/helper';
|
||||
import { DateTime } from 'luxon';
|
||||
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 {
|
||||
@service intl;
|
||||
@service declare intl: IntlService;
|
||||
|
||||
compute([date]) {
|
||||
compute([date]: Positional) {
|
||||
if (date instanceof Date) {
|
||||
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 { assert } from '@ember/debug';
|
||||
|
||||
function elementIsNotVisible(element) {
|
||||
let elementPosition = element.getBoundingClientRect();
|
||||
let windowHeight = window.innerHeight;
|
||||
function elementIsNotVisible(element: Element) {
|
||||
const elementPosition = element.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// an element is not visible if
|
||||
return (
|
||||
false ||
|
||||
// it's above the current view port
|
||||
// check if the element is within current view port
|
||||
if (
|
||||
// above current view port
|
||||
elementPosition.top <= 0 ||
|
||||
// it's below the current view port
|
||||
elementPosition.bottom >= windowHeight ||
|
||||
// it's in current view port but hidden by fixed navigation
|
||||
(getComputedStyle(document.querySelector('.cr-steps-bottom-nav'))
|
||||
.position === 'fixed' &&
|
||||
elementPosition.bottom >=
|
||||
windowHeight -
|
||||
document.querySelector('.cr-steps-bottom-nav').offsetHeight)
|
||||
// below current view port
|
||||
elementPosition.bottom >= windowHeight
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if element is within current view port button hidden behind
|
||||
// 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
|
||||
// delaying to next runloop therefore
|
||||
next(function () {
|
||||
let invalidInput = document.querySelector(
|
||||
const invalidInput = document.querySelector(
|
||||
'.form-control.is-invalid, .custom-control-input.is-invalid',
|
||||
);
|
||||
) as HTMLInputElement;
|
||||
assert(
|
||||
'Atleast one form control must be marked as invalid if form submission was rejected as invalid',
|
||||
invalidInput,
|
||||
|
@ -46,7 +56,7 @@ export function scrollFirstInvalidElementIntoViewPort() {
|
|||
// 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
|
||||
// inputs and the `for` of the input group `<label>` (which should be a `<legend>`).
|
||||
let scrollTarget =
|
||||
const scrollTarget =
|
||||
document.querySelector(
|
||||
`label[for="${invalidInput.id.substr(
|
||||
0,
|
|
@ -7,7 +7,7 @@ export default class Option {
|
|||
// 1) ISO 8601 date string: `YYYY-MM-DD`
|
||||
// 2) ISO 8601 datetime string: `YYYY-MM-DDTHH:mm:ss.0000+01:00`
|
||||
// 3) Free text if poll type is MakeAPoll
|
||||
title;
|
||||
title: string;
|
||||
|
||||
get datetime() {
|
||||
const { title } = this;
|
||||
|
@ -16,16 +16,18 @@ export default class Option {
|
|||
return null;
|
||||
}
|
||||
|
||||
return DateTime.fromISO(title);
|
||||
const datetime = DateTime.fromISO(title);
|
||||
|
||||
return datetime.isValid ? datetime : null;
|
||||
}
|
||||
|
||||
get isDate() {
|
||||
const { datetime } = this;
|
||||
return datetime !== null && datetime.isValid;
|
||||
return datetime !== null;
|
||||
}
|
||||
|
||||
get day() {
|
||||
if (!this.isDate) {
|
||||
if (!this.datetime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,10 @@ export default class Option {
|
|||
}
|
||||
|
||||
get jsDate() {
|
||||
if (!this.datetime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.datetime.toJSDate();
|
||||
}
|
||||
|
||||
|
@ -41,14 +47,14 @@ export default class Option {
|
|||
}
|
||||
|
||||
get time() {
|
||||
if (!this.isDate || !this.hasTime) {
|
||||
if (!this.datetime || !this.hasTime) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -6,48 +6,68 @@ import { decrypt, encrypt } from '../utils/encryption';
|
|||
import answersForAnswerType from '../utils/answers-for-answer-type';
|
||||
import fetch from 'fetch';
|
||||
import config from 'croodle/config/environment';
|
||||
import type { SelectionInput } from './selection';
|
||||
|
||||
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 {
|
||||
// Is participation without user name possibile?
|
||||
anonymousUser;
|
||||
anonymousUser: boolean;
|
||||
|
||||
// YesNo, YesNoMaybe or Freetext
|
||||
answerType;
|
||||
answerType: AnswerType;
|
||||
|
||||
// ISO-8601 combined date and time string in UTC
|
||||
creationDate;
|
||||
creationDate: string;
|
||||
|
||||
// poll's description
|
||||
description;
|
||||
description: string;
|
||||
|
||||
// ISO 8601 date + time string in UTC
|
||||
expirationDate;
|
||||
expirationDate: string;
|
||||
|
||||
// Must all options been answered?
|
||||
forceAnswer;
|
||||
forceAnswer: boolean;
|
||||
|
||||
// ID of the poll
|
||||
id;
|
||||
id: string;
|
||||
|
||||
// array of poll's options
|
||||
options;
|
||||
options: Option[];
|
||||
|
||||
// FindADate or MakeAPoll
|
||||
pollType;
|
||||
pollType: 'FindADate' | 'MakeAPoll';
|
||||
|
||||
// timezone poll got created in (like "Europe/Berlin")
|
||||
timezone;
|
||||
timezone: string | null;
|
||||
|
||||
// polls title
|
||||
title;
|
||||
title: string;
|
||||
|
||||
// participants of the poll
|
||||
users;
|
||||
users: TrackedArray<User>;
|
||||
|
||||
// Croodle version poll got created with
|
||||
version;
|
||||
version: string;
|
||||
|
||||
get answers() {
|
||||
const { answerType } = this;
|
||||
|
@ -90,7 +110,7 @@ export default class Poll {
|
|||
title,
|
||||
users,
|
||||
version,
|
||||
}) {
|
||||
}: PollInput) {
|
||||
this.anonymousUser = anonymousUser;
|
||||
this.answerType = answerType;
|
||||
this.creationDate = creationDate;
|
||||
|
@ -106,7 +126,7 @@ export default class Poll {
|
|||
this.version = version;
|
||||
}
|
||||
|
||||
static async load(id, passphrase) {
|
||||
static async load(id: string, passphrase: string) {
|
||||
const url = apiUrl(`polls/${id}`);
|
||||
|
||||
// TODO: Handle network connectivity error
|
||||
|
@ -125,26 +145,51 @@ export default class Poll {
|
|||
}
|
||||
|
||||
// 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({
|
||||
anonymousUser: decrypt(payload.poll.anonymousUser, passphrase),
|
||||
answerType: decrypt(payload.poll.answerType, passphrase),
|
||||
creationDate: decrypt(payload.poll.creationDate, passphrase),
|
||||
description: decrypt(payload.poll.description, passphrase),
|
||||
expirationDate: decrypt(payload.poll.expirationDate, passphrase),
|
||||
forceAnswer: decrypt(payload.poll.forceAnswer, passphrase),
|
||||
anonymousUser: decrypt(payload.poll.anonymousUser, passphrase) as boolean,
|
||||
answerType: decrypt(payload.poll.answerType, passphrase) as AnswerType,
|
||||
creationDate: decrypt(payload.poll.creationDate, passphrase) as string,
|
||||
description: decrypt(payload.poll.description, passphrase) as string,
|
||||
expirationDate: decrypt(
|
||||
payload.poll.expirationDate,
|
||||
passphrase,
|
||||
) as string,
|
||||
forceAnswer: decrypt(payload.poll.forceAnswer, passphrase) as boolean,
|
||||
id: payload.poll.id,
|
||||
options: decrypt(payload.poll.options, passphrase),
|
||||
pollType: decrypt(payload.poll.pollType, passphrase),
|
||||
timezone: decrypt(payload.poll.timezone, passphrase),
|
||||
title: decrypt(payload.poll.title, passphrase),
|
||||
options: decrypt(payload.poll.options, passphrase) as OptionInput,
|
||||
pollType: decrypt(payload.poll.pollType, passphrase) as PollType,
|
||||
timezone: decrypt(payload.poll.timezone, passphrase) as string,
|
||||
title: decrypt(payload.poll.title, passphrase) as string,
|
||||
users: payload.poll.users.map((user) => {
|
||||
return new User({
|
||||
creationDate: decrypt(user.creationDate, passphrase),
|
||||
creationDate: decrypt(user.creationDate, passphrase) as string,
|
||||
id: user.id,
|
||||
name: decrypt(user.name, passphrase),
|
||||
selections: decrypt(user.selections, passphrase),
|
||||
name: decrypt(user.name, passphrase) as string,
|
||||
selections: decrypt(user.selections, passphrase) as SelectionInput[],
|
||||
version: user.version,
|
||||
});
|
||||
}),
|
||||
|
@ -162,15 +207,24 @@ export default class Poll {
|
|||
options,
|
||||
pollType,
|
||||
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 version = config.APP.version;
|
||||
const timezone =
|
||||
pollType === 'FindADate' &&
|
||||
options.some(({ title }) => {
|
||||
return title >= 'YYYY-MM-DDTHH:mm'.length;
|
||||
return title.length >= 'YYYY-MM-DDTHH:mm'.length;
|
||||
})
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: 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 { encrypt } from '../utils/encryption';
|
||||
import { apiUrl } from '../utils/api';
|
||||
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 {
|
||||
// ISO 8601 date + time string
|
||||
creationDate;
|
||||
creationDate: string;
|
||||
|
||||
id;
|
||||
id: string;
|
||||
|
||||
// user name
|
||||
name;
|
||||
name: string;
|
||||
|
||||
// array of users selections
|
||||
// must be in same order as options property of poll
|
||||
selections;
|
||||
selections: Selection[];
|
||||
|
||||
// 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.id = id;
|
||||
this.name = name;
|
||||
|
@ -28,7 +37,14 @@ export default class User {
|
|||
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 version = config.APP.version;
|
||||
|
|
@ -1,9 +1,20 @@
|
|||
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;
|
||||
|
||||
modify(element, positional, { enabled = true }) {
|
||||
modify(element: HTMLInputElement, _: [], { enabled = true }: Named) {
|
||||
// element should be only autofocused on initial render
|
||||
// not when `enabled` option is invalidated
|
||||
if (this.isInstalled) {
|
|
@ -17,5 +17,4 @@ Router.map(function () {
|
|||
this.route('options-datetime');
|
||||
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 { tracked } from '@glimmer/tracking';
|
||||
import IntlMessage from '../../utils/intl-message';
|
||||
import type { CreateRouteModel } from '../create';
|
||||
|
||||
class FormData {
|
||||
@tracked title;
|
||||
@tracked description;
|
||||
@tracked title: string;
|
||||
@tracked description: string;
|
||||
|
||||
get titleValidation() {
|
||||
const { title } = this;
|
||||
|
@ -22,19 +23,24 @@ class FormData {
|
|||
return null;
|
||||
}
|
||||
|
||||
constructor({ title, description }) {
|
||||
constructor({ title, description }: { title: string; description: string }) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetaRoute extends Route {
|
||||
export default class CreateMetaRoute extends Route {
|
||||
model() {
|
||||
const { title, description } = this.modelFor('create');
|
||||
const { title, description } = this.modelFor('create') as CreateRouteModel;
|
||||
|
||||
return {
|
||||
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 { tracked } from '@glimmer/tracking';
|
||||
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 {
|
||||
@tracked value = null;
|
||||
|
@ -21,13 +23,13 @@ class FormDataSelections {
|
|||
return this.valueValidation === null;
|
||||
}
|
||||
|
||||
constructor(valueIsRequired) {
|
||||
constructor(valueIsRequired: boolean) {
|
||||
this.valueIsRequired = valueIsRequired;
|
||||
}
|
||||
}
|
||||
|
||||
class FormData {
|
||||
@tracked name = null;
|
||||
@tracked name: null | string = null;
|
||||
nameIsRequired;
|
||||
namesTaken;
|
||||
selections;
|
||||
|
@ -39,8 +41,7 @@ class FormData {
|
|||
return new IntlMessage('poll.error.name.valueMissing');
|
||||
}
|
||||
|
||||
// TODO: Validate that name is unique for this poll
|
||||
if (namesTaken.includes(name)) {
|
||||
if (name && namesTaken.includes(name)) {
|
||||
return new IntlMessage('poll.error.name.duplicate');
|
||||
}
|
||||
|
||||
|
@ -57,7 +58,18 @@ class FormData {
|
|||
return null;
|
||||
}
|
||||
|
||||
constructor(options, { nameIsRequired, namesTaken, selectionIsRequired }) {
|
||||
constructor(
|
||||
options: Option[],
|
||||
{
|
||||
nameIsRequired,
|
||||
namesTaken,
|
||||
selectionIsRequired,
|
||||
}: {
|
||||
nameIsRequired: boolean;
|
||||
namesTaken: string[];
|
||||
selectionIsRequired: boolean;
|
||||
},
|
||||
) {
|
||||
this.nameIsRequired = nameIsRequired;
|
||||
this.namesTaken = namesTaken;
|
||||
this.selections = new TrackedArray(
|
||||
|
@ -68,7 +80,7 @@ class FormData {
|
|||
|
||||
export default class ParticipationRoute extends Route {
|
||||
model() {
|
||||
const poll = this.modelFor('poll');
|
||||
const poll = this.modelFor('poll') as Poll;
|
||||
const { anonymousUser, forceAnswer, options, users } = poll;
|
||||
const formData = new FormData(options, {
|
||||
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';
|
||||
|
||||
export default function (answerType) {
|
||||
export type Answer = {
|
||||
labelTranslation: string;
|
||||
icon: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export default function (
|
||||
answerType: 'YesNo' | 'YesNoMaybe' | 'FreeText',
|
||||
): Answer[] {
|
||||
switch (answerType) {
|
||||
case 'YesNo':
|
||||
return [
|
|
@ -10,7 +10,7 @@ const baseUrl = window.location.pathname
|
|||
// add api/index.php
|
||||
.concat('/api/index.php');
|
||||
|
||||
function apiUrl(path) {
|
||||
function apiUrl(path: string) {
|
||||
return `${baseUrl}/${path}`;
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
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));
|
||||
}
|
||||
|
||||
function encrypt(plainValue, passphrase) {
|
||||
function encrypt(plainValue: unknown, passphrase: string) {
|
||||
return sjclEncrypt(passphrase, JSON.stringify(plainValue));
|
||||
}
|
||||
|
||||
function generatePassphrase() {
|
||||
function generatePassphrase(): string {
|
||||
const length = 40;
|
||||
const possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
@ -18,7 +18,9 @@ function generatePassphrase() {
|
|||
|
||||
let passphrase = '';
|
||||
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;
|
|
@ -2,7 +2,7 @@ export default class IntlMessage {
|
|||
key;
|
||||
options;
|
||||
|
||||
constructor(key, options) {
|
||||
constructor(key: string, options?: Record<string, string>) {
|
||||
this.key = key;
|
||||
this.options = options;
|
||||
}
|
|
@ -11,8 +11,8 @@
|
|||
"codemodsSource": "ember-app-codemods-manifest@1",
|
||||
"isBaseBlueprint": true,
|
||||
"options": [
|
||||
"--yarn",
|
||||
"--no-welcome"
|
||||
"--ci-provider=github",
|
||||
"--typescript"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"application-template-wrapper": false,
|
||||
"default-async-observers": true,
|
||||
"jquery-integration": false,
|
||||
"template-only-glimmer-components": true
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ module.exports = function (defaults) {
|
|||
],
|
||||
},
|
||||
'ember-cli-babel': {
|
||||
includePolyfill: true,
|
||||
enableTypeScriptTransform: true,
|
||||
},
|
||||
'ember-composable-helpers': {
|
||||
only: ['array', 'object-at', 'pick'],
|
||||
|
|
9107
package-lock.json
generated
9107
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:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint . --cache",
|
||||
"release": "release-it",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"release": "release-it",
|
||||
"start": "ember serve",
|
||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||
"test:ember": "ember test",
|
||||
|
@ -28,14 +29,21 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@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/string": "^3.1.1",
|
||||
"@ember/test-helpers": "^3.2.0",
|
||||
"@glimmer/component": "^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",
|
||||
"@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",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.2.1",
|
||||
|
@ -98,6 +106,7 @@
|
|||
"stylelint-config-standard-scss": "^11.0.0",
|
||||
"stylelint-prettier": "^4.0.2",
|
||||
"tracked-built-ins": "^3.3.0",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -2,13 +2,14 @@ import {
|
|||
setupApplicationTest as upstreamSetupApplicationTest,
|
||||
setupRenderingTest as upstreamSetupRenderingTest,
|
||||
setupTest as upstreamSetupTest,
|
||||
type SetupTestOptions,
|
||||
} from 'ember-qunit';
|
||||
|
||||
// This file exists to provide wrappers around ember-qunit's
|
||||
// test setup functions. This way, you can easily extend the setup that is
|
||||
// needed per test type.
|
||||
|
||||
function setupApplicationTest(hooks, options) {
|
||||
function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||
upstreamSetupApplicationTest(hooks, options);
|
||||
|
||||
// Additional setup for application tests can be done here.
|
||||
|
@ -27,13 +28,13 @@ function setupApplicationTest(hooks, options) {
|
|||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
function setupRenderingTest(hooks, options) {
|
||||
function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||
upstreamSetupRenderingTest(hooks, options);
|
||||
|
||||
// Additional setup for rendering tests can be done here.
|
||||
}
|
||||
|
||||
function setupTest(hooks, options) {
|
||||
function setupTest(hooks: NestedHooks, options?: SetupTestOptions) {
|
||||
upstreamSetupTest(hooks, options);
|
||||
|
||||
// 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