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.
}}
<th colspan={{count}}>
{{!
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"
}}
{{format-date jsDate dateStyle="full" timeZone=@timeZone}}
</th>
{{/each-in}}
</tr>
@ -49,7 +35,11 @@
But TypeScript does not support narrowing through a chain of getters
currently.
}}
{{format-date option.jsDate timeStyle="short"}}
{{! @glint-ignore }}{{! prettier-ignore }}
{{format-date option.jsDate
timeStyle="short"
timeZone=@timeZone
}}
{{/if}}
{{else if @poll.isFindADate}}
{{!
@ -59,7 +49,7 @@
But TypeScript does not support narrowing through a chain of getters
currently.
}}
{{format-date option.jsDate dateStyle="full"}}
{{format-date option.jsDate dateStyle="full" timeZone=@timeZone}}
{{else}}
{{option.title}}
{{/if}}

View file

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

View file

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

View file

@ -3,12 +3,12 @@ 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';
import type { CreateIndexRouteModel } from 'croodle/routes/create/index';
export default class CreateIndex extends Controller {
@service declare router: RouterService;
declare model: CreateRouteIndexModel;
declare model: CreateIndexRouteModel;
@action
submit() {

View file

@ -63,7 +63,7 @@ export default class PollController extends Controller {
get timezone() {
const { model: poll, shouldUseLocalTimezone } = this;
return shouldUseLocalTimezone ? undefined : poll.timezone;
return shouldUseLocalTimezone || !poll.timezone ? undefined : poll.timezone;
}
@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 { action } from '@ember/object';
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 {
@service router;
@service declare router: RouterService;
@controller('poll')
pollController;
@controller('poll') declare pollController: PollController;
declare model: PollParticipationRouteModel;
@tracked name = '';
@tracked savingFailed = false;
newUserData: {
name: string | null;
poll: Poll;
selections: SelectionInput[];
} | null = null;
@action
async submit() {
const { formData, poll } = this.model;
@ -30,18 +42,21 @@ export default class PollParticipationController extends Controller {
}
// map selection to answer if it's not freetext
let answer = answers.find(({ type }) => type === value);
let { icon, label, labelTranslation, type } = answer;
const answer = answers.find(({ type }) => type === value);
if (!answer) {
throw new Error('Mapping selection to answer failed');
}
const { icon, labelTranslation, type } = answer;
return {
icon,
label,
labelTranslation,
type,
};
});
this.newUserRecord = {
this.newUserData = {
name,
poll,
selections,
@ -51,12 +66,24 @@ export default class PollParticipationController extends Controller {
@action
async save() {
const { model, newUserRecord: user } = this;
const { model, newUserData: userData } = this;
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 {
await User.create(user, encryptionKey);
await User.create(userData, encryptionKey);
this.savingFailed = false;
} catch (error) {

View file

@ -182,7 +182,7 @@ export default class Poll {
id: payload.poll.id,
options: decrypt(payload.poll.options, passphrase) as OptionInput,
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,
users: payload.poll.users.map((user) => {
return new User({

View file

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

View file

@ -8,7 +8,7 @@ import type Poll from './poll';
type UserInput = {
creationDate: string;
id: string;
name: string;
name: string | null;
selections: SelectionInput[];
version: string;
};
@ -20,7 +20,7 @@ export default class User {
id: string;
// user name
name: string;
name: string | null;
// array of users selections
// must be in same order as options property of poll
@ -42,7 +42,7 @@ export default class User {
name,
poll,
selections,
}: { name: string; poll: Poll; selections: SelectionInput[] },
}: { name: string | null; poll: Poll; selections: SelectionInput[] },
passphrase: string,
) {
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;
export type CreateRouteIndexModel = Resolved<
export type CreateIndexRouteModel = Resolved<
ReturnType<CreateIndexRoute['model']>
>;

View file

@ -1,7 +1,13 @@
import Route from '@ember/routing/route';
import type { PollRouteModel } from '../poll';
export default class EvaluationRoute extends Route {
export default class PollEvaluationRoute extends Route {
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() {
const poll = this.modelFor('poll') as Poll;
const { anonymousUser, forceAnswer, options, users } = poll;
const formData = new FormData(options, {
nameIsRequired: !anonymousUser,
namesTaken: users.map(({ name }) => name),
namesTaken: users
.map(({ name }) => name)
.filter((_) => _ !== null) as string[],
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|
}}
{{#if @model.poll.isFreeText}}
{{! prettier-ignore }}
<form.element
@controlType="text"
{{!
TODO: Simplify date formating to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6
}}
@label={{if
@model.poll.isFindADate
(format-date
option.jsDate
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
)
@glint-ignore
We know that jsDate is not null if `poll.isFindADate` is `true`.
But Glint does not understand that.
}}
@label={{if @model.poll.isFindADate (format-date option.jsDate
dateStyle=(if shouldShowDate "full" undefined)
timeStyle=(if option.hasTime "short" undefined)
timeZone=this.pollController.timezone
)
option.title
}}
@ -59,25 +51,17 @@
data-test-form-element={{concat "option-" option.title}}
/>
{{else}}
{{! prettier-ignore }}
<form.element
{{!
TODO: Simplify date formating to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6
}}
@label={{if
@model.poll.isFindADate
(format-date
option.jsDate
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
)
@glint-ignore
We know that jsDate is not null if `poll.isFindADate` is `true`.
But Glint does not understand that.
}}
@label={{if @model.poll.isFindADate (format-date option.jsDate
dateStyle=(if shouldShowDate "full" undefined)
timeStyle=(if option.hasTime "short" undefined)
timeZone=this.pollController.timezone
)
option.title
}}

View file

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

View file

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

View file

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