converts poll/participation and poll/evaluation controllers to TypeScript (#724)

This commit is contained in:
Jeldrik Hanschke 2023-11-04 17:21:35 +01:00 committed by GitHub
parent 386910bdd3
commit bf87f6f305
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 135 additions and 173 deletions

View file

@ -14,21 +14,7 @@
is not sure about it. is not sure about it.
}} }}
<th colspan={{count}}> <th colspan={{count}}>
{{! {{format-date jsDate dateStyle="full" timeZone=@timeZone}}
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
Cannot use optionGroup.value because that's a ISO8601 day string
(e.g. "2023-10-01"), which is parsed by browsers in UTC and not
locale time zone. Therefore we need parse a JS Date representation
of that string, which has been parsed by Luxon in correct timezone.
}}
{{format-date
jsDate
weekday="long"
day="numeric"
month="long"
year="numeric"
}}
</th> </th>
{{/each-in}} {{/each-in}}
</tr> </tr>
@ -49,7 +35,11 @@
But TypeScript does not support narrowing through a chain of getters But TypeScript does not support narrowing through a chain of getters
currently. currently.
}} }}
{{format-date option.jsDate timeStyle="short"}} {{! @glint-ignore }}{{! prettier-ignore }}
{{format-date option.jsDate
timeStyle="short"
timeZone=@timeZone
}}
{{/if}} {{/if}}
{{else if @poll.isFindADate}} {{else if @poll.isFindADate}}
{{! {{!
@ -59,7 +49,7 @@
But TypeScript does not support narrowing through a chain of getters But TypeScript does not support narrowing through a chain of getters
currently. currently.
}} }}
{{format-date option.jsDate dateStyle="full"}} {{format-date option.jsDate dateStyle="full" timeZone=@timeZone}}
{{else}} {{else}}
{{option.title}} {{option.title}}
{{/if}} {{/if}}

View file

@ -5,6 +5,7 @@ import { DateTime } from 'luxon';
export interface PollEvaluationParticipantsTableSignature { export interface PollEvaluationParticipantsTableSignature {
Args: { Args: {
poll: Poll; poll: Poll;
timeZone: string | undefined;
}; };
} }

View file

@ -6,7 +6,7 @@ interface PollEvaluationSummaryOptionSignature {
Named: { Named: {
evaluationBestOption: BestOption; evaluationBestOption: BestOption;
isFindADate: boolean; isFindADate: boolean;
timeZone: string | null | undefined; timeZone: string | undefined;
}; };
}; };
Element: HTMLButtonElement; Element: HTMLButtonElement;

View file

