Introduce typed templates with Glint (#714)

This commit is contained in:
Jeldrik Hanschke 2023-11-04 14:54:30 +01:00 committed by GitHub
parent 146f531a37
commit 76586f165d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 859 additions and 216 deletions

View file

@ -4,8 +4,6 @@ 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;

View file

@ -0,0 +1,16 @@
import templateOnlyComponent from '@ember/component/template-only';
interface BackButtonSignature {
Args: { onClick?: () => void };
Element: HTMLButtonElement;
}
const BackButton = templateOnlyComponent<BackButtonSignature>();
export default BackButton;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BackButton: typeof BackButton;
}
}

View file

@ -1,5 +1,5 @@
{{#let @form as |form|}}
<form.element
{{#let @formElement as |FormElement|}}
<FormElement
@label={{t "create.options.dates.label"}}
@property="options"
data-test-form-element-for="days"
@ -17,10 +17,7 @@
<InlineDatepicker
@center={{this.calendarCenter}}
@selectedDays={{this.selectedDays}}
@onCenterChange={{action
(mut this.calendarCenter)
value="datetime"
}}
@onCenterChange={{fn this.handleCalenderCenterChange 0}}
@onSelect={{this.handleSelectedDaysChange}}
/>
</div>
@ -28,14 +25,11 @@
<InlineDatepicker
@center={{this.calendarCenterNext}}
@selectedDays={{this.selectedDays}}
@onCenterChange={{action
(mut this.calendarCenter)
value="datetime"
}}
@onCenterChange={{fn this.handleCalenderCenterChange -1}}
@onSelect={{this.handleSelectedDaysChange}}
/>
</div>
</div>
</div>
</form.element>
</FormElement>
{{/let}}

View file

@ -3,12 +3,13 @@ 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';
import type BsFormElementComponent from 'ember-bootstrap/components/bs-form/element';
export interface CreateOptionsDatesSignature {
Args: {
options: TrackedSet<FormDataOption>;
formElement: BsFormElementComponent;
options: Array<FormDataOption>;
updateOptions: (options: string[]) => void;
};
}
@ -20,7 +21,7 @@ export default class CreateOptionsDates extends Component<CreateOptionsDatesSign
: DateTime.local();
get selectedDays(): DateTime[] {
return Array.from(this.args.options).map(
return this.args.options.map(
({ value }) => DateTime.fromISO(value) as DateTime,
);
}
@ -45,4 +46,18 @@ export default class CreateOptionsDates extends Component<CreateOptionsDatesSign
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate() as string),
);
}
@action
handleCalenderCenterChange(
offset: number,
{ datetime }: { datetime: DateTime },
) {
this.calendarCenter = datetime.plus({ months: offset });
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
CreateOptionsDates: typeof CreateOptionsDates;
}
}

View file

@ -1,6 +1,6 @@
<div class="cr-form-wrapper box">
{{#if this.errorMessage}}
<BsAlert type="warning">
<BsAlert @type="warning">
{{t this.errorMessage}}
</BsAlert>
{{/if}}
@ -15,33 +15,15 @@
>
<div class="days">
{{#each-in this.formData.datetimes as |date timeOptions|}}
{{!--
@glint-ignore
Types for value returned by `{{#each-in}}` are broken if used
with a `Map`. https://github.com/typed-ember/glint/issues/645
--}}
{{#each timeOptions as |timeOption indexInTimeOptions|}}
{{!
show summarized validation state for all times in a day
}}
<div
{{!
TODO: daysValidationState is not defined!
}}
class={{if
(get this.daysValidationState date)
(concat "label-has-" (get this.daysValidationState date))
"label-has-no-validation"
}}
data-test-day={{date}}
>
<div data-test-day={{date}}>
<form.element
{{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
}}
@label={{format-date
timeOption.jsDate
weekday="long"
day="numeric"
month="long"
year="numeric"
}}
@label={{format-date timeOption.jsDate dateStyle="full"}}
{{!
show label only for the first time of this date
}}

View file

@ -102,6 +102,19 @@ class FormData {
return this.datetimes.size > 1;
}
get validationStatePerDate() {
const validationState: Map<string, boolean> = new Map();
for (const [date, timeOptions] of this.datetimes.entries()) {
validationState.set(
date,
Array.from(timeOptions).every((time) => time.isValid),
);
}
return validationState;
}
@action
addOption(date: string) {
this.datetimes
@ -215,7 +228,7 @@ export default class CreateOptionsDatetime extends Component<CreateOptoinsDateti
// validate input field for being partially filled
@action
validateInput(option: FormDataTimeOption, event: InputEvent) {
validateInput(option: FormDataTimeOption, event: Event) {
const element = event.target as HTMLInputElement;
// update partially filled time validation error
@ -224,7 +237,7 @@ export default class CreateOptionsDatetime extends Component<CreateOptoinsDateti
// remove partially filled validation error if user fixed it
@action
updateInputValidation(option: FormDataTimeOption, event: InputEvent) {
updateInputValidation(option: FormDataTimeOption, event: Event) {
const element = event.target as HTMLInputElement;
if (element.checkValidity() && option.isPartiallyFilled) {
@ -266,3 +279,9 @@ export default class CreateOptionsDatetime extends Component<CreateOptoinsDateti
this.router.on('routeWillChange', this.handleTransition);
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
CreateOptionsDatetime: typeof CreateOptionsDatetime;
}
}

View file

@ -1,6 +1,6 @@
{{#let @form as |form|}}
{{#let @formElement as |FormElement|}}
{{#each @options as |option index|}}
<form.element
<FormElement
{{! show label only on first item }}
@label={{unless index (t "create.options.options.label")}}
@model={{option}}
@ -48,6 +48,6 @@
></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
</BsButton>
</form.element>
</FormElement>
{{/each}}
{{/let}}

View file

@ -0,0 +1,24 @@
import templateOnlyComponent from '@ember/component/template-only';
import type { FormDataOption } from './create-options';
import type BsFormElementComponent from 'ember-bootstrap/components/bs-form/element';
interface CreateOptionsTextSignature {
Args: {
Named: {
addOption: (value: string, afterPosition: number) => void;
deleteOption: (option: FormDataOption) => void;
formElement: BsFormElementComponent;
options: FormDataOption[];
};
};
}
const CreateOptionsText = templateOnlyComponent<CreateOptionsTextSignature>();
export default CreateOptionsText;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
CreateOptionsText: typeof CreateOptionsText;
}
}

View file

@ -12,13 +12,13 @@
@options={{this.formData.options}}
@addOption={{this.formData.addOption}}
@deleteOption={{this.formData.deleteOption}}
@form={{form}}
@formElement={{form.element}}
/>
{{else}}
<CreateOptionsDates
@options={{this.formData.options}}
@updateOptions={{this.formData.updateOptions}}
@form={{form}}
@formElement={{form.element}}
/>
{{/if}}
@ -27,7 +27,7 @@
<NextButton />
</div>
<div class="col-6 col-md-4 order-1 text-right">
<BackButton @onClick={{action "previousPage"}} />
<BackButton @onClick={{this.previousPage}} />
</div>
</div>
</BsForm>

View file

@ -104,7 +104,7 @@ export interface CreateOptionsSignature {
};
}
export default class CreateOptionsComponent extends Component<CreateOptionsSignature> {
export default class CreateOptions extends Component<CreateOptionsSignature> {
@service declare router: RouterService;
formData = new FormData(
@ -136,3 +136,9 @@ export default class CreateOptionsComponent extends Component<CreateOptionsSigna
this.router.on('routeWillChange', this.handleTransition);
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
CreateOptions: typeof CreateOptions;
}
}

View file

@ -9,7 +9,7 @@
<button
type="button"
class="ember-power-calendar-nav-control"
onclick={{action calendar.actions.moveCenter -1 "month"}}
{{on "click" (fn calendar.actions.moveCenter -1 "month")}}
>
«
</button>
@ -19,7 +19,7 @@
<button
type="button"
class="ember-power-calendar-nav-control"
onclick={{action calendar.actions.moveCenter 1 "month"}}
{{on "click" (fn calendar.actions.moveCenter 1 "month")}}
>
»
</button>

View file

@ -0,0 +1,23 @@
import templateOnlyComponent from '@ember/component/template-only';
import type { DateTime } from 'luxon';
interface InlineDatepickerSignature {
Args: {
Named: {
center: DateTime;
onCenterChange: (day: { datetime: DateTime }) => void;
onSelect: (days: { datetime: DateTime[] }) => void;
selectedDays: DateTime[];
};
};
}
const InlineDatepicker = templateOnlyComponent<InlineDatepickerSignature>();
export default InlineDatepicker;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
InlineDatepicker: typeof InlineDatepicker;
}
}

View file

@ -30,3 +30,9 @@ export default class LanguageSelect extends Component {
}
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
LanguageSelect: typeof LanguageSelect;
}
}

View file

@ -1 +1,5 @@
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>

View file

@ -0,0 +1,13 @@
import templateOnlyComponent from '@ember/component/template-only';
interface LoadingSpinnerSignature {}
const LoadingSpinner = templateOnlyComponent<LoadingSpinnerSignature>();
export default LoadingSpinner;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
LoadingSpinner: typeof LoadingSpinner;
}
}

View file

@ -1,7 +1,7 @@
<BsButton
@buttonType="submit"
@type="primary"
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit"
...attributes
>
<span class="cr-steps-bottom-nav__label">

View file

@ -0,0 +1,20 @@
import templateOnlyComponent from '@ember/component/template-only';
interface NextButtonSignature {
Args: {
Named: {
isPending?: boolean;
};
};
Element: HTMLButtonElement;
}
const NextButton = templateOnlyComponent<NextButtonSignature>();
export default NextButton;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
NextButton: typeof NextButton;
}
}

View file

@ -7,6 +7,12 @@
{{! column for name }}
</th>
{{#each-in this.optionsPerDay as |jsDate count|}}
{{!
@glint-ignore
We can be sure that count is a number because it is destructed from a
Map, which values are only numbers. But somehow Glint / TypeScript
is not sure about it.
}}
<th colspan={{count}}>
{{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
@ -37,21 +43,23 @@
{{#if (and @poll.isFindADate @poll.hasTimes)}}
{{#if option.hasTime}}
{{!
TODO: Simplify to timeStyle="short" after upgrading to Ember Intl v6
@glint-ignore
Narrowring is not working here correctly. Due to the only executing if
`option.hasTime` is `true`, we know that `option.jsDate` cannot be `null`.
But TypeScript does not support narrowing through a chain of getters
currently.
}}
{{format-date option.jsDate hour="numeric" minute="numeric"}}
{{format-date option.jsDate timeStyle="short"}}
{{/if}}
{{else if @poll.isFindADate}}
{{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
}}
{{format-date
option.jsDate
weekday="long"
day="numeric"
month="long"
year="numeric"
@glint-ignore
Narrowring is not working here correctly. Due to the only executing if
`option.hasTime` is `true`, we know that `option.jsDate` cannot be `null`.
But TypeScript does not support narrowing through a chain of getters
currently.
}}
{{format-date option.jsDate dateStyle="full"}}
{{else}}
{{option.title}}
{{/if}}
@ -67,7 +75,7 @@
{{user.name}}
</td>
{{#each @poll.options as |option index|}}
{{#let (object-at index user.selections) as |selection|}}
{{#let (get user.selections index) as |selection|}}
<td
class={{selection.type}}
data-test-is-selection-cell

View file

@ -12,11 +12,19 @@ export default class PollEvaluationParticipantsTable extends Component<PollEvalu
get optionsPerDay() {
const { poll } = this.args;
const optionsPerDay = new Map();
const optionsPerDay: Map<string, number> = new Map();
for (const option of poll.options) {
if (!option.day) {
throw new Error(
`Excepts all options to have a valid ISO8601 date string when using optionsPerDay getter`,
);
}
optionsPerDay.set(
option.day,
optionsPerDay.has(option.day) ? optionsPerDay.get(option.day) + 1 : 0,
optionsPerDay.has(option.day)
? (optionsPerDay.get(option.day) as number) + 1
: 0,
);
}
@ -35,3 +43,9 @@ export default class PollEvaluationParticipantsTable extends Component<PollEvalu
);
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
PollEvaluationParticipantsTable: typeof PollEvaluationParticipantsTable;
}
}

View file

@ -2,24 +2,20 @@
There must not be a line break between option text and "</strong>." cause otherwise
we will see a space between option string and following dot.
}}
{{#if @isFindADate}}
{{! Need to disable block indentation rule cause there shouldn't be a space between date and dot }}
{{! template-lint-disable block-indentation }}
<strong data-test-best-option={{@evaluationBestOption.option.title}}>
{{!
TODO: Simplify to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6
Checking `@evaluationBestOption.option.jsDate` is the same as checking `@isFindADate`.
If poll type is `FindADate` we can be sure that every option is a valid ISO861
string. Therefore `Option.jsDate` must be `true` by design. But Glint / TypeScript
does not understand that. Therefore we need to use the less readable form.
}}
{{#if @evaluationBestOption.option.jsDate}}
<strong data-test-best-option={{@evaluationBestOption.option.title}}>
{{format-date
@evaluationBestOption.option.jsDate
weekday="long"
day="numeric"
month="long"
year="numeric"
hour=(if @evaluationBestOption.option.hasTime "numeric" undefined)
minute=(if @evaluationBestOption.option.hasTime "numeric" undefined)
dateStyle="full"
timeStyle=(if @evaluationBestOption.option.hasTime "short" undefined)
timeZone=(if @timeZone @timeZone undefined)
}}</strong>.
{{! template-lint-enable block-indentation }}
{{else}}
<strong
data-test-best-option={{@evaluationBestOption.option.title}}

View file

@ -0,0 +1,24 @@
import templateOnlyComponent from '@ember/component/template-only';
import type { BestOption } from './poll-evaluation-summary';
interface PollEvaluationSummaryOptionSignature {
Args: {
Named: {
evaluationBestOption: BestOption;
isFindADate: boolean;
timeZone: string | null | undefined;
};
};
Element: HTMLButtonElement;
}
const PollEvaluationSummaryOption =
templateOnlyComponent<PollEvaluationSummaryOptionSignature>();
export default PollEvaluationSummaryOption;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
PollEvaluationSummaryOption: typeof PollEvaluationSummaryOption;
}
}

View file

@ -20,7 +20,7 @@
}}
{{/if}}
{{#if (get this.bestOptions.length 1)}}
{{#if (gt this.bestOptions.length 1)}}
<ul>
{{#each this.bestOptions as |evaluationBestOption|}}
<li>
@ -34,6 +34,11 @@
</ul>
{{else}}
<PollEvaluationSummaryOption
{{!
@glint-ignore
We can be sure that `this.bestOptions` contains at least one item
as a poll must always have at least one option.
}}
@evaluationBestOption={{get this.bestOptions 0}}
@isFindADate={{@poll.isFindADate}}
@timeZone={{@timeZone}}
@ -42,9 +47,16 @@
</p>
<p class="last-participation">
{{#if this.lastParticipationAt}}
{{t
"poll.evaluation.lastParticipation"
ago=(format-date-relative this.lastParticipationAt)
}}
{{else}}
{{!
No need for the else block as user cannot enter evaluation page if
no one participated in the poll yet.
}}
{{/if}}
</p>
</div>

View file

@ -9,49 +9,67 @@ import type Poll from 'croodle/models/poll';
export interface PollEvaluationSummarySignature {
Args: {
poll: Poll;
timeZone: string | undefined;
};
}
export interface BestOption {
answers: Record<'yes' | 'no' | 'maybe', number>;
option: Option;
score: number;
}
export default class PollEvaluationSummary extends Component<PollEvaluationSummarySignature> {
@service declare intl: IntlService;
get bestOptions() {
get bestOptions(): BestOption[] | null {
const { poll } = this.args;
const { isFreeText, options, users } = poll;
// can not evaluate answer type free text
if (isFreeText) {
return undefined;
return null;
}
// can not evaluate a poll without users
if (users.length < 1) {
return undefined;
return null;
}
const answers = poll.answers.reduce(
(answers: Record<string, number>, answer: Answer) => {
(answers, answer: Answer) => {
answers[answer.type] = 0;
return answers;
},
{},
{} as Record<'yes' | 'no' | 'maybe', number>,
);
const evaluation: {
answers: Record<string, number>;
option: Option;
score: number;
}[] = options.map((option: Option) => {
const evaluation: BestOption[] = options.map((option: Option) => {
return {
answers: { ...answers },
option,
score: 0,
};
});
const bestOptions = [];
users.forEach((user: User) => {
user.selections.forEach(({ type }, i) => {
evaluation[i]!.answers[type]++;
if (!type) {
// type may be undefined if poll does not force an answer to all options
return;
}
const evaluationForOption = evaluation[i];
if (evaluationForOption === undefined) {
throw new Error(
'Mismatch between number of options in poll and selections for user',
);
}
if (type !== 'yes' && type !== 'no' && type !== 'maybe') {
throw new Error(
`Encountered not supported type of user selection: ${type}`,
);
}
evaluationForOption.answers[type]++;
switch (type) {
case 'yes':
@ -71,10 +89,11 @@ export default class PollEvaluationSummary extends Component<PollEvaluationSumma
evaluation.sort((a, b) => b.score - a.score);
const bestOptions = [];
const bestScore = evaluation[0]!.score;
for (let i = 0; i < evaluation.length; i++) {
if (bestScore === evaluation[i]!.score) {
bestOptions.push(evaluation[i]);
for (const evaluationForOption of evaluation) {
if (evaluationForOption.score === bestScore) {
bestOptions.push(evaluationForOption);
} else {
break;
}
@ -97,3 +116,9 @@ export default class PollEvaluationSummary extends Component<PollEvaluationSumma
return lastParticipationAt;
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
PollEvaluationSummary: typeof PollEvaluationSummary;
}
}

View file

@ -1,7 +1,7 @@
<BsButton
@buttonType="submit"
@type="primary"
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit"
...attributes
>
<span class="cr-steps-bottom-nav__label">

View file

@ -0,0 +1,20 @@
import templateOnlyComponent from '@ember/component/template-only';
interface SaveButtonSignature {
Args: {
Named: {
isPending: boolean;
};
};
Element: HTMLButtonElement;
}
const SaveButton = templateOnlyComponent<SaveButtonSignature>();
export default SaveButton;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
SaveButton: typeof SaveButton;
}
}

View file

@ -85,4 +85,9 @@ export default class PollController extends Controller {
this.shouldUseLocalTimezone = true;
this.timezoneChoosen = true;
}
@action
usePollTimezone() {
this.timezoneChoosen = true;
}
}

View file

@ -9,19 +9,27 @@ export interface FormatDateRelativeHelperSignature {
Args: {
Positional: Positional;
};
Return: string;
}
export default class FormatDateRelativeHelper extends Helper {
export default class FormatDateRelative extends Helper<FormatDateRelativeHelperSignature> {
@service declare intl: IntlService;
compute([date]: Positional) {
if (date instanceof Date) {
date = date.toISOString();
}
compute([dateOrIsoString]: Positional) {
const isoString =
dateOrIsoString instanceof Date
? dateOrIsoString.toISOString()
: dateOrIsoString;
return DateTime.fromISO(date).toRelative({
return DateTime.fromISO(isoString).toRelative({
locale: this.intl.primaryLocale,
padding: 1000,
});
})!;
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'format-date-relative': typeof FormatDateRelative;
}
}

View file

@ -1,6 +0,0 @@
import { helper } from '@ember/component/helper';
import { htmlSafe } from '@ember/template';
export default helper(function markAsSafeHtml([html]) {
return htmlSafe(html);
});

View file

@ -9,8 +9,14 @@ export interface MarkAsSafeHtmlHelperSignature {
};
}
export default helper<MarkAsSafeHtmlHelperSignature>(function markAsSafeHtml([
html,
]) {
const markAsSafeHtml = helper<MarkAsSafeHtmlHelperSignature>(([html]) => {
return htmlSafe(html);
});
export default markAsSafeHtml;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'mark-as-safe-html': typeof markAsSafeHtml;
}
}

View file

@ -32,7 +32,8 @@ function elementIsNotVisible(element: Element) {
);
}
export function scrollFirstInvalidElementIntoViewPort() {
const scrollFirstInvalidElementIntoViewPort = helper(() => {
return () => {
// `schedule('afterRender', function() {})` would be more approperiate but there seems to be a
// timing issue in Firefox causing the Browser not scrolling up far enough if doing so
// delaying to next runloop therefore
@ -71,8 +72,13 @@ export function scrollFirstInvalidElementIntoViewPort() {
scrollTarget.scrollIntoView({ behavior: 'smooth' });
}
});
}
export default helper(function () {
return scrollFirstInvalidElementIntoViewPort;
};
});
export default scrollFirstInvalidElementIntoViewPort;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'scroll-first-invalid-element-into-view-port': typeof scrollFirstInvalidElementIntoViewPort;
}
}

View file

@ -27,7 +27,7 @@ export default class Option {
}
get day() {
if (!this.datetime) {
if (this.datetime === null) {
return null;
}
@ -35,7 +35,7 @@ export default class Option {
}
get jsDate() {
if (!this.datetime) {
if (this.datetime === null) {
return null;
}

View file

@ -1,11 +1,11 @@
import Modifier from 'ember-modifier';
type Named = {
enabled: boolean;
enabled?: boolean;
};
interface AutofocusModifierSignature {
Element: HTMLInputElement;
Element: HTMLInputElement | HTMLSelectElement;
Args: {
Named: Named;
};
@ -29,3 +29,9 @@ export default class AutofocusModifier extends Modifier<AutofocusModifierSignatu
element.focus();
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
autofocus: typeof AutofocusModifier;
}
}

View file

@ -1,20 +1,20 @@
{{page-title 'Croodle'}}
{{page-title "Croodle"}}
<nav class='cr-navbar navbar navbar-dark'>
<h1 class='cr-logo'>
<LinkTo @route='index' class='navbar-brand'>
<nav class="cr-navbar navbar navbar-dark">
<h1 class="cr-logo">
<LinkTo @route="index" class="navbar-brand">
Croodle
</LinkTo>
</h1>
<div class='collapse' id='headerNavbar'>
<form class='form-inline my-2 my-lg-0'>
<LanguageSelect class='custom-select custom-select-sm' />
<div class="collapse" id="headerNavbar">
<form class="form-inline my-2 my-lg-0">
<LanguageSelect />
</form>
</div>
</nav>
<main class='container cr-main'>
<div id='messages'>
<main class="container cr-main">
<div id="messages">
{{#each this.flashMessages.queue as |flash|}}
<FlashMessage @flash={{flash}}>
{{t flash.message}}

View file

@ -1,23 +1,6 @@
{{page-title (t "create.title")}}
<BsButtonGroup @justified={{true}} class="cr-steps-top-nav form-steps">
{{#each this.formSteps as |formStep|}}
{{#unless formStep.hidden}}
<BsButton
@onClick={{fn this.transitionTo formStep.route}}
@type={{if
(eq this.router.currentRouteName formStep.route)
"primary is-active"
"default"
}}
disabled={{formStep.disabled}}
class="cr-steps-top-nav__button"
data-test-form-step={{formStep.route}}
>
{{t formStep.label}}
</BsButton>
{{/unless}}
{{/each}}
<BsButton
@onClick={{fn this.transitionTo "create.index"}}
@type={{if

View file

@ -1,5 +1,4 @@
<CreateOptions
@isFindADate={{eq @model.pollType "FindADate"}}
@isMakeAPoll={{eq @model.pollType "MakeAPoll"}}
@options={{if
(eq @model.pollType "FindADate")

View file

@ -1,6 +0,0 @@
{{!--
Add content you wish automatically added to the documents head
here. The 'model' available in this template can be populated by
setting values on the 'head-data' service.
--}}
<title>{{this.model.title}}</title>

View file

@ -34,7 +34,13 @@
<div class="col-lg-5 offset-lg-1">
<h3>{{t "index.hoster.title"}}</h3>
<p>
{{t "index.hoster.text" gitHubLink=(mark-as-safe-html "<a href=\"https://github.com/jelhan/croodle\">GitHub</a>") htmlSafe=true}}
{{t
"index.hoster.text"
gitHubLink=(mark-as-safe-html
'<a href="https://github.com/jelhan/croodle">GitHub</a>'
)
htmlSafe=true
}}
</p>
</div>
</div>

View file

@ -60,6 +60,7 @@
@onError={{fn this.linkAction "selected"}}
@onSuccess={{fn this.linkAction "copied"}}
class="btn btn-secondary cr-poll-link__copy-btn btn-sm"
data-test-button="copy-link"
>
{{t "poll.link.copy-label"}}&nbsp;
<span
@ -145,7 +146,7 @@
{{t "poll.modal.timezoneDiffers.button.useLocalTimezone"}}
</BsButton>
<BsButton
@onClick={{action (mut this.timezoneChoosen) true}}
@onClick={{this.usePollTimezone}}
data-test-button="use-poll-timezone"
>
{{t "poll.modal.timezoneDiffers.button.usePollTimezone"}}

View file

@ -24,10 +24,7 @@
{{#each @model.poll.options as |option index|}}
{{#let
(if
(eq
option.day
(get (object-at (sub index 1) @model.poll.options) "day")
)
(eq option.day (get (get @model.poll.options (sub index 1)) "day"))
false
true
)
@ -57,7 +54,7 @@
)
option.title
}}
@model={{object-at index @model.formData.selections}}
@model={{get @model.formData.selections index}}
@property="value"
data-test-form-element={{concat "option-" option.title}}
/>
@ -84,7 +81,7 @@
)
option.title
}}
@model={{object-at index @model.formData.selections}}
@model={{get @model.formData.selections index}}
@property="value"
@showValidationOn="change"
@useIcons={{false}}

View file

@ -3,7 +3,7 @@ import { assert } from '@ember/debug';
export type Answer = {
labelTranslation: string;
icon: string;
type: string;
type: 'yes' | 'no' | 'maybe';
};
export default function (

View file

@ -34,7 +34,7 @@ module.exports = function (defaults) {
enableTypeScriptTransform: true,
},
'ember-composable-helpers': {
only: ['array', 'object-at', 'pick'],
only: ['array', 'pick'],
},
'ember-math-helpers': {
only: ['lte', 'sub'],

132
package-lock.json generated
View file

@ -15,8 +15,9 @@
"@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",
"@glint/core": "^1.2.1",
"@glint/environment-ember-loose": "^1.2.1",
"@glint/template": "^1.2.1",
"@release-it-plugins/lerna-changelog": "^6.0.0",
"@tsconfig/ember": "^3.0.1",
"@types/luxon": "^3.3.3",
@ -8913,6 +8914,84 @@
"@simple-dom/interface": "^1.4.0"
}
},
"node_modules/@glint/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@glint/core/-/core-1.2.1.tgz",
"integrity": "sha512-25Zn65aLSN1M7s0D950sTNElZYRqa6HFA0xcT03iI/vQd1F6c3luMAXbFrsTSHlktZx2dqJ38c2dUnZJQBQgMw==",
"dev": true,
"dependencies": {
"@glimmer/syntax": "^0.84.2",
"escape-string-regexp": "^4.0.0",
"semver": "^7.5.2",
"silent-error": "^1.1.1",
"uuid": "^8.3.2",
"vscode-languageserver": "^8.0.1",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-uri": "^3.0.8",
"yargs": "^17.5.1"
},
"bin": {
"glint": "bin/glint.js",
"glint-language-server": "bin/glint-language-server.js"
},
"peerDependencies": {
"typescript": ">=4.8.0"
}
},
"node_modules/@glint/core/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@glint/core/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@glint/core/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@glint/core/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@glint/core/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/@glint/environment-ember-loose": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@glint/environment-ember-loose/-/environment-ember-loose-1.2.1.tgz",
@ -56210,6 +56289,55 @@
"node": ">= 0.8"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz",
"integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageserver": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz",
"integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==",
"dev": true,
"dependencies": {
"vscode-languageserver-protocol": "3.17.3"
},
"bin": {
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz",
"integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==",
"dev": true,
"dependencies": {
"vscode-jsonrpc": "8.1.0",
"vscode-languageserver-types": "3.17.3"
}
},
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz",
"integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==",
"dev": true
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
"integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==",
"dev": true
},
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"dev": true
},
"node_modules/walk-sync": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.3.4.tgz",

View file

@ -19,7 +19,7 @@
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"lint:types": "tsc --noEmit",
"lint:types": "glint",
"release": "release-it",
"start": "ember serve",
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
@ -34,8 +34,9 @@
"@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",
"@glint/core": "^1.2.1",
"@glint/environment-ember-loose": "^1.2.1",
"@glint/template": "^1.2.1",
"@release-it-plugins/lerna-changelog": "^6.0.0",
"@tsconfig/ember": "^3.0.1",
"@types/luxon": "^3.3.3",

View file

@ -10,8 +10,6 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import pageParticipation from 'croodle/tests/pages/poll/participation';
import { DateTime } from 'luxon';
import { triggerCopySuccess } from 'ember-cli-clipboard/test-support';
module('Acceptance | view poll', function (hooks) {
hooks.beforeEach(function () {
window.localStorage.setItem('locale', 'en');
@ -32,7 +30,7 @@ module('Acceptance | view poll', function (hooks) {
'share link is shown',
);
await triggerCopySuccess();
await click('.copy-btn');
/*
* Can't test if link is actually copied to clipboard due to api
* restrictions. Due to security it's not allowed to read from clipboard.

View file

@ -13,5 +13,8 @@
],
"*": ["types/*"]
}
},
"glint": {
"environment": "ember-loose"
}
}

View file

@ -0,0 +1,17 @@
import { ComponentLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BsAlert: ComponentLike<{
Args: {
Named: {
type: string;
};
};
Blocks: {
default: [];
};
Element: HTMLDivElement;
}>;
}
}

View file

@ -0,0 +1,17 @@
import { ComponentLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BsButtonGroup: ComponentLike<{
Args: {
Named: {
justified: boolean;
};
};
Blocks: {
default: [];
};
Element: HTMLDivElement;
}>;
}
}

View file

@ -0,0 +1,19 @@
import { ComponentLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BsButton: ComponentLike<{
Args: {
Named: {
onClick?: () => void;
size?: 'sm' | 'md' | 'lg';
type?: string;
};
};
Blocks: {
default: [];
};
Element: HTMLButtonElement;
}>;
}
}

View file

@ -0,0 +1,30 @@
import { ComponentLike } from '@glint/template';
import type BsFormElementComponent from './bs-form/element';
type BsFormComponent = ComponentLike<{
Args: {
Named: {
formLayout: 'horizontal' | 'vertical';
model: unknown;
onInvalid?: () => void;
onSubmit: () => void;
};
};
Blocks: {
default: [
{
element: BsFormElementComponent;
isSubmitting: boolean;
},
];
};
Element: HTMLDivElement;
}>;
export default BsFormComponent;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BsForm: BsFormComponent;
}
}

View file

@ -0,0 +1,34 @@
import { ComponentLike } from '@glint/template';
type BsFormElementComponent = ComponentLike<{
Args: {
Named: {
controlType?: 'checkbox' | 'select' | 'text' | 'textarea' | 'time';
invisibleLabel?: boolean;
label?: string;
model?: unknown;
property?: string;
showValidationOn?: string | string[];
useIcons?: boolean;
};
};
Blocks: {
default: [
{
control: ComponentLike<{
Args: {
Named: Record<string, unknown>;
};
Element: HTMLInputElement;
}>;
id: string;
setValue: (value: unknown) => void;
validation: 'success' | 'error' | 'warning' | null;
value: unknown;
},
];
};
Element: HTMLDivElement;
}>;
export default BsFormElementComponent;

View file

@ -0,0 +1,35 @@
import { ComponentLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
BsModal: ComponentLike<{
Args: {
Named: {
autoClose: boolean;
closeButton: boolean;
footer: boolean;
keyboard: boolean;
open: boolean;
title: string;
};
};
Blocks: {
default: [
{
body: ComponentLike<{
Blocks: {
default: [];
};
}>;
footer: ComponentLike<{
Blocks: {
default: [];
};
}>;
},
];
};
Element: HTMLDivElement;
}>;
}
}

View file

@ -0,0 +1,19 @@
import { ComponentLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
CopyButton: ComponentLike<{
Args: {
Named: {
onError: () => void;
onSuccess: () => void;
text: string;
};
};
Blocks: {
default: [];
};
Element: HTMLButtonElement;
}>;
}
}

View file

@ -0,0 +1,17 @@
import { ComponentLike } from '@glint/template';
import type FlashObject from 'ember-cli-flash/flash/object';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
FlashMessage: ComponentLike<{
Args: {
Named: {
flash: FlashObject;
};
};
Blocks: {
default: [];
};
}>;
}
}

View file

@ -8,6 +8,7 @@ declare module 'ember-cli-flash/flash/object' {
exitTimer: number;
isExitable: boolean;
initializedTime: number;
message: string;
destroyMessage(): void;
exitMessage(): void;
preventExit(): void;

View file

@ -0,0 +1,12 @@
import { HelperLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
pick: HelperLike<{
Args: {
Positional: [path: string, action?: (_: unknown) => unknown];
};
Return: (_: unknown) => unknown;
}>;
}
}

View file

@ -0,0 +1,18 @@
import { HelperLike } from '@glint/template';
// Ember Intl ships glint types. But as of today (October 29, 2023)
// they are buggy and cannot be used.
// Types provided by Ember Intl should be used instead as soon as
// type issues have been fixed in the addon itself.
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'format-date': HelperLike<{
Args: {
Positional: [Date | string];
Named: Record<string, unknown>;
};
Return: string;
}>;
}
}

18
types/ember-intl/helpers/t.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
import { HelperLike } from '@glint/template';
// Ember Intl ships glint types. But as of today (October 29, 2023)
// they are buggy and cannot be used.
// Types provided by Ember Intl should be used instead as soon as
// type issues have been fixed in the addon itself.
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
t: HelperLike<{
Args: {
Positional: [string];
Named: Record<string, unknown>;
};
Return: string;
}>;
}
}

View file

@ -0,0 +1,15 @@
import { HelperLike } from '@glint/template';
// Glint support in ember-page-title itself is tracked here:
// https://github.com/ember-cli/ember-page-title/issues/239
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'page-title': HelperLike<{
Args: {
Positional: [string];
};
Return: void;
}>;
}
}

View file

@ -0,0 +1,29 @@
import { ComponentLike } from '@glint/template';
import type { DateTime } from 'luxon';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
PowerCalendarMultiple: ComponentLike<{
Args: {
Named: {
center: DateTime;
onCenterChange: (day: { datetime: DateTime }) => void;
onSelect: (days: { datetime: DateTime[] }) => void;
selected: DateTime[];
};
};
Blocks: {
default: [
{
actions: {
moveCenter: (step: number, unit: 'month') => void;
};
center: Date;
Days: ComponentLike;
},
];
};
Element: HTMLDivElement;
}>;
}
}

8
types/global.d.ts vendored
View file

@ -1 +1,9 @@
import '@glint/environment-ember-loose';
import type EmberMathRegistry from 'ember-math-helpers/template-registry';
import type EmberTruthRegistry from 'ember-truth-helpers/template-registry';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry
extends EmberMathRegistry,
EmberTruthRegistry {}
}