converts poll/participation and poll/evaluation controllers to TypeScript (#724)
This commit is contained in:
parent
386910bdd3
commit
bf87f6f305
18 changed files with 135 additions and 173 deletions
|
@ -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}}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { DateTime } from 'luxon';
|
|||
export interface PollEvaluationParticipantsTableSignature {
|
||||
Args: {
|
||||
poll: Poll;
|
||||
timeZone: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ interface PollEvaluationSummaryOptionSignature {
|
|||
Named: {
|
||||
evaluationBestOption: BestOption;
|
||||
isFindADate: boolean;
|
||||
timeZone: string | null | undefined;
|
||||
timeZone: string | undefined;
|
||||
};
|
||||
};
|
||||
Element: HTMLButtonElement;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
21
app/controllers/poll/evaluation.ts
Normal file
21
app/controllers/poll/evaluation.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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']>
|
||||
>;
|
||||
|
|
|
@ -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']>
|
||||
>;
|
||||
|
|
|
@ -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']>
|
||||
>;
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default Factory.extend({
|
|||
],
|
||||
pollType: 'FindADate',
|
||||
title: 'default title',
|
||||
timezone: '',
|
||||
timezone: null,
|
||||
version: 'v0.3',
|
||||
|
||||
afterCreate(poll, server) {
|
||||
|
|
20
types/ember-bootstrap/components/bs-modal.d.ts
vendored
20
types/ember-bootstrap/components/bs-modal.d.ts
vendored
|
@ -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;
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
2
types/ember-intl/helpers/format-date.d.ts
vendored
2
types/ember-intl/helpers/format-date.d.ts
vendored
|
@ -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;
|
||||
}>;
|
||||
|
|
Loading…
Reference in a new issue