@ -3,12 +3,12 @@ import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import type RouterService from '@ember/routing/router-service'; import type RouterService from '@ember/routing/router-service';
import type Transition from '@ember/routing/transition'; import type Transition from '@ember/routing/transition';
import type { CreateRouteIndexModel } from 'croodle/routes/create/index'; import type { CreateIndexRouteModel } from 'croodle/routes/create/index';
export default class CreateIndex extends Controller { export default class CreateIndex extends Controller {
@service declare router: RouterService; @service declare router: RouterService;
declare model: CreateRouteIndexModel; declare model: CreateIndexRouteModel;
@action @action
submit() { submit() {

View file

@ -63,7 +63,7 @@ export default class PollController extends Controller {
get timezone() { get timezone() {
const { model: poll, shouldUseLocalTimezone } = this; const { model: poll, shouldUseLocalTimezone } = this;
return shouldUseLocalTimezone ? undefined : poll.timezone; return shouldUseLocalTimezone || !poll.timezone ? undefined : poll.timezone;
} }
@action @action

View file

@ -1,84 +0,0 @@
import Controller, { inject as controller } from '@ember/controller';
import { inject as service } from '@ember/service';
export default class PollEvaluationController extends Controller {
@service intl;
@controller('poll') pollController;
get isEvaluable() {
const { model: poll } = this;
const { isFreeText, users } = poll;
const hasUsers = users.length > 0;
return hasUsers && !isFreeText;
}
/*
* evaluates poll data
* if free text answers are allowed evaluation is disabled
*/
get evaluation() {
if (!this.isEvaluable) {
return [];
}
const { model: poll } = this;
let evaluation = [];
let options = [];
let lookup = [];
// init options array
poll.options.forEach((option, index) => {
options[index] = 0;
});
// init array of evalutation objects
// create object for every possible answer
poll.answers.forEach((answer) => {
evaluation.push({
id: answer.label,
label: answer.label,
options: [...options],
});
});
// create object for no answer if answers are not forced
if (!poll.forceAnswer) {
evaluation.push({
id: null,
label: 'no answer',
options: [...options],
});
}
// create lookup array
evaluation.forEach(function (value, index) {
lookup[value.id] = index;
});
// loop over all users
poll.users.forEach((user) => {
// loop over all selections of the user
user.selections.forEach(function (selection, optionIndex) {
let answerIndex;
// get answer index by lookup array
if (typeof lookup[selection.value.label] === 'undefined') {
answerIndex = lookup[null];
} else {
answerIndex = lookup[selection.get('value.label')];
}
// increment counter
try {
evaluation[answerIndex].options[optionIndex]++;
} catch (e) {
// ToDo: Throw an error
}
});
});
return evaluation;
}
}

View file

@ -0,0 +1,21 @@
import Controller, { inject as controller } from '@ember/controller';
import { inject as service } from '@ember/service';
import type IntlService from 'ember-intl/services/intl';
import type PollController from '../poll';
import type { PollEvaluationRouteModel } from 'croodle/routes/poll/evaluation';
export default class PollEvaluationController extends Controller {
@service declare intl: IntlService;
@controller('poll') declare pollController: PollController;
declare model: PollEvaluationRouteModel;
get isEvaluable() {
const { model: poll } = this;
const { isFreeText, users } = poll;
const hasUsers = users.length > 0;
return hasUsers && !isFreeText;
}
}

View file

@ -3,16 +3,28 @@ import User from '../../models/user';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import type RouterService from '@ember/routing/router-service';
import type PollController from '../poll';
import type { PollParticipationRouteModel } from 'croodle/routes/poll/participation';
import type Poll from 'croodle/models/poll';
import type { SelectionInput } from 'croodle/models/selection';
export default class PollParticipationController extends Controller { export default class PollParticipationController extends Controller {
@service router; @service declare router: RouterService;
@controller('poll') @controller('poll') declare pollController: PollController;
pollController;
declare model: PollParticipationRouteModel;
@tracked name = ''; @tracked name = '';
@tracked savingFailed = false; @tracked savingFailed = false;
newUserData: {
name: string | null;
poll: Poll;
selections: SelectionInput[];
} | null = null;
@action @action
async submit() { async submit() {
const { formData, poll } = this.model; const { formData, poll } = this.model;
@ -30,18 +42,21 @@ export default class PollParticipationController extends Controller {
} }
// map selection to answer if it's not freetext // map selection to answer if it's not freetext
let answer = answers.find(({ type }) => type === value); const answer = answers.find(({ type }) => type === value);
let { icon, label, labelTranslation, type } = answer; if (!answer) {
throw new Error('Mapping selection to answer failed');
}
const { icon, labelTranslation, type } = answer;
return { return {
icon, icon,
label,
labelTranslation, labelTranslation,
type, type,
}; };
}); });
this.newUserRecord = { this.newUserData = {
name, name,
poll, poll,
selections, selections,
@ -51,12 +66,24 @@ export default class PollParticipationController extends Controller {
@action @action
async save() { async save() {
const { model, newUserRecord: user } = this; const { model, newUserData: userData } = this;
const { poll } = model; const { poll } = model;
const { encryptionKey } = this.router.currentRoute.parent.queryParams; // As know that the route is `poll.participation`, which means that there
// is a parent `poll` for sure.
const { encryptionKey } = this.router.currentRoute.parent!.queryParams;
if (!userData) {
throw new Error(
'save method called before submit method has set the user data',
);
}
if (!encryptionKey) {
throw new Error('Can not lookup encryption key');
}
try { try {
await User.create(user, encryptionKey); await User.create(userData, encryptionKey);
this.savingFailed = false; this.savingFailed = false;
} catch (error) { } catch (error) {

View file

@ -182,7 +182,7 @@ export default class Poll {
id: payload.poll.id, id: payload.poll.id,
options: decrypt(payload.poll.options, passphrase) as OptionInput, options: decrypt(payload.poll.options, passphrase) as OptionInput,
pollType: decrypt(payload.poll.pollType, passphrase) as PollType, pollType: decrypt(payload.poll.pollType, passphrase) as PollType,
timezone: decrypt(payload.poll.timezone, passphrase) as string, timezone: decrypt(payload.poll.timezone, passphrase) as string | null,
title: decrypt(payload.poll.title, passphrase) as string, title: decrypt(payload.poll.title, passphrase) as string,
users: payload.poll.users.map((user) => { users: payload.poll.users.map((user) => {
return new User({ return new User({

View file

@ -1,15 +1,15 @@
export type SelectionInput = { export type SelectionInput = {
icon: string; icon?: string;
label: string; label?: string;
labelTranslation: string; labelTranslation?: string;
type: string; type?: string;
}; };
export default class Selection { export default class Selection {
icon: string; icon?: string;
label: string; label?: string;
labelTranslation: string; labelTranslation?: string;
type: string; type?: string;
constructor({ icon, label, labelTranslation, type }: SelectionInput) { constructor({ icon, label, labelTranslation, type }: SelectionInput) {
this.icon = icon; this.icon = icon;

View file

@ -8,7 +8,7 @@ import type Poll from './poll';
type UserInput = { type UserInput = {
creationDate: string; creationDate: string;
id: string; id: string;
name: string; name: string | null;
selections: SelectionInput[]; selections: SelectionInput[];
version: string; version: string;
}; };
@ -20,7 +20,7 @@ export default class User {
id: string; id: string;
// user name // user name
name: string; name: string | null;
// array of users selections // array of users selections
// must be in same order as options property of poll // must be in same order as options property of poll
@ -42,7 +42,7 @@ export default class User {
name, name,
poll, poll,
selections, selections,
}: { name: string; poll: Poll; selections: SelectionInput[] }, }: { name: string | null; poll: Poll; selections: SelectionInput[] },
passphrase: string, passphrase: string,
) { ) {
const creationDate = new Date().toISOString(); const creationDate = new Date().toISOString();

View file

@ -23,6 +23,6 @@ export default class CreateIndexRoute extends Route {
} }
type Resolved<P> = P extends Promise<infer T> ? T : P; type Resolved<P> = P extends Promise<infer T> ? T : P;
export type CreateRouteIndexModel = Resolved< export type CreateIndexRouteModel = Resolved<
ReturnType<CreateIndexRoute['model']> ReturnType<CreateIndexRoute['model']>
>; >;

View file

@ -1,7 +1,13 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import type { PollRouteModel } from '../poll';
export default class EvaluationRoute extends Route { export default class PollEvaluationRoute extends Route {
model() { model() {
return this.modelFor('poll'); return this.modelFor('poll') as PollRouteModel;
} }
} }
type Resolved<P> = P extends Promise<infer T> ? T : P;
export type PollEvaluationRouteModel = Resolved<
ReturnType<PollEvaluationRoute['model']>
>;

View file

@ -78,13 +78,15 @@ class FormData {
} }
} }
export default class ParticipationRoute extends Route { export default class PollParticipationRoute extends Route {
model() { model() {
const poll = this.modelFor('poll') as Poll; const poll = this.modelFor('poll') as Poll;
const { anonymousUser, forceAnswer, options, users } = poll; const { anonymousUser, forceAnswer, options, users } = poll;
const formData = new FormData(options, { const formData = new FormData(options, {
nameIsRequired: !anonymousUser, nameIsRequired: !anonymousUser,
namesTaken: users.map(({ name }) => name), namesTaken: users
.map(({ name }) => name)
.filter((_) => _ !== null) as string[],
selectionIsRequired: forceAnswer, selectionIsRequired: forceAnswer,
}); });
@ -94,3 +96,8 @@ export default class ParticipationRoute extends Route {
}; };
} }
} }
type Resolved<P> = P extends Promise<infer T> ? T : P;
export type PollParticipationRouteModel = Resolved<
ReturnType<PollParticipationRoute['model']>
>;

View file

@ -31,26 +31,18 @@
as |shouldShowDate| as |shouldShowDate|
}} }}
{{#if @model.poll.isFreeText}} {{#if @model.poll.isFreeText}}
{{! prettier-ignore }}
<form.element <form.element
@controlType="text" @controlType="text"
{{! {{!
TODO: Simplify date formating to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6 @glint-ignore
We know that jsDate is not null if `poll.isFindADate` is `true`.
But Glint does not understand that.
}} }}
@label={{if @label={{if @model.poll.isFindADate (format-date option.jsDate
@model.poll.isFindADate dateStyle=(if shouldShowDate "full" undefined)
(format-date timeStyle=(if option.hasTime "short" undefined)
option.jsDate timeZone=this.pollController.timezone
weekday=(if shouldShowDate "long" undefined)
day=(if shouldShowDate "numeric" undefined)
month=(if shouldShowDate "long" undefined)
year=(if shouldShowDate "numeric" undefined)
hour=(if option.hasTime "numeric" undefined)
minute=(if option.hasTime "numeric" undefined)
timeZone=(if
this.pollController.timezone
this.pollController.timezone
undefined
)
) )
option.title option.title
}} }}
@ -59,25 +51,17 @@
data-test-form-element={{concat "option-" option.title}} data-test-form-element={{concat "option-" option.title}}
/> />
{{else}} {{else}}
{{! prettier-ignore }}
<form.element <form.element
{{! {{!
TODO: Simplify date formating to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6 @glint-ignore
We know that jsDate is not null if `poll.isFindADate` is `true`.
But Glint does not understand that.
}} }}
@label={{if @label={{if @model.poll.isFindADate (format-date option.jsDate
@model.poll.isFindADate dateStyle=(if shouldShowDate "full" undefined)
(format-date timeStyle=(if option.hasTime "short" undefined)
option.jsDate timeZone=this.pollController.timezone
weekday=(if shouldShowDate "long" undefined)
day=(if shouldShowDate "numeric" undefined)
month=(if shouldShowDate "long" undefined)
year=(if shouldShowDate "numeric" undefined)
hour=(if option.hasTime "numeric" undefined)
minute=(if option.hasTime "numeric" undefined)
timeZone=(if
this.pollController.timezone
this.pollController.timezone
undefined
)
) )
option.title option.title
}} }}

View file

@ -21,7 +21,7 @@ export default Factory.extend({
], ],
pollType: 'FindADate', pollType: 'FindADate',
title: 'default title', title: 'default title',
timezone: '', timezone: null,
version: 'v0.3', version: 'v0.3',
afterCreate(poll, server) { afterCreate(poll, server) {

View file

@ -5,12 +5,14 @@ declare module '@glint/environment-ember-loose/registry' {
BsModal: ComponentLike<{ BsModal: ComponentLike<{
Args: { Args: {
Named: { Named: {
autoClose: boolean; autoClose?: boolean;
closeButton: boolean; closeButton?: boolean;
footer: boolean; footer?: boolean;
keyboard: boolean; keyboard?: boolean;
onHidden?: () => void;
onSubmit?: () => void;
open: boolean; open: boolean;
title: string; title?: string;
}; };
}; };
Blocks: { Blocks: {
@ -21,11 +23,19 @@ declare module '@glint/environment-ember-loose/registry' {
default: []; default: [];
}; };
}>; }>;
close: () => void;
header: ComponentLike<{
Args: {
closeButton: boolean;
title: string;
};
}>;
footer: ComponentLike<{ footer: ComponentLike<{
Blocks: { Blocks: {
default: []; default: [];
}; };
}>; }>;
submit: () => void;
}, },
]; ];
}; };

View file

@ -10,7 +10,7 @@ declare module '@glint/environment-ember-loose/registry' {
'format-date': HelperLike<{ 'format-date': HelperLike<{
Args: { Args: {
Positional: [Date | string]; Positional: [Date | string];
Named: Record<string, unknown>; Named: Intl.DateTimeFormatOptions;
}; };
Return: string; Return: string;
}>; }>;