From f0cff27e99d03f1e43bb70723d1e2b91ef4986da Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Sun, 29 Oct 2023 19:16:33 +0100 Subject: [PATCH] Convert to TypeScript (#713) * setup typescript * covert to TypeScript --- .ember-cli | 2 +- .eslintrc.js | 23 +- app/{app.js => app.ts} | 2 + app/components/create-options-dates.js | 38 - app/components/create-options-dates.ts | 48 + ...datetime.js => create-options-datetime.ts} | 99 +- .../{create-options.js => create-options.ts} | 39 +- ...{language-select.js => language-select.ts} | 17 +- ... => poll-evaluation-participants-table.ts} | 9 +- ...-summary.js => poll-evaluation-summary.ts} | 48 +- app/config/environment.d.ts | 16 + .../{application.js => application.ts} | 3 +- app/controllers/{create.js => create.ts} | 16 +- app/controllers/create/{index.js => index.ts} | 10 +- app/controllers/create/{meta.js => meta.ts} | 10 +- ...ptions-datetime.js => options-datetime.ts} | 8 +- .../create/{options.js => options.ts} | 10 +- .../create/{settings.js => settings.ts} | 29 +- app/controllers/{error.js => error.ts} | 0 .../{poll-error.js => poll-error.ts} | 0 app/controllers/{poll.js => poll.ts} | 16 +- ...te-relative.js => format-date-relative.ts} | 13 +- app/helpers/mark-as-safe-html.ts | 16 + ...l-first-invalid-element-into-view-port.ts} | 46 +- app/locales/{meta.js => meta.ts} | 0 app/models/{option.js => option.ts} | 20 +- app/models/{poll.js => poll.ts} | 116 +- app/models/selection.js | 13 - app/models/selection.ts | 20 + app/models/{user.js => user.ts} | 32 +- app/modifiers/{autofocus.js => autofocus.ts} | 15 +- app/{router.js => router.ts} | 1 - app/routes/create.js | 43 - app/routes/create.ts | 52 + app/routes/create/index.js | 21 - app/routes/create/index.ts | 28 + app/routes/create/{meta.js => meta.ts} | 18 +- app/routes/create/options-datetime.js | 7 - app/routes/create/options-datetime.ts | 13 + app/routes/create/options.js | 7 - app/routes/create/options.ts | 13 + app/routes/create/settings.js | 7 - app/routes/create/settings.ts | 13 + app/routes/poll.js | 33 - app/routes/poll.ts | 36 + .../poll/{evaluation.js => evaluation.ts} | 0 .../{participation.js => participation.ts} | 26 +- app/templates/404.hbs | 6 - ...wer-type.js => answers-for-answer-type.ts} | 10 +- app/utils/{api.js => api.ts} | 2 +- app/utils/{encryption.js => encryption.ts} | 10 +- .../{intl-message.js => intl-message.ts} | 2 +- config/ember-cli-update.json | 4 +- config/optional-features.json | 1 + ember-cli-build.js | 2 +- package-lock.json | 9109 +++-------------- package.json | 15 +- tests/helpers/{index.js => index.ts} | 7 +- tests/{test-helper.js => test-helper.ts} | 0 tsconfig.json | 17 + types/ember-cli-flash/flash/object.d.ts | 19 + types/ember-cli-flash/services/intl.d.ts | 46 + .../services/power-calendar.d.ts | 7 + types/global.d.ts | 1 + 64 files changed, 2405 insertions(+), 7905 deletions(-) rename app/{app.js => app.ts} (93%) delete mode 100644 app/components/create-options-dates.js create mode 100644 app/components/create-options-dates.ts rename app/components/{create-options-datetime.js => create-options-datetime.ts} (68%) rename app/components/{create-options.js => create-options.ts} (71%) rename app/components/{language-select.js => language-select.ts} (52%) rename app/components/{poll-evaluation-participants-table.js => poll-evaluation-participants-table.ts} (79%) rename app/components/{poll-evaluation-summary.js => poll-evaluation-summary.ts} (53%) create mode 100644 app/config/environment.d.ts rename app/controllers/{application.js => application.ts} (54%) rename app/controllers/{create.js => create.ts} (78%) rename app/controllers/create/{index.js => index.ts} (58%) rename app/controllers/create/{meta.js => meta.ts} (64%) rename app/controllers/create/{options-datetime.js => options-datetime.ts} (59%) rename app/controllers/create/{options.js => options.ts} (71%) rename app/controllers/create/{settings.js => settings.ts} (80%) rename app/controllers/{error.js => error.ts} (100%) rename app/controllers/{poll-error.js => poll-error.ts} (100%) rename app/controllers/{poll.js => poll.ts} (77%) rename app/helpers/{format-date-relative.js => format-date-relative.ts} (59%) create mode 100644 app/helpers/mark-as-safe-html.ts rename app/helpers/{scroll-first-invalid-element-into-view-port.js => scroll-first-invalid-element-into-view-port.ts} (68%) rename app/locales/{meta.js => meta.ts} (100%) rename app/models/{option.js => option.ts} (68%) rename app/models/{poll.js => poll.ts} (66%) delete mode 100644 app/models/selection.js create mode 100644 app/models/selection.ts rename app/models/{user.js => user.ts} (71%) rename app/modifiers/{autofocus.js => autofocus.ts} (51%) rename app/{router.js => router.ts} (96%) delete mode 100644 app/routes/create.js create mode 100644 app/routes/create.ts delete mode 100644 app/routes/create/index.js create mode 100644 app/routes/create/index.ts rename app/routes/create/{meta.js => meta.ts} (54%) delete mode 100644 app/routes/create/options-datetime.js create mode 100644 app/routes/create/options-datetime.ts delete mode 100644 app/routes/create/options.js create mode 100644 app/routes/create/options.ts delete mode 100644 app/routes/create/settings.js create mode 100644 app/routes/create/settings.ts delete mode 100644 app/routes/poll.js create mode 100644 app/routes/poll.ts rename app/routes/poll/{evaluation.js => evaluation.ts} (100%) rename app/routes/poll/{participation.js => participation.ts} (75%) delete mode 100644 app/templates/404.hbs rename app/utils/{answers-for-answer-type.js => answers-for-answer-type.ts} (84%) rename app/utils/{api.js => api.ts} (94%) rename app/utils/{encryption.js => encryption.ts} (66%) rename app/utils/{intl-message.js => intl-message.ts} (63%) rename tests/helpers/{index.js => index.ts} (81%) rename tests/{test-helper.js => test-helper.ts} (100%) create mode 100644 tsconfig.json create mode 100644 types/ember-cli-flash/flash/object.d.ts create mode 100644 types/ember-cli-flash/services/intl.d.ts create mode 100644 types/ember-power-calendar/services/power-calendar.d.ts create mode 100644 types/global.d.ts diff --git a/.ember-cli b/.ember-cli index 8c1812c..978eea2 100644 --- a/.ember-cli +++ b/.ember-cli @@ -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 } diff --git a/.eslintrc.js b/.eslintrc.js index de95346..88f8304 100644 --- a/.eslintrc.js +++ b/.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, diff --git a/app/app.js b/app/app.ts similarity index 93% rename from app/app.js rename to app/app.ts index 52d3d22..f0e3dae 100644 --- a/app/app.js +++ b/app/app.ts @@ -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; diff --git a/app/components/create-options-dates.js b/app/components/create-options-dates.js deleted file mode 100644 index 6d42a58..0000000 --- a/app/components/create-options-dates.js +++ /dev/null @@ -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'); - } -} diff --git a/app/components/create-options-dates.ts b/app/components/create-options-dates.ts new file mode 100644 index 0000000..a087a47 --- /dev/null +++ b/app/components/create-options-dates.ts @@ -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; + updateOptions: (options: string[]) => void; + }; +} + +export default class CreateOptionsDates extends Component { + @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), + ); + } +} diff --git a/app/components/create-options-datetime.js b/app/components/create-options-datetime.ts similarity index 68% rename from app/components/create-options-datetime.js rename to app/components/create-options-datetime.ts index 330c874..2d6e7dd 100644 --- a/app/components/create-options-datetime.js +++ b/app/components/create-options-datetime.ts @@ -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>; 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; + times: Map>; + }) { 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) : [null]; datetimes.set( date, @@ -164,12 +175,22 @@ class FormData { } } -export default class CreateOptionsDatetime extends Component { - @service router; +export interface CreateOptoinsDatetimeSignature { + Args: { + dates: TrackedSet; + onNextPage: () => void; + onPrevPage: () => void; + times: Map>; + updateOptions: (datetimes: Map>) => void; + }; +} + +export default class CreateOptionsDatetime extends Component { + @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( - // 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( + // 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()) - .map(([key, timeOptions]) => [ - key, - new Set( - Array.from(timeOptions) - .map(({ time }) => time) - // There might be FormDataTime objects without a time, which - // we need to filter out - .filter((time) => time !== null), - ), - ]) + .map(([key, timeOptions]): [string, Set] => { + return [ + key, + new Set( + Array.from(timeOptions) + .map(({ time }: FormDataTimeOption) => time) + // There might be FormDataTime objects without a time, which + // we need to filter out + .filter((time) => time !== null), + ) as Set, + ]; + }) // 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); } diff --git a/app/components/create-options.js b/app/components/create-options.ts similarity index 71% rename from app/components/create-options.js rename to app/components/create-options.ts index 897ba34..645a650 100644 --- a/app/components/create-options.js +++ b/app/components/create-options.ts @@ -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 }, + { 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; + onNextPage: () => void; + onPrevPage: () => void; + updateOptions: (options: { value: string }[]) => void; + }; +} + +export default class CreateOptionsComponent extends Component { + @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); } diff --git a/app/components/language-select.js b/app/components/language-select.ts similarity index 52% rename from app/components/language-select.js rename to app/components/language-select.ts index f118e3b..6de9725 100644 --- a/app/components/language-select.js +++ b/app/components/language-select.ts @@ -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; diff --git a/app/components/poll-evaluation-participants-table.js b/app/components/poll-evaluation-participants-table.ts similarity index 79% rename from app/components/poll-evaluation-participants-table.js rename to app/components/poll-evaluation-participants-table.ts index 56fdaaa..84ccc7d 100644 --- a/app/components/poll-evaluation-participants-table.js +++ b/app/components/poll-evaluation-participants-table.ts @@ -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 { get optionsPerDay() { const { poll } = this.args; diff --git a/app/components/poll-evaluation-summary.js b/app/components/poll-evaluation-summary.ts similarity index 53% rename from app/components/poll-evaluation-summary.js rename to app/components/poll-evaluation-summary.ts index 3783402..4693fc7 100644 --- a/app/components/poll-evaluation-summary.js +++ b/app/components/poll-evaluation-summary.ts @@ -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 { + @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) => { - answers[answer.type] = 0; - return answers; - }, {}); - let evaluation = options.map((option) => { + const answers = poll.answers.reduce( + (answers: Record, answer: Answer) => { + answers[answer.type] = 0; + return answers; + }, + {}, + ); + const evaluation: { + answers: Record; + 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; diff --git a/app/config/environment.d.ts b/app/config/environment.d.ts new file mode 100644 index 0000000..0174d76 --- /dev/null +++ b/app/config/environment.d.ts @@ -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; diff --git a/app/controllers/application.js b/app/controllers/application.ts similarity index 54% rename from app/controllers/application.js rename to app/controllers/application.ts index 5a41ac6..0d63bf2 100644 --- a/app/controllers/application.js +++ b/app/controllers/application.ts @@ -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; } diff --git a/app/controllers/create.js b/app/controllers/create.ts similarity index 78% rename from app/controllers/create.js rename to app/controllers/create.ts index f2abfd6..372c785 100644 --- a/app/controllers/create.js +++ b/app/controllers/create.ts @@ -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); } diff --git a/app/controllers/create/index.js b/app/controllers/create/index.ts similarity index 58% rename from app/controllers/create/index.js rename to app/controllers/create/index.ts index 12ff0bf..0bb3244 100644 --- a/app/controllers/create/index.js +++ b/app/controllers/create/index.ts @@ -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); diff --git a/app/controllers/create/meta.js b/app/controllers/create/meta.ts similarity index 64% rename from app/controllers/create/meta.js rename to app/controllers/create/meta.ts index eb655f9..5e76c6b 100644 --- a/app/controllers/create/meta.js +++ b/app/controllers/create/meta.ts @@ -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); diff --git a/app/controllers/create/options-datetime.js b/app/controllers/create/options-datetime.ts similarity index 59% rename from app/controllers/create/options-datetime.js rename to app/controllers/create/options-datetime.ts index 4ba2339..77fa41f 100644 --- a/app/controllers/create/options-datetime.js +++ b/app/controllers/create/options-datetime.ts @@ -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>) { this.model.timesForDateOptions = new Map(datetimes.entries()); } } diff --git a/app/controllers/create/options.js b/app/controllers/create/options.ts similarity index 71% rename from app/controllers/create/options.js rename to app/controllers/create/options.ts index d9bbce3..84482e5 100644 --- a/app/controllers/create/options.js +++ b/app/controllers/create/options.ts @@ -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); diff --git a/app/controllers/create/settings.js b/app/controllers/create/settings.ts similarity index 80% rename from app/controllers/create/settings.js rename to app/controllers/create/settings.ts index cafd55c..eaee98f 100644 --- a/app/controllers/create/settings.js +++ b/app/controllers/create/settings.ts @@ -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, diff --git a/app/controllers/error.js b/app/controllers/error.ts similarity index 100% rename from app/controllers/error.js rename to app/controllers/error.ts diff --git a/app/controllers/poll-error.js b/app/controllers/poll-error.ts similarity index 100% rename from app/controllers/poll-error.js rename to app/controllers/poll-error.ts diff --git a/app/controllers/poll.js b/app/controllers/poll.ts similarity index 77% rename from app/controllers/poll.js rename to app/controllers/poll.ts index 43e08d1..f9ff813 100644 --- a/app/controllers/poll.js +++ b/app/controllers/poll.ts @@ -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`); diff --git a/app/helpers/format-date-relative.js b/app/helpers/format-date-relative.ts similarity index 59% rename from app/helpers/format-date-relative.js rename to app/helpers/format-date-relative.ts index 2865043..26b4f41 100644 --- a/app/helpers/format-date-relative.js +++ b/app/helpers/format-date-relative.ts @@ -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(); } diff --git a/app/helpers/mark-as-safe-html.ts b/app/helpers/mark-as-safe-html.ts new file mode 100644 index 0000000..bd3a953 --- /dev/null +++ b/app/helpers/mark-as-safe-html.ts @@ -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(function markAsSafeHtml([ + html, +]) { + return htmlSafe(html); +}); diff --git a/app/helpers/scroll-first-invalid-element-into-view-port.js b/app/helpers/scroll-first-invalid-element-into-view-port.ts similarity index 68% rename from app/helpers/scroll-first-invalid-element-into-view-port.js rename to app/helpers/scroll-first-invalid-element-into-view-port.ts index 38d4636..6e35fdd 100644 --- a/app/helpers/scroll-first-invalid-element-into-view-port.js +++ b/app/helpers/scroll-first-invalid-element-into-view-port.ts @@ -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 